문제
쿠키에 JWT 를 저장하고, 토큰값 기반 로그인을 구현 해 두었다. 그런데 Chrome 이나 Firefox, Opera 등 다른 브라우저에서는 다 문제 없이 작동하는데 유독 사파리에서만 동작이 안된다.
Network를 확인 해 보면, 로그인 성공시 정상적으로 Set-Cookie가 내려 온다.
그런데 저장된 쿠키를 확인 해보면
방금 저장하도록 한 쿠키값이 저장이 되어 있지 않다.
한가지 특이한건, localhost가 아닌 운영중인 서버에서는 Safari 에서도 쿠키가 정상적으로 저장되었다는 것이다.
원인
일단 용의자는 아래와 같이 잡고 몇가지 테스트를 진행해 보았다.
- domain
- SameSite
- Secure
- 제일 먼저 Domain을 확인 해 봤는데, 기존에는 위 스샷처럼 Response Cookie에 Domain은 따로 명시를 하지 않고 있었기 때문
하지만 도메인은 localhost를 정확히 기입한다고 해서 해결되지는 않았다.
- 다음으로 SameSite 도 변경해봤는데, NONE, LAX, STRICT 모두 소용 없었다.
- Secure 가 범인이었다.
위에서 정상적으로 저장된 Cookie 목록을 확인 해 보면, SameSite는 각자 다르지만 공통적으로 Secure 옵션이 꺼져있는데
지금은 아래와같이 쿠키를 저장할 때 Secure 옵션을 true로 하고 있었음. 그래서 세 중 Secure 옵션이 꺼져 있는 rememberMe 쿠키값만 저장이 되고 있었음.
해결
해결1
쿠키를 저장할 때 secure를 빼고 테스트 해 보니
JwtCookie.kt
val jwtCookie = ResponseCookie.from("SESSION", token)
.httpOnly(true)
.path("/")
// .secure(true)
.maxAge(jwtConfig.tokenValidityInSeconds)
.sameSite(SameSite.STRICT.name)
.build()
쿠키값이 정상적으로 저장이되며 로그인도 문제 없이 진행 되었다.
Secure 없이 SESSION 토큰 저장
정상적으로 저장 완료
원인을 알고 나니 도메인을 통해 접속 했을때는 아무런 문제가 없고, 유독 개발 환경에서만 이슈가 발생 했던 것이 이해가 되었다.
하지만 운영 환경에서는 여전히 민감한 정보를 담고 있는 쿠키가 secure 되기를 원하기 때문에 이렇게 마무리를 지을 수는 없으니 해결을 이어나가보자.
해결2
일단 한 가지 특이한거는 이 현상이 유독 Safari 에서만 일어난다는 것.
이를 토대로 볼 때, 원칙상 Secure 쿠키는 HTTPS 에서만 저장이 되어야 하지만, localhost는 개발 편의를 위해 예외로 취급해서 대부분의 브라우저에서 허용을 해주고 있는 것으로 보인다.
Cookie secure - consider to allow secure cookies for localhost 이슈를 확인 해보면, 크롬에서도 예전에는 허용을 해주지 않았지만 불편함으로 인해 여러가지 제안이 있었던 것 으로 보인다.
https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie
mozilla 공식 문서에도 localhost는 예외로 취급 한다고 정확히 작성 되어 있다.
IE가 공식적으로 세상에서 퇴출된 (국내에서는 아직도 IE 호환을 요구하는 기관이 많다..) 이 시점에 Reddit 등지에서는 Safari가 new IE 라고 불리고 있다.
https://www.safari-is-the-new-ie.com/ 심지어 이런 사이트도 있다.
어쨌든 Safari로 localhost에서도 테스트 하고 싶다면 개발환경에서는 secure 옵션이 false 로 들어가게끔 코드를 작성 하는 방법이 있다.
서버에서의 Profile 정보 혹은 Property를 확인 해서 개발환경일 경우에는 false 옵션을 넣는 방법인데, 쿠키의 Secure 옵션을 꼭 사용해야 하고 Safari의 localhost 에서 쿠키 사용의 테스트가 꼭 필요하다면 관련 코드를 추가로 작성 해서라도 해결을 해야한다.
코드 작성은 그렇게 어렵지 않은데, 스프링 부트 기준으로 server.ssl.enable
옵션이 개발/운영 환경을 다르게 하고 있어서 아래와 같이 secure 여부에 따라 쿠키 설정시에도 변경되도록 하였다.
AuthController.kt
@RestController
class AuthController(
...
@Value("\${server.ssl.enabled}") private val isSecure: Boolean
) {
...
@PostMapping("/login")
fun login(): ResponseEntity<String> {
...
val jwtCookie = ResponseCookie.from("SESSION", token)
.httpOnly(true)
.path("/")
.secure(isSecure)
.maxAge(jwtConfig.tokenValidityInSeconds)
.sameSite(SameSite.STRICT.name)
.build()
return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, jwtCookie.toString())
}
}
해결3
테스트 하면서 발견한 또 다른 특이점은, SameSite를 None, Lax, Strict 등으로 설정하지 않고 애초에 빈칸으로 넣어 두면 localhost에서도 Secure 쿠키를 저장 할 수 있었다. 사실 이 방법은 일종의 꼼수라고 생각하는데 https://shanepark.tistory.com/349 에서 한번 겪었던 상황.
애초에 SameSite 옵션을 쓰지 않고 있었다면 이런 문제가 발생하지도 않았을 것이다. 심지어 jakarta.servlet.http.Cookie
클래스에는 SameSite 옵션을 설정할 방법도 없기 때문에 해당 옵션을 주고 싶다면 response.addCookie
대신 아래처럼 코드를 작성해줘야 한다.
val sessionCookie = ResponseCookie.from("SESSION", jwt)
.httpOnly(true)
.path("/")
.secure(isSecure)
.maxAge(tokenValidityInSeconds)
.sameSite(SameSite.STRICT.name)
.build()
response.addHeader(HttpHeaders.SET_COOKIE, sessionCookie.toString())
다만 SameSite 정책 자체가 도입된지 오래 되지 않았고, 비워둔 상황에 대해 각 브라우저별로 언제까지 용납해줄지도 불투명하다.
개인적으로는 모든 보안옵션을 다 켜고 secure 옵션만 환경에 따라 다르게 넣어 해결하는게 가장 이상적인 방법이라고 생각한다.
References
'Development > Daily Error' 카테고리의 다른 글
Spring Boot 3 에서 MYSQL 의존성 못찾는 경우 (0) | 2023.04.02 |
---|---|
NVM 설치 후 터미널이 느리게 뜨는 문제 해결 (0) | 2023.03.18 |
[일간에러] Different lower_case_table_names settings for server ('2') and data dictionary ('0'). (0) | 2023.01.07 |
Cannot find a (Map) Key deserializer for type 해결 (0) | 2022.11.17 |
[iRods] 데이터 오브젝트가 포함된 리소스 삭제하기 CAT_RESOURCE_NOT_EMPTY (0) | 2022.10.17 |