티스토리 뷰

반응형

이번 포스팅에서는 JWT 토큰 인증 방식을 Spring Boot를 사용해 구현한 내용을 다루려고 한다. 따라서 JWT에 대한 포스팅과 Spring Security를 사용해 로그인 시스템을 구현한 포스팅을 보고 오는 것을 추천한다.

 

https://ch4njun.tistory.com/231

 

[Concept] JWT(Json Web Token)?

JWT JWT는 Json Web Token의 약자로 토큰에서 사용되는 컨텐츠를 Json으로 구성해 토큰화하는 시스템을 말한다. 즉, Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Web Token 이라고 설명할 수 있다.

ch4njun.tistory.com

https://ch4njun.tistory.com/228

 

[Spring Boot] Spring Security

Vue.js와 Spring Boot를 사용 해 진행중인 나의 프로젝트에서 로그인 시스템을 만들려던 중 Spring Boot에서 인증/인가를 구현하기 위한 프레임워크인 Spring Security에 대해서 알게되었다. 하지만 단순히

ch4njun.tistory.com

 

들어가기에 앞서 기본적인 프로세스에 대해서 소개한다.

  1. Client가 ID/Password를 사용해 로그인에 시도한다. (HTTP Request)
  2. 서버는 ID/Password를 받아 Database에 저장된 정보를 통해 인증을 진행한다.
    이 과정을 구현하기 위해 Spring Security를 사용할 수 있고, 이 내용이 이전 포스팅이다.
  3. 인증에 실패했다면 그에 따른 HTTP Response를 Client에게 전송한다. (HTTP Response)
  4. 인증에 성공했다면 JWT 토큰을 발행해 HTTP Response에 포함시켜 Client에게 전송한다. (HTTP Response)
  5. Client는 발급받은 JWT 토큰을 안전한 곳에 저장한다.
    예를 들면, SPA의 상태관리 모듈(Redux, Vuex ..)에 저장할 수 있다.
  6. Client가 HTTP Request를 보낼 때 발급받은 JWT 토큰을 포함해 보낸다. (HTTP Request)
  7. 서버는 HTTP Request를 Intercept해서 포함된 JWT 토큰을 검증한다.
    이 과정을 해당 포스팅에서 Spring Security를 사용해 구현할 예정이다.
  8. 유효한 JWT 토큰이라면 요청한 서비스를 제공한다.
  9. 유효하지 않은 JWT 토큰이라면 그에 맞는 HTTP Response를 Client에게 전송한다.

구현해야 할 것들


앞선 포스팅을 읽었다면 알겠지만 구현해야 할 것들은 다음과 같다. 

 

JWT 인증에 대해서 처리할 Filter, 인증 성공/실패시 호출할 Handler, 실질적인 인증 관련 코드가 들어갈 Provider, Provider 호출시 함께 전달할 Token(Authentication 객체)을 기본적으로 구현해야 한다.

 

추가로 JWT 토큰을 발급하기 위한 Factory와 전달받은 JWT 토큰을 디코딩하고 검증하기 위한 Decoder 를 구현해야 한다. 그러면 차례대로 구현하며 자세한 부분을 설명하도록 한다.

구현


그럼 위에서 소개한 요소들을 실제 구현하며 자세한 내용을 설명하도록 한다.

JwtAuthenticationFilter

AbstractAuthenticationProcessingFilter 클래스를 상속받아 Filter Chain에 포함시킬 Filter를 만든다. attemptAuthentication, successfulAuthentication, unsuccessfulAuthentication 세 개의 메서드를 기본적으로 오버라이딩 한다.

 

HTTP Request가 해당 Filter에 도착하면 attemptAuthentication 메서드가 호출되고 JWT 토큰을 Authentication 객체(token)에 저장한 후 Provider의 authenticate(token) 메서드를 호출한다.

 

"Authorization: Bearer { Token }" 와 같이 넘어오기 때문에 파싱한 후 Authentication 객체 token을 생성한다. 이후 이 객체를 Provider의 authenticate() 메서드에 전달함으로써 인증코드를 수행한다.

 

여기서 successfulAuthentication 메서드가 로그인 시스템을 구현할때와 조금 다른데 이 메서드가 수행하는 역할은 SecurityContextHolder에 Authentication 객체를 저장해 해당 스레드에 대해서 인증된 상태라는 것을 명시한다. 추후 Controller에서 AOP와 같은 구현을 위해서 Authentication 객체를 보관하는 것이다.

 

비슷한 이유에서 unsuccessfulAuthentication 메서드에서는 clearContext() 를 호출해 해당 스레드에 대한 Authentication 객체를 초기화해 인증되지 않은 스레드임을 명시해준다. 

JwtPreProcessingToken

여기서 사용되는 Authentication 객체는 UsernamePasswordAuthenticationToken 클래스를 상속받은 클래스로 생성되며, principal 속성에 토큰에 대한 정보를 저장하도록 설계했다.

( credentials엔 임의의 값을 추가해 진행했다. - 사실 더 적합한 Authentication 객체를 만드는게 더 좋아보이긴 한다.)

 

JwtAuthenticationProvider

Authentication Provider는 Filter에 의해서 호출되어서 Authentication 객체에 대한 인증을 진행해주는 역할을 한다. authenticate() 메서드를 통해서 인증을 진행하게 된다.

 

전달받은 JWT 토큰에 대한 검증과정은 JwtDecoder 클래스에서 진행한다.

 

검증과정은 JWT 토큰이 유효한 토큰(Secret Key, Expired Time ...)인지 검증하고 유효한 토큰이라면 해당 토큰의 Claim 값들을 뽑아와 AccountContext 객체를 생성해 반환하게 된다. 이렇게 받은 AccountContext 객체를 사용해 인증된 Authentication 객체를 만들어 반환하게 된다.

JwtFactory

JWT 토큰을 발행해주는 코드는 사실 여기가 아니라 이전에 소개했던 로그인 시스템에 추가되어야 한다. 정확히 말하면 로그인이 성공했을 때 호출되는 successfulAuthentication 메서드에서 사용되어야 하는 코드다. 하지만 JwtDecoder에 대한 코드를 보기전 참고하면 좋을 것 같아서 여기서 소개한다.

 

크게 복잡한 내용은 없고 JWT 라이브러리를 사용해 토큰을 생성하고 다양한 정보(Claim)을 추가하는 모습이다. 마지막으로 알고리즘을 선택해 Signing을 진행하면 문자열 형태의 토큰이 발행된다. 이 토큰을 Client에게 전달하는 HTTP Response에 포함시켜 응답하게 된다.

JwtDecoder

isValidToken 메서드를 통해서 해당 토큰에 대한 검증을 진행한다. 여기서 사용되는 키가 Secret Key인데 지금은 하드코딩 되어 있지만 실제 시스템에서는 Configure Service/Property File 등의 방법으로 안전하게 관리되어야 한다.

 

유효한 토큰으로 검증되었다면 AccountContext를 생성해 Authentication 객체를 만들어 반환한다. 이렇게 반환된 Authentication 객체는 successfulAuthentication 메서드로 돌아간 후 SecurityContextHolder에 저장된다.

Security Config에 관련내용 추가


protected JwtAuthenticationFilter jwtFilter() throws Exception {
	FilterSkipMatcher matcher = new FilterSkipMatcher(Arrays.asList("/formLogin"), "/api/**");
    JwtAuthenticationFilter filter = new JwtAuthenticationFilter(matcher, jwtAuthenticationFailureHandler, extractor);
    filter.setAuthenticationManager(super.authenticationManager());
    return filter;
}

이 내용에서 중요한 점은 matcher를 통해 해당 Filter를 호출할 HTTP Request를 명시해준다는 것이다. 왜냐하면 로그인 페이지에 대한 요청은 토큰검증을 할 필요가 없기 때문에 이와 같이 필터링해줘야 한다.

 

로그인을 통해 토큰을 발행받아야 하는데 토큰을 검증하는건 당연히 말이안된다.

 

@Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
	auth.authenticationProvider(this.formLoginAuthenticationProvider);
	auth.authenticationProvider(this.jwtAuthenticationProvider);
}

이 내용을 통해 Authentication Provider들이 모여있는 목록에 생성한 JwtAuthenticationProvider를 추가한다. 이렇게 함으로써 해당 Authentication 객체가 인증이 필요할 때 해당 Provider를 사용할 수 있다.

 

Provider를 생성할때 supports 메서드를 통해 인증할 Authentication 객체를 지정할 수 있다. 하지만 Authentication Provider가 모여있는 목록에 생성한 Provider를 추가해주지 않게되면 지정한 Authentication 객체가 들어와도 해당 Provider를 사용할 수 없게된다. 따라서 위 코드를 통해서 해당 Provider를 등록해줘야 한다.
@Override
protected void configure(HttpSecurity http) throws Exception {
	http
		.sessionManagement()
		.sessionCreationPolicy(SessionCreationPolicy.STATELESS);
	http
		.csrf().disable();
	http
		.headers().frameOptions().disable();
	http
		.addFilterBefore(formLoginFilter(), UsernamePasswordAuthenticationFilter.class)
		.addFilterBefore(jwtFilter(), UsernamePasswordAuthenticationFilter.class);
}

이 내용을 통해서 해당 Filter를 Filter Chain에 추가해주게 된다.

반응형
반응형
공지사항
최근에 올라온 글
최근에 달린 댓글
Total
Today
Yesterday
링크
TAG
more
«   2024/05   »
1 2 3 4
5 6 7 8 9 10 11
12 13 14 15 16 17 18
19 20 21 22 23 24 25
26 27 28 29 30 31
글 보관함