로그아웃 로직은 간단하게 이루어진다
사용자가 로그아웃 버튼을 누르면 Refresh 토큰을 DB에서 삭제해주어야 할 것이다
스프링 시큐리티에는 기본 LogoutHandler 인터페이스가 있다
이 인터페이스를 커스텀하여 로그아웃을 구현할 것이다
@Component
@RequiredArgsConstructor
public class CustomLogoutHandler implements LogoutHandler {
private final TokenService tokenService;
@Override
public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
// 로그아웃 로직을 TokenService에 위임
tokenService.logout(request, response);
}
}
로그아웃의 기능은 토큰을 삭제하는 것이므로 TokenService에 정의하여 관리하겠다
로그아웃 요청이 들어오면 Refresh 토큰을 검증한 뒤 DB에서 해당 토큰을 제거한다
response의 refresh 쿠키를 null로 설정하고 MaxAge를 0으로 만들어준다
@Service
@RequiredArgsConstructor
public class TokenService {
private final RefreshTokenRepository refreshTokenRepository;
private final JwtUtil jwtUtil;
// 로그아웃 처리
@Transactional
public void logout(HttpServletRequest request, HttpServletResponse response) {
String refresh = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals("refresh")) {
refresh = cookie.getValue();
break;
}
}
}
// 토큰 검증
validateRefreshToken(refresh);
// DB에서 Refresh 토큰 제거
deleteByTokenValue(refresh);
// response에서 쿠키 제거
Cookie cookie = new Cookie("refresh", null);
cookie.setMaxAge(0);
cookie.setPath("/");
response.addCookie(cookie);
}
// DB에 Refresh 토큰 저장
@Transactional
public void addRefreshToken(User user, String refresh) {
RefreshToken refreshToken = RefreshToken.builder()
.user(user)
.tokenValue(refresh)
.build();
refreshTokenRepository.save(refreshToken);
}
// 쿠키 생성
public Cookie createCookie(String key, String value) {
Cookie cookie = new Cookie(key, value);
cookie.setMaxAge(24 * 60 * 60);
cookie.setHttpOnly(true);
return cookie;
}
// DB에 Refresh 토큰이 존재하는지 확인
public Boolean existsByTokenValue(String tokenValue) {
return refreshTokenRepository.existsByTokenValue(tokenValue);
}
// DB에서 Refresh 토큰을 삭제
@Transactional
public void deleteByTokenValue(String tokenValue) {
refreshTokenRepository.deleteByTokenValue(tokenValue);
}
// 리프레시 토큰 검증
public void validateRefreshToken(String token) {
if (token == null) {
throw new InvalidTokenException("리프레시 토큰이 없습니다.");
}
try {
jwtUtil.isExpired(token);
} catch (ExpiredJwtException e) {
throw new InvalidTokenException("만료된 리프레시 토큰입니다.");
}
String category = jwtUtil.getCategory(token);
if (!category.equals("refresh")) {
throw new InvalidTokenException("유효하지 않은 카테고리의 토큰입니다.");
}
if (!existsByTokenValue(token)) {
throw new InvalidTokenException("DB에 존재하지 않는 리프레시 토큰입니다.");
}
}
// 액세스 토큰 검증
public void validateAccessToken(String token) {
if (token == null) {
throw new InvalidTokenException("액세스 토큰이 없습니다.");
}
try {
jwtUtil.isExpired(token);
} catch (ExpiredJwtException e) {
throw new InvalidTokenException("만료된 액세스 토큰입니다.");
}
String category = jwtUtil.getCategory(token);
if (!category.equals("access")) {
throw new InvalidTokenException("유효하지 않은 카테고리의 토큰입니다.");
}
}
}
내가 원하는 url을 사용하기 위해 logoutUrl을 /api/auth/logout으로 설정
customLogoutHandler를 추가해주고 성공하면 200을 반환하도록 설정한다
@Configuration
@EnableWebSecurity
@RequiredArgsConstructor
public class SecurityConfig {
private final JwtUtil jwtUtil;
private final UserRepository userRepository;
private final TokenService tokenService;
private final CustomLogoutHandler customLogoutHandler;
@Bean
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
return configuration.getAuthenticationManager();
}
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(AbstractHttpConfigurer::disable)
.formLogin(AbstractHttpConfigurer::disable)
.httpBasic(AbstractHttpConfigurer::disable)
.sessionManagement(session -> session
.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/", "/login", "/signup", "/css/**", "/js/**", "/oauth2/**").permitAll()
.requestMatchers("/api/auth/**").permitAll()
.anyRequest().authenticated())
.logout(logout -> logout
.logoutUrl("/api/auth/logout")
.addLogoutHandler(customLogoutHandler)
.logoutSuccessHandler((request, response, authentication) -> {
response.setStatus(HttpServletResponse.SC_OK);
}))
.addFilterBefore(new JwtAuthenticationFilter(jwtUtil, userRepository, tokenService), UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
로그인 진행 후 토큰이 DB에 저장되어 있다

Postman을 통해 로그아웃을 진행하면 200이 뜨고

DB에 토큰이 삭제된 것을 볼 수 있다

다음 목표는 OAuth2를 우리 서비스에 붙여 소셜 로그인을 만드는 것이다
처음 하면 좀 어렵다고 한다..