❓문제
Refresh Token을 http only 쿠키로 사용하려고 다음과 같이 서버 코드를 작성해서 배포했습니다.
/**
* 관리자 로그인 API
*/
@PostMapping("/api/admins/login")
public ResponseEntity<LoginAdminResponse> login(@RequestBody @Valid LoginAdminRequest loginAdminRequest) {
Jwt jwt = adminService.login(loginAdminRequest);
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, getRefreshToken(jwt).toString())
.body(AdminMapper.INSTANCE.toLoginAdminResponse(jwt));
}
private ResponseCookie getRefreshToken(Jwt jwt) {
return ResponseCookie.from("refreshToken", jwt.getRefreshToken())
.maxAge(jwtProperties.getRefreshTokenExpiration())
.path("/")
.secure(true)
.httpOnly(true)
.build();
}
프론트 어드민 'jazzmeet-admin.site'에서 서버 'jazzmeet.site'로 로그인 API 요청을 보낼 때 HTTP Response 에는 쿠키가 포함돼서 나가지만 브라우저의 쿠키에는 저장되지 않는 문제가 발생했습니다.
⁉️ 해결 과정
1. 프론트의 요청 코드에 설정 추가
프론트의 로그인 요청 코드에 credentials:include
옵션을 추가했습니다. 이때 CORS 문제가 발생해서 백엔드에서도 cors 설정에 프론트 어드민을 allowedOrigins로 설정하고 allowCredentials(true)
옵션을 추가했습니다.
@Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/api/**")
.allowedOrigins("https://www.jazzmeet-admin.site", "https://jazzmeet.site")
.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS")
.allowCredentials(true)
.allowedHeaders("*");
}
- addMapping : /api로 시작하는 모든 URL에 대해 CORS 정책이 적용됩니다.
- allowedOrigins : 지정된 출처(origin)의 요청만 서버가 수락하도록 허용합니다.
- 여기에 구체적인 출처를 명시하면 스프링은
Access-Control-Allow-Origin
헤더를 자동으로 만들어줍니다. 브라우저의 보안 정책에 따라,allowCredentials(true)
설정을 사용하면Access-Control-Allow-Origin
헤더에 구체적인 출처(와일드카드*
X)를 요구합니다.
- 여기에 구체적인 출처를 명시하면 스프링은
- allowedMethods : 허용할 HTTP 메서드를 지정합니다.
- OPTIONS: 서버가 지원하는 메서드를 조회할 때 사용되며, CORS preflight 요청에 대응하기 위해 필요합니다.
- allowCredentials : 크로스 오리진 요청 시 쿠키와 같은 인증 정보를 포함할 수 있도록 허용합니다.
- allowedHeaders : 설정한 HTTP 헤더를 요청에서 허용하겠다는 것을 의미합니다.
CORS 문제는 해결됐지만 여전히 쿠키가 저장되지 않았습니다.
2. 쿠키 생성 시 옵션 추가
쿠키 생성 시 sameSite("None")
, secure(true)
옵션을 추가해주었습니다.
크롬의 20년도 업데이트에서 same site 속성의 기본 값이 None
에서 Lax
로 변경되었을 알 수 있었습니다. 이 속성은 웹 사이트 간 요청 위조(CSRF, Cross-Site Request Forgery) 공격을 방지하는 데 도움을 줍니다. 이는 다른 도메인 간 요청에서 사용자의 데이터 보호를 강화합니다.
same site 속성에는 세 가지 설정이 있습니다.
- Strict: 쿠키는 오직 같은 사이트에서 발생한 요청에만 전송됩니다. 이는 가장 제한적인 옵션입니다.
- Lax: 더 유연하며, 사용자가 다른 사이트에서 링크를 클릭해 사이트에 접근했을 때 쿠키를 전송할 수 있습니다.
- None: 모든 크로스 사이트 요청에 쿠키를 전송하도록 합니다. 이 옵션을 사용하려면 반드시 쿠키를
Secure
로도 설정해야 합니다, 즉 HTTPS를 통해서만 쿠키가 전송됩니다.
프론트 어드민 jazzmeet-admin.site
와 서버 어드민 jazzmeet.site
는 아무런 연관이 없는 cross domain 이기 때문에 크로스 사이트 요청에도 쿠키를 요청할 수 있는 sameSite("None") 과 secure(true) 옵션을 추가해주었습니다.
그럼에도 여전히 브라우저의 쿠키에 저장이 되지 않고 있었습니다.
사실 여기서부터 조금 멘탈이 흔들리기 시작했습니다. 무슨 문제인지 알 수가 없어 몇 번 시도를 했지만 결국 문제는 2번과 5번의 시도로 해결할 수 있었습니다. 3, 4번은 이 문제 해결에 직접적인 영향을 주지 않았지만 일단 기록으로 남겼습니다.
3. 쿠키 생성 시 프론트 도메인을 명시
기본으로 옵션이 들어가는 도메인이 프론트 도메인과 달라 쿠키가 저장이 안 되는가 싶어서 쿠키 생성 시 domain("jazzmeet-admin.site")
옵션을 추가해주었습니다.
private ResponseCookie getRefreshToken(Jwt jwt) {
return ResponseCookie.from("refreshToken", jwt.getRefreshToken())
.maxAge(jwtProperties.getRefreshTokenExpiration())
.path("/")
.secure(true)
.httpOnly(true)
.domain("jazzmeet-admin.site")
.build();
}
그러나 여전히 프론트 어드민 브라우저에는 쿠키가 저장되지 않았습니다.
4. 프론트 어드민을 서버의 서브 도메인으로 변경
서버 도메인과 프론트 어드민 도메인이 완전히 다른 cross domain이라 쿠키가 저장되는 않는 것일 수도 있겠다고 생각하여 프론트 어드민 도메인을 서버의 서브 도메인 admin.jazzmeet.site
로 변경했습니다.
이때 우리의 프론트 어드민은 Amplify에 배포되어 있었기 때문에 Amplify의 도메인 관리에서 jazzmeet.site
를 루트 도메인으로 설정하고, 서브 도메인으로 admin.jazzmeet.site
를 추가한 후 www로 redirect 되는 옵션을 껐습니다. Amlify에서 발급된 키를 가비아의 jazzmeet.site DNS 관리에서 추가해주었고, admin.jazzmeet.site에 배포된 프론트 어드민 주소를 저장해주었습니다.
더해서 jazzmeet.site
를 포함한 서브 도메인들에서도 쿠키를 공유할 수 있도록 하기 위해 domain(".jazzmeet.site")
으로 옵션을 수정해주었습니다.
private ResponseCookie getRefreshToken(Jwt jwt) {
return ResponseCookie.from("refreshToken", jwt.getRefreshToken())
.maxAge(jwtProperties.getRefreshTokenExpiration())
.path("/")
.secure(true)
.httpOnly(true)
.domain(".jazzmeet.site")
.build();
}
여전히 쿠키는 브라우저에 저장되지 않았습니다.
5. 프론트의 로그인 요청에 옵션 추가
cross domain 상태라도 sameSite("None")과 secure(true) 옵션을 설정해주었다면 프론트 어드민을 서버의 서브 도메인으로 변경하지 않아도 정상적으로 쿠키가 공유되어야 했습니다. 그럼에도 계속해서 쿠키 저장이 되지 않아 프론트 코드를 살펴보았습니다.
여기서, 토큰 재발급 로직에만 credentials:'include'
옵션이 설정되어 있었고 로그인 요청 로직에는 설정되지 않았습니다. 이 옵션이 프론트에서 서버로 쿠키를 보낼 때에만 사용되는 줄 알았는데 서버에서 보낸 쿠키를 브라우저에 저장하기 위해서도 필요한 옵션이었습니다.
따라서 프론트 로그인 요청에 credentials:'include'
옵션을 추가해주어 성공적으로 쿠키를 공유할 수 있게 되었습니다.
❗정리
- cross domain 이라면
sameSite("None")
과secure(true)
옵션을 주고 프론트에서 쿠키를 보내거나 받는 요청에 'credentials:'include'`옵션을 설정하면 쿠키 공유가 가능합니다. - 같은 도메인이라면 same site는 기본 설정(Lax)으로 두고 프론트 코드에 'credentials:'indluce'` 옵션을 추가하면 쿠키 공유가 가능합니다.
⭐ 배운 점
브라우저의 업데이트는 관심을 가지고 꾸준히 살펴봐야 제때 대응할 수 있다는 점을 깨달았습니다. 또한 문제를 마주했을 때 담당한 분야 뿐만 아니라 다른 분야도 살펴봐야 문제를 빨리 해결할 수 있다는 것을 배웠습니다.
📜 참고
'project' 카테고리의 다른 글
[JazzMeet]Cookie & Session vs. JWT (0) | 2024.05.16 |
---|