React + Java Spring - 크로스 도메인에서 HttpOnly true로 쿠키 전송하기 (Feat 로그인, Refresh Token)

Jinnny·2023년 8월 26일
0

React

목록 보기
5/11

이전 글에서 로그인 시 access token과 refresh token을 쿠키에 저장하는 방법과 refresh token을 사용하여 새로운 access token을 가져오면서 로그인을 유지하는 방법에 대해 알아보았다.

React + Redux + Redux-Saga + Typescript - 로그인 구현 및 로그인 유지 (Feat JWT Token, Cookie)

이때 Refresh token의 경우 만료시간을 1시간으로 해놓았었는데 만료시간이 길 경우 보안에 취약하고 프론트에서 서버로부터 token을 받아 쿠키에 저장하게 되면 서버에서 받아올 때 탈취될 가능성도 있어 로그인 요청이 들어오면 refresh token만 서버에서 httpOnly true로 쿠키에 저장해주기로 하였다.

❓ 문제 발생

이상하게 refresh token은 클라이언트에 잘 보내지는데 클라이언트에서 요청할 때만 쿠키에 있는 token 값이 서버로 전송이 되지 않았다,,,
그래서 백엔드 분들과 밤을 새서 원인을 찾아보았고 결국 우리가 원하는 방법으로 쿠키 전송을 할 수 있게 되었다. 이때 문제를 해결하면서 쿠키에 대해 많은 것을 알게 되었고 문제 해결 방법을 찾아보면서 우리처럼 동일한 문제를 겪고 해결하지 못한 분들도 많은 것 같아 글을 쓰게 되었다.

나는 벡엔드 부분은 잘 모르기 때문에 내가 적은 아래 Java spring 코드는 틀릴 수도 있다는 것을 알아주었으면 한다,,,,

📖 HttpOnly 란

현재 많은 웹사이트에서 민감한 정보를 쿠키에 저장하고 있어 해커들이 다양한 접근 방법을 통해 쿠키를 탈취하는 사례가 늘어나고 있다. 그 중 대표적인 방법으로는 XSS(Cross Site Scripting)로 자바스크립트를 활용하여 쿠키를 획득하는 방식이다.

XSS 취약점을 해결하기 위해 브라우저에서 쿠키에 접근하지 못하도록 제한해야하는데 이러한 역할을 할 수 있는 것이 httpOnly이다. 쿠키를 저장해 줄 때 httpOnly를 true로 하면 자바스크립트와 같은 클라이언트 측에서의 쿠키 접근이 제한되게 된다.

👩‍💻 구현

먼저 요청과 응답에 쿠키를 전송해주기 위해 서버와 프론트 각각 web config에 withCredentials을 true로 하고 프론트/서버에서 오는 요청을 허용하기 위해 Access-Control-Allow-Origin에 프론트/서버 도메인과 로컬 주소를 넣어주었다.

처음에는 백에서 refresh token을 전송하고 쿠키에 저장하기 때문에 프론트에서 추가해야할 코드가 없을 거라 생각했는데 쿠키를 제대로 받아오지 못하는 것을 보고 프론트에서도 config를 설정해 주어야 한다는 것을 알게 되었다.

서버의 경우 (Java Spring 기반)

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
@EnableWebMvc
public class WebConfig implements WebMvcConfigurer {

    @Override
    public void addCorsMappings(CorsRegistry registry) {
        registry.addMapping("/**")
            // 허용할 도메인 주소
            .allowedOrigins("http://localhost:3000", "프론트 주소") 
            // 허용할 HTTP 메서드
            .allowedMethods("*") 
            // 쿠키를 사용하는 경우 true로 설정
            .allowCredentials(true) 
    }
}

프론트 경우 (React 기반)

import axios from 'axios';

export const refreshAxios = axios.create({
  baseURL: `${process.env.REACT_APP_SERVER_URL}`,
  withCredentials: true,
  headers: {
    'Access-Control-Allow-Origin': `${process.env.REACT_APP_SERVER_URL}`,
  },
});

이후 백엔드에서 refresh token을 보내주고 쿠키에 저장해주기 때문에 백에서 httpOnly를 true로 해주었다.

이렇게 하면 httpOnly에서도 쿠키를 클라이언트로 보내고 클라이언트에서 요청하면 자동으로 cookie를 포함하여 보낼 수 있다고 했었다. 하지만 우리 프로젝트에서는 쿠키에 refresh token은 저장되었지만 쿠키가 클라이언트 요청에 포함되어 전송되지 않았고 클라이언트에서 서버로 쿠키 값을 받아올 수 없었다.

이렇게 access token이 만료되어 refresh token을 보내주는 요청에서 "refresh token is required"가 발생했다.

💡 해결 방법
백엔드 분들과 함께 찾아본 결과 도메인의 문제라는 것을 알게되었다. 내가 진행하고 있는 프로젝트에서는 각각 다른 프론트와 서버 주소를 사용하고 있었는데 이 부분 때문에 쿠키를 제대로 전송할 수가 없었고 크로스 사이트에서 쿠키를 전송할 수 있도록 sameSite와 secure을 추가해주었다.

SameSite
sameSite의 경우 Strict, Lax, none 총 3가지 속성값이 있다.

  • Strict : 같은 도메인에서만 쿠키 전송이 가능하다.
  • Lax : 크로스 도메인이라도 Get 방식이거나 a나 link 태그를 통한 접근은 가능하다.
  • None : 크로스 도메인에서도 쿠키를 전송 가능하다. 단, none을 사용할 경우 secure를 무조건 true로 해주어야 한다. 아니면 에러가 발생하면서 쿠키가 전송이 되지 않는다.

서버의 경우 (Java Spring 기반)

public class CookieController {
    public String setCookie(HttpServletResponse response) {
        // 쿠키 생성
        Cookie cookie = new Cookie();
        // Javascript로 쿠키에 접근 못하도록
        cookie.setHttpOnly(true);
        // HTTPS 연결에서만 쿠키 전송
        cookie.setSecure(true); 
        // Cross-site 요청에서도 쿠키 전송
        cookie.setSameSite("none"); 

        // 쿠키를 응답 헤더에 추가
        response.addCookie(cookie);

        return "success"
    }
}

💻 결과

endpoint가 tmi인 api 요청이 서버로 보낼 때 access token을 함께 보내는데 token이 만료되었을 경우 401 에러가 뜨게된다. 이때 refresh token을 보내게 되는데 refresh token이 정상적으로 서버로 잘 보내지는 것을 확인할 수 있었다.

0개의 댓글