본문 바로가기
SpringBoot

[Spring Boot] 31. Google OAuth with JWT (3)

by 청양호박이 2021. 4. 20.

지난 시간에는 Back-End에서 생성한 Token을 Front-End로 전송하는 단계를 진행하였습니다. 이렇게 전달된 Token은 개별 사용자의 Browser내 Local Storage, Session Storage 혹은 Cookies에 저장하여 사용할 수 있도록 준비하는 것 까지 개발을 완료 했습니다. 

 

이번에는 그 이후에 사용자가 Front-End를 통해서 Token을 header에 추가하여 API를 요청할 때, Token을 확인하고 유효할 경우 서비스를 제공하는 단계를 진행해 보겠습니다. 전체적인 그림은 이전에도 보셨지만 아래와 같고, /apis로 서비스 요청 + Token부터 그 이하에 대한 부분의 적용이 되겠습니다.

 

현재 사용자의 Local Storage에는 Back-End로부터 전달받은 Token이 저장되어 있는 상태입니다. 이제 Front-End에서는 API를 요청할때마다 저장한 Token을 httpHeader에 담아서 보내게 됩니다. Back-End는 당연히 httpHeader에서 Token을 분석해서 유효한지 검증을 한 후 해당 API의 결과를 전달하게 됩니다.

 

따라서 평소의 MVC life cycle에 따른 Request - Controller - Service - Mapper로는 구현이 불가능 합니다. 

이때 필요한 것이 바로 Interceptor입니다. Interceptor는 Request이후 Controller로 들어가기 전에 해당 Request를 먼저 가져와서 작업을 처리해 주는 모듈입니다. 따라서 Interceptor에서는 httpRequest에서 header에 Token의 유효성을 판단하고, 다시 Controller에게 전달해 주는 역할을 합니다.

 

 

1. JWT validation


우선 Interceptor를 만들기 전에 Front-End로 부터 전달받은 Token을 검증하는 부분을 먼저 작성해 보겠습니다. Back-End가 발행한 Token을 검증하기 위해서는 아래의 단계로 작업이 진행 됩니다. 

 

  1. JwtParserBuilder instance를 만들어 Token을 분해할 준비를 함
  2. JWS 서명을 증명할 SecretKey를 지정
  3. JwtParser를 얻기위해서 build method 호출
  4. decode된 JWS를 얻기위해서 Token전체를 parameter로하는 parseClaimJsw method 호출

 

실제 코드는 아래와 같이 생기게 됩니다. 저는 해당 로직은 JwtService에 작성하였습니다. 

 

[JwtService]

public boolean validate(String token) {

    Jws<Claims> jws;
    SecretKey secretKey = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(encodeKey));

    try {
        JwtParserBuilder jpb = Jwts.parserBuilder();
        jpb.setSigningKey(secretKey);
        jws = jpb.build().parseClaimsJws(token);

        System.out.println(jws);
        System.out.println(jws.getBody().getSubject());

        return true;
    }catch(JwtException e) {
        return false;
    }

}

valitate( )의 결과로... 유효한 Token일 경우 decode된 내용을 출력해주고, 유효하지 않은 Token일 경우 응답으로 false를 return해 줍니다. 하지만 우선 당장은 확인할 수가 없겠네요...^ ^;; 왜냐하면 해당 method는 Interceptor내에서 Header를 통해서 전달받은 Token을 가지고 요청을 하기 때문입니다. 그렇다면, 이제 Interceptor를 추가해 보겠습니다. 

 

 

2. JWT Interceptor


Interceptor를 동작시키기 위해서는 2가지 단계가 필요합니다. 우선, 실질적인 Interceptor본체가 있어야 하고... 두번째로 configuration에서 어떤 요청에 대해서 어떤 Interceptor를 적용할지 설정을 추가해 주어야 합니다. 

우선 Interceptor본체의 경우는 몸이 반응하듯이 HandlerInterceptor interface를 implements해주고 preHandle을 override하여 구현해 줍니다. return type이 boolean이기 때문에 결과에 true 혹은 false로 작성 해 주면 됩니다. override된 preHandle에 작성되는 코드는 3가지로 구성됩니다. 

 

  • 첫째, request header에서 Token을 추출
  • 둘째, 해당 Token이 있을경우 JwtService의 validate method를 호출해서 결과 받음
  • 셋째, 결과에 따라서 + Token이 없을경우 등 각 조건을 구분해서 boolean값을 return 함

 

[JwtInterceptor]

@Component
public class JwtInterceptor implements HandlerInterceptor {
	
	@Autowired
	JwtService jwtService;
	
	@Override
	public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler)
			throws Exception {
		
		System.out.println("####### Interceptor preHandle Start!!!");
		
		String atJwtToken = request.getHeader("at-jwt-token");
		
		if(atJwtToken != null && atJwtToken.length() > 0) {
			if(jwtService.validate(atJwtToken)) return true;
			else throw new IllegalArgumentException("Token Error!!!");
		}else {
			throw new IllegalArgumentException("No Token!!!");
		}
	}

}

custom header에 정의된 key값으로 request.getHeader를 통해서 value를 가져오고, 그 값을 바로 jwtService.validate에 요청하여 boolean결과를 받아옵니다. 아참!! 당연히 해당 class를 @component로 Bean에 등록해 주어야 합니다. 

 

저는 로직은 Token이 null이거나 길이가 0보다 클경우는 무조건 해당 service를 타도록 하였고, 그를 제외할 경우는 에러를 발생시켰습니다. 물론 Token이 이상하거나, 기간이 만료된 경우는 service에서 false가 return될 경우 에러를 발생시켰습니다.

 

다음으로 Interceptor를 동작시키기위한 configuration을 하겠습니다. 해당 config를 위해서는 webMvcConfigurer interface를 implements한 config파일을 찾아가서 addInterceptors를 override해서 구현하면 됩니다. 

저는 해당 config를 WebConfiguration에 작성하였습니다. 

 

[WebConfiguration]

@Override
public void addInterceptors(InterceptorRegistry registry) {
    System.out.println("####### Register Interceptor: JwtInterceptor!!!");
    registry.addInterceptor(jwtInterceptor).addPathPatterns("/qss/**");
}

 



 

3. 동작확인


빠른 확인을 위해서, postman을 통해서 우선 진행해 보겠습니다. 일단 spring boot내 application을 기동하면 다음과 같이 JwtInterceptor가 정상적으로 등록이 됩니다.

INFO 20700 --- [           main] o.s.s.concurrent.ThreadPoolTaskExecutor  : Initializing ExecutorService 'applicationTaskExecutor'
####### Register Interceptor: JwtInterceptor!!!
INFO 20700 --- [           main] o.s.b.a.w.s.WelcomePageHandlerMapping    : Adding welcome page: class path resource [static/index.html]
INFO 20700 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8888 (http) with context path ''
INFO 20700 --- [           main] c.a.atproject.AtprojectApplication       : Started AtprojectApplication in 3.108 seconds (JVM running for 3.67)

 

1. Front-End를 통해서 Google Token 검증 진행 후 Back-End에서 발행한 Token을 Session Storage에 저장한 값 확인

2. 해당 key와 value를 복사하여, postman을 통한 REST API요청

   (적용된 path는 /qss 이기 때문에 관련 API로 요청)

3. Back-End Log확인

####### Interceptor preHandle Start!!!
header={typ=JWT, alg=HS512},body={iss=AyoteraLab, sub=AT, exp=1618885168, iat=1618884868}

preHandle을 정확히 실행시켰으며, Token도 유효한 값으로 정상적으로 decode되었습니다. 

 

 

4. Error 상황 확인


발생가능한 Error상황은 3가지 있습니다. 각 상황별로 정상동작을 확인해 보겠습니다. 

 

[Token 값 이상]

 

postman으로 요청 시, Token의 일부를 조작하여 보내보겠습니다. 방법은 간단할테니 캡처는 생략하겠습니다. 그 결과, 아래와 같이 Error를 발생시키게 됩니다. 

####### Interceptor preHandle Start!!!
ERROR 20700 --- [nio-8888-exec-7] o.a.c.c.C.[.[.[/].[dispatcherServlet]    
	: Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception 
     [Request processing failed; nested exception is java.lang.IllegalArgumentException: Token Error!!!] 
      with root cause

[Token 값 null]

 

Token에 아무런 값이 들어오지 않을 때, 역시 아래의 Error를 발생시키게 됩니다. 

####### Interceptor preHandle Start!!!
ERROR 20700 --- [nio-8888-exec-9] o.a.c.c.C.[.[.[/].[dispatcherServlet]    
	: Servlet.service() for servlet 
	[dispatcherServlet] in context with path [] threw exception 
    [Request processing failed; nested exception is java.lang.IllegalArgumentException: No Token!!!] 
    with root cause

마지막으로 Token의 유효기간이 만료가 될 경우입니다. 제가 설정한 Token은 5분의 유효기간을 가집니다. 따라서 5분후에 동일한 Token으로 요청을 하게되면, 당연히 Token Error가 발생하게 됩니다.

 

[Token 기간만료]

####### Interceptor preHandle Start!!!
ERROR 20700 --- [nio-8888-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet]    
	: Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception 
    [Request processing failed; nested exception is java.lang.IllegalArgumentException: Token Error!!!] 
    with root cause

이렇게 정상적으로 구현은 되었습니다. 이제 추가적으로 구현할 부분은... Token 기간의 갱신... Token내 claim 정보추가... Interceptor에서 Token의 특정값에 대한 request 추가... 뭐 이정도가 되지 않을까 싶습니다.

 

- Ayotera Lab -

댓글