본문으로 바로가기

728x90

이전 포스트까지는 OAuth2 Server에 데이터베이스를 붙이는 작업을 진행하였습니다. 본 포스트에서는 JWT기반의 토큰을 관리하는 기능을 추가합니다.

 

Access Token이 발급된 이후에는 리소스에 접근하기 위해서는 발급된 토큰으로 인증 여부 및 인증정보를 OAuth Server에 질의해서 얻어와야 합니다. 테스트나 작은 시스템에서는 크게 문제가 발생하지 않으나 사용자가 많아지거나 리소스의 접근이 많아지는 경우에는 부하에 따른 문제가 발생할 수 있습니다.
즉, 사용자가 작업할 때마다 매번 OAuth Server의 /oauth/check_token으로 지속적으로 검증을 수행합니다. 따라서, 작업이 많아지게 되면 서버에 부하가 발생하게 됩니다. JWT를 사용하여 OAuth Server에서 검증에서 수행하는 데이터를 사용자측에서 제공받게 되면 지속적인 검증이 불필요하여 부하가 감소하게 됩니다.

 

Spring Security에서는 JWT를 통하여 인증정보를 Access Token, Refresh Token에 포함하여 전달하는 기능을 제공하고 있습니다.

 

JWT (JSON Web Token) 는 JSON 형식을 기반으로 액세스 토큰을 작성하기위한 개방형 표준 (RFC 7519)입니다. 일반적으로 클라이언트-서버 응용 프로그램에서 인증 데이터를 전송하는 데 사용됩니다.토큰은 서버에서 생성하고 비밀 키로 서명 한 후 나중에이 토큰을 사용하여 자신의 신원을 확인하는 클라이언트로 전송됩니다.

 

JWT(Json Web Token)에 관한 자세한 사항은 참고사이트에서 확인해 주시기 바랍니다.

 

미리보기

접근 토큰(Access Token) 예

Access Token

갱신 토큰(Refresh Token) 예

Refresh Token

라이브러리 추가

JWT를 사용하기 위해 아래 구문을 추가하여 종속 라이브러리를 추가합니다.

<dependency>
	<groupId>org.springframework.security</groupId>
	<artifactId>spring-security-jwt</artifactId>
	<version>1.1.0.RELEASE</version>
</dependency>

 

JWT OAuth 인증

기존의 데이터베이스를 사용하는 TokenStoreService는 더 이상 필요하지 않습니다. 기본적으로 JWT 토큰은 자제적으로 인증정보를 포함하고 있어 별도의 저장소는 필요하지 않습니다.

Authorization Server 코드

데이터베이스를 사용하는 TokenStore 서비스를 수석처리하고 아래와 같이 JwtTokenStore의 인스턴스를 생성합니다. 기존 데이터베이스 기반 ApprovalStore의 사용을 위해 아래와 같이 전달값으로 ApprovalStore의 인스턴스를 지정합니다.

//	@Autowired
//	@Qualifier("oauthTokenStoreService")
//	private TokenStore tokenStoreService;
	
	@Bean
	public TokenStore tokenStore() {
		return new JwtTokenStore(accessTokenConverter());
		tokenStore.setApprovalStore(approvalStore);	// 기존 데이터베이스 기반 ApprovalStore를 사용
		return tokenStore;
	}

	@Bean
	public JwtAccessTokenConverter accessTokenConverter() {
		return new JwtAccessTokenConverter();		// JWT 토큰의 변환 및 검증을 위해 Converter 지정
	}

	@Override
	public void configure(AuthorizationServerEndpointsConfigurer endpoints) throws Exception {
		endpoints
			.authenticationManager(authenticationManager)
			.authorizationCodeServices(authorizationCodeServices)
			.approvalStore(approvalStore)
//			.tokenStore(tokenStoreService)		// 기존 TokenStoreService 주석처리
			.tokenStore(tokenStore())			// 신규 JwtTokenStore 지정
			.accessTokenConverter(accessTokenConverter())
			.userDetailsService(userDetailsService);
	}

Resources Server 코드

JwtAccessTokenConverter와 JwtTokenStore를 지정합니다. @Autowire를 사용하여 처리해도 됩니다. AuthorizationServerConfig와 충돌이 발생할 수 있으니 @Bean 구문을 주석처리해야 할 수 있습니다.

//	@Autowire
//	private JwtAccessTokenConverter accessTokenConverter;
    
	@Bean
	public JwtAccessTokenConverter accessTokenConverter() {
		return new JwtAccessTokenConverter();
	}

//	@Autowire
//	private TokenStore tokenStore;
    
	@Bean
	public TokenStore tokenStore() {
		return new JwtTokenStore(accessTokenConverter());
	}
    
    /*
	@Primary
	@Bean

     Remote Token Checking..
	 --- application.yml ---
security:
  oauth2:
    client:
      client-id: testClientId
      client-secret: testSecret
    resource:
      token-info-uri: http://localhost:8081/oauth/check_token
      
      OR
      
	public RemoteTokenServices tokenService() {
		RemoteTokenServices tokenService = new RemoteTokenServices();
		tokenService.setCheckTokenEndpointUrl("http://localhost:8080/spring-security-oauth-server/oauth/check_token");
		tokenService.setClientId("foo");
		tokenService.setClientSecret("bar");
		return tokenService;
	}
	*/

 

대칭키 방식 JWT 검증

위에서 설정한 Jwt기반 OAuth 인증 코드는 검증코드가 인증서버 구동시에 동적으로 생성되어 Jwt의 검증을 수행합니다. 따라서, 인증서버가 재시작하게 되면 기본의 토큰의 정합성이 깨지는 문제가 발생하게 됩니다. 본 장에서는 서명키를 지정하여 인증서버가 재시작하여도 서명의 정합성이 깨지지 않도록 수정합니다.

 

토큰 검증 설정

환경설정 파일 application.properties에 security.oauth2.signkey 속성을 추가하고 정합성 검증을 위한 키를 지정합니다.

security.oauth2.signkey=password

 

Authorization Server 코드

JwtAccessTokenConverter에 setSigningKey 함수를 사용하여 정합성 검증을 위한 키를 지정합니다.

	@Value("${security.oauth2.signkey}")
	private String signKey;
    
//	@Bean
//	public JwtAccessTokenConverter accessTokenConverter() {
//		return new JwtAccessTokenConverter();
//	}

	@Bean
	public JwtAccessTokenConverter accessTokenConverter() {
		JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
		converter.setSigningKey(signKey);
		return converter;
	}

Resources Server 코드

동일하게 Resource Server 설정에서도 JwtAccessTokenConverter에 setSigningKey 함수를 사용하여 정합성 검증을 위한 키를 지정합니다.

	@Value("${security.oauth2.signkey}")
	private String signKey;
    
//	@Bean
//	public JwtAccessTokenConverter accessTokenConverter() {
//		return new JwtAccessTokenConverter();
//	}

	@Bean
	public JwtAccessTokenConverter accessTokenConverter() {
		JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
		converter.setSigningKey(signKey);
		return converter;
	}

 

공개키/개인키 방식 JWT 검증

대칭키를 사용한 Jwt 검증에서는 대칭키가 노출되면 악의적인 공격자에 의해 토큰을 임의로 발급할 수 있게 됩니다. 본 장에서는 공개키 기반으로 Jwt의 검증 사용하는 방법에 대해 설명합니다.

 

공개키쌍 생성

아래의 명령을 실행하여 공개키쌍을 생성합니다. 인증서 별명, 길이, 키암호, 키저장소이름, 키저장서암호 등은 필요에 맞게 수정해야 합니다.

keytool -genkeypair -alias oauth -keyalg RSA –keysize 2048 -keypass keypass -keystore kr.ejsoft.oauth2.server.jks -storepass storepass

실제 명령은 아래와 같이 실행됩니다.

keytool
키 및 인증서 관리 툴

명령:

 -certreq            인증서 요청을 생성합니다.
 -changealias        항목의 별칭을 변경합니다.
 -delete             항목을 삭제합니다.
 -exportcert         인증서를 익스포트합니다.
 -genkeypair         키 쌍을 생성합니다.
 -genseckey          보안 키를 생성합니다.
 -gencert            인증서 요청에서 인증서를 생성합니다.
 -importcert         인증서 또는 인증서 체인을 임포트합니다.
 -importpass         비밀번호를 임포트합니다.
 -importkeystore     다른 키 저장소에서 하나 또는 모든 항목을 임포트합니다.
 -keypasswd          항목의 키 비밀번호를 변경합니다.
 -list               키 저장소의 항목을 나열합니다.
 -printcert          인증서의 콘텐츠를 인쇄합니다.
 -printcertreq       인증서 요청의 콘텐츠를 인쇄합니다.
 -printcrl           CRL 파일의 콘텐츠를 인쇄합니다.
 -storepasswd        키 저장소의 저장소 비밀번호를 변경합니다.

command_name 사용법에 "keytool -command_name -help" 사용

keytool -genkeypair -alias oauth -keyalg RSA –keysize 2048 -keypass keypass -keystore kr.ejsoft.oauth2.server.jks -storepass storepass
이름과 성을 입력하십시오.
  [Unknown]:
조직 단위 이름을 입력하십시오.
  [Unknown]:
조직 이름을 입력하십시오.
  [Unknown]:
구/군/시 이름을 입력하십시오?
  [Unknown]:
시/도 이름을 입력하십시오.
  [Unknown]:
이 조직의 두 자리 국가 코드를 입력하십시오.
  [Unknown]:
CN=Unknown, OU=Unknown, O=Unknown, L=Unknown, ST=Unknown, C=Unknown이(가) 맞습니까?
  [아니오]:  y


Warning:
JKS 키 저장소는 고유 형식을 사용합니다. "keytool -importkeystore -srckeystore kr.ejsoft.oauth2.server.jks -destkeystore kr.ejsoft.oauth2.server.jks -deststoretype pkcs12"를 사용하는 산업 표준 형식인 PKCS12로 이전하는 것이 좋습니다.

공개키 출력

리눅스나 맥킨토시 계열에서는 openssl 명령을 수행하여 공개키를 pem으로 출력할 수 있습니다.

keytool -list -rfc --keystore kr.ejsoft.oauth2.server.jks | openssl x509 -inform pem -pubkey

아래는 명령을 수행한 예시입니다.

$ keytool -list -rfc --keystore kr.ejsoft.oauth2.server.jks | openssl x509 -inform pem -pubkey
키 저장소 비밀번호 입력:  storepass
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArJerQDKo7Vwl2an9UEZD
9npx8vXoB+setdeZn5OKEntdpdXMdc1KE07Q8aejXnEdzTHqeWxfdctyJ0FZzphX
......
gfgmiAYl6XD1o3iui3vEJzNLN9nrbte4rjc+Rqv3aE23hu3f8QImNzdSZo6HJhQC
NwIDAQAB
-----END PUBLIC KEY-----
-----BEGIN CERTIFICATE-----
MIIDdzCCAl+gAwIBAgIEJg3+SjANBgkqhkiG9w0BAQsFADBsMRAwDgYDVQQGEwdV
bmtub3duMRAwDgYDVQQIEwdVbmtub3duMRAwDgYDVQQHEwdVbmtub3duMRAwDgYD
......
BxE/r713L3I7rGbM0DMnTeDEDaCytN6TmwxeqJiUMePfWdKAZUV/WZeKTZxePfUf
ZK9J+FFEaua3hay2iY7s+eGPZ93rcNfrudARARdn34/coWx4lD8kh6At/K1qJrM2
7/QGMJeFP2zwBhhEhAwAN2ogad7cXOoy6OwcM0sMuZ+y5Vt2WKFLbHtRPj1V5mhk
Nq/ACKliqWctpd8P5M/4Oat+zdyR3YjnU3+r
-----END CERTIFICATE-----

윈도우에서 위 명령을 수행하려면 openssl을 추가 설치해야 합니다. 아래는 openssl 없이 공개키를 PEM 규격으로 출력하는 방법입니다. writePublicKey함수를 사용하여 PEM규격으로 공개키를 변환합니다.

	public void printKeypair() {
		KeyPair keypair = new KeyStoreKeyFactory(new ClassPathResource("kr.ejsoft.oauth2.server.jks"), "storepass".toCharArray()).getKeyPair("oauth", "keypass".toCharArray());
		PublicKey publickey = keypair.getPublic();
		String pem = writePublicKey(publickey);
		log.info(pem);
	}
	
	public static String writePublicKey(PublicKey key) {
		return writeObject("PUBLIC KEY", key.getEncoded());
	}
	
	private static String writeObject(String type, byte[] bytes){
		final int LINE_LENGTH = 64;
		StringWriter sw = new StringWriter();
		BufferedWriter bw = null;
		try{
			String obj64 = Base64.getEncoder().encodeToString(bytes);
			bw = new BufferedWriter(sw);
			bw.write("-----BEGIN " + type + "-----");
			bw.newLine();
			int index = 0;
			int length = obj64.length() % LINE_LENGTH == 0 ? obj64.length() / LINE_LENGTH : obj64.length() / LINE_LENGTH + 1;
			while(index < length) {
				int start = LINE_LENGTH * index;
				int end = LINE_LENGTH * (index + 1);
				end = end > obj64.length() ? obj64.length() : end;
				
				String sub = obj64.substring(start, end);
				bw.append(sub);
				bw.newLine();
				index++;
			}
			bw.write("-----END " + type + "-----");
			bw.newLine();
		}catch(Exception e){
//			e.printStackTrace();
		} finally {
			if(bw != null){
				try{ bw.flush(); } catch(Exception e) { }
				try{ bw.close(); } catch(Exception e) { }
			}
		}
		
		return sw.toString();
	}

 

출력 예시

아래와 같이 공개키를 출력합니다. 이 PEM 규격의 내용은 publickey.pem 으로 복사하여 공개키 내용을 저장하여 사용할 수 있습니다.

09:18 INFO  k.e.o.s.c.AuthorizationServerConfig - -----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA5a7i4jFrBA2TMaCWQYvW
jvg14KnmL1z9PlEDv2xCmoFfMzD+Ck4LQQ08L3VCK5ixYksB1jxly+ZtC+z4e9BP
eqXRANZWdFEYKfKjdnmqjkBpGbYTQ8J1LW/kdws8nOXRV+ALcMmLbIyjvrrlOEqm
+MEtSNruATu7436GGkO+Y3xaYUDysae8hHUwAcpw3d6DCVIuggFDgz8im4na+Gaw
192OT1z703ilnfOuzellMMJ4B+qG0ERWUidc8EZoW32gfKPLAsPP9COACTcN7llr
zn2Rrrzp1B2hyBtlhJinS6YaDtnCol/hA0TzWrnSWXmvWGm7aJLUErw+alwaEX7l
wQIDAQAB
-----END PUBLIC KEY-----

출력된 PEM 규격의 공개키 내용을 복사하여 임의의 평문파일(kr.ejsft.oauth2.publickey.txt)로 저장합니다.

 

공개키 및 개인키 설정

Authorization Server 코드

accessTokenConverter() 함수를 아래와 같이 수정합니다. KeyStore에서 개인키를 읽어와서 JwtAccessTokenConverter에 전달합니다.

//	@Bean
//	public JwtAccessTokenConverter accessTokenConverter() {
//		JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//		converter.setSigningKey(signKey);
//		return converter;
//	}

	@Bean
	public JwtAccessTokenConverter accessTokenConverter() {
		JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
		KeyPair keypair = new KeyStoreKeyFactory(new ClassPathResource("kr.ejsoft.oauth2.server.jks"), "storepass".toCharArray()).getKeyPair("oauth", "keypass".toCharArray());
		converter.setKeyPair(keypair);
		return converter;
	}

Resources Server 코드

accessTokenConverter() 함수를 아래와 같이 수정합니다. 공개키를 읽어와서 JwtAccessTokenConverter에 전달합니다.

//	@Bean
//	public JwtAccessTokenConverter accessTokenConverter() {
//		JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
//		converter.setSigningKey(signKey);
//		return converter;
//	}

	@Bean
	public JwtAccessTokenConverter accessTokenConverter() {
		JwtAccessTokenConverter converter = new JwtAccessTokenConverter();
		Resource resource = new ClassPathResource("kr.ejsoft.oauth2.publickey.txt");
		String publickey = null;
		try {
			publickey = asString(resource);
		} catch(final IOException e) {
			throw new RuntimeException(e);
		}
		
		converter.setVerifierKey(publickey);
		return converter;
	}
	
	public static String asString(Resource resource) throws IOException {
		Reader reader = new InputStreamReader(resource.getInputStream(), "UTF-8");
		return FileCopyUtils.copyToString(reader);
	}

 

동작 확인

Execute - Authorization Code Grant Type

웹브라우저에 아래의 주소를 입력합니다.

http://localhost:9090/oauth/authorize?response_type=code&client_id=client&redirect_uri=http%3A%2F%2Flocalhost%3A9000%2Fcallback&scope=read_profile

 

주소 이동 시 자동으로 Spring Security의 기본 인증 페이지로 리다이렉트 됩니다.

Spring Security 인증

WebSecurityConfig에서 지정한 사용자 user/pass로 인증처리합니다. 인증이 완료되면 지정한 콜백주소로 코드를 반환합니다.

 

http://localhost:9000/callback?code=PYpN8k

반환된 권한 부여코드는 PYpN8k입니다.

 

POST방식으로 토큰을 요청합니다.

요청주소 : http://localhost:9090/oauth/token

 

요청헤더 : 

Content-Type=application/x-www-form-urlencoded 
Authorization: Basic Y2xpZW50OnNlY3JldA==

 

헤더의 Authorization 값은 makeAuthorizationRequestHeader() 함수에서 출력되는 로그를 복사하여 사용합니다.

 

요청내용

grant_type=authorization_code&code=PYpN8k&redirect_uri=http%3A%2F%2Flocalhost%3A9000%2Fcallback&scope=read_profile&

 

요청본문

POST /oauth/token HTTP/1.1
Authorization: Basic Y2xpZW50OnNlY3JldA==
aAuthorization: Basic dXNlcjpwYXNz
Content-Type: application/x-www-form-urlencoded
User-Agent: http4e/5.0.12
Host: localhost:9090
Content-Length: 115

code=PYpN8k&grant_type=authorization_code&redirect_uri=http%3A%2F%2Flocalhost%3A9000%2Fcallback&scope=read_profile&

 

응답본문

HTTP/1.1 200 
Pragma: no-cache
Cache-Control: no-store
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 27 Nov 2019 01:16:12 GMT

{"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzQ4NTMzNzIsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJ1c2VyIl0sImp0aSI6IjVjYzZmZDgzLWQxOGQtNGE0ZS1iNjQyLTg1Mzk5ZDNmNWQyMSIsImNsaWVudF9pZCI6ImNsaWVudCIsInNjb3BlIjpbInJlYWRfcHJvZmlsZSJdfQ.XSQMEk9mKracvD72Kuzur8zuVqniWeJI3X7dsexacH8vfHz5H_Wj5FEPAxOIbmbhMaRHbdhEyEz8-IZ0jsK_7USnXBVG6cvMa9oJGQcFgfoGDHIwYxHh1lBG_qJm4g8gj6N8n6GPtr3TLAjomLZSvPa_rsUrYJiBiAjO1zoL9Oxp_k4fuCqJaNjy1lglY4KpW5uGyUWnM4HE58k41wwl4eAq8LL6AzGrE0BQvsMH7CADVUSef4VbnvZgbBZ8ZAITJtF2r11xgUgRsscb5yBaywZgcSrxvsOaSXQjY74Ti9H3n73FwnblgpEGFa1JQVvTMc5dtDuK1C35rWLaeupHDw","token_type":"bearer","refresh_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsicmVhZF9wcm9maWxlIl0sImF0aSI6IjVjYzZmZDgzLWQxOGQtNGE0ZS1iNjQyLTg1Mzk5ZDNmNWQyMSIsImV4cCI6MTU3NzQwOTM3MiwiYXV0aG9yaXRpZXMiOlsidXNlciJdLCJqdGkiOiJkNjQ3YzIyYy1kNTQyLTQzYTAtYjA0My02Yzc0ZmZiODA0OTEiLCJjbGllbnRfaWQiOiJjbGllbnQifQ.eeRVPy000aAdFsfwxGFGFinIoMJ9iZzkzVVSbQ4TSB-P6G8a_ad5fG5sNhFma4Cs4fs06MdqpVuz82R2K1elE14ic1bdLR3PvFLXX4hq9bvWssZXGyG21InGsMkOZydmEbp5VuXlG_oPUPMKV5ncX0Yhze433xDRi0sEuQHSZGBHZS618o8HM7slH0GUYwJfn_qE_8dIYyVRpBEcSQG5plMs6z8uJLMh_Qe0tv-wW8fPmuFiGlDhCCsrMI3Fmfhk_Mw4qVZ9KOt9y2eGuLulvi364PjmwkGLqMTl0o6cf-vdYzQrDmBiUGUTcIrWnr0uvuwUGbj_9Bf1ZABf9kesuA","expires_in":35999,"scope":"read_profile","jti":"5cc6fd83-d18d-4a4e-b642-85399d3f5d21"}
{
	"access_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzQ4NTMzNzIsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJ1c2VyIl0sImp0aSI6IjVjYzZmZDgzLWQxOGQtNGE0ZS1iNjQyLTg1Mzk5ZDNmNWQyMSIsImNsaWVudF9pZCI6ImNsaWVudCIsInNjb3BlIjpbInJlYWRfcHJvZmlsZSJdfQ.XSQMEk9mKracvD72Kuzur8zuVqniWeJI3X7dsexacH8vfHz5H_Wj5FEPAxOIbmbhMaRHbdhEyEz8-IZ0jsK_7USnXBVG6cvMa9oJGQcFgfoGDHIwYxHh1lBG_qJm4g8gj6N8n6GPtr3TLAjomLZSvPa_rsUrYJiBiAjO1zoL9Oxp_k4fuCqJaNjy1lglY4KpW5uGyUWnM4HE58k41wwl4eAq8LL6AzGrE0BQvsMH7CADVUSef4VbnvZgbBZ8ZAITJtF2r11xgUgRsscb5yBaywZgcSrxvsOaSXQjY74Ti9H3n73FwnblgpEGFa1JQVvTMc5dtDuK1C35rWLaeupHDw"
    ,"token_type":"bearer"
    ,"refresh_token":"eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyX25hbWUiOiJ1c2VyIiwic2NvcGUiOlsicmVhZF9wcm9maWxlIl0sImF0aSI6IjVjYzZmZDgzLWQxOGQtNGE0ZS1iNjQyLTg1Mzk5ZDNmNWQyMSIsImV4cCI6MTU3NzQwOTM3MiwiYXV0aG9yaXRpZXMiOlsidXNlciJdLCJqdGkiOiJkNjQ3YzIyYy1kNTQyLTQzYTAtYjA0My02Yzc0ZmZiODA0OTEiLCJjbGllbnRfaWQiOiJjbGllbnQifQ.eeRVPy000aAdFsfwxGFGFinIoMJ9iZzkzVVSbQ4TSB-P6G8a_ad5fG5sNhFma4Cs4fs06MdqpVuz82R2K1elE14ic1bdLR3PvFLXX4hq9bvWssZXGyG21InGsMkOZydmEbp5VuXlG_oPUPMKV5ncX0Yhze433xDRi0sEuQHSZGBHZS618o8HM7slH0GUYwJfn_qE_8dIYyVRpBEcSQG5plMs6z8uJLMh_Qe0tv-wW8fPmuFiGlDhCCsrMI3Fmfhk_Mw4qVZ9KOt9y2eGuLulvi364PjmwkGLqMTl0o6cf-vdYzQrDmBiUGUTcIrWnr0uvuwUGbj_9Bf1ZABf9kesuA"
    ,"expires_in":35999
    ,"scope":"read_profile"
    ,"jti":"5cc6fd83-d18d-4a4e-b642-85399d3f5d21"
}

Execute - 리소스 페이지 요청

요청주소 : http://localhost:9090/api/access

 

요청헤더 : 

Content-Type=application/x-www-form-urlencoded 
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzQ4NTMzNzIsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJ1c2VyIl0sImp0aSI6IjVjYzZmZDgzLWQxOGQtNGE0ZS1iNjQyLTg1Mzk5ZDNmNWQyMSIsImNsaWVudF9pZCI6ImNsaWVudCIsInNjb3BlIjpbInJlYWRfcHJvZmlsZSJdfQ.XSQMEk9mKracvD72Kuzur8zuVqniWeJI3X7dsexacH8vfHz5H_Wj5FEPAxOIbmbhMaRHbdhEyEz8-IZ0jsK_7USnXBVG6cvMa9oJGQcFgfoGDHIwYxHh1lBG_qJm4g8gj6N8n6GPtr3TLAjomLZSvPa_rsUrYJiBiAjO1zoL9Oxp_k4fuCqJaNjy1lglY4KpW5uGyUWnM4HE58k41wwl4eAq8LL6AzGrE0BQvsMH7CADVUSef4VbnvZgbBZ8ZAITJtF2r11xgUgRsscb5yBaywZgcSrxvsOaSXQjY74Ti9H3n73FwnblgpEGFa1JQVvTMc5dtDuK1C35rWLaeupHDw

 

Authorization Server에서 반환된 접근 토큰을 요청 헤더에 추가합니다.

 

요청본문

GET /api HTTP/1.1
Authorization: Bearer eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjE1NzQ4NTMzNzIsInVzZXJfbmFtZSI6InVzZXIiLCJhdXRob3JpdGllcyI6WyJ1c2VyIl0sImp0aSI6IjVjYzZmZDgzLWQxOGQtNGE0ZS1iNjQyLTg1Mzk5ZDNmNWQyMSIsImNsaWVudF9pZCI6ImNsaWVudCIsInNjb3BlIjpbInJlYWRfcHJvZmlsZSJdfQ.XSQMEk9mKracvD72Kuzur8zuVqniWeJI3X7dsexacH8vfHz5H_Wj5FEPAxOIbmbhMaRHbdhEyEz8-IZ0jsK_7USnXBVG6cvMa9oJGQcFgfoGDHIwYxHh1lBG_qJm4g8gj6N8n6GPtr3TLAjomLZSvPa_rsUrYJiBiAjO1zoL9Oxp_k4fuCqJaNjy1lglY4KpW5uGyUWnM4HE58k41wwl4eAq8LL6AzGrE0BQvsMH7CADVUSef4VbnvZgbBZ8ZAITJtF2r11xgUgRsscb5yBaywZgcSrxvsOaSXQjY74Ti9H3n73FwnblgpEGFa1JQVvTMc5dtDuK1C35rWLaeupHDw
Content-Type: application/x-www-form-urlencoded
User-Agent: http4e/5.0.12
Host: localhost:9090

 

응답본문(접근 토큰이 유효한 경우)

HTTP/1.1 404 
X-Content-Type-Options: nosniff
X-XSS-Protection: 1; mode=block
Cache-Control: no-cache, no-store, max-age=0, must-revalidate
Pragma: no-cache
Expires: 0
X-Frame-Options: DENY
Content-Type: application/json;charset=UTF-8
Transfer-Encoding: chunked
Date: Wed, 27 Nov 2019 01:18:55 GMT

{"timestamp":"2019-11-27T01:18:55.068+0000","status":404,"error":"Not Found","message":"No message available","path":"/api"}

- 접근이 정상적으로 처리된 경우입니다. 404오류는 요청한 페이지가 실제 존재하지 않기 때문에 발생된 오류입니다.

 

글을 마치며

공개키 기반의 Jwt 검증을 구현하였습니다. Jwt기반으로 OAuth를 사용할 경우 다음과 같은 내용을 검토해 보아야 합니다. (아직 OAuth를 학습하고 있는 관계로 아직 아래 궁금증에 대한 적절한 답은 찾지 못하였습니다.)

  • 공개키/개인키의 교체는 어떻게 할 것인가?
  • 고가용성(HA, Hign Availability)를 위해 여러 개의 인증서버를 구성할 경우 토큰 데이터공유는 어떻게 할 것인가?
  • HA구성했을 경우 Jwt Access Token과 Jwt Refersh Token의 검증은 어떻게 할 것인가?
  • Jwt Acces Token 및 Jwt Refresh Token의 만료처리는 어떻게 할 것인가?
  • Jwt가 토큰정보와 서명으로 통해 검증하지만 과연 신뢰할 만한 것인가?

 

참고사이트

 

소스코드

소스코드는여기에서 다운로드 가능합니다.

728x90