Spring Boot 프로젝트에서 회원가입 및 로그인 이후 JWT 토큰을 사용해서 인증/인가 절차를 수행하려고 하는데, 해당 절차를 간단히 정리하고자 한다.
인증 vs 인가
- 인증: 유저가 누구인지 확인하는 절차, 회원가입하고 로그인 하는 것.
- 인가: 유저에 대한 권한을 허락하는 것. 인증 이후의 프로세스
HTTP의 stateless 특징
웹 사이트는 HTTP 통신 위에서 동작한다. 따라서 웹 사이트 내의 모든 요청과 응답은 stateless한 특성을 가진다. 즉, Stateless 구조에서 서버는 단순히 요청이 오면 응답을 보내는 역할만 수행하며, 상태 관리는 전적으로 클라이언트에게 책임이 있는 것이다.
즉, 클라이언트와 서버간의 통신에 필요한 모든 상태 정보들은 클라이언트에서 가지고 있다가 서버와 통신할때 데이터를 실어 보내는 것이 stateless 구조이다.
장점
- stateless는 상태를 보관하지 않으므로 클라이언트의 요청에 어느 서버가 응답해도 상괸이 없다. 따라서 클라이언트의 요청이 대폭 증가해도 서버를 증설해 해결할 수 있다.
- 서버에서는 단순히 클라이언트의 요청 정보만 받아서 처리하면 되므로 서버의 부하가 줄어든다.
단점
- HTTP의 stateless라는 특성을 인증과 함께 생각해보면 로그인을 통해 인증을 거쳐도 이후 요청에서는 이전의 인증된 상태를 유지하지 않게 된다. 이러한 상황에서 웹 사이트를 이용하려면 인증/인가가 필요한 모든 상황에서 사용자는 반복적으로 ID/PW를 입력해야 하는 번거로움이 발생한다.
인증 구현
- Cookie / Session / Token 이 세 가지 방식 중 하나로 인증/인가를 구현하는 것으로 보이는데, Token(JWT)를 사용해서 구현하는 것을 결정함.
- Token은 세션과는 달리 서버가 아닌 클라이언트에 저장되기 때문에 메모리나 스토리지 등을 통해 세션을 관리했던 서버의 부담을 덜 수 있다. 토큰 자체에 데이터가 들어있기 때문에 클라이언트에서 받아 데이터가 이상이 없는지 확인만 하면 된다.
- Access Token과 Refresh Token을 사용하여 관리하며, Refresh Token의 경우 AWS Elastic Cache(Redis)에 저장하기로 하였으며, 기존에 사용 중이던 MySQL 대신 Redis를 굳이 사용한 이유는, expire 명령어를 사용하여 만료 시간을 설정할 수 있기 때문이다. https://redis.io/commands/expire/
- Access Token은 만료 시간을 1일, Refresh Token은 만료 시간을 7일로 설정.
- 상세 절차는 아래 그림과 같다.
12번에서, Access token과 Refresh token을 동시에 보내지 않고, Refresh token만 보내도록 변경- 9~13번 과정을 간소화시키기 위해, 9번에서 데이터를 요청할 때, Access Token, Refresh Token을 동시에 보내도록 변경
HTTP Authorization header 결정: Bearer or Etc
Http header example]
GET /resource HTTP/1.1
Host: server.example.com
Authorization: Bearer eyJhbGciOiJIUzI1NiIXVCJ9TJV...r7E20RMHrHDcEfxjoYZgeFONFh7HgQ
- 기존에는 Authorization header type에 Bearer를 사용하려고 하였으나, access token과 refresh token 값을 모두 담아야 하므로, Bearer type 하나로는 부족하다는 생각이 들었다.
- 아래 stackoverflow 참고 링크와 Chatgpt 답변을 참고하여, 그림의 12번에서
공백으로 access token과 refresh token을 구분하여 보내려고 하였으나, 그 대신Refresh Token만 서버로 전달하는 방식으로 변경Access Token과 Refresh Token을 공백으로 분리해서 함께 보내는 방식으로 변경
상세 절차
- Google, Kakao OAuth를 통해 로그인 요청이 온다. 이미 회원가입 된 사용자의 경우, Access Token과 Refresh Token을 ResponseBody로 보낸다.
- 클라이언트에서 요청을 보낼 때, Access token과 Refresh token을 Header에 담아서 같이 보낸다. Authorization: Bearer [Access Token] [Refresh Token]
- 클라이언트에서 요청이 들어오면, Access Token이 유효한지 JwtAuthenticationFilter에서 검사한다.
그런데, JwtAuthenticationFilter에서, Token으로 들어온 값이 Access Token인지, Refresh Token인지 어떻게 알 수 있을까?https://wildeveloperetrain.tistory.com/245 : 여기서 아이디어를 얻어서, Refresh Token의 경우 요청 url을 변경하는 것도 고려하였으나,그보다는 jwt 생성시 subject 값을 다르게 설정한 뒤, Token 값을 검증할 때 subject의 값을 확인하도록 함.- Filter의 경우, @Component를 붙여서 사용하지 말고, WebConfig 등의 파일을 만들어서 @Bean으로 등록해서 관리하는게 좋다. 특히 Filter가 여러 개인 경우 알아보기가 쉽다.
- 여기서 4가지 Case가 존재한다.
- Access Token 유효, Refresh Token 유효
- Access Token 유효, Refresh Token 만료: 가장 애매한 케이스
- Access Token 만료, Refresh Token 유효
- Access Token 만료, Refresh Token 만료
- https://devtalk.kakao.com/t/refresh-token/19265 참고하면, 아래 내용과 같다. 즉, 아래 내용처럼 구현한다면 위에서 가장 애매한 2번 케이스를 생각하지 않아도 된다.
access_token은 발급 받은 후 12시간-24시간(정책에 따라 변동 가능)동안 유효합니다. refresh token은 한달간 유효하며, refresh token 만료가 1주일 이내로 남은 시점에서 사용자 토큰 갱신 요청을 하면 갱신된 access token과 갱신된 refresh token이 함께 반환됩니다.
Case 분리
- Access Token과 Refresh Token 만료시간을 각각 1시간, 14일로 설정한다.
- 매 요청 시 Access Token의 유효성과 함께, Refresh Token의 만료 날짜를 검토한다.
- 만약 Refresh Token의 만료 날짜가 4일 전이라면, Refresh Token도 갱신해서 Access Token과 함께 클라이언트로 응답한다.
Case 1: Header Authorization 값이 잘못된 경우
403 Forbidden
- Header의 Authorization 값이 존재하지 않거나, Authorization에 Bearer 타입이 아닌 경우
- 요청이 진행되지 않고, 아래 json 데이터가 응답 값으로 클라이언트로 보내진다.
{
"status": 403,
"message": "Header의 Authorization 값이 존재하지 않거나, Header의 Authorization 값이 두 개가 아닙니다."
}
403 Forbidden
- Authorization에서 Bearer 값이 2개(Access token, Refresh Token) 이 아닌 경우, 혹은 공백으로 분리되어 있지 않은 경우
- 요청이 진행되지 않고, 아래 json 데이터가 응답 값으로 클라이언트로 보내진다.
{
"status": 403,
"message": "Authorization의 Bearer 값이 두 개가 아니거나, 전송 형식이 잘못되었습니다"
}
Case 2: Access Token, Refresh Token이 모두 유효한 경우
- Access Token 유효, Refresh Token 유효하면(만료 날짜가 4일보다 많이 남았다면), JwtAuthenticationFilter를 통과한다.
Access Token 유효, Refresh Token이 유효하지만 만료 날짜가 4일보다 적게 남은 경우, Access Token과 Refresh Token을 다시 응답 메시지로 보낸다.이 부분은 Case 3-2에서 처리하므로, 고려할 필요 없다.- Access Token이 유효한데 Refresh Token이 만료된 경우는 존재하지 않는다. (Refresh Token의 만료 날짜도 검토해서, 갱신하기 때문에)
Case 3: Access Token이 만료된 경우
- Access Token이 만료되었으나, Refresh Token이 유효한 경우 + Refresh Token의 만료 기간이 4일보다 많이 남은 경우: Access Token 만 재발급해서 응답
- Access Token이 만료되었으나, Refresh Token이 유효한 경우 + Refresh Token의 만료 기간이 4일보다 적게 남은 경우: Access Token, Refresh Token 모두 재발급해서 응답
- Access, Refresh Token 모두 만료 => 로그아웃 처리
- 만약 Access Token은 만료, Refresh Token이 만료되지는 않았으나 변조된 상황이라면?
- 이 경우 강제 로그아웃 처리를 한다.
그 외 고려한 사항들
- Jwt 토큰 검증에서, Filter와 Interceptor 중 어떤 것을 사용해서 인증 처리를 구현할지 고민하였으나, Filter의 경우 interceptor와 다르게 request의 값을 건드릴 수 있으므로, Filter를 사용하기로 하였다.
- 또한 토큰 검증 외에도, Guest / User 간에 접근할 수 있는 url 범위를 제한할 필요가 있기 때문에, 마찬가지로 Filter에서 먼저 처리하는 것이 적절하다고 생각됨.
로그아웃
클라이언트에서 관리하는 JWT 제거하기
JWT 데이터는 클라이언트의 storage(쿠키 등)에 저장이 된다. 따라서 프론트엔드 storage에 있는 JWT를 clear 하면 된다. 하지만, 이 방법의 단점은 만약 유저가 토큰을 미리 카피를 했다면 계속해서 서버에 요청을 보낼 수 있다. 따라서 서버에서 블랙리스트 처리를 해야 한다.
서버에서 처리하기
- 사용자가 로그아웃을 하는 경우에는, Refresh Token을 clear한다.
- Redis 블랙리스트에 Access Token, Refresh Token 값을 등록한다.
- 서버에서는 Redis의 Refresh token을 expire 처리하며, 클라이언트는 token 데이터를 삭제한다.
참고 링크
HTTP의 stateless 특징
HTTP Authorization header 결정: Bearer or etc
- https://stackoverflow.com/questions/33265812/best-http-authorization-header-type-for-jwt
- https://velog.io/@hyex/HTTP-Authorization-header에-Bearer와-jwt-중-무엇을-사용할까
- [Spring/Security/JWT] JWT를 사용한 로그인 및 Refresh Token을 활용한 로그인 상태 유지: https://hungseong.tistory.com/67
Redis
로그아웃
1. Redis 로그아웃 절차 설명: https://velog.io/@joonghyun/SpringBoot-Jwt%EB%A5%BC-%EC%9D%B4%EC%9A%A9%ED%95%9C-%EB%A1%9C%EA%B7%B8%EC%95%84%EC%9B%83
2.
기타
2. https://velog.io/@cada/토근-기반-인증에서-bearer는-무엇일까
4. [Spring] 필터(Filter)가 스프링 빈 등록과 주입이 가능한 이유(DelegatingFilterProxy의 등장) - (2): https://mangkyu.tistory.com/221
'Spring Boot' 카테고리의 다른 글
Spring Boot] org.springframework.orm.jpa.JpaSystemException: ids for this class must be manually assigned before calling save() (0) | 2023.07.15 |
---|---|
Spring Boot] 채팅 시스템 설계 (0) | 2023.07.11 |
Spring Boot] java.lang.IllegalArgumentException: Key argument cannot be null (0) | 2023.07.08 |
Spring Boot] NCP Chatbot과 연동하기-기초 (0) | 2023.07.06 |
Spring Boot] OncePerRequestFilter 에서 json으로 응답 결과 보내기 (0) | 2023.07.02 |