0. 시작하기 전에 간단 개념 정리
- AT(Access Token): 사용자 식별을 위해 요청에 포함하는 토큰으로, 만료기간을 짧게 한다.(ex. 30분~1시간) 만약 탈취 되더라도 금방 만료되기 때문
- RT(Refresh Token): AT를 재발급하기 위한 용도로, 만료기간이 길다(ex. 2주~1달 정도)
- AT를 갱신할 때만 서버에 전송되기 때문에 유출의 위험에선 안전한 편이라고 함.
- RTR(Refresh Token Rotation): RT를 가지고 AT를 재발급 받을 때, 이 RT도 무효화하고 함께 재발급하는 것. 만약 RT를 탈취당했을 경우 해커가 이 RT로 AT를 재발급하여 RT 만료기간동안 데이터 접근하는 것을 방지하기 위함
1. React.js
토큰을 어디에 저장하는 게 좋을까?
(1) 로컬스토리지
- 브라우저를 닫아도 유지됨
- XSS(Cross-Site Scripting) 공격에 취약
- 단, React는 기본적으로 XSS 공격을 방지하는 설계가 적용되어 있다고 함(자동 이스케이프 처리)
- 자동 이스케이프: 특수 문자를 본래의 기능이 아닌 '단순 문자'로 해석
그래서 dangerouslySetInnerHTML 같은 것을 사용해서 DOM에 직접 적용하지 않는 한 거의 안전하다고 볼 수 있음- 만약 프로젝트에서 사용하고 있는 서드파티 라이브러리에 취약점이 있다면 결국 XSS에 취약한 것은 그대로.
로컬스토리지 용량 제한에 걸릴 수 있음- Chrome 기준 5MB 정도라고 하기 때문에 대부분의 경우 문제가 되지 않을 것으로 보임
(2) 쿠키
- HttpOnly 플래그를 설정하면 XSS 이슈 해소됨
- JavaScript 코드로 쿠키에 접근할 수 없게 됨.
- 추가로, Secure와 SameSite 플래그를 설정하면 더 보안이 강화됨.
- Secure: HTTPS 프로토콜 상에서 암호화된 요청일 경우에만 전송됨.
- SameSite: 사이트 간 요청과 함께 쿠키를 보낼지 여부를 제어함. CSRF 공격을 방어하기 위해선 Lax나 Strict로 설정.
- Lax | Strict | None
- Lax(최신 브라우저들의 기본값): 같은 도메인이거나, 주소창이 바뀌는 GET 요청(외부 사이트에서 내 사이트 링크를 클릭하여 접속한 요청)에만 쿠키를 전송함
- Strict: 같은 도메인만 허용
- 이 경우 구글 검색 결과에서 내 사이트 링크를 클릭하여 들어올 경우 쿠키가 안 넘어감.
- None: cross-site에서도 쿠키 전송 가능. 그래서 Secure 옵션 추가 필수.
- 다만, HttpOnly 옵션이 있을 경우 JavaScript에서 쿠키 접근이 불가능하기 때문에 요청 로직에 직접 포함할 수 없음.
- 그래서 Cross-Origin으로 요청할 경우에는 백엔드와 추가적으로 상의해서 각자 헤더를 추가해야 함.
- 프론트엔드에서는 credentials: include 옵션을 추가하여 요청에 쿠키를 포함시키고,
- 백엔드에서는
- Access-Control-Allow-Origin: 요청하는 도메인
- Access-Control-Allow-Credentials: true
(3) 메모리 변수
- 메모리에만 저장되므로 상대적으로 XSS 난이도가 올라감.
- 페이지를 새로고침하면 토큰이 사라지고, 여러 탭이나 창에서 공유되지 않음
- BroadcastChannel을 사용하여 탭 간 AT를 공유하여 불필요한 refresh 남발 최소화할 수 있다고 함
- 또는 revalidateOnFocus 방식을 사용해서 탭이 포커스될 때마다 토큰 재검증을 한 번 하는 방법
- (아직 시도해보진 않았다. 구현의 복잡성이 존재함)
- 단, AT 재발급 요청(과 RT 무효화)이 너무 자주 일어날 수도 있음. 새로고침할 때마다 로그아웃됨.
참조: https://mori29.tistory.com/25
JWT 인증 시리즈 3편: Frontend에서의 JWT 구현 (React/Next.js)
안녕하세요! JWT 인증 시리즈의 세 번째 글입니다. [1편](https://mori29.tistory.com/23)에서는 JWT의 기본 개념과 구조에 대해 알아보았고, [2편](https://mori29.tistory.com/24)에서는 Node.js와 Express를 사용한 서버
mori29.tistory.com
만료기간이 짧은 AT는 메모리 변수에, 만료기간이 긴 RT는 HttpOnly 쿠키에 저장하는 것이 권장된다고 함
+) 또는 RT를 Redis에 저장하기도 한다는 것을 알게 됐다.
Redis는 휘발성 메모리를 사용하기 때문에 장애가 발생하거나 전원이 끊어질 경우 데이터가 손실될 수 있다. 이런 점에서 RDB가 더 안정적이라고 생각할 수 있다.
하지만 Redis와 RDB는 각각의 특징이 존재한다.
- Redis: In-memory 캐시 서버로서 빠른 속도가 강점.
- RDB: 영속적인 저장소로, 데이터의 안정성을 보장.
참조: https://trysolve.tistory.com/42
RTR 방식과 Refresh Token을 Redis에 저장하는 이유
Token과 RTR 방식AccessToken: 사용자가 특정 서비스에 접근할 권한을 증명하는 문자열이다. 사용자의 인증 정보를 확인한 후 발급되며, 사용자가 서비스를 이용할 때마다 이 토큰을 사용해 자신의 인
trysolve.tistory.com
리액트 환경에서 구현 방법 정리
- (이전 프로젝트에서 했던 방법)만료기간이 짧은 AT만 사용하기
- Nuxt.js + BE(Spring) 환경이었고, 백엔드에서 Response Body로 AT 내려줌(만료기간 30분. 수명이 짧은 AT라서 탈취되더라도 30분만 유효하다고 생각하여 그렇게 했으나 RT가 없기 때문에 보안에 좋지 않음. 잘못하면 무한하게 토큰이 갱신될 수 있음)
- Nuxt.js 서버에서 이를 set-cookie 하고(httpOnly, secure, sameSite 기본)
- API 요청 시 요청 헤더에 Bearer token 설정해서 백엔드에 보내고,
- 요청할 때마다 Nuxt.js 미들웨어 코드에서 AT 만료기간을 체크하고,
- 5분 미만으로 남았을 때, AT 갱신 요청을 백엔드한테 보낸다.
- 새로 받은 AT를 다시 set-cookie 해서 하려던 요청을 그대로 이어간다. 그러면 유저는 재로그인 할 필요없어짐(UX 개선)
- 이 방식의 단점은, 만약 해커가 AT를 탈취한 상황에서 api 호출을 지속한다면, AT가 알아서 갱신되므로 보안에 취약해진다.
- 그래서 간단한 미니 프로젝트에서나 적절하다.
- 백엔드에서 RT는 Set-Cookie, AT는 Response Body로 내려주는 방법
- 프론트에서 AT를 메모리 변수에 저장한 다음, API 요청 시 Request Header에 Authorization: Bearer AT로 보내기
그리고 이때 AT의 만료여부를 체크하여 만료된 경우, RTR 하여 새 AT를 요청 헤더에 적용하기- 프론트에서 exp 체크하는 것은 단순히 참고용일 뿐이고 완전히 신뢰할 순 없기 때문에 결국엔 백엔드에서 401 내려주는 게 정확함(시간 차이도 있을 수 있음)
- 그리고 백엔드에서 401을 내려주면 이 에러로 트리거해서 RTR 갱신하는 게 탈취 당하더라도 자동 갱신되지 않고 좋음
- 일반 API 요청 시 401 응답이 내려오면, 프론트에서 재발급 요청 API를 보내고, 다시 (1) 과정으로.
- 이때 401 응답을 가로채서 재발급 API 호출하는 로직을 적용해야 한다(사용자 화면에 401을 띄우지 않기 위해)
- 단, 이때 다수 요청이 동시에 401을 받으면 refresh가 중복 호출될 수 있음. 이미 refresh 중이면 대기하는 로직을 인터셉터에 추가
- 단, 재발급 API 요청 시에도 401이 내려오면 그때는 사용자 화면에 띄우거나, 다시 로그인 페이지로 이동시켜야 함.
- 정리하면, 일반 API 401 → refresh 시도
- → 성공 시 원래 요청 재시도
- → 401 에러 → 로그인 이동
- (OAuth 2.0과 OpenID Connect에서 표준적으로 권장하는 방법은 Bearer Token 방식)
- 단, 새로고침 하면 항상 로그인이 풀리고 RT가 있을 경우 AT 갱신 API를 요청해야 함.
- 백엔드에서 AT, RT 둘다 Set-Cookie로 내려주는 방법
- Bearer Token 방식을 사용할 수 없음. AT, RT 둘다 HttpOnly라서 리액트 JS 코드로 이 쿠키들에 접근할 수 없음.
- 프론트에서 API 호출 시 credentials: 'include'를 포함(동일 오리진일 경우는 불필요)
- AT가 만료된 경우, 백엔드에서 401을 내려주고, 프론트가 재발급 요청 API를 보내면 그때 백엔드가 다시 AT, RT를 Set-Cookie
- 마찬가지로 이때도 401 응답을 가로채서 재발급 API 호출하는 로직을 적용
- 이때도 다수 요청이 동시에 401을 받으면 refresh가 중복 호출될 수 있음. 이미 refresh 중이면 대기하는 로직을 인터셉터에 추가
- 이때도 401이 내려오면 그때는 사용자 화면에 띄우거나, 다시 로그인 페이지로 이동시켜야 함.
- 백엔드에서 AT, RT 둘다 Response Body로 내려주는 방법(Supabase 기본 방식)
- 프론트에서 이 둘을 localStorage에 저장하고, 프론트에서 전역 상태 관리 시스템을 도입해서 세션 관리하기
2. Next.js
(1) 기본 방식
- 백엔드에서 AT, RT를 Response Body로 내려줄 경우, Next.js 서버가 RT를 Set-Cookie하고 AT는 Response Body로 내려서 클라이언트에 두는 방식(넥스트 서버가 프록시 서버 역할을 하는 방식)
- Next Client - Next Server - BE (BFF패턴)
(2) Next-Auth를 사용하는 방식
'TIL*' 카테고리의 다른 글
| 브라우저 엔진, 자바스크립트 엔진 (0) | 2025.10.22 |
|---|---|
| 모노레포 프로젝트에 Turborepo 도입하여 빌드 최적화하기 (0) | 2025.10.13 |
| Next.js App Router에서 MSW 사용하기 (0) | 2025.09.16 |
| OAuth 소셜 로그인 redirect uri 관련 정리 (0) | 2025.09.07 |
| Prettier 우선순위 (0) | 2025.08.22 |