이어달리기 프로젝트를 하면서 이번에 유저에 관한 모델 API 제작을 맡게 되었다. 그 중 스타트는 회원가입과 로그인이다.
GetIT 프로젝트 때는 내 담당이 아니었어서 JPA로 구현하는 건 처음이다.
이번에 직접 Spring Security를 적용해서 어떤 과정으로 적용되며 access, refresh token을 발급할 수 있는지 과정에 대해 알아볼 것이다!
보안성과 편의성 모두를 잡을 수 있는 Sliding Sessions 전략이 있는데, 이 전략은 세션을 지속적으로 이용하는 유저에게 자동으로 만료 기한을 늘려주는 방법이다. 이 방법으로는 프로젝트 중후순쯤 어느정도 마무리되고 다시 공부해서 적용해볼 예정이다.
이번 포스팅에서는 JWT와 token, 그리고 Spring security에 관한 이론적인 부분에 대해서 알아볼 것이다.
JWT란?
JWT(Json Web Token)란 Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token이다. JWT는 토큰 자체를 정보로 사용하는 Self-Contained 방식으로 정보를 안전하게 전달한다. 주로 회원 인증이나 정보 전달에 사용되는 JWT는 아래의 로직을 따라서 처리된다.
JWT 는 어떤 상황에서 사용될까?
- 회원 인증: JWT 를 가장 많이 사용하는 상황이다. 유저가 로그인을 하면, 서버는 유저의 정보에 기반한 토큰을 발급하여 유저에게 전달해준다. 그 후, 유저가 서버에 요청을 할 때 마다 JWT를 포함하여 전달한다. 서버가 클라이언트에게서 요청을 받을때 마다, 해당 토큰이 유효하고 인증됐는지 검증을 하고 유저가 요청한 작업에 권한이 있는지 확인하여 작업을 처리한다.
서버측에서는 유저의 세션을 유지 할 필요가 없다. 즉 유저가 로그인되어있는지 안되어있는지 신경 쓸 필요가 없고, 유저가 요청을 했을때 토큰만 확인하면 된다. 세션 관리가 필요 없어서 서버 자원을 많이 아낄 수 있다. - 정보 교류: JWT는 두 개체 사이에서 안정성있게 정보를 교환하기에 좋은 방법이다. 정보가 sign 이 되어있기 때문에 정보를 보낸이가 바뀌진 않았는지, 또 정보가 도중에 조작되지는 않았는지 검증할 수 있다.
Spring Security 인증과정
우선 Sprint security의 인증 과정은 위의 그림과 같다.
- Http Request가 서버로 넘어와서, 가장 먼저 AuthenticationFilter가 요청을 낚아챈다.
- AuthenticationFilter에서 Request의 Username, password를 이용하여 UsernamePasswordAuthenticationToken을 생성한다.
- AuthenticationManager가 토큰을 받는다.
- AuthenticationManager는 토큰을 AuthenticationProvider에게 토큰을 넘겨준다.
- AuthenticationProvider는 UserDetailsService로 토큰의 사용자 아이디(username)을 전달하여 DB에 존재하는지 확인한다. 이 때, UserDetailsService는 DB의 회원정보를 UserDetails 라는 객체로 반환한다.
- AuthenticationProvider는 반환받은 UserDetails 객체와 실제 사용자의 입력정보를 비교한다.
- 비교가 완료되면 사용자 정보를 가진 Authentication 객체를 SecurityContextHolder에 담은 이후 AuthenticationSuccessHandle를 실행한다.(실패시 AuthenticationFailureHandler를 실행한다.)
Spring Security Filter
스프링 시큐리티는 필터를 기반으로 수행된다.
필터와 인터셉터의 차이는 실행되는 시점의 차이이다.
- 필터는 dispatcher servlet으로 요청이 도착하기 전에 동작한다.
- 인터셉터는 dispatcher servlet을 지나고 controller에 도착하기 전에 동작한다.
- SecurityContextPersistenceFilter : SecurityContextRepository에서 SecurityContext를 가져오거나 저장하는 역할을 한다.
- LogoutFilter : 설정된 로그아웃 URL로 오는 요청을 감시하며, 해당 유저를 로그아웃 처리
- (UsernamePassword)AuthenticationFilter : (아이디와 비밀번호를 사용하는 form 기반 인증) 설정된 로그인 URL로 오는 요청을 감시하며, 유저 인증 처리
- AuthenticationManager를 통한 인증 실행
- 인증 성공 시, 얻은 Authentication 객체를 SecurityContext에 저장 후 AuthenticationSuccessHandler 실행
- 인증 실패 시, AuthenticationFailureHandler 실행
- DefaultLoginPageGeneratingFilter : 인증을 위한 로그인폼 URL을 감시한다.
- BasicAuthenticationFilter : HTTP 기본 인증 헤더를 감시하여 처리한다.
- RequestCacheAwareFilter : 로그인 성공 후, 원래 요청 정보를 재구성하기 위해 사용된다.
- SecurityContextHolderAwareRequestFilter : HttpServletRequestWrapper를 상속한 SecurityContextHolderAwareRequestWapper 클래스로 HttpServletRequest 정보를 감싼다. SecurityContextHolderAwareRequestWrapper 클래스는 필터 체인상의 다음 필터들에게 부가정보를 제공한다.
- AnonymousAuthenticationFilter : 이 필터가 호출되는 시점까지 사용자 정보가 인증되지 않았다면 인증토큰에 사용자가 익명 사용자로 나타난다.
- SessionManagementFilter : 이 필터는 인증된 사용자와 관련된 모든 세션을 추적한다.
- ExceptionTranslationFilter : 이 필터는 보호된 요청을 처리하는 중에 발생할 수 있는 예외를 위임하거나 전달하는 역할을 한다.
- FilterSecurityInterceptor : 이 필터는 AccessDecisionManager 로 권한부여 처리를 위임함으로써 접근 제어 결정을 쉽게해준다.
필터들은 위의 그림과 같이 체인되어 있다. 필요한 부분이 있을 때 해당 필터를 찾아 공부하면 될 듯하다.
Cookie & Session & JWT
Cookie, Session, JWT는 모두 비연결성인 네트워크 서버 특징을 연결성으로 사용하기 위한 방법이다.
* Cookie & Session은 서버의 어떠한 저장소에 해당 값과 매칭되는 value를 가지고 있어야 한다. 그래서 서버 자원이 많이 사용되는 단점이 있다.
* JWT는 Cookie & Session의 자원 문제를 해결하기 위한 방법이다. JWT는 토큰 자체에 유저 정보를 담아서 암호화한 토큰이라고 생각하면 된다. 암호화된 내용은 디코딩 과정을 통해서 해석이 가능하다.
JWT 구조
JWT는 3개의 구역이 있다.
- header : Header, Payload, Verify Signature 를 암호화할 방식(alg), 타입(Type) 등을 포함한다. alg는 해싱 알고리즘을 지정한다. 보통 HMAC SHA256 또는 RSA가 사용되며, 이 알고리즘은 토큰을 검증할 때 사용되는 signature부분에서도 사용된다.
{
"typ": "JWT",
"alg": "HS256"
}
위 예제에서는 HMAC SHA256 해싱 알고리즘을 사용했고, (곧 나올) Spring Security를 활용한 JWT 예제에서 Base64로 인코딩하는 경우 헤더에 담긴 값을 발급할 수 있다.
- Payload :서버에서 보낼 데이터. 일반적으로 user의 id, 유효기간을 포함한다. 여기에 담는 정보의 한 조각을 클레임(claim)이라 부르고 이는 name/value의 한 쌍으로 이뤄져있다. 클레임의 종류는 크게 세 분류로 나뉘어져있다.
- 등록된 (registered) 클레임 : 서비스에서 필요한 정보들이 아닌, 토큰에 대한 정보들을 담기위하여 이름이 이미 정해진 클레임들이다. 등록된 클레임의 사용은 모두 선택적 (optional)이며, 이에 포함된 클레임 이름들은 다음과 같다.
- iss: 토큰 발급자 (issuer)
- sub: 토큰 제목 (subject)
- aud: 토큰 대상자 (audience)
- exp: 토큰의 만료시간 (expiraton), 시간은 NumericDate 형식으로 되어있어야 하며 (예: 1480849147370) 언제나 현재 시간보다 이후로 설정되어있어야 한다.
- nbf: Not Before 를 의미하며, 토큰의 활성 날짜와 비슷한 개념이다. 여기에도 NumericDate 형식으로 날짜를 지정하며, 이 날짜가 지나기 전까지는 토큰이 처리되지 않는다.
- iat: 토큰이 발급된 시간 (issued at), 이 값을 사용하여 토큰의 age 가 얼마나 되었는지 판단 할 수 있다.
- jti: JWT의 고유 식별자로서, 주로 중복적인 처리를 방지하기 위하여 사용된다. 일회용 토큰에 사용하면 유용하다.
- 공개 (public) 클레임 : 충돌이 방지된 (collision-resistant) 이름을 가지고 있어야 한다. 충돌을 방지하기 위해서는, 클레임 이름을 URI 형식으로 짓는다.
- 비공개 (private) 클레임 : 등록된 클레임도아니고, 공개된 클레임들도 아니다. 양 측간에 (보통 클라이언트 <->서버) 협의하에 사용되는 클레임 이름들이다. 공개 클레임과는 달리 이름이 중복되어 충돌이 될 수 있으니 사용할때에 유의해야한다.
- 하단의 payload 예제는 2개의 등록된 클레임, 1개의 등록된 클레임, 2개의 비공개 클레임으로 이뤄져있다.
- 등록된 (registered) 클레임 : 서비스에서 필요한 정보들이 아닌, 토큰에 대한 정보들을 담기위하여 이름이 이미 정해진 클레임들이다. 등록된 클레임의 사용은 모두 선택적 (optional)이며, 이에 포함된 클레임 이름들은 다음과 같다.
{
"iss": "velopert.com",
"exp": "1485270000000",
"https://velopert.com/jwt_claims/is_admin": true,
"userId": "11028373727102",
"username": "velopert"
}
- 서명(Verify Signature) : Base64 방식으로 인코딩한 Header, Payload, Secret key 를 더한 값을 주어진 비밀키로 hash하여 생성한다.
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret)
서명 부분을 만드는 슈도코드의 구조로 이렇게 만든 해쉬를 base64 형태로 나타내면 된다. 이렇게 각 세 단에서 발급받은 값들 사이 .을 넣어주고 합친 후, 비밀키의 값을 secret으로 해싱한 뒤 base64로 인코딩한다. 이 값을 .를 중간자로 다 합쳐주면 하나의 토큰이 생성된다!
JWT를 통한 인증절차
- 사용자가 로그인을 한다.
- 서버에서는 계정 정보를 읽어 사용자를 확인 후, 사용자의 고유 ID 값을 부여한 후 기타 정보와 함께 Payload 에 집어넣는다.
- JWT 토큰의 유효기간을 설정한다.
- 암호화할 Secret key 를 이용해 Access Token 을 발급한다.
- 사용자는 Access Token 을 받아 저장 후, 인증이 필요한 요청마다 토큰을 헤더에 실어 보낸다.
- 서버에서는 해당 토큰의 Verify Signature 를 Secret key 로 복호화한 후, 조작 여부, 유효기간을 확인한다.
- 검증이 완료되었을 경우, Payload 를 디코딩 하여 사용자의 ID 에 맞는 데이터를 가져온다.
JWT는 보통 Access Token의 유효기간은 보안상 매우 짧다. 그래서 Refresh Token을 따로 발급해주는데, 유효 기간이 지나고 Access Token이 만료되면 새로운 JWT를 발급할 수 있는 토큰이다.
Access Token & Refresh Token
- Access Token의 만료기간을 매우매우 길게 설정해준다. -> 보안 상 불가능
- Access Token을 매번 요청마다 새롭게 갱신한다. -> 서버에 너무나 많은 요청을 하게 된다.
- Refresh Token을 도입한다. -> 가장 괜찮은 기법
그럼 Refresh Token에 대해서 알아보자.
Refresh Token
간단하게, Access Token을 재발급 받기위한 Token이다.
OAuth2.0을 이용하여 타서비스 로그인 기능을 구현한 경험이 있다면 누구나 들어보았을 것이다. refresh token을 활용한 회원가입 과정은 다음과 같다.
- 클라이언트에서 로그인한다.
- 서버는 클라이언트에게 Access Token과 Refresh Token을 발급한다. 동시에 Refresh Token은 서버에 저장된다.
- 클라이언트는 local 저장소에 두 Token을 저장한다.
- 매 요청마다 Access Token을 헤더에 담아서 요청한다.
- 이 때, Access Token이 만료가 되면 서버는 만료되었다는 Response를 하게 된다.
- 클라이언트는 해당 Response를 받으면 Refresh Token을 보낸다.
- 서버는 Refresh Token 유효성 체크를 하게 되고, 새로운 Access Token을 발급한다.
- 클라이언트는 새롭게 받은 Access Token을 기존의 Access Token에 덮어쓰게 된다.
Refresh Token을 사용하여 다음과 같은 이득을 얻을 수 있다.
- Access Token의 유효기간을 짧게 하여 탈취 방지
- Access Token이 탈취당하더라도 유효기간이 짧아 사용할 수 있는 기간이 줄어들어 탈취 방지 효과가 있음
- Access 토큰의 유효기간에 짧음에도 불구하고 Refresh Token이 만료될때까지 추가적인 로그인을 하지 않아도 됨
- 마치 세션이 유지되는 것 같은 효과
이렇게 Jwt를 활용한 로그인 인증 방식에 대해 알아보았다.
요즘은 서버에 부하가 발생할 경우 서버 자체 스팩을 늘리는 것이 아니라, 서버를 여러대 두고 로드밸런싱을 하는 것이 대세다. 이러한 방법을 사용할 경우 각기 다른 서버에 요청을 보내는 경우가 생기는데, session을 그냥 사용하는 경우에는 로그인 정보가 달라져 로그인이 풀릴 수 있다. 또한 성질이 다른 서버에서 공통으로 사용할 인증 로직으로 사용하기에는 적절하지 않을 수 있다.
JWT를 사용하면 위와 같은 클러스터링 환경에서 쉽게 인증 로직을 사용할 수 있을 것 같다.
'Backend > springboot' 카테고리의 다른 글
[JPA] 영속성 전이 cascade, 고아 객체 (0) | 2023.01.30 |
---|---|
[SpringBoot] JPA Spring Security + refresh token으로 회원가입 구현하기 (2) (0) | 2023.01.23 |
[SpringBoot] 트랜잭션 @Transactional 정리 및 관리 방법 (0) | 2023.01.12 |
[SpringBoot] 스프링부트 구글 로그인 API REST 방식으로 구현하기 (2) | 2023.01.11 |
[SpringBoot] 영속성 컨텍스트, 변경 감지와 병합(merge) (0) | 2023.01.06 |