[250327] TIL

오늘 한 일

Spring boot 에서 JWT 토큰 쿠키로 관리하기 (HttpOnly)

CORS 설정

config.setAllowCredentials(true)

  • 인증 정보(쿠키, HTTP인증, 클라이언트 측 SSL 인증서)를 포함한 크로스 도메인 요청을 허용함
  • true 로 설정하면 쿠키와 같은 인증 정보를 포함한 요청이 가능해짐
    @Bean
    CorsConfigurationSource corsConfigurationSource() {
        return request -> {
            CorsConfiguration config = new CorsConfiguration();
            config.setAllowCredentials(true); 
            config.setAllowedHeaders(Collections.singletonList("*"));
            config.setAllowedMethods(Collections.singletonList("*"));
            config.setAllowedOriginPatterns(Arrays.asList("http://localhost:5500", "http://127.0.0.1:5500"));

            return config;
        };
    }

쿠키 저장, 삭제, 읽기 함수 만들기 ( feat. jwt token )

  • jwt 토큰 관련된 함수는 예시 코드에서 삭제했음
  • SameSite 옵션이 Lax 이어야함 (중요)
    • None 옵션은 Secure 속성과 함께 사용 되어야하기 때문에 쿠키 전송이 안되는 오류가 발생함
@Component
public class JwtUtil {
    private static final String COOKIE_NAME = "auth_token";

    public void addTokenToCookie(HttpServletResponse response, String token){
        response.setHeader("Set-Cookie", String.format("%s=%s; Max-Age=%d; Path=/; HttpOnly; SameSite=Lax",
        COOKIE_NAME, token, (int)(EXPIRATION_TIME / 1000)));
    }

    public String extractTokenFromCookie(HttpServletRequest request){
        Cookie[] cookies = request.getCookies();

        if(cookies != null){
            for (Cookie cookie: cookies){
                if(COOKIE_NAME.equals(cookie.getName())){
                    return cookie.getValue();
                }
            }
        }
        return null;
    }

    public void deleteCookie(HttpServletResponse response){
        Cookie cookie = new Cookie(COOKIE_NAME, null);
        cookie.setHttpOnly(true);
        cookie.setPath("/");
        cookie.setMaxAge(0);
        response.addCookie(cookie);
        System.out.println("쿠키 삭제 완료");
    }

}

Jwt 필터에서 쿠키 읽고 인증 여부 구분하기

@Component
@RequiredArgsConstructor
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtUtil jwtUtil;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        String authorizationHeader = request.getHeader("Authorization");
        String token = null;

        if(authorizationHeader != null && authorizationHeader.startsWith("Bearer ")) {
            token = authorizationHeader.substring(7);
        } else {
            token = jwtUtil.extractTokenFromCookie(request);
        }

        if(token != null) {
            try {
                Long userId = jwtUtil.extractUserId(token);
                request.setAttribute("userId", userId);

                UsernamePasswordAuthenticationToken authentication = new UsernamePasswordAuthenticationToken(
                        userId, null, Collections.singletonList(new SimpleGrantedAuthority("ROLE_USER"))
                );
                SecurityContextHolder.getContext().setAuthentication(authentication);
            } catch (Exception e) {
                System.out.println("JWT 인증 실패: " + e.getMessage());
            }
        }
        filterChain.doFilter(request, response);
    }
}

서비스, 컨트롤러에서 함수 소비

HttpServletResponse

  • 클라이언트에서 HTTP 요청을 보내면 서버는 요청을 처리하고 HttpServletResponse 객체를 사용하여 응답을 구성함
  • HTTP 응답 헤더 설정, 상태 코드 설정, 쿠키 추가, 본문 작성, 리다이렉트 처리등 기능을 제공함
 public LoginResponseDto login(LoginRequestDto requestDto, HttpServletResponse response){
        User user = userRepository.findByEmail(requestDto.getEmail())
                .orElseThrow(()->new CustomException(ErrorCode.USER_NOT_FOUND));

        if(!user.getPassword().equals(requestDto.getPassword())){
            throw new CustomException(ErrorCode.INVALID_PASSWORD);
        }

        String token = jwtUtil.generateToken(user.getId());
        jwtUtil.addTokenToCookie(response, token);

        return new LoginResponseDto(user.getId(), user.getEmail(), user.getNickname(), user.getProfile());
    }

    public void logout(HttpServletResponse response){
        jwtUtil.deleteCookie(response);
        SecurityContextHolder.clearContext();
    }

    public boolean checkLogin(HttpServletRequest request){
        String token = jwtUtil.extractTokenFromCookie(request);
        if(token == null){
            return false;
        }

        try{
            Long userId = jwtUtil.extractUserId(token);
            return !jwtUtil.isTokenExpired(token) && userRepository.existsById(userId);
        } catch (Exception e) {
            return false;
        }
    }

+ 프론트엔드에서 요청할 때

credentials: “include”

  • 위 옵션 넣어줘야한다.
  • 크로스 도메인 요청에서 쿠키와 인증 정보를 함께 전송할지 결정하는 것
 const response = await fetch(`${API_CONFIG.BASE_URL}${endpoint}`, {
            ...defaultOptions,
            ...options,
            headers: {
                ...defaultOptions.headers,
                ...options.headers,
            },
            credentials: "include"
        });

크로스 도메인이란?

  • 크로스 도메인이란 현재 보고 있는 웹페이지의 도메인과 다른 도메인으로 리소스를 요청하는 것을 의미

아래 요소들이 모두 같을 때만 같은 도메인으로 간주

프로토콜: http, https
호스트명: example.com, api.example.com
포트: 80(http), 443(https), 3000 

다른 도메인 (Cross-Domain/Cross-Origin)

  • https://example.com → https://api.example.com (서브도메인 다름)
  • https://example.com → https://example.org (최상위 도메인 다름)
  • https://example.com → http://example.com (프로토콜 다름)
  • https://example.com → https://example.com:8080 (포트 다름)

Categories:

Updated:

Leave a comment