본문 바로가기
Spring/Spring Security

AuthToken 사용하기

코동이 2021. 10. 26.

이전에는 Session으로 구성했지만, 이제는 JWT 토큰을 사용하여 시큐리티를 구성한다. 핵심은 Bearer Token을 Client에 주도록 설정하는 것이며, 로그인할 때 Token을 검사하는 것이다.

 

@Override
protected void configure(HttpSecurity http) throws Exception {
	JWTLoginFilter loginFilter = new JWTLoginFilter(authenticationManager(), spUserService);
	JWTCheckFilter checkFilter = new JWTCheckFilter(authenticationManager(), spUserService);
	http
			.csrf().disable()
			.sessionManagement(session ->
					session.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
			)
			.addFilterAt(loginFilter, UsernamePasswordAuthenticationFilter.class)
			.addFilterAt(checkFilter, BasicAuthenticationFilter.class);
}

 

 

JWTLoginFilter는 로그인을 검증하고 Bearer Token을 Client에 주는 역할을 한다.

JWTCheckFilter는 매번 요청시마다 Bearer Token을 검사해서 요청의 유효성을 판단하는 역할을 한다.

 

* JWTLoginFilter

 

public class JWTLoginFilter extends UsernamePasswordAuthenticationFilter {
private final ObjectMapper objectMapper = new ObjectMapper();
private final SpUserService spUserService;

public JWTLoginFilter(AuthenticationManager authenticationManager,
					  SpUserService spUserService) {
	super(authenticationManager);
	this.spUserService = spUserService;
	setFilterProcessesUrl("/login");
}

 

 로그인 유효성 검사를 커스터마이징하기 위해 UsernamePasswordAuthenticationFilter를 상속한다. 생성자에서, 로그인을 하는 경로를 알려주기 위해 setFilterProcessesUrl("/login");도 추가한다.

 

@SneakyThrows
@Override
public Authentication attemptAuthentication(
		HttpServletRequest request,
		HttpServletResponse response) throws AuthenticationException {
	UserLoginForm userLogin = objectMapper.readValue(request.getInputStream(), UserLoginForm.class);
	if(userLogin.getRefreshToken() == null) {
		UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(
				userLogin.getUsername(), userLogin.getPassword(), null
		);
		return getAuthenticationManager().authenticate(token);
	}

	VerifyResult verify = JWTUtil.verify(userLogin.getRefreshToken());
	if(verify.isSuccess()){
		SpUser user = (SpUser) spUserService.loadUserByUsername(verify.getUsername());
		return new UsernamePasswordAuthenticationToken(
				user, user.getAuthorities()
		);
	} else {
		throw new TokenExpiredException("refresh token expired");
	}
}

 

AbstractAuthenticationProcessingFilter 클래스에서 attemptAuthentication에 대한 설명은 다음과 같다.

 

Performs actual authentication. The implementation should do one of the following:

1. Return a populated authentication token for the authenticated user, indicating successful authentication

2. Return null, indicating that the authentication process is still in progress. Before returning, the implementation should perform any additional work required to complete the process.

3. Throw an AuthenticationException if the authentication process fails

Params:
request – from which to extract parameters and perform the authentication
response – the response, which may be needed if the implementation has to do a redirect as part of a multi-stage authentication process (such as OpenID).
Returns: the authenticated user token, or null if authentication is incomplete.

Throws: AuthenticationException – if authentication fails.

 

핵심은, 인증된 사용자 정보가 담긴 인증토큰을 구현하고 리턴하는 것이다. UsernamePasswordAuthenticationToken 방식을 사용하여, 로그인을 시도한 정보를 검사한다. 만약에 refreshToken이 있다면, 마찬가지로 로그인 검사를 하고 유효하다면, 해당 사용자 인증토큰을 구현하여 리턴한다.

 

 

 

@Override
protected void successfulAuthentication(
		HttpServletRequest request,
		HttpServletResponse response,
		FilterChain chain,
		Authentication authResult) throws IOException, ServletException {
	SpUser user = (SpUser) authResult.getPrincipal();

	response.setHeader("auth_token", JWTUtil.makeAuthToken(user));
	response.setHeader("refresh_token", JWTUtil.makeRefreshToken(user));
	response.setHeader(HttpHeaders.CONTENT_TYPE, MediaType.APPLICATION_JSON_VALUE);
	response.getOutputStream().write(objectMapper.writeValueAsBytes(user));
}

 

AbstractAuthenticationProcessingFilter 클래스에서 successfulAuthentication에 대한 설명은 다음과 같다.

 

Default behaviour for successful authentication.

1. Sets the successful Authentication object on the SecurityContextHolder
2. Informs the configured RememberMeServices of the successful login
3. Fires an InteractiveAuthenticationSuccessEvent via the configured ApplicationEventPublisher
4. Delegates additional behaviour to the AuthenticationSuccessHandler.

Subclasses can override this method to continue the FilterChain after successful authentication.

Params:
request –
response –
chain –
authResult –
the object returned from the attemptAuthentication method.
Throws:
IOException –
ServletException

 

핵심은, SecurityContextHolder에 인증정보가 저장된다는 것이다. 또한 하위 클래스에서 재정의를 하면 성공적인 인증 이후에 FilterChain에 행위를 추가할 수 있다. 우리는 매개변수에서 로그인 된 정보인 authResult를 가지고 auth_token과 refresh_token을 응답 Header에 추가해준다. 따라서 로그인이 되면, 해당 필터 덕분에 토큰 2개를 발급받는다.

 

 

* JWTCheckFilter

 

public class JWTCheckFilter extends BasicAuthenticationFilter {
private final SpUserService spUserService;

public JWTCheckFilter(AuthenticationManager authenticationManager,
					  SpUserService spUserService) {
	super(authenticationManager);
	this.spUserService = spUserService;
}

 

매번 요청을 검사하기 위해서 BasicAuthenticationFilter를 상속한다.

 

@Override
protected void doFilterInternal(HttpServletRequest request,
								HttpServletResponse response,
								FilterChain chain) throws IOException, ServletException {
	String bearer = request.getHeader(HttpHeaders.AUTHORIZATION);
	if(bearer == null || !bearer.startsWith("Bearer ")){
		chain.doFilter(request, response);
		return;
	}
	String token = bearer.substring("Bearer ".length());
	VerifyResult result = JWTUtil.verify(token);
	if(result.isSuccess()){
		SpUser user = (SpUser) spUserService.loadUserByUsername(result.getUsername());
		UsernamePasswordAuthenticationToken userToken = new UsernamePasswordAuthenticationToken(
				user.getUsername(), null, user.getAuthorities()
		);
		SecurityContextHolder.getContext().setAuthentication(userToken);
		chain.doFilter(request, response);
	}else{
		throw new TokenExpiredException("Token is not valid");
	}
}

 

기본적으로 BasicAuthenticationFilter는 request의 header를 검사하여 토큰들을 가지고 로그인 인증정보를 구성한다. 이 부분을 Bearer Token에 맞게 변경한다.

 

"Bearer " 가 AUTHORIZATION에 들어있다면, 토큰을 검사해서 성공 시, SecurityContextHolder에 해당 계정 정보를 넣고, 실패하면 토큰이 만료되었다는 예외를 던진다.

 

이 검사는 Bearer Token이 들어있는 모든 요청에 대해서 검사를 한다.

반응형

'Spring > Spring Security' 카테고리의 다른 글

Spring Security 구조 메커니즘  (0) 2024.05.17
Security AccessDecisionManager 구현 학습하기  (0) 2024.05.15
JWT 토큰  (0) 2021.10.26
임시권한 부여하기  (0) 2021.10.23
@Secured 기반 권한체크  (0) 2021.10.20