이어서
요구사항에 따라 스프링 시큐리티 OAuth2 처리 이후 IOS에게 JWT를 발급해야한다.
시큐리티에서 제공하는 OAuth2LoginAuthenticationFilter 는 AbstractAuthenticationProcessingFilter라는 가상클래스를 상속받고 있는데 이곳에서 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 를 발급한다. 파라미터 중 하나인 Authentication는 OAuth2AuthenticatedPrincipal로 getAttributes() 를 통해 실제 유저의 정보를 가져올 수 있다.
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 를 넣어 주었다.

결과적으로 정상적으로 작동한다.
'스프링' 카테고리의 다른 글
| [Spring security] OAuth2Login - Rest API로 커스텀 (1) (0) | 2024.03.09 |
|---|---|
| [Spring] elasticsearch NativeQuery (multi_match) (0) | 2023.11.11 |