본문 바로가기

스프링

[Spring security] OAuth2Login - JWT 발급 (2)

이어서

요구사항에 따라 스프링 시큐리티 OAuth2 처리 이후 IOS에게 JWT를 발급해야한다.
시큐리티에서 제공하는 OAuth2LoginAuthenticationFilterAbstractAuthenticationProcessingFilter라는 가상클래스를 상속받고 있는데 이곳에서 AuthenticationSuccessHandler 를 호출하여 성공 로직을 실행한다. 따라서 이를 커스텀하여 JWT를 발급했다.

gradle

dependencies {
    /**
     * jwt
     */
    implementation 'io.jsonwebtoken:jjwt-api:0.12.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-impl:0.12.5'
    runtimeOnly 'io.jsonwebtoken:jjwt-jackson:0.12.5'
}

jwt를 생성하기 위해서 jjwt 라이브러리를 추가했다.

CustomAuthenticationSuccessHandler

@Slf4j
@Component
@RequiredArgsConstructor
public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    private final OAuth2LoginRegistrationIdResolver oAuth2LoginRegistrationIdResolver;
    private final UserRepository userRepository;
    private final UserJwtTokenService userJwtTokenService;
    @Override
    @SneakyThrows
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication){
        String registrationId = oAuth2LoginRegistrationIdResolver.getRegistrationId(request);
        OAuth2AuthenticatedPrincipal responsePrincipal = (OAuth2AuthenticatedPrincipal)authentication.getPrincipal();
        OAuth2AuthorizedClientUserInfoDto oAuth2AuthorizedClientUserInfo = new OAuth2AuthorizedClientUserInfoDto(
                registrationId,
                responsePrincipal.getAttributes()
        );
        String id = null;
        switch (registrationId){
            case "naver"->{
                NaverOAuth2Dto naverUser = OAuth2AuthorizedClientConverterFactory.getConverter(NaverOAuth2Dto.class).convert(oAuth2AuthorizedClientUserInfo);
                id = Objects.requireNonNull(naverUser).getAttribute("id");
            }
        }
        responseJWT(response, id, registrationId);
    }

    private void responseJWT(HttpServletResponse response, String id, String registrationId) throws IOException {
        Check.notNull(id, StatusCode.Internal_Server_Error);
        Optional<User> findUser = userRepository.findByRegistrationIdAndOauth2IdWithOAuth2UserAndAuthorities(registrationId, id);
        if (findUser.isPresent()){
            response.setContentType(ContentType.APPLICATION_JSON.getType());
            response.setCharacterEncoding("UTF-8");
            response.getWriter().write(new ObjectMapper().writeValueAsString(userJwtTokenService.createJWT(findUser.get())));
        }else{
            log.debug("유저 정보를 찾을 수 없습니다.");
            throw new CustomException(StatusCode.Internal_Server_Error);
        }
    }
}

 

해당 코드에서는 OAuth2AuthorizedClientRepository 구현체에서 저장한 유저 정보를 가져와 데이터베이스상의 user를 가져와 JWT 를 발급한다. 파라미터 중 하나인 AuthenticationOAuth2AuthenticatedPrincipalgetAttributes() 를 통해 실제 유저의 정보를 가져올 수 있다.

 

oAuth2LoginRegistrationIdResolver는 스프링 시큐리티에서 제공하는 OAuth2AuthorizationRequestResolver에서 사용하는 기능을 일부 구현한 것으로 단순히 사용자 요청 uri로부터 registrationId 를 추출한다. registrationId 는 naver, kakao, apple 과 같은 인증 서버를 구별하는 용도이다. 스프링의 OAuth2Login은 /login/oauth2/code/{registrationId} 로 요청을 받으므로 동일하게 구현했다.

OAuth2AuthorizedClientConverterFactory 는 naver, kakao 별로 응답 형태가 다르기 때문에 그에 맞게 변환해주는 Conveter 구현체이다.

 

createJwt 의 구현은 다음과 같다.

 @Override
    public OAuth2ResponseDto createJWT(User user){
        Map<String, Object> info = new HashMap<>();
        List<String> roleList = user.getUserAuthorities().stream().map(auth -> auth.getRole().toString()).toList();
        info.put("role", roleList);
        String accessToken = jwtTokenProvider.createAccessToken(info, user.getId().toString());
        String refreshToken = jwtTokenProvider.createRefreshToken(new HashMap<>(), user.getId().toString());
        return  OAuth2ResponseDto.builder()
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .accessTokenExpiresIn(jwtTokenProvider.getAccessTokenValidityInMil())
                .refreshTokenExpiresIn(jwtTokenProvider.getRefreshTokenValidityInMil()).build();
    }

JwtTokenProvider - createAccessToken() & createRefreshToken()

@Override
    public String createAccessToken(Map<String, Object> info, String userId) {
        KeyAndSerialNumberDto dto = jwtSecretKey.getCurrentAccessTokenKey();
        return JwtUtil.getToken(info, userId, dto.getSerialNumber(), dto.getKey(), accessTokenValidityInMil, issuer);
    }

    @Override
    public String createRefreshToken(Map<String, Object> info, String userId) {
        KeyAndSerialNumberDto dto = jwtSecretKey.getCurrentRefreshTokenKey();
        return JwtUtil.getToken(info, userId, dto.getSerialNumber(), dto.getKey(), refreshTokenValidityInMil, issuer);
    }

JwtUtil

public class JwtUtil {
    public static String getToken(Map<String, Object> info, String subject, String keySerialNumberKey, String signatureKey, long validityTime, String issuer) {
        Check.notNull(signatureKey, StatusCode.Internal_Server_Error);
        Check.notNull(validityTime, StatusCode.Internal_Server_Error);
        Claims claims = Jwts.claims().subject(subject).add(info).build();

        Date now = new Date();
        Date validity = new Date(now.getTime() + validityTime);

        Key accesstokenKey = getKey(signatureKey);

        return Jwts.builder()
                .claims(claims)
                .issuedAt(now)
                .issuer(issuer)
                .id(UUID.randomUUID().toString())
                .header().keyId(keySerialNumberKey).add("type", "JWT").and()
                .expiration(validity)
                .signWith(accesstokenKey)
                .compact();
    }

    private static Key getKey(String key) {
        byte[] keyBytes = Decoders.BASE64.decode(key);
        return Keys.hmacShaKeyFor(keyBytes);
    }
}

 

키가 jwtSecretKey.getCurrentAccessTokenKey() 를 통해 동적으로 변하도록 구현했으므로 key id에 시리얼 넘버로 UUID 를 넣어 주었다.

결과적으로 정상적으로 작동한다.