반응형
JWT를 이용해 인증, 인가를 구현하는 방법에 대해 설명한다.
로그인
- 로그인하기
- 클라이언트에서 아이디, 패스워드 전달
- 서버에서 패스워드 암호화 후 DB에서 아이디, 패스워드 검증
- 서버에서 Access Token, Refresh Token 토큰 발행
- Access Secret Key, Refresh Secret Key 각각 다르게 생성
- 서버에서 Refresh Token DB 저장
- 추후 서버에서 강제 로그아웃하기 위해 필요
- 서버에서 Response 쿠키에 HttpOnly 사용상태로 리프레시 토큰 저장 후 클라이언트로 보내기
- 서버에서 클라이언트로 Access Token 값 보낸후 클라이언트에서 메모리(Redux) 저장
권한 필요한 API 통신
일반적으로 토큰은 요청 헤더의 Authorization 필드에 담겨서 보내지게 되는데 JWT에 해당하는 type은 Bearer입니다 → Authorization: <type> <credentials>
Global Guard에 Guards를 적용시키고 필요하지 않은 건 Public Decorator로 Pass하게 한다 (로그인, 회원가입 기능 등…)
Admin만 호출할 수 있는 게 있으면 Role에 담아서 보낸다
- 클라이언트에서 요청시 헤더에 Authorization: Bearer ${Access Token}을 붙여서 Access Token과 함께 API 요청을 하고 쿠키의 Refresh Token 내용을 같이 보낸다.
- 서버에서 Access Token을 검증하며 정상일 땐 그대로 처리한다 비정상일 땐 Access Token 만료 시나리오대로 진행한다.
- Refresh Token이 만료된 경우는 Refresh Token 만료 시나리오대로 한다.
권한에 따른 페이지 접근 처리
- Jwt의 payload 데이터에 해당 사용자의 역할 정보를 넣어서 암호화 시킨다.
- Access Token 발급 받을 때 Header에 Role 정보를 보낸다
- Role정보에 따라 페이지를 달리 처리한다
- Next.js의 경우 admin이라는 라우팅하나 만들고 layout안에 검증식을 넣어놨다
Access Token 만료
잘못된 토큰 요청인지 만료되었는지를 클라이언트에게 보여줄 필욘 없기 때문에 유효하지만 않는지 체크만 하면 된다.
- 클라이언트에서 서버에 HTTP 요청을 보낸다
- 모든 요청에는 Access Token은 Bearer에 담아서 보내고 Refresh Token은 쿠키에 담아 보낸다
- Access Token가 유효하지 않을 때 쿠키에 담아서 같이 보낸 Refresh Token 값으로 검증 후 정상일 경우 Access Token을 재발급하고 요청한 HTTP통신을 처리하며 Access Token값을 클라이언트에 Response Header로 보낸다
- 조회 버튼 눌렀는데 발급만 해서 보내면 다시 또 조회버튼을 눌러야하니 사용자가 불편할 수도 있으니 이렇게 처리한다. 또한 HTTPS통신을 사용하면 Response Header값이 암호화 되어서 중간자 탈취에도 안전하다
- 클라이언트에서는 Fetcher의 Response에 Access Token키의 값이 왔을 때 Redux에 저장한다
Refresh Token 만료
Refresh Token이 만료된 경우 일반적으로 보안상의 이유로 사용자에게 재로그인을 요청하는 것이 일반적인 접근입니다.
재로그인을 통해 새로운 Access Token과 Refresh Token을 발급받는 것이 안전합니다.
- 클라이언트에서 Access Token이 만료되었을 때 쿠키에 담아서 보낸 Refresh Token에 대한 검증을 한다. 정상일 경우 Access Token을 재발급하지만 비정상인 경우 다시 로그인 하게끔 해야한다.
- 서버에서는 Cookie에 담긴 Refresh Token을 지운다.
- HTTPS통신으로 Header에 Refresh Token이 만료 되었다는 값을 담아서 보낸다.
- HTTPS통신이라 암호화가 되어서 중간 탈취에 안전하다.
- 클라이언트는 Fetcher를 통해 Header에 만료되었다는 값이 들어오면 체크해서 Redux에 있는 Access Token을 초기화시키고 Login페이지로 이동시킨다.
자동 로그인
- 로그인 여부가 true이면 아래 내용 진행 (쿠키 저장을 브라우저 영역으로 저장한다)
- 페이지 처음 들어오면 Refresh Token이 존재하는 경우 같이 보낸다
- 서버에서 Refresh Token이 정상인 경우 Access Token 만료 시나리오를 따라간
- 서버에서 Refresh Token이 비정상인 경우 Refresh Token 만료 시나리오를 따라간다
새로고침시 로그인 유효 / 로그인 후 사용 가능한 페이지 접근
- 로그인 여부가 true이면 아래 내용 진행 (자동 로그인이 아니면 로그인시 세션 영역으로 만들어짐)
- 페이지 처음 들어오면 Refresh Token이 존재하는 경우 같이 보낸다
- 서버에서 Refresh Token이 정상인 경우 Access Token 만료 시나리오를 따라간다
- 서버에서 Refresh Token이 비정상인 경우 Refresh Token 만료 시나리오를 따라간다
로그아웃 (버튼)
- 서버에서 Refresh Token값이 있는 쿠키를 삭제한다.
- 클라이언트에서 메모리에 저장된 Access Token 삭제한다.
- 로그인 페이지로 이동시킨다.
강제 로그아웃
- 클라이언트는 항상 Refresh Token값이 정상인지랑 DB값하고 같은지 체크해야한다
- 블랙리스트 방식도 존재하지만 시간이 지나면 쓸모 없는 데이터(어차피 유효기간 지나서 인증 안 되는 JWT 등…)을 주기적으로 삭제해야하는 번거로움이 있기 때문에 선택 안 함
- 인메모리를 사용하면 더 효율적이긴하다
- Token으로 클라이언트에게 주체가 넘어간 경우 서버에서 토큰을 폐기시킬 수 있는 방법은 쿠키로 보낸 Refresh Token값하고 DB에 저장된 Refresh Token값하고 같은지 체크해야한다
- 로그인시 Redis 같은 인메모리DB에 token 정보를 저장하고, 로그아웃시 Redis에서 token 정보를 지움으로써 로그아웃 여부를 확인할 수 있다
소셜로그인
passport.js로 구현한다
- passport docs에 있는대로 처리하고 eamil정보를 받아서 따로 회원가입을 진행시키던가 강제로 회원가입 시켜서 회원정보정도는 관리해야한다
- 소셜로그인을 클릭한 이후에 동작은 로그인 프로세스와 동일하게 한다
- 서버에서 Access Token, Refresh Token 토큰 발행
- 서버에서 Refresh Token DB 저장
- 서버에서 Response 쿠키에 Http Only 사용상태로 리프레시 토큰 저장 후 클라이언트로 보내기
- 서버에서 클라이언트로 Access Token 값 보낸후 클라이언트에서 메모리(Redux) 저장
Access Token 탈취
- 메모리에 저장하기 때문에 사용자가 직접 주지 않는 이상 탈취되진 않는다.
- 기간을 짧게 잡아서 탈취되더라도 못 쓰게한다
- 메모리에 담기 때문에 XSS나 CSRF 방지가 가능하다
- HTTPS를 통해 패킷 탈취되어도 암호화가 되어서 안 보이게 된다
Refresh Token 탈취
- 사용자는 리프레시 토큰을 쿠키에 저장해서 보낸다. (Http only, Secure 보안 사항 등록)
- 리프레시 토큰은 DB에 저장한다
- JWT 장점은 클라이언트가 인증 권한을 관리해서 서버에서 로그아웃 시킬 수 있는 방법이 없는데 DB나 Redis에 Refresh Token값을 저장하고 Refresh Token 검증시 저장값 하고 다르면 로그아웃시켜버리게 한다
XSS 방어로 쿠키 탈취 막기
- XSS 방어 관련된 라이브러리 (Express의 Helmet 등…)
- 웹 방화벽
- Cookie의 Http Only 사용
CSRF 막기
- SameSite설정 → 브라우저 호환 문제 발생으로 사용하기 힘듬
- 쿠키에 인가 정보 넣지 않기 → 메모리 보관 (Private 변수, Redux)
🔗 참고 및 출처
https://inpa.tistory.com/entry/AXIOS-📚-CORS-쿠키-전송withCredentials-옵션
https://velog.io/@cada/토근-기반-인증에서-bearer는-무엇일까
반응형