[친구하자] 크로스 도메인 환경에서 Refresh Token 쿠키 소실 문제 #2
in ProjectDiary
백엔드, 프론트 도메인이 다른 크로스 도메인의 Production 환경에서 Refresh Token이 사라지는 문제에 대해 정리해보았습니다!
크로스 도메인 환경에서 Refresh Token 쿠키 소실 트러블슈팅 2
저번 편에서 해결까지는 다뤘지만, 그로 인해 발생하는 CSRF 공격에 대한 약한 방어를 방지하는 로직에 대해서는 이번 편에서 다루기로 한다. (너무 길어져서..)
CSRF 공격 방어
SamSite=None으로 하면 CSRF 공격을 방어할 수 없다. 따라서 추가 설정을 해줘야 한다.- 방어 방법들은 아래와 같다.
1. CSRF 토큰 패턴
// SecurityConfig.java
@Configuration
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
return http
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
.csrfTokenRequestHandler(new SpaCsrfTokenRequestHandler())
)
// ... 나머지 설정
.build();
}
}
// 프론트엔드
const csrfToken = getCookie("XSRF-TOKEN");
axios.post("/api/v1/users/delete", data, {
headers: {
"X-XSRF-TOKEN": csrfToken, // CSRF 토큰 포함
},
});
- 이렇게 하면 악의적인 사이트는
- JavaScript로 쿠키를 읽을 수 없음
- 따라서 헤더에 토큰을 포함할 수 없음
2. Origin/Referer 검증
- 서버에서 요청의
Origin또는Referer헤더를 확인하여 신뢰할 수 있는 도메인에서 온 요청인지 검증
// CorsConfig.java
@Bean
@Profile("prod")
public CorsConfigurationSource prodCorsConfigurationSource() {
CorsConfiguration configuration = new CorsConfiguration();
// ✅ 허용된 출처만 명시
configuration.setAllowedOriginPatterns(Arrays.asList(
"https://chingoo-frontend.vercel.app",
"https://www.chingoo-frontend.vercel.app"
));
configuration.setAllowCredentials(true);
// ...
}
이렇게 하면
DELETE https://your-backend.com/api/v1/users/delete
Origin: https://evil.com ← 이 값으로 검증!
Cookie: refreshToken=...
- 백엔드가
Origin헤더 확인:evail.com은 허용 목록에 없음- CORS 오류 발생으로 요청 차단
- 이 방법의 단점이 있다.
- Origin 헤더를 조작할 수 있는 환경에서는 우회 가능
- 브라우저가 아닌 요청(Postman, curl)에서는 검증 안됨
3. JWT의 이중 검증
- 가장 현대적인 방법이다.
// JwtAuthenticationFilter.java
@Override
protected void doFilterInternal(HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) {
// 1️⃣ 쿠키에서 RefreshToken 추출
String refreshTokenFromCookie = extractTokenFromCookie(request);
// 2️⃣ 헤더에서 AccessToken 추출
String accessTokenFromHeader = extractTokenFromHeader(request);
// 3️⃣ 둘 다 검증
if (isValidToken(refreshTokenFromCookie) &&
isValidToken(accessTokenFromHeader)) {
// 4️⃣ 토큰의 userId가 일치하는지 확인
Long userIdFromRefresh = getUserIdFromToken(refreshTokenFromCookie);
Long userIdFromAccess = getUserIdFromToken(accessTokenFromHeader);
if (userIdFromRefresh.equals(userIdFromAccess)) {
// ✅ 인증 성공
SecurityContextHolder.getContext()
.setAuthentication(createAuthentication(userIdFromAccess));
}
}
filterChain.doFilter(request, response);
}
- 쿠키(Refresh Token)이 자동으로 포함됨
- 하지만 해더(Access Token)는 JavaScript로 명시적으로 설정해야함
- 악의적인 사이트는 정상 사이트의 localStorage에 접근 불가 (Cross-Origin)
- 따라서 Access Token을 얻을 수 없음
4. SamSite + CSRF 토큰 조합
@Configuration
public class SecurityConfig {
@Bean
@Profile("prod")
public SecurityFilterChain prodFilterChain(HttpSecurity http) {
return http
// CSRF 토큰 활성화
.csrf(csrf -> csrf
.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse())
)
// CORS 설정 (Origin 검증)
.cors(cors -> cors.configurationSource(corsConfigurationSource))
// ...
.build();
}
}
# application-prod.yml
app:
cookie:
secure: true
same-site: None # 크로스 도메인 지원
max-age: 2592000
- 이렇게 하면 다중 방어선이 만들어진다.
- Origin 검증 (CORS)
- CSRF 토큰 검증
- JWT 검증