OAuth 2.0 + JWT 적용하기

Nicky·2024년 5월 3일
0
post-thumbnail

지금까지 총 4개의 소셜 로그인을 통해 사용자 정보를 불러왔는데 이번 포스팅은 불러온 사용자 정보로 JWT 토큰을 발급하고 인가를 위한 필터를 구현해보도록 하겠다.

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'

JwtProperties

@Getter
@RequiredArgsConstructor
public enum JwtProperties {

    HEADER_STRING("Authorization"),
    TOKEN_PREFIX("Bearer ");

    private final String value;

}

TokenProvider

@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);

OAuth2LoginSuccessHandler

먼저 인증 객체를 통해 토큰을 발급하고,
설정한 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)

JwtAuthorizationFilter

요청 헤더의 토큰을 통해 인증 객체를 만들고 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를 요청해보며 확인해보자.

	// 인가 필터 테스트 API
    @GetMapping("/api/test")
    @ResponseBody
    public TestResponse testAuthorization(@AuthenticationPrincipal CustomUserDetails principal) {
        return new TestResponse(principal.getUsername());
    }

인가 성공

인가 실패

profile
코딩 연구소

0개의 댓글

관련 채용 정보