이번에 프로젝트를 진행하면서 네모 프로젝트에서 진행했던 카카오 로그인 서비스와 시큐리티레거시 코드들을 가져오려고 했으나 여러 문제점들을 발견하고 시큐리티의 버전 상승으로 인한 코드 변경으로 인해 kakao developers 문서를 정독하며 새롭게 코드를 작성해봤습니다.
기존 코드의 문제점
먼저 카카오 로그인 서비스에 대한 문제점입니다.
public String kakaoLogin(String code) {
KakaoTokenDTO userToken = getKakaoAccessToken(code);
log.info("get kakao token");
KakaoAccountDTO kakaoAccount = null;
try {
kakaoAccount = kakaoAuthClient.getInfo(URI.create(kakaoAuthProperties.getUserApiUrl()),
userToken.getTokenType() + " " + userToken.getAccessToken()).getKakaoAccount();
log.info("get kakao account");
} catch (HttpStatusCodeException e) {
switch (e.getStatusCode().value()) {
case 400:
throw new BadRequestException("잘못된 요청입니다");
case 401:
throw new UnauthorizedException("인증되지 않은 사용자입니다");
case 403:
throw new ForbiddenException("접근이 허용되지 않습니다");
case 404:
throw new NotFoundException("해당 사용자를 찾을 수 없습니다");
default:
throw new InternalSeverErrorException("유저 정보 확인 오류입니다");
}
} catch (Exception e) {
throw new InternalSeverErrorException("유저 정보 확인 오류입니다");
}
UserAccountDTO userAccount = new UserAccountDTO(kakaoAccount.getEmail(), kakaoAccount.getProfile().getNickname());
log.info("kakao account: " + userAccount.toString());
User user = userService.getUser(userAccount);
log.info("kakao user");
String token = jwtTokenProvider.create(user);
log.info("kakao jwt token");
redisTemplate.opsForValue().set(JWT_TOKEN + user.getId(), token, JwtTokenProvider.EXP);
return token;
}
기존 코드는 kakao accessToken을 통해 user의 정보를 가져와 이를 jwt 형식으로 만들어 클라이언트에게 반환하는 형식이었습니다.
이 과정에서 user의 email과 nickname을 확인하고 이를 jwt 토큰에 담아 추후에 클라이언트에게 해당 jwt 토큰을 받고 확인할 때 user DB에서 확인 후 존재한다면 인증되었다고 판단했습니다.
이는 중대한 문제점들을 가지고 있었습니다.
첫번째는 발급된 AccessToken으로 사용자의 프로필 정보를 확인한 뒤에 회원가입을 시키는것은. 잘못된 로직입니다. 만약 사용자가 다른 앱에 가입 후 해당 AccessToken을 현재 앱에 보내도 오직 프로필 정보만 확인하기 때문에 회원가입이 될 것 입니다.
두번째는 jwt 토큰을 확인할 때 그저 user DB에 존재한다면 인증되었다고 판단하기 때문에 다른 유저의 AccessToken을 보내도 존재하기 때문에 로그인 될 것 입니다.
마지막으로 AccessToken을 통해 email과 nickname을 인증 용도로 사용한다는 것입니다. 아래는 이에 대한 kakao developers의 주의사항입니다.

이러한 문제점들의 원인은 AccessToken을 인증의 용도로 사용했다는 것입니다. Oauth Access Token의 경우 인가 즉, 카카오 리소스 서버에 접근할수 있는 토큰이고 유저의 정보에 접근할 수 있는 토큰입니다

Open ID Connect (OIDC)
이러한 문제점들을 해결하기 위해 OIDC를 사용했습니다.
OpenID Connect(OIDC)는 사용자가 안전하게 로그인하는 데 사용할 수 있는 OAuth 2.0 기반의 표준 인증 프로토콜입니다.
자세한 내용은 아래의 문서를 참고하시면 됩니다.
OIDC는 앱 키의 정보를 jwt 토큰 내에서 확인할 수 있기 때문에 검증된 토큰이라면 현재 서버에서 등록한 앱으로 발급된 토큰임을 확인할 수 있습니다. 또한 서비스에서 각 사용자를 식별할 수 있는 고유한 회원번호 제공하기 때문에 변경되지 않는 고유한 값으로 인증할 수 있고 인증 프로토콜이기 때문에 OIDC를 지원하는 곳이라면 사용할 수 있습니다.
이러한 이유로 이번 프로젝트는 jwt 토큰을 직접 발행하는 것이 아닌 카카오에서 지원하는 OIDC를 사용하고자 했습니다.
OIDC는 카카오 로그인 시 받는 code를 사용해 다음과 같이 받아올 수 있습니다.
// 20240320221829
// <http://localhost:5000/auth/kakao/callback?code=9clXwjtFoZRnCsENCEWEIkzq_CNef7stSvuK6jTYFOVdA98ItNsHvcTpLD0KPXKYAAABjlwDW6ykJA3lYdtGWQ>
{
"token_type": "bearer",
"access_token": "B3QrZLmBK0pHp9sqcGS3iKIHh0HUpmIbfwgKPXPrAAABjlwDXSpV7imzm104lw",
"id_token": ID 토큰 값,
"expires_in": 21599,
"refresh_token": "WaXouGfiFyCWAuOK0wqCwc-BkX3f7PqK9MQKPXPrAAABjlwDXSJV7imzm104lw",
"refresh_token_expires_in": 5183999
}
ID 토큰 유효성 검증
하나씩 천천히 진행해보겠습니다.
OIDC를 받았다고 해서 해당 토큰은 검증된 토큰일까요? 이를 해결하기 위해 카카오에서는 아래의 절차를 따르도록 권장합니다.

먼저 서명 검증을 위한 공개키 값을 캐싱(Caching)하여 가져와봤습니다. 현재 프로젝트는 Feign Client를 사용하고 있고 캐싱을 위해 Redis를 사용했습니다. Feign Client에 대해선 아래 글을 통해 확인하실 수 있습니다.
//KakaoOauthClient
@Cacheable(cacheNames = "KakaoOICD", cacheManager = "oidcCacheManager")
@GetMapping("${feign.client.kakao.oicd-open-key-uri}")
OIDCPublicKeysResponse getKakaoOIDCOpenKeys();
//RedisCacheConfig
@EnableCaching
@Configuration
public class RedisCacheConfig {
@Bean
public CacheManager oidcCacheManager(RedisConnectionFactory cf) {
RedisCacheConfiguration redisCacheConfiguration =
RedisCacheConfiguration.defaultCacheConfig()
.serializeKeysWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new StringRedisSerializer()))
.serializeValuesWith(
RedisSerializationContext.SerializationPair.fromSerializer(
new GenericJackson2JsonRedisSerializer()))
.entryTtl(Duration.ofDays(7L));
return RedisCacheManager.RedisCacheManagerBuilder.fromConnectionFactory(cf)
.cacheDefaults(redisCacheConfiguration)
.build();
}
}
다음과 같이 설정 후 getKakaoOIDCOpenKeys 를 실행하면 OIDCPublicKeysResponse DTO엔 다음과 같은 응답이 들어옵니다.
//getKakaoOIDCOpenKeys 실행
OIDCPublicKeysResponse oidcPublicKeysResponse = kakaoOauthClient.getKakaoOIDCOpenKeys();
//공개 키 응답
{
"keys": [
{
"kid": "3f96980381e451efad0d2ddd30e3d3",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "q8zZ0b_MNaLd6Ny8wd4cjFomilLfFIZcmhNSc1ttx_oQdJJZt5CDHB8WWwPGBUDUyY8AmfglS9Y1qA0_fxxs-ZUWdt45jSbUxghKNYgEwSutfM5sROh3srm5TiLW4YfOvKytGW1r9TQEdLe98ork8-rNRYPybRI3SKoqpci1m1QOcvUg4xEYRvbZIWku24DNMSeheytKUz6Ni4kKOVkzfGN11rUj1IrlRR-LNA9V9ZYmeoywy3k066rD5TaZHor5bM5gIzt1B4FmUuFITpXKGQZS5Hn_Ck8Bgc8kLWGAU8TzmOzLeROosqKE0eZJ4ESLMImTb2XSEZuN1wFyL0VtJw",
"e": "AQAB"
}, {
"kid": "9f252dadd5f233f93d2fa528d12fea",
"kty": "RSA",
"alg": "RS256",
"use": "sig",
"n": "qGWf6RVzV2pM8YqJ6by5exoixIlTvdXDfYj2v7E6xkoYmesAjp_1IYL7rzhpUYqIkWX0P4wOwAsg-Ud8PcMHggfwUNPOcqgSk1hAIHr63zSlG8xatQb17q9LrWny2HWkUVEU30PxxHsLcuzmfhbRx8kOrNfJEirIuqSyWF_OBHeEgBgYjydd_c8vPo7IiH-pijZn4ZouPsEg7wtdIX3-0ZcXXDbFkaDaqClfqmVCLNBhg3DKYDQOoyWXrpFKUXUFuk2FTCqWaQJ0GniO4p_ppkYIf4zhlwUYfXZEhm8cBo6H2EgukntDbTgnoha8kNunTPekxWTDhE5wGAt6YpT4Yw",
"e": "AQAB"
}
]
}
이제 받은 id token의 kid와 동일한 공개 키를 가져와 해당 공개 키의 n과 e를 통해 RSAPublicKey를 만들어서 서명 인증을 하면 됩니다.
id token에서 kid 값을 가져오는 코드는 다음과 같습니다.
public String getKidFromTokenHeader(String token) {
String KID = "kid";
String encodedHeader = getEncodedHeader(token);
String decodedHeader = getDecodedHeader(encodedHeader);
try {
JSONObject jsonObject = new JSONObject(decodedHeader);
return jsonObject.get(KID).toString();
} catch (JSONException e) {
return e.toString();
}
}
private static String getDecodedHeader(String encodedHeader) {
byte[] decodedHeaderBytes = Base64.getDecoder().decode(encodedHeader);
return new String(decodedHeaderBytes);
}
private static String getEncodedHeader(String token) {
String[] splitToken = token.split("\\\\.");
return splitToken[0];
}
RSAPublicKey를 만드는 코드는 다음과 같습니다.
private PublicKey getRSAPublicKey(String modulus, String exponent) throws NoSuchAlgorithmException, InvalidKeySpecException {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] decodeN = Base64.getUrlDecoder().decode(modulus);
byte[] decodeE = Base64.getUrlDecoder().decode(exponent);
BigInteger n = new BigInteger(1, decodeN);
BigInteger e = new BigInteger(1, decodeE);
RSAPublicKeySpec keySpec = new RSAPublicKeySpec(n, e);
return keyFactory.generatePublic(keySpec);
}
이제 해당 공개 키를 통해 서명 검증과 페이로드의 각 변수들의 값을 확인하는 일이 남았었습니다.
처음에는 이를 하나씩 확인해봐야하나 고민했고 더 좋은 방법이 없는지 다방면으로 찾았습니다.
찾던 중 JJWT 라이브러리를 통해 한번에 확인할 수 있겠다고 생각이 들었고 다음의 코드를 통해 인증된 ID 토큰을 얻을 수 있었습니다.
public Jws<Claims> getOIDCTokenJws(String token, OIDCPublicKeyDto oidcPublicKeyDto, String iss, String aud) {
try {
return Jwts.parser()
//공개키로 서명 검증
.verifyWith(getRSAPublicKey(oidcPublicKeyDto.n(), oidcPublicKeyDto.e()))
//Rest API Key 값 확인
.requireAudience(aud)
//인증 기관 값 확인
.requireIssuer(iss)
.build()
.parseSignedClaims(token);
//ID 토큰이 만료되지 않았는지 확인
} catch (ExpiredJwtException e) {
throw new Exception500(e.getMessage());
} catch (Exception e) {
log.error(e.toString());
throw new Exception500(e.getMessage());
}
}
이제 인증된 ID 토큰의 payload를 다음과 같이 DTO에 담아 사용하면 됩니다.
public OIDCDecodePayload getOIDCTokenBody(String token, OIDCPublicKeyDto oidcPublicKeyDto, String iss, String aud) {
Claims payload = getOIDCTokenJws(token, oidcPublicKeyDto, iss, aud).getPayload();
return new OIDCDecodePayload(
payload.getIssuer(),
payload.getAudience().toString(),
payload.getSubject(),
payload.get("email", String.class));
}
현재 프로젝트 같은 경우 User DB에 회원번호(sub)가 존재하고 로그인 시 해당 유저의 회원번호를 통해 DB를 조회하고 없으면 회원가입을 시켜주는 로직을 사용하고 있습니다.
public void isUserRegistered(KakaoTokenDto kakaoTokenDto) {
OIDCDecodePayload oidcDecodePayload = oauthOIDCHelper.getKakaoOIDCDecodePayload(kakaoTokenDto.idToken());
Optional<UserAccount> userAccount = userAccountRepository.findBySub(oidcDecodePayload.sub());
if (userAccount.isEmpty()) {
saveUserAccount(kakaoTokenDto);
}
}
private void saveUserAccount(KakaoTokenDto kakaoTokenDto) {
KakaoUserInfoDto kakaoInformationResponse = kakaoInfoClient.kakaoUserInfo(kakaoTokenDto.tokenType() + " " + kakaoTokenDto.accessToken());
UserAccountDto userAccountDto = UserAccountDto.of(kakaoInformationResponse.nickname(), kakaoInformationResponse.email(), kakaoInformationResponse.sub());
userAccountRepository.save(userAccountDto.toEntity());
}
먼저 위에서 진행했던 검증된 id 토큰의 payload를 받아 내부에 회원번호 즉, sub를 가져와 user DB를 조회합니다. 만약 존재하지 않는다면 DB에 해당 user의 정보를 저장합니다.
해당 user의 정보를 저장할 때 id 토큰의 payload의 값으로 충분히 저장할 수 있기 때문에 한번 더 accessToken을 통해 user의 정보를 가져오는 코드는 불필요하다고 생각하실 수 있습니다. 하지만 카카오는 다음과 같이 가입 미완료 사용자 처리를 하고 있기 때문에 OIDC: 사용자 정보 가져오기 API를 사용해 user의 정보를 DB에 저장했습니다.
ID 토큰 유효성 검증 코드 전문
@Component
public class JwtOIDCProvider {
public String getKidFromTokenHeader(String token) {
String KID = "kid";
String encodedHeader = getEncodedHeader(token);
String decodedHeader = getDecodedHeader(encodedHeader);
try {
JSONObject jsonObject = new JSONObject(decodedHeader);
return jsonObject.get(KID).toString();
} catch (JSONException e) {
return e.toString();
}
}
public Jws<Claims> getOIDCTokenJws(String token, OIDCPublicKeyDto oidcPublicKeyDto, String iss, String aud) {
try {
return Jwts.parser()
.verifyWith(getRSAPublicKey(oidcPublicKeyDto.n(), oidcPublicKeyDto.e()))
.requireAudience(aud)
.requireIssuer(iss)
.build()
.parseSignedClaims(token);
} catch (ExpiredJwtException e) {
throw new Exception500(e.getMessage());
} catch (Exception e) {
log.error(e.toString());
throw new Exception500(e.getMessage());
}
}
public OIDCDecodePayload getOIDCTokenBody(String token, OIDCPublicKeyDto oidcPublicKeyDto, String iss, String aud) {
Claims payload = getOIDCTokenJws(token, oidcPublicKeyDto, iss, aud).getPayload();
return new OIDCDecodePayload(
payload.getIssuer(),
payload.getAudience().toString(),
payload.getSubject(),
payload.get("email", String.class));
}
private static String getDecodedHeader(String encodedHeader) {
byte[] decodedHeaderBytes = Base64.getDecoder().decode(encodedHeader);
return new String(decodedHeaderBytes);
}
private static String getEncodedHeader(String token) {
String[] splitToken = token.split("\\\\.");
return splitToken[0];
}
private PublicKey getRSAPublicKey(String modulus, String exponent) throws NoSuchAlgorithmException, InvalidKeySpecException {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
byte[] decodeN = Base64.getUrlDecoder().decode(modulus);
byte[] decodeE = Base64.getUrlDecoder().decode(exponent);
BigInteger n = new BigInteger(1, decodeN);
BigInteger e = new BigInteger(1, decodeE);
RSAPublicKeySpec keySpec = new RSAPublicKeySpec(n, e);
return keyFactory.generatePublic(keySpec);
}
}
@Component
@RequiredArgsConstructor
public class OauthOIDCHelper {
private final JwtOIDCProvider jwtOIDCProvider;
private final KakaoOauthClient kakaoOauthClient;
private final KakaoProperties kakaoProperties;
private OIDCDecodePayload getPayloadFromIdToken(String token, String iss, String aud, OIDCPublicKeysResponse oidcPublicKeysResponse) {
String kid = jwtOIDCProvider.getKidFromTokenHeader(token);
OIDCPublicKeyDto oidcPublicKeyDto = oidcPublicKeysResponse.keys().stream()
.filter(o -> o.kid().equals(kid))
.findFirst()
.orElseThrow();
return jwtOIDCProvider.getOIDCTokenBody(token, oidcPublicKeyDto, iss, aud);
}
//해당 method를 통해 검증된 id 토큰의 payload를 받을 수 있다.
public OIDCDecodePayload getKakaoOIDCDecodePayload(String token) {
OIDCPublicKeysResponse oidcPublicKeysResponse = kakaoOauthClient.getKakaoOIDCOpenKeys();
return getPayloadFromIdToken(
token,
kakaoProperties.getBaseUrl(),
kakaoProperties.getRestApiKey(),
oidcPublicKeysResponse
);
}
}
시큐리티 설정
이를 통해 카카오 로그인 서비스와 회원가입은 완료했습니다. 이제 전달한 id 토큰을 로그인이 필요한 서비스 실행 시 클라이언트에게 다시 받아 시큐리티를 통해 인증하고 권한을 인가하는 기능을 구현했습니다.
시큐리티를 설정하기 위해 큰 틀을 말씀드리자면
먼저 userDetails 객체를 만들고 이를 커스텀한 jwt filter의 SecurityContextHolder에 Authentication로 담아야합니다. 그리고 jwt filter를 security filter에 집어 넣고 시큐리티 환경 설정 및 API의 권한을 설정해야합니다.
말로는 이해하기 어렵기 때문에 아래의 코드로 설명드리겠습니다.
@RequiredArgsConstructor
public class CustomUserDetails implements UserDetails {
private final UserAccount userAccount;
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singleton(new SimpleGrantedAuthority("ROLE_USER"));
}
@Override
public String getPassword() {
return userAccount.getSub();
}
@Override
public String getUsername() {
return userAccount.getProfileNickname();
}
UserDetails 인터페이스를 상속 받고 추상 메서드들을 오버라이딩했습니다.
현재 프로젝트는 서버 개발자가 저 혼자이기 때문에 모든 user의 권한을 ROLE_USER로 통일했습니다. getPassword method의 값을 회원번호 즉, sub로 설정한 이유에 대해선 뒤에 말씀드리겠습니다.
이렇게 CustomUserDetails를 구현하고 CustomUserDetailsService class를 구현했습니다.
@Service
@RequiredArgsConstructor
public class CustomUserDetailsService implements UserDetailsService {
private final UserAccountRepository userAccountRepository;
@Override
public CustomUserDetails loadUserByUsername(String sub) throws UsernameNotFoundException {
Optional<UserAccount> findUser = userAccountRepository.findBySub(sub);
return findUser.map(CustomUserDetails::new).orElseThrow();
}
}
해당 클래스는 회원번호를 통해 User DB에서 user의 정보를 가져와 CustomUserDetails로 매핑하는 loadUserByUsername method를 오버라이딩 했습니다.
이제 위에서 말한 jwt filter를 구현해야하는데요 먼저 코드를 보여드리고 하나씩 설명드리겠습니다.
@Slf4j
public class JwtAuthenticationFilter extends BasicAuthenticationFilter {
public static final String AUTH_HEADER = "Authorization";
public static final String BEARER = "Bearer ";
private final OauthOIDCHelper oauthOIDCHelper;
private final CustomUserDetailsService customUserDetailsService;
public JwtAuthenticationFilter(AuthenticationManager authenticationManager, OauthOIDCHelper oauthOIDCHelper,
CustomUserDetailsService customUserDetailsService) {
super(authenticationManager);
this.oauthOIDCHelper = oauthOIDCHelper;
this.customUserDetailsService = customUserDetailsService;
}
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null) {
Authentication authentication = getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
private String resolveToken(HttpServletRequest request) {
String rawHeader = request.getHeader(AUTH_HEADER);
if (rawHeader != null
&& rawHeader.length() > BEARER.length()
&& rawHeader.startsWith(BEARER)) {
return rawHeader.substring(BEARER.length());
}
return null;
}
private Authentication getAuthentication(String token) {
OIDCDecodePayload oidcDecodePayload = oauthOIDCHelper.getKakaoOIDCDecodePayload(token);
CustomUserDetails customUserDetails = customUserDetailsService.loadUserByUsername(oidcDecodePayload.sub());
return new UsernamePasswordAuthenticationToken(customUserDetails, customUserDetails.getPassword(),
customUserDetails.getAuthorities());
}
}
resolveToken method를 통해 id 토큰을 받아옵니다.
이제 id 토큰을 통해 CustomUserDetails를 Authentication로 매핑하는 getAuthentication method를 구현했습니다. 여기서 getPassword method의 값을 회원번호 즉, sub로 설정한 이유가 나옵니다. UsernamePasswordAuthenticationToken을 살펴보면 아래와 같습니다.
/**
* This constructor should only be used by <code>AuthenticationManager</code> or
* <code>AuthenticationProvider</code> implementations that are satisfied with
* producing a trusted (i.e. {@link #isAuthenticated()} = <code>true</code>)
* authentication token.
* @param principal
* @param credentials
* @param authorities
*/
public UsernamePasswordAuthenticationToken(Object principal, Object credentials,
Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
this.credentials = credentials;
super.setAuthenticated(true); // must use super, as we override
}
파라미터로 principal, credentials 그리고 authorities를 받고 있습니다. 여기서 principal는 인증에 사용되는 주체를 나타내고 credentials 는 인증 과정에서 필요한 주요 정보를 나타냅니다. 따라서 credentials 에 CustomUserDetails의 getPassword method를 통해 주요 정보를 넣습니다. 현재 프로젝트에서 인증 과정에 필요한 정보는 id 토큰의 회원 번호이기 때문에 getSub()로 설정했습니다.
getAuthentication method로 매핑한 Authentication를 SecurityContextHolder에 담는 doFilterInternal method를 구현했습니다.
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
throws ServletException, IOException {
String token = resolveToken(request);
if (token != null) {
Authentication authentication = getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
chain.doFilter(request, response);
}
이렇게 jwt filter를 설정하고 이를 다음과 같이 security filter 내부에 집어 넣습니다.
@RequiredArgsConstructor
@Configuration
public class SecurityConfig {
private final OauthOIDCHelper oauthOIDCHelper;
private final CustomUserDetailsService customUserDetailsService;
public class CustomSecurityFilterManager extends AbstractHttpConfigurer<CustomSecurityFilterManager, HttpSecurity> {
@Override
public void configure(HttpSecurity builder) throws Exception {
AuthenticationManager authenticationManager = builder.getSharedObject(AuthenticationManager.class);
builder.addFilter(new JwtAuthenticationFilter(authenticationManager, oauthOIDCHelper, customUserDetailsService));
super.configure(builder);
}
}
마지막으로 시큐리티 환경 설정을 통해 마무리했습니다.
//securityConfig
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
final String[] swaggerPermitUrls = {
"/v3/api-docs/**",
"/swagger-ui/**"
};
http
//.csrf(csrf -> csrf.disable())와 아래의 형태가 동일
.csrf(AbstractHttpConfigurer::disable)
.cors(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.rememberMe(AbstractHttpConfigurer::disable)
.sessionManagement(sessionManagement -> sessionManagement.sessionCreationPolicy(
SessionCreationPolicy.STATELESS))
//.apply(new CustomSecurityFilterManager())가 아래의 코드로 변경됨
.with(new CustomSecurityFilterManager(), Customizer.withDefaults())
.headers(headers -> headers.frameOptions(FrameOptionsConfig::sameOrigin));
http
.authorizeHttpRequests(
authorizeHttpRequests -> authorizeHttpRequests
.requestMatchers(swaggerPermitUrls).permitAll()
.requestMatchers("/library/**").permitAll()
.requestMatchers("/auth/**").permitAll());
return http.build();
}
시큐리티 설정이 저가 원래 알고 있던 방식으로는 컴파일 오류가 나와 공식 문서를 찾아봤습니다.
Configuration Migrations :: Spring Security
다음과 같이 Security가 Lambda DSL을 사용하도록 변경되었기 때문에 코드를 하나씩 변경했습니다. 또한 변경되거나 없어진 method들 (예를 들어 apply method는 with method를 사용하도록 변경)이 꽤 있어서 내부 로직에 들어가 하나씩 확인하느라 시간을 좀 쓴 것 같습니다.
/**
* Applies a {@link SecurityConfigurerAdapter} to this {@link SecurityBuilder} and
* invokes {@link SecurityConfigurerAdapter#setBuilder(SecurityBuilder)}.
* @param configurer
* @return the {@link SecurityConfigurerAdapter} for further customizations
* @throws Exception
* **@deprecated For removal in 7.0. Use**
* {@link #**with(SecurityConfigurerAdapter, Customizer)} instead**.
*/
@Deprecated(since = "6.2", forRemoval = true)
@SuppressWarnings("unchecked")
public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer) throws Exception {
configurer.addObjectPostProcessor(this.objectPostProcessor);
configurer.setBuilder((B) this);
add(configurer);
return configurer;
}
이렇게 시큐리티까지 설정 후 로그인 서비스 구현을 완료했습니다.
ID 토큰 유효성 검사를 구현하는데 벽을 만났던 것 같습니다. 그래서 주위에 하소연하고 했는데 들어주셔서 감사한 분들이 있습니다. 덕분에 잘 극복할 수 있었던 것 같습니다.
마지막으로 이번 주제 같은 경우 코드를 하나씩 떼서 보면 이해가 안되는 부분도 많고 카카오 로그인 서비스 같은 경우 주변에서 흔히들 사용하시기 때문에 코드 전문을 올렸습니다. 이 때문에 글이 많이 긴 점 양해부탁드립니다.🙏
'Spring' 카테고리의 다른 글
| ResponEntity class 내부 뜯어보기 (0) | 2026.01.18 |
|---|---|
| Spring Batch란? (0) | 2026.01.18 |
| Swagger UI란? (0) | 2026.01.18 |
| QueryDSL이란? (0) | 2026.01.18 |
| feign 적용기 (0) | 2026.01.18 |