[미니프로젝트] 로그아웃 구현

김동혁·2025년 12월 7일

미니프로젝트

목록 보기
6/16

로그아웃 로직은 간단하게 이루어진다
사용자가 로그아웃 버튼을 누르면 Refresh 토큰을 DB에서 삭제해주어야 할 것이다

로그아웃 로직 구현

스프링 시큐리티에는 기본 LogoutHandler 인터페이스가 있다
이 인터페이스를 커스텀하여 로그아웃을 구현할 것이다

CustomLogoutHandler

@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에 정의하여 관리하겠다

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("유효하지 않은 카테고리의 토큰입니다.");
        }

    }

}

SecurityConfig 추가

SecurityConfig

내가 원하는 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를 우리 서비스에 붙여 소셜 로그인을 만드는 것이다
처음 하면 좀 어렵다고 한다..

0개의 댓글