지금까지 총 4개의 소셜 로그인을 통해 사용자 정보를 불러왔는데 이번 포스팅은 불러온 사용자 정보로 JWT 토큰을 발급
하고 인가를 위한 필터
를 구현해보도록 하겠다.
// JWT
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
@Getter
@RequiredArgsConstructor
public enum JwtProperties {
HEADER_STRING("Authorization"),
TOKEN_PREFIX("Bearer ");
private final String value;
}
@Component
public class TokenProvider {
private final Key jwtSecretKey;
private final Long accessTokenExpiration;
public TokenProvider(@Value("${jwt.secret}") String secretKey,
@Value("${jwt.access.expiration}") String accessTokenExpiration) {
this.jwtSecretKey = Keys.hmacShaKeyFor(secretKey.getBytes(StandardCharsets.UTF_8));
this.accessTokenExpiration = Long.valueOf(accessTokenExpiration);
}
// 토큰 생성
public String generateToken(CustomUserDetails userDetails) {
long now = (new Date()).getTime();
// access 토큰 생성
return Jwts.builder()
.setSubject(userDetails.getUsername())
.setExpiration(new Date(now + accessTokenExpiration))
.signWith(jwtSecretKey, SignatureAlgorithm.HS256)
.compact();
}
// 토큰 복호화
public Claims parseClaims(String token) {
return Jwts.parser()
.setSigningKey(jwtSecretKey)
.parseClaimsJws(token)
.getBody();
}
// 토큰 검증
public void validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(jwtSecretKey).build().parseClaimsJws(token);
} catch (ExpiredJwtException | MalformedJwtException | UnsupportedJwtException | SignatureException e) {
throw new JwtException("유효하지 않은 토큰입니다.");
}
}
}
OAuth2 로그인 설정에 핸들러 등록을 통해 인증 이후 리다이렉트 경로를 설정할 수 있다. 이제 OAuth 인증 성공 핸들러를 커스텀해보자.
// Oauth2 로그인
.oauth2Login()
.loginPage("/login")
.successHandler(oAuth2LoginSuccessHandler)
.userInfoEndpoint()
.userService(customOauth2UserService);
먼저 인증 객체를 통해 토큰을 발급하고,
설정한 URI의 쿼리 파라미터에 토큰을 담는 형식으로 리다이렉트하게 된다.
(리다이렉트는 헤더에 토큰을 담는게 불가하기 때문에 쿼리 파라미터나 쿠키를 통해 보내야 한다.
)
@Component
@RequiredArgsConstructor
public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final TokenProvider tokenProvider;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
String accessToken = tokenProvider.generateToken(userDetails);
String uri = createURI(accessToken).toString();
getRedirectStrategy().sendRedirect(request, response, uri);
}
// Redirect URI 생성. JWT를 쿼리 파라미터로 담아 전달한다.
private URI createURI(String accessToken) {
MultiValueMap<String, String> queryParams = new LinkedMultiValueMap<>();
queryParams.add("access_token", accessToken);
return UriComponentsBuilder
.newInstance()
.scheme("http")
.host("localhost")
.port(8080)
.path("/")
.queryParams(queryParams)
.build()
.toUri();
}
}
소셜 로그인을 하게 되면..
리다이렉트된 경로를 통해 access 토큰을 확인할 수 있다.
이제 해당 토큰을 통해 권한 부여를 할 수 있도록 인가 필터를 구현하자.
// 인가 필터 추가
.addFilterBefore(jwtAuthorizationFilter, UsernamePasswordAuthenticationFilter.class)
요청 헤더의 토큰을 통해 인증 객체를 만들고 SecurityContext
에 저장하게된다. 만약 토큰이 존재하지 않거나 유효하지 않다면 401 에러 응답을 받게 된다.
@Component
@RequiredArgsConstructor
public class JwtAuthorizationFilter extends OncePerRequestFilter {
private final TokenProvider tokenProvider;
private final CustomUserDetailsService customUserDetailsService;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws IOException, ServletException {
// 헤더에 토큰 정보 확인
String header = request.getHeader(HEADER_STRING.getValue());
if (header == null || !header.startsWith(TOKEN_PREFIX.getValue())) {
chain.doFilter(request, response);
return;
}
// 헤더에서 토큰 정보 추출
String token = request.getHeader(HEADER_STRING.getValue()).replace(TOKEN_PREFIX.getValue(), "");
try {
// 토큰 검증
tokenProvider.validateToken(token);
// 인증 정보 추출
Claims claims = tokenProvider.parseClaims(token);
String userName = claims.getSubject();
CustomUserDetails userDetails = customUserDetailsService.loadUserByUsername(userName);
Authentication authentication = new UsernamePasswordAuthenticationToken(userDetails, "",
userDetails.getAuthorities());
// 사용자 인증 정보 저장
SecurityContextHolder.getContext().setAuthentication(authentication);
} catch (JwtException e) {
response.setStatus(SC_UNAUTHORIZED);
response.setCharacterEncoding("UTF-8");
response.getWriter().write(e.getMessage());
}
chain.doFilter(request, response);
}
}
이제 인가가 올바르게 수행되는지 테스트 API를 요청해보며 확인해보자.
// 인가 필터 테스트 API
@GetMapping("/api/test")
@ResponseBody
public TestResponse testAuthorization(@AuthenticationPrincipal CustomUserDetails principal) {
return new TestResponse(principal.getUsername());
}