JWT(Json Web Token)은 인증에 필요한 정보들을 암호화 시킨 Json 토큰으로 리소스에 대한 엑세스 권한을 부여하기 위한 인증 방식으로 사용된다.  Base64(이진 데이터를 문자열로 인코딩하기 위해 사용) 방식으로 인코딩을 하며 HMAC-SHA 이라는 해시 기반의 암호화 알고리즘을 사용한다.

 

HMAC은 암호화와 인증을 위한 코드를 해시 알고리즘과 secret키를 사용해서 생성하는 메시지 인증 코드(Message Authentication Code)이며 뒤에 붙은 SHA(Secret Hash Algorithm)는 대표적인 해시 알고리즘이다. 

 

해시란?

단방향성 암호화를 해싱(암호화를 시킨 값으로만 같은지 확인 가능, 원본이 변하면 해시 값도 변함)이라 하고 해시에 의해 암호화된 데이터를 다이제스트라고 하는데 SHA-256으로 암호화를 한다고 하면 같은 입력값에 대해서는 항상 똑같은 다이제스트가 나오게 된다.

 

 

JWT 토큰헤더와 페이로드, 시그니처(서명)으로 구분이 되며 헤더에는 알고리즘의 정보가 들어있고 페이로드에는 claims 데이터들이 들어간다. 일반적으로 권장되는 sub(제목), exp(만료 시간) 등이 있으며 따로 필요한 정보를 넣기도 있다. 


JWT는 기본적으로 암호화에 특화된 것이 아니라 데이터가 변하지 않았다는 무결성에 특화되어 있기 때문에 노출되면 안되는 민감한 정보를 페이로드에 넣지 않아야 한다. 

 

마지막으로 시그니처는 Base64Url로 인코딩된 헤더와 페이로드, 서버측에서 관리하는 시크릿키를 이용하여 HMAC-SHA 알고리즘으로 생성해낸다. 

 

왜 JWT를 사용하는가?

 

세션 방식은 서버 자원을 사용하기 때문에 부담이 많이 갈 수 있고 서버 장애시 사용자가 재로그인을 해야 되는 등의 문제가 생길 수 있다. 반면 JWT는 클라이언트에게 인증 토큰을 전달하면 이후 클라이언트에서 JWT 토큰을 헤더에 담아서 전달하기면 하면 되기 때문에 세션 방식의 문제점을 해결할 수 있다.

세션 방식도 메모리 DB를 이용해 세션을 관리하는 식으로 이런 문제를 해결할 수 있지만 서버 부하나 복잡성 면에서 JWT가 사용하기 편리할 것 같다. 하지만 JWT는 토큰을 탈취 당했을 때 서버 측에서 해결하기가 어려워서 클라이언트에는 만료 시간이 짧은 AccessToken을 전달하고 서버에서 비교적 만료 시간이 긴 RefreshToken을 관리하면서 AccessToken을 재발급 해주는 방식으로 관리를 해야 한다. 그래서 토큰을 새로 발급받기 위해 RefreshToken 확인을 빈번하게 하기 때문에 Redis 같은 메모리 DB에 관리를 하는 경우도 있다고 하는데 아직은 JWT를 처음 구현해보기도 하고 트래픽이 많지 않기 때문에 RDB에서 관리를 하는 방식으로 하였다.

 

그리고 JWT를 사용하면 서버를 무상태성(Stateless)으로 관리할 수 있는 것이 세션 방식과의 큰 차이점이다. ㅇJWT 토큰도 많은 데이터를 담으면 부하가 심해지기 때문에 되도록 인증을 위한 데이터만 담아서 전달해야 한다. (애초에 중요한 데이터는 노출하면 안 된다.)

 

이전 글에서 login 메서드를 보면 스프링 시큐리티로 인증 과정을 거치고 인증이 완료된 Authentication 인스턴스를 생성하는 것까지 완료했다. 다음에는 JwtAuthenticationProvider를 통해 JWT 토큰을 생성한다.

 

 

위에서 보았듯이 JWT 토큰은 헤더 + 페이로드 + 서명으로 구성이 되는데 기본 헤더는 알아서 생성이 되고 페이로드에는 사용자의 이름, 권한, 만료시간을 담는다. 마지막으로 서명은 헤더와 페이로드, 시크릿키를 사용해서 HS512(HMAC-SHA512)로 암호화를 하는데 시크릿키는 사용하는 SHA에 따라 최소 길이를 충족시키도록 Base64로 인코딩해서 생성한다. (예시는 짧게 입력한 것)

 

 

RefreshToken은 만료시간만 들어가며 DB에서 따로 관리하기 위해 아이디와 토큰, 만료시간을 담은 DTO를 추가로 생성해서 반환한다.

 

 

JWT 토큰이 생성이 되었으면 응답 헤더에 AccessToken을 담아 클라이언트에 반환을 해준다.

 

이전 글을 보면 SecurityConfig 설정에서 아래와 같이 Jwt 토큰 확인을 위한 커스텀 필터를 추가해줬었다.

JwtFilter 필터는 OncePerRequestFilter를 상속 받아서 구현하였는데 이는 Request에 대해 딱 한번만 적용되는 필터로 JwtFilter는 처음 요청에 대한 인증을 하기 위함이므로 OncePerRequestFilter를 상속 받았다.

 

 

먼저 토큰 재발급(reissue)의 경우에는 인증을 스킵하고 그 외에는 accessToken의 존재 여부와 JwtAuthenticationProvider의 토큰 검증 메서드를 통해 유효성을 확인한다.

 

유효성 검증이 되었으면 해당 토큰으로 Authentication 인스턴스를 생성해서 SecurityContext에 저장하고 SecurityContextHoder에 담는다.

 

 

 

 

 

스프링 시큐리티(Spring Security)는 스프링 기반의 사용자 인증을 처리하는 스프링 하위 프레임워크로 사용자 인증과 권한에 대한 부분을 Filter로 처리해준다.

 

 

핵심 용어


SecurityContextHolder

 

보안 주체의 세부 정보를 포함하여 응용 프로그램의 현재 보안 컨텍스트에 대한 세부 정보가 저장되며 로그인 인증이 되면 스프링 시큐리티는 SecurityContextHolder에 Authentication을 담아 세션 저장소에 있는 전용 공간에 보관한다.

 

SecurityContext

 

Authentication 객체를 보관하는 역할을 하며 SequrityContext를 통해 Authentication 객체를 꺼낼 수 있다.

 

Authentication

 

현재 접근하는 주체의 정보와 권한을 담는 인터페이스로 Authentication 객체는 SecurityContext에 저장되며, SecurityContextHolder를 통해 SecurityContext에 접근해서 Authentication 객체를 가져올 수 있다.

 

UsernamePasswordAuthenticationToken

 

Authentication을 implements한 AbstractAuthenticationToken의 하위 클래스로 사용자 인증 전 객체를 생성하는 생성자가 있고, 인증이 완료된 객체를 생성하는 생성자가 있다.

 

AuthenticationManager, AuthenticationProvider

 

인증을 처리하는 방법을 정의한 인터페이스로 구현체인 ProviderManager가 있다. ProviderManager는 실제 인증을 처리하는AuthenticaitonProvider 리스트를 실행시켜 인증을 거치고 직접 Provider를 구현하여 등록할 수도 있다. 사용자 입력 정보가 있는 인증 전 객체인 Authentication으로 authenticate() 메서드를 실행하여 인증을 완료하고 하면 인증이 완료된 Authentication을 반환한다.

 

UserDetailsService, UserDetails

 

UserDetailsService는 loadUserByUsername() 메서드를 통해 DB에서 데이터 조회를 하는 역할을 하며 UserDetailsService를 implements한 클래스를 스프링 빈으로 등록하면 스프링이 자동으로 실행된다. UserDetailsService는 DB에서 조회한 User 객체를 담은 UserDetails 객체를 생성하여 반환한다.

UserDetails는 사용자 정보를 담고 있는 인터페이스로 계정이 가지고 있는 권한 리스트 등의 메서드를 Override 할 수 있으며 로그인 요청 정보와 비교하여 인증 완료된 Authentication 객체를 생성하는데 사용된다.

 

 

 

인증 과정


1. 사용자가 아이디, 비밀번호로 로그인 요청을 하면 AuthenticationFilter에서 인증 전 상태의 Authentication 객체(UserPasswordAuthenticationToken)을 생성하여 AuthenticationManager에 전달한다.

 

2. AuthenticationManager는 AuthenticationProvier를 실행하며 사용자 인증을 처리한다.

 

3. AuthenticationProvider는 UserDetailsService를 통해 인증을 처리한다.

 

4. UserDetailsService는 loadUserByUsername() 메서드를 실행하여 DB에서 데이터를 조회하고 UserDetails 객체를 생성하여 반환한다.

 

5. AuthenticationProvider는 UserDetails 객체를 받아서 비밀번호 비교 등 추가 인증을 한다. 

 

6. AuthenticationManager는 인증이 완료된 Authentication 객체 (UsernamePasswordAuthenticationToken)를 AuthenticationFilter, LoginSuccessHandler로 전달해서 SecurityContextHolder에 저장한다.

 

7. SecurityContextHolder는 세션 저장소에 있는 스프링 시큐리티 전용 공간에 저장이 된다.

 

 

 

 

 

 

[참고]

 

[SpringBoot] Spring Security란?

대부분의 시스템에서는 회원의 관리를 하고 있고, 그에 따른 인증(Authentication)과 인가(Authorization)에 대한 처리를 해주어야 한다. Spring에서는 Spring Security라는 별도의 프레임워크에서 관련된 기능

mangkyu.tistory.com

 

 

[무료] 스프링부트 시큐리티 & JWT 강의 - 인프런 | 강의

스프링부트 시큐리티에 대한 개념이 잡힙니다., - 강의 소개 | 인프런...

www.inflearn.com

 

+ Recent posts