이번 개인 프로젝트에서 Spring Security를 활용하여 OAuth2 로그인을 구현했다. Spring Security의 OAuth2를 활용하는 방법과 JWT 발급까지 모두 정리해보려고 한다.
참고) 개발 환경은 Spring boot 3, Java 17을 사용하였다.
먼저 OAuth 2.0이 무엇인지 알아보자.
OAuth 2.0 (Open Authorization 2.0)
인증을 위한 개방형 표준 프로토콜로, third-party 프로그램에게 리소스 소유자를 대신해 리소스 서버에서 제공하는 자원에 대한 접근 권한을 위임하는 방식으로 작동된다.
쉽게 말해서 third-party 프로그램(구글, 카카오 등)에게 로그인 및 개인정보 관리에 대한 권한을 위임하여 third-party 프로그램이 가지고 있는 사용자에 대한 리소스를 조회할 수 있다.
구현하기 앞서 OAuth2 로그인 흐름에 대해 알아야 한다.
OAuth2 로그인 흐름
흐름에 대해 이해하기 위해 직접 그려본 다이어그램이다.
Spring Security의 OAuth2는 권한 부여 코드 승인 방식(Authorization Code Grant Type)으로 위와 같이 진행된다.
여기서 Client는 우리가 개발하는 Application Server를 말한다.
위 flow를 기반으로 보면 Client가 처리해줘야 할 부분이 많아 보인다. 하지만 Spring Security가 이미 다 구현해두었기 때문에 직접 다 만들지 않고, 이를 잘 활용하는 방법으로 구현할 것이다. (활용할 수 있는 부분은 최대한 활용하면서 서비스에서 중요한 비즈니스 로직이나 성능 최적화에 더 신경을 쓰자는게 내 생각이다.)
1. 로그인 요청
Spring Security에서 기본적으로 제공하는 URL이 있다. -> http://{domain}/oauth2/authorization/{registrationId}
시큐리티에서 이미 구현을 해두었고, 따로 Controller를 만들지 않아도 된다.
4. 리다이렉트 URL
Spring Security에서 기본적으로 제공하는 URL이 있다. -> http://{domain}/login/oauth2/code/{registrationId}
시큐리티에서 이미 구현을 해두었고, 따로 Controller를 만들지 않아도 된다.
즉, 구현해야할 부분은 9. 최종 응답 전 후처리만 남게된다.
후처리는 로그인한 유저가 처음 가입하는 회원인지, 기존의 등록된 회원인지 검증 후 우리 애플리케이션에 접근할 수 있는 토큰을 발급해주는 것이다.
이제 9번에 해당하는 구현 과정을 살펴볼 건데, 그 전에 먼저 OAuth2 설정을 해줘야 한다.
application-oauth.yml
spring:
security:
oauth2:
client:
registration:
google:
client-id: ${GOOGLE_CLIENT}
client-secret: ${GOOGLE_SECRET}
scope: # google API의 범위 값
- profile
- email
kakao:
client-id: ${KAKAO_CLIENT}
client-secret: ${KAKAO_SECRET}
redirect-uri: {baseUrl}/login/oauth2/code/kakao
client-authentication-method: client_secret_post # kakao는 인증 토큰 발급 요청 메서드가 post이다. (최근 버전에는 작성 방법이 이렇게 바뀌었다.)
authorization-grant-type: authorization_code
scope: # kakao 개인 정보 동의 항목 설정의 ID 값
- profile_nickname
- profile_image
- account_email
client-name: kakao
# kakao provider 설정
provider:
kakao:
authorization-uri: https://kauth.kakao.com/oauth/authorize
token-uri: https://kauth.kakao.com/oauth/token
user-info-uri: https://kapi.kakao.com/v2/user/me
user-name-attribute: id # 유저 정보 조회 시 반환되는 최상위 필드명으로 해야 한다.
인증을 할 때 OAuth2를 사용하도록 사용할 클라이언트(third-party 프로그램) 설정을 해준다.
google, github, facebook은 CommonOAuth2Provider에 기본 설정 값이 등록되어 제공되지만 kakao, naver 등은 provider에 필요한 값들을 등록해줘야 한다.
SecurityConfig
...
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.config.annotation.web.configurers.HeadersConfigurer.FrameOptionsConfig;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
@RequiredArgsConstructor
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
private final CustomOAuth2UserService oAuth2UserService;
private final OAuth2SuccessHandler oAuth2SuccessHandler;
private final TokenAuthenticationFilter tokenAuthenticationFilter;
@Bean
public WebSecurityCustomizer webSecurityCustomizer() { // security를 적용하지 않을 리소스
return web -> web.ignoring()
// error endpoint를 열어줘야 함, favicon.ico 추가!
.requestMatchers("/error", "/favicon.ico");
}
@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
http
// rest api 설정
.csrf(AbstractHttpConfigurer::disable) // csrf 비활성화 -> cookie를 사용하지 않으면 꺼도 된다. (cookie를 사용할 경우 httpOnly(XSS 방어), sameSite(CSRF 방어)로 방어해야 한다.)
.cors(AbstractHttpConfigurer::disable) // cors 비활성화 -> 프론트와 연결 시 따로 설정 필요
.httpBasic(AbstractHttpConfigurer::disable) // 기본 인증 로그인 비활성화
.formLogin(AbstractHttpConfigurer::disable) // 기본 login form 비활성화
.logout(AbstractHttpConfigurer::disable) // 기본 logout 비활성화
.headers(c -> c.frameOptions(
FrameOptionsConfig::disable).disable()) // X-Frame-Options 비활성화
.sessionManagement(c ->
c.sessionCreationPolicy(SessionCreationPolicy.STATELESS)) // 세션 사용하지 않음
// request 인증, 인가 설정
.authorizeHttpRequests(request ->
request.requestMatchers(
new AntPathRequestMatcher("/"),
new AntPathRequestMatcher("/auth/success"),
...
).permitAll()
.anyRequest().authenticated()
)
// oauth2 설정
.oauth2Login(oauth -> // OAuth2 로그인 기능에 대한 여러 설정의 진입점
// OAuth2 로그인 성공 이후 사용자 정보를 가져올 때의 설정을 담당
oauth.userInfoEndpoint(c -> c.userService(oAuth2UserService))
// 로그인 성공 시 핸들러
.successHandler(oAuth2SuccessHandler)
)
// jwt 관련 설정
.addFilterBefore(tokenAuthenticationFilter,
UsernamePasswordAuthenticationFilter.class)
.addFilterBefore(new TokenExceptionFilter(), tokenAuthenticationFilter.getClass()) // 토큰 예외 핸들링
// 인증 예외 핸들링
.exceptionHandling((exceptions) -> exceptions
.authenticationEntryPoint(new CustomAuthenticationEntryPoint())
.accessDeniedHandler(new CustomAccessDeniedHandler()));
return http.build();
}
}
Security 및 OAuth2 설정에 대한 설명은 주석으로 대체한다.
참고)
1. "/error"를 열어주지 않으면 401 에러에서 벗어날 수 없다.. 다른 에러가 발생해도 에러에 대한 response body가 없어서 디버깅이 무척이나 힘들었다. (참고)
2. "/favicon.ico"도 열어주지 않으면 마찬가지로 401 에러에서 벗어날 수 없다.. OAuth2 로그인 시 필요하다.
이제 구현 로직을 보자.
CustomOAuth2UserService
...
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
@RequiredArgsConstructor
@Service
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
private final MemberRepository memberRepository;
@Transactional
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
// 1. 유저 정보(attributes) 가져오기
Map<String, Object> oAuth2UserAttributes = super.loadUser(userRequest).getAttributes();
// 2. resistrationId 가져오기 (third-party id)
String registrationId = userRequest.getClientRegistration().getRegistrationId();
// 3. userNameAttributeName 가져오기
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();
// 4. 유저 정보 dto 생성
OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfo.of(registrationId, oAuth2UserAttributes);
// 5. 회원가입 및 로그인
Member member = getOrSave(oAuth2UserInfo);
// 6. OAuth2User로 반환
return new PrincipalDetails(member, oAuth2UserAttributes, userNameAttributeName);
}
private Member getOrSave(OAuth2UserInfo oAuth2UserInfo) {
Member member = memberRepository.findByEmail(oAuth2UserInfo.email())
.orElseGet(oAuth2UserInfo::toEntity);
return memberRepository.save(member);
}
}
SecurityConfig에서 로그인 성공 이후 사용자 정보를 가져올 클래스로 CustomOAuth2UserService를 등록해주었다.
주석에 있는 순번대로 살펴보자.
1. 유저 attributes 가져오기
DefaultOAuth2UserService는 리소스 서버에서 사용자 정보를 받아오는 클래스인데, 이를 상속 받아 사용자 정보(DefaultOAuth2User의 attributes)를 가져온다.
구글 기준 attributes
{
"sub": "1234567890",
"name": "user-name",
"email": "user-email",
...
}
2. registrationId 가져오기
registrationId는 oauth 관련 yml에서 설정한 client.registration의 값을 말한다. (google, kakao)
3. userNameAttributeName 가져오기
oauth 관련 yml에서 설정한 provider의 user-name-attribute 값을 말한다. (구글은 "sub"이다. CommonOAuth2Provider에서 확인 가능하다.) 이는 유저 attributes에서 식별자에 접근할 때 사용된다. -> attributes.get("sub") (DefaultOAuth2User에서 확인 가능하다.)
4. 유저 정보 dto 생성
어떤 소셜 로그인인지 구별하여 유저 정보 dto(OAuth2UserInfo)를 생성한다.
OAuth2UserInfo
@Builder
public record OAuth2UserInfo(
String name,
String email,
String profile
) {
public static OAuth2UserInfo of(String registrationId, Map<String, Object> attributes) {
return switch (registrationId) { // registration id별로 userInfo 생성
case "google" -> ofGoogle(attributes);
case "kakao" -> ofKakao(attributes);
default -> throw new AuthException(ILLEGAL_REGISTRATION_ID);
};
}
private static OAuth2UserInfo ofGoogle(Map<String, Object> attributes) {
return OAuth2UserInfo.builder()
.name((String) attributes.get("name"))
.email((String) attributes.get("email"))
.profile((String) attributes.get("picture"))
.build();
}
private static OAuth2UserInfo ofKakao(Map<String, Object> attributes) {
Map<String, Object> account = (Map<String, Object>) attributes.get("kakao_account");
Map<String, Object> profile = (Map<String, Object>) account.get("profile");
return OAuth2UserInfo.builder()
.name((String) profile.get("nickname"))
.email((String) account.get("email"))
.profile((String) profile.get("profile_image_url"))
.build();
}
public Member toEntity() {
return Member.builder()
.name(name)
.email(email)
.profile(profile)
.memberKey(KeyGenerator.generateKey())
.role(Role.USER)
.build();
}
}
registrationId 별로 유저 정보를 생성한다.
attributes의 키 값은 각 소셜의 응답 값(소셜 사이트 확인)을 보면 알 수 있다.
5. 회원가입 및 로그인
생성한 유저 정보를 가지고 이전에 가입한 회원인지 확인 후 새로운 회원이면 저장한다.
6. OAuth2User로 반환
new DefaultOAuth2User()로 반환하지 않고, 인증 객체 생성 시 member에 대한 값을 추가하기 위해 Principal 객체를 작성해주었다.
PrincipalDetails
...
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;
public record PrincipalDetails(
Member member,
Map<String, Object> attributes,
String attributeKey) implements OAuth2User, UserDetails {
@Override
public String getName() {
return attributes.get(attributeKey).toString();
}
@Override
public Map<String, Object> getAttributes() {
return attributes;
}
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return Collections.singletonList(
new SimpleGrantedAuthority(member.getRole().getKey()));
}
@Override
public String getPassword() {
return null;
}
@Override
public String getUsername() {
return member.getMemberKey();
}
@Override
public boolean isAccountNonExpired() {
return true;
}
@Override
public boolean isAccountNonLocked() {
return true;
}
@Override
public boolean isCredentialsNonExpired() {
return true;
}
@Override
public boolean isEnabled() {
return true;
}
}
UserDetails도 같이 구현하여 토큰 생성 시 authentication 객체에서 getName() 호출 시 getUsername()값이 리턴되도록 했다.
위와 같이 되는 이유는 내부 코드를 확인해 보면 찾을 수 있는데,
authentication.getName() -> Principal 객체의 getName()을 호출한다.
Principal 객체에 담기는 것은 UserDetails를 구현하여 직접 생성한 PrincipalDetails 객체이다.
AbstractAuthenticationToken에서 getName() 호출 시 principal이 UserDetails이면 userDetails.getUsername()을 리턴하도록 되어 있기 때문이다.
여기까지 하면 OAuth2 로그인은 끝났다. 시큐리티 덕분에 구현 양이 확 줄었기 때문이다.
이제 OAuth2 로그인 성공 시 인증 토큰을 발급해주는 부분만 남았다.
OAuth2SuccessHandler
...
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
import org.springframework.web.util.UriComponentsBuilder;
@RequiredArgsConstructor
@Component
public class OAuth2SuccessHandler implements AuthenticationSuccessHandler {
private final TokenProvider tokenProvider;
private static final String URI = "/auth/success";
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
// accessToken, refreshToken 발급
String accessToken = tokenProvider.generateAccessToken(authentication);
tokenProvider.generateRefreshToken(authentication, accessToken);
// 토큰 전달을 위한 redirect
String redirectUrl = UriComponentsBuilder.fromUriString(URI)
.queryParam("accessToken", accessToken)
.build().toUriString();
response.sendRedirect(redirectUrl);
}
}
인증 토큰 발급은 로그인에 성공했다는 조건이 있기 때문에 로그인이 성공적으로 끝나면 호출되는 successHandler에서 발급해준다.
발급된 토큰을 프론트에게 응답으로 내려주기 위해 내부적으로 리다이렉트를 해주었다.
토큰 발급부터 살펴보자.
TokenProvider
...
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.ExpiredJwtException;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.MalformedJwtException;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.User;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
@RequiredArgsConstructor
@Component
public class TokenProvider {
@Value("${jwt.key}")
private String key;
private SecretKey secretKey;
private static final long ACCESS_TOKEN_EXPIRE_TIME = 1000 * 60 * 30L;
private static final long REFRESH_TOKEN_EXPIRE_TIME = 1000 * 60 * 60L * 24 * 7;
private static final String KEY_ROLE = "role";
private final TokenService tokenService;
@PostConstruct
private void setSecretKey() {
secretKey = Keys.hmacShaKeyFor(key.getBytes());
}
public String generateAccessToken(Authentication authentication) {
return generateToken(authentication, ACCESS_TOKEN_EXPIRE_TIME);
}
// 1. refresh token 발급
public void generateRefreshToken(Authentication authentication, String accessToken) {
String refreshToken = generateToken(authentication, REFRESH_TOKEN_EXPIRE_TIME);
tokenService.saveOrUpdate(authentication.getName(), refreshToken, accessToken); // redis에 저장
}
private String generateToken(Authentication authentication, long expireTime) {
Date now = new Date();
Date expiredDate = new Date(now.getTime() + expireTime);
String authorities = authentication.getAuthorities().stream()
.map(GrantedAuthority::getAuthority)
.collect(Collectors.joining());
return Jwts.builder()
.subject(authentication.getName())
.claim(KEY_ROLE, authorities)
.issuedAt(now)
.expiration(expiredDate)
.signWith(secretKey, Jwts.SIG.HS512)
.compact();
}
public Authentication getAuthentication(String token) {
Claims claims = parseClaims(token);
List<SimpleGrantedAuthority> authorities = getAuthorities(claims);
// 2. security의 User 객체 생성
User principal = new User(claims.getSubject(), "", authorities);
return new UsernamePasswordAuthenticationToken(principal, token, authorities);
}
private List<SimpleGrantedAuthority> getAuthorities(Claims claims) {
return Collections.singletonList(new SimpleGrantedAuthority(
claims.get(KEY_ROLE).toString()));
}
// 3. accessToken 재발급
public String reissueAccessToken(String accessToken) {
if (StringUtils.hasText(accessToken)) {
Token token = tokenService.findByAccessTokenOrThrow(accessToken);
String refreshToken = token.getRefreshToken();
if (validateToken(refreshToken)) {
String reissueAccessToken = generateAccessToken(getAuthentication(refreshToken));
tokenService.updateToken(reissueAccessToken, token);
return reissueAccessToken;
}
}
return null;
}
public boolean validateToken(String token) {
if (!StringUtils.hasText(token)) {
return false;
}
Claims claims = parseClaims(token);
return claims.getExpiration().after(new Date());
}
private Claims parseClaims(String token) {
try {
return Jwts.parser().verifyWith(secretKey).build()
.parseSignedClaims(token).getPayload();
} catch (ExpiredJwtException e) {
return e.getClaims();
} catch (MalformedJwtException e) {
throw new TokenException(INVALID_TOKEN);
} catch (SecurityException e) {
throw new TokenException(INVALID_JWT_SIGNATURE);
}
}
}
참고) jjwt 관련 코드들이 Spring boot 3 버전에 맞춰 업데이트 된 것 같았다. 위 코드에서는 deprecated된 것은 사용하지 않았고, 문서를 참고하여 작성하였다.
주석을 바탕으로 주요한 부분만 설명하겠다.
1. refreshToken 발급
refreshToken은 발급 시 accessToken을 key로 레디스에 저장한다. 유효기간은 refreshToken의 만료일과 동일하게 잡았다. 또한 refreshToken은 프론트에게 전달하지 않고 백엔드에서만 가지고 있을 계획이라서 accessToken과 refreshToken 생성 부분은 공통으로 사용하였다.
2. security의 User 객체 생성
토큰을 파싱하여 Authentication 객체를 리턴하는 메서드에서 데이터베이스에 접근하고 싶지 않았기 때문에 UserDetails를 구현한 User 객체를 생성하였다. 이는 추후 Controller에서 UserDetails를 받기 위한 이유도 된다.
3. accessToken 재발급
accessToken은 refreshToken으로 재발급되는데, 과정은 이러하다.
먼저 filter에서 accessToken validation을 거친다. 만료가 되었다면 재발급을 시도한다.
redis에서 accessToken으로 refreshToken을 찾고, validation을 거친다. 만료되지 않았다면 accessToken을 재발급하고 redis에 업데이트 한다.
위 코드를 보면 알겠지만 tokenProvider에서 발생하는 예외가 좀 있다.
parseClaim() 메서드에서 토큰을 파싱하는 부분, refreshToken을 조회하는 부분인데 예외가 발생하는 것을 그냥 둔다면 말 그대로 exception을 던지고 만다. 필터 단에서 던져지는 예외는 @ControllerAdvice가 처리할 수 없기 때문이다. (@ControllerAdvice는 Servlet에서 발생하는 예외만 핸들링할 수 있다.)
그래서 토큰 관련 예외를 처리하기 위해 예외를 핸들링하는 필터를 만들어 주었다.
TokenExceptionFilter
...
import jakarta.servlet.FilterChain;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import org.springframework.web.filter.OncePerRequestFilter;
public class TokenExceptionFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
try {
filterChain.doFilter(request, response);
} catch (TokenException e) {
response.sendError(e.getErrorCode().getHttpStatus().value(), e.getMessage());
}
}
}
여기서 바로 ErrorResponse를 구성하여 response.getWriter().write()로 필터에서 바로 응답을 써주도록 해도 되지만, 간단하게 처리하기 위해 sendError()로 servlet으로 예외를 전달했다. (사실 예외를 전달하는 것보다 바로 응답을 내려주는게 속도 측면에서는 더 빠를 것이다. 예외를 전달하는데 걸리는 과정이 꽤 길었다. 예외 발생 지점부터 servlet까지 계속 전달하고, 다시 컨트롤러로 내려가야하기 때문이다.)
참고) sendError()가 호출되면 모든 에러는 "/error"로 간다. 이 엔드포인트는 스프링이 만들어둔 BasicErrorController에 매핑되어 있어서 이 컨트롤러에서 응답을 내려준다.
토큰 예외는 간단하게 처리하였고, 이 필터를 사용하도록 등록해줘야 한다.
위치는 TokenAuthenticationFilter 전으로 등록하여 주면 된다. 코드는 위에 작성된 SecurityConfig를 참고하자.
이제 마지막으로 토큰에 대한 인증 처리를 하는 TokenAuthenticationFilter를 보자.
TokenAuthenticationFilter
...
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;
@RequiredArgsConstructor
@Component
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final TokenProvider tokenProvider;
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException {
String accessToken = resolveToken(request);
// accessToken 검증
if (tokenProvider.validateToken(accessToken)) {
setAuthentication(accessToken);
} else {
// 만료되었을 경우 accessToken 재발급
String reissueAccessToken = tokenProvider.reissueAccessToken(accessToken);
if (StringUtils.hasText(reissueAccessToken)) {
setAuthentication(reissueAccessToken);
// 재발급된 accessToken 다시 전달
response.setHeader(AUTHORIZATION, TokenKey.TOKEN_PREFIX + reissueAccessToken);
}
}
filterChain.doFilter(request, response);
}
private void setAuthentication(String accessToken) {
Authentication authentication = tokenProvider.getAuthentication(accessToken);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
private String resolveToken(HttpServletRequest request) {
String token = request.getHeader(AUTHORIZATION);
if (ObjectUtils.isEmpty(token) || !token.startsWith(TokenKey.TOKEN_PREFIX)) {
return null;
}
return token.substring(TokenKey.TOKEN_PREFIX.length());
}
}
플로우는 이렇다.
request 헤더에서 accessToken을 가져온 뒤 유효한지 검증한다.
유효하다면 인증 객체를 생성하고 요청을 다음 필터로 보낸다.
유효하지 않다면 accessToken을 재발급하고, 재발급 된 accessToken을 헤더에 실어 다음 필터로 보낸다.
이렇게 OAuth 로그인 후 토큰 발급 과정까지 모두 끝났다.
정리 및 회고
예전에 썼던 JWT 관련 글이 있는데, 그때 코드를 다시보니 리팩터링이 많이 필요해 보인다.. 그 당시엔 Spring boot도 제대로 써본적이 없던 시절이라 많이 미숙했다. 그래서 이번 기회에 OAuth 부분 정리하면서 JWT도 다시 한번 언급했다. 조금은 발전된 내 모습이 보이는 것 같다.
한줄 평을 남기자면, 할 때마다 어렵지만 알고나면 편리함에 감사한 시큐리티다..
'spring & java' 카테고리의 다른 글
[Spring] SSE + Redis pub/sub (1) | 2024.01.18 |
---|---|
[Spring] 스프링 부트 핵심 가이드 - API 작성 기초 (4) | 2023.10.29 |
[Spring] 스프링 부트 핵심 가이드 - Spring 기초 지식 (0) | 2023.10.22 |
[Spring] Spring AOP 적용 (0) | 2023.06.19 |
[Java] 비동기 + multi threading 구현 (0) | 2023.03.01 |