Spring에서 JWT를 사용한 인증/인가를 진행할 때, Server에서 Client로 AccessToken과 RefreshToken을 발행해줄 것이다. 이때 Cookie에 저장하는 방법과 로컬 스토리지에 저장하는 방법이 있는데, 이번 시간에는 두 방법의 장/단점을 알아보고 어떻게 사용하면 좋을지 고민해보자
Web Browser에 토큰을 저장할 때의 취약점
우리는 성능 향상과 확장성을 위해 세션 대신 JWT를 사용한다. 성능 향상은 메모리 사용량과 DB Connection을 줄일 수 있어서 그렇다 치지만, 확장성은 어떻게 보장해줄까?
그건 바로 서버에서 보내주는 토큰을 브라우저에 저장함으로써 서버에서 사용자의 인증 정보를 기억하지 않아도 되기 때문이다.
그러나 이러한 확장성 때문에 브라우저에 토큰을 저장할 때, 몇 가지 공격들에 취약하다는 단점이 있다. 어떤 공격들인지 알아보자
CSRF(Cross-Site Request Forgery)
- CSRF 공격은 해커가 유저의 의지와 무관하게 해커가 의도한 행동을 하도록 request를 위조하는 공격을 의미한다
- 이 공격은 브라우저에 저장된 쿠키가 서버로 보내는 모든 Request에 자동으로 포함되어 전달되는 특성 때문에 일어난다.
- 예시 상황을 통해 알아보자
- 해커는 유저가 이미지를 열람하도록 하거나 Link를 클릭하도록 유도한다
- 예를들어 게시글을 작성할 때, 이미지나 Link의 src에 해커가 의도한 Request URI를 입력한다
(ex. src = http://mysite.com/like?post=1234, 1234번 게시글에 좋아요를 누르는 요청) - 로그인을 해서 cookie에 access token이 설정되어있는 유저가 해커의 게시글을 조회한다
- 유저가 이미지를 조회하거나 Link를 클릭하면 src에 입력한 request가 보내지게 되고, request가 서버로 보내질 때 자동으로 쿠키에 저장된 유저들의 'access token'이 request에 포함된다
- 유저의 의지와 관계없이 해커의 게시물(1234번)에 좋아요가 올라간다
XSS(Cross-Site Scripting)
- XSS 공격은 게시판이나 웹 메일 등에 JS와 같은 Script Code를 삽입 해, 개발자가 고려하지 않은 기능이 작동하게 하는 공격을 의미한다
- 이를 통해 해커는 유저의 쿠키 정보나 로컬 스토리지에 저장된 값을 얻을 수 있다
LocalStorage? Cookie?
이제 위의 두 가지 공격 방법을 읽었다면 한가지 드는 생각이 있을것이다.
"로컬스토리지는 XSS에만 취약한데, 쿠키는 CSRF랑 XSS 모두한테 취약하네? 그렇다면 로컬스토리지를 써야하는거 아니야?"
이런 생각이 떠올랐다면 다음 글에서 '쿠키의 속성' 파트를 읽어보고 오길 바란다.
https://kdh0518.tistory.com/entry/Network-Cookie
1. Cookie에는 보안 속성이 있단다
쿠키에는 XSS 공격을 예방할 수 있는 'HttpOnly' 속성과, CSRF 공격을 예방할 수 있는 'SameSite' 속성이 존재한다.
HttpOnly
- HttpOnly 옵션을 설정하면 JS에서 쿠키에 접근 자체가 불가능해진다
- 따라서 JS Script Code에 의한 공격을 막을 수 있다
SameSite
- CSRF 공격 및 의도하지 않은 정보 유출을 막기 위한 옵션으로, 서로 다른 도메인간의 쿠키 전송에 대한 보안을 설정한다
- Strict나 Lax로 설정하면 어느정도의 CSRF 공격을 예방할 수 있다
따라서 쿠키에 두 가지 보안 옵션을 설정해줬다면 XSS와 CSRF를 어느정도 예방할 수 있다
2. 그래서 Cookie를 사용해라고?
그렇지 않다. Cookie에 HttpOnly를 설정하더라도 JS로 request를 보내면 토큰 값을 알 필요 없이 자동으로 담기기 때문에 위험하다(CSRF). 그렇다고 이를 예방하고자 SameSite를 사용한다면 Strict 전략은 사용자의 편의성을 많이 해치게 되고, Lax는 일부를 허용해주기에 완벽하게 막을 수 있다고 볼 수는 없다.
3. 어쩔 수 없이 Local Storage를 사용해야겠네?
물론 Local Storage가 CSRF 공격에는 안전하지만, XSS 공격에 너무나도 취약하다. 해커가 JS 코드 한 줄만 주입하면 Local Storage를 내 집 처럼 드나들 수 있다.
4. 그래서 뭐 어쩌란거지..?
명확한 정답은 없고 자신의 서비스와 인프라 환경에 따라서 적절한 방법을 사용하면 된다.
하지만 나는 열린 결말을 싫어하기에, 내가 찾아봤던 수 많은 블로그 들에서 추천하는 방법을 읽어보고, 나도 여러 사람들과 같이 고민하면서 제일 괜찮다 생각한 방법을 소개하고 마무리 짓겠다.
- Server에서 Client로 Token 전달
- Client에서 로그인 요청이 들어오고, id/pw가 유효하다면 클라이언트로 토큰을 전달해줘야 할 것이다
- 이때 Access Token은 ResponseBody에, Refresh Token은 Cookies에 담아서 전달한다
- Cookie에는 'HttpOnly, Secure, SameSite(Lax)' 옵션을 설정해준다
- HttpOnly = true 를 사용해서 XSS를 예방한다
- Secure = true 를 사용해서 HTTPS 프로토콜에서만 요청을 보낼 수 있게 설정한다
- SameSite = Lax 를 사용해서 몇몇 예외 사항을 제외하고 Cross-Site에서 쿠키를 사용할 수 없게 한다
- Client에서 토큰 저장
- 서버로부터 넘겨받은 Access Token은 in-memory에 저장한다. 즉, 프론트에서 특정 variable을 만들고 거기에 Access Token을 담는다
(ex. const accessToken = response.body.get("accessToken");)- 이 경우 유저가 페이지를 새로고침 하거나 사이트를 나간다면 곧바로 AccessToken이 사라지겠지만, 이를 위해 Cookie에 Refresh Token을 설정해주었으므로 전혀 상관이 없다
- 서버로부터 넘겨받은 Access Token은 in-memory에 저장한다. 즉, 프론트에서 특정 variable을 만들고 거기에 Access Token을 담는다
- Refresh Token을 사용해서 Access Token 재발급
- 만약 유저의 Access Token이 만료되었거나 사라졌다면, Request를 보낼 때 자동으로 쿠키에 Refresh Token이 포함되었을 것이므로 Access Token을 재발급 받을 수 있다
- 이때, Access Token을 재발급 받을 때 RTR(Refresh Token Rotation)를 적용해서 Refresh Token도 새로 발급해주면 훨씬 더 안전하게 사용할 수 있다
이상으로 Token 저장에 대한 깊은 고민을 해봤다. 항상 느끼는건데 개발에 은탄환은 없는 것 같다. 항상 자기 상황에 맞춰서 최선의 방법을 선택할 뿐..
혹시라도 잘못된 정보가 있을 수 있습니다. 피드백 주시면 감사하겠습니다 !
Reference
- JWT는 어디에 저장해야할까?: https://velog.io/@0307kwon/JWT%EB%8A%94-%EC%96%B4%EB%94%94%EC%97%90-%EC%A0%80%EC%9E%A5%ED%95%B4%EC%95%BC%ED%95%A0%EA%B9%8C-localStorage-vs-cookie
- CSRF란?/CSRF Filter 처리 방식: https://tlatmsrud.tistory.com/77
- LocalStorage vs Cookies: JWT 토큰을 안전하게 저장하기 위해 알아야할 모든 것: https://hshine1226.medium.com/localstorage-vs-cookies-jwt-%ED%86%A0%ED%81%B0%EC%9D%84-%EC%95%88%EC%A0%84%ED%95%98%EA%B2%8C-%EC%A0%80%EC%9E%A5%ED%95%98%EA%B8%B0-%EC%9C%84%ED%95%B4-%EC%95%8C%EC%95%84%EC%95%BC%ED%95%A0-%EB%AA%A8%EB%93%A0%EA%B2%83-4fb7fb41327c
- Refresh Token Rotation과 Redis로 토큰 탈취 시나리오 대응: https://junior-datalist.tistory.com/352