JWT 토큰은 유저의 신원 및 권한을 결정하는 정보를 가지고 있는 데이터 조각이다. JWT 토큰의 인증방식은 비밀키(개인키 or 대칭키)로 암호화 되어 있기 때문에 클라이언트와 서버는 안전하게 통신한다. BUT 탈취 당한경우 문제가 발생한다고 한다 JWT 토큰을 탈취한 사람은 마치 신뢰할 사람으로 인증을 통과할 수 있기때문 심지어 본주인과 탈취한 사람인지에 대한 것을 서버는 구분할 수 없기 때문에 유효기간을 두는것이다.
유효기간이 짧은 경우 사용자가 로그인을 자주해야하므로 불편하고, 유효기간을 길게한 경우 탈취위험 노출이 크다. 그래서 해결 방법인 토큰 2가지 (Access Token 과 Refresh Token)을 두는 것이라고 한다.
우선 핵심만 먼저 이야기하자면
Access Token의 expiration(토큰만료시간)을 짧게 설정하고, Refresh Token의 expiration(토큰만료시간)을 길게 설정한다.
이렇게 하면 사용자의 편의성과 보안성이 증가한다.
토큰 관리방법
로그인요청 후 정보를 비교하여 사용자로 확인된다면 Refresh Token을 발급하여 후 RefreshToken을 이용해 Access Token을 발급한다.
Access Token의 만료 시 Refresh Token을 비교후 일치 시 access token을 발급한다.
클라이언트는 헤더(header)에 Access Toekn은 넣고 API 통신을 진행한다. (Authorization)
서버의 경우 Refresh Token을 저장하고 있는다.
Refresh Token의 만료가 된경우 재로그인을 하도록 조치한다.
Access Token과 Refresh Token을 이용한 인증 절차
1. Access Token 전송 시 만료 시간에 의한 실패의 경우 Refresh Token을 체크한다.
2. Refresh Token이 일치 한 경우 Access Token을 자동으로 발급하여 새로운 만료시간을 갖는다. (사용자는 로그아웃 X)
= Access Token가 탈취 된 경우 만료시간이 짧아 보안성이 좋음
= Refresh Token의 경우 위험
Refresh Token
저장위치 : 서버 즉 DB에 저장
→ Local Storage와 Cookie 보다 보안성이 좋음
토큰이 저장된 DB의 인뎃스 값을 Local Stoage나 Cookie에 저장
-> 인덱스 값을 hash 처리해서 보관 시 더욱 우수한 보안을 가질 수 있음. => DB는 Refrsh Token을 보관하는 곳을 HashMap, HashTable로 구현하게됨
JWT(JSON WEB TOKEN)이란 Json 포맷을 이용하여 사용자에 대한 속성으 저장하는 Claim 기반의 Web Token이다.
JWT는 토큰 자체를 정보로 사용하는 Self-Contained 방식으로 정보를 안전하게 전달한다.
주로 회원인증이나 정보전달 사용한다.
2. JWT 구조
[JWT 개념]
JWT의 구조는 3가지 부분으로 이루어지며 각각 Json 형태인 각부분은 Base64URl로 인코딩 되어 표현된다.
또한 각부분을 이어주기 위해 .(점)을 구분자로 사용한다.
Header (헤더)
Payload (페이로드)
Signature (서명)
JWT의 구조
1. Header
구조는 typ와 alg로 구성됨
typ : 토크의 타입지정 ex) JWT
alg : 알고리즘 방식을 지정하며, Signature(서명) 및 토큰 검증에 사용됨 ex) H256(SHA256) or RSA
대표적으로 사용되는 알고리즘 (alg)은 아래의 리스트들이 있다
HMAC
SHA256
RSA
HS256 OR RS256
{
"alg" : "HS256",
"typ" : "JWT"
}
2.Payload
토큰에서 사용할 정보의 조각들인 Claim(클레임)이 담겨 있다. Claim은 name/value의 한 쌍으로 이루어져 있다.
{
"sub": "1234567890", // 등록된 플레임
"name": "John Doe", // 비공개 플레임
"iat": 1516239022 // 등록된 플레임
}
Claim은 3가지로 나누어 진다
등록된 클레임 (Registered)
공개 클레임 (Public)
비공개 클레임 (Private)
1. 등록된 클레임 (Registered Claim) 토큰의 정보를 표현 하기 위해 이미 정해진 종류의 데이터 (7종류가 있다) 선택적으로 작성가능 하며, key의 길이는 간결성을 위해 String이 3의 길이이다.
iss : 토큰 발급자 (issuer)
sub : 토큰의 제목 (subject) - unique한 값 사용, 사용자의 email 주로 사용
aud : 토큰의 대상자 (audience)
nbf : 토큰 활성날짜 (notbefore) - 이날이 지나기전 토크 활성화 되지 않음
exp: 토큰 만료시간 (expiration) - numericDate형식으로 되어 있어야함
iat : 토큰 발급시간 (issued at) - 토큰 발급 이후의 경과시간을 알 수 있음
jti : JWT 토큰 식별자 (JWT Id) - 중복 방지를 위해 사용, 일회용토큰(access Token)등에 사용됨
2. 공개 클레임 (Public Claim) 사용자 정의 클레임 공개용 정보를 위해 사용하며 충돌방지를 위해 Url 포맷을 이용
{
"https:yongoh1253.tistory.com":true
}
3. 비공개 클레임 (Private Claim) 사용자 정의 클레임 서버와 클라이언트 사이에 임의로 지정한 정보를 저장
{
"name": "John Doe"
}
3. Signature (서명)
토큰을 인코딩하거나 유효성 검증을 할때 사용하는 고유화한 암호화 코드 Header와 Payload의 값을 각각 Base64Url로 인코딩하고 인코딩한 값을 다시 비밀키를 이용해 Header에서 정의한 알고리즘으로 해싱하고 이 값을 다시 Base64Url로 인코딩하여 생성 생성된 토큰은 Http 통신을 할때 'Authorization'이라는 key의 value로 사용 → 일반적으로 value앞에 'Bearer '이 붙음
스프링 시큐리티 (Spring Security)는 스프링 기반의 어플리케이션의 보안(인증과 권한, 인가)을 담당하는 스프링 하위 프레임워크. 보안과 관련해 체계적으로 많은 옵션들을 제공해주기 때문에 개발자의 입장에서는 하나하나 보안 관련 로직을 작성하지 않아도 된다는 장점이 있음.
1-1. Spring Security 관련 용어
인증(Authentication) : 해당 사용자가 본인인지 확인하는 절차
인가(Authorization) : 인증된 사용자가 요청한 자원에 접근 가능한지 결정하는 절차
접근 주체(Principal) : 보호받는 Resource에 접근하는 대상
비밀번호(Credential) : Resource에 접근하는 대상의 비밀번호
권한 : 인증된 주체가 어플리케이션의 동작을 수행할 수 있도록 허락되어 있는지 결정
인증 과정을 통해 주체가 증명된 이후 권한을 부여할 수 있다.
권한 부여에 두 가지 영역이 존재하는데 웹 요청 권한과 메서드 호출 및 도메인 인스턴스에 대한 접근 권한 부여가 있다.
기본적으로 인증(Authentication) → 인증성공 → 인가(Authorization)
Spring Security는 기본적으로 인증 절차를 거친 후에 인가 절차를 진행하게 되며, 인가 과정에서 해당 리소스에 대한 접근 권한이 있는지를 확인하게 됩니다. 이러한 인증과 인가를 위해 Principal을 아이디로, Credential을 비밀번호로 사용하는 인증 방식을 사용합니다.
기본적으로 인증 정보는 최종적으로 인메모리 세션 저장소인 SecurityContextHolder에 세션 - 쿠키 방식으로저장
2. Spring Security 동작원리
2-1 . Spring Security 동작원리 상세 (1~4)
동작원리의 1-4까지 흐름
1. 요청 (Request) 사용자가 로그인 정보를 요청한다. 예시) 로그인페이지에서 아이디와 비밀번호를 입력한다. 그럼 아이디와 비밀번호가 서버로 전송하게 된다.
AuthenticationFilter 는 사용자의 세션 ID(JSESSIONID)가 Security Context에 있는지 확인한다. (여기서 Security Context란 아래의 모든 로직을 통과한 인증된 사용자의 정보(인증 개체)를 저장하는 공간이다.) Security Context에 세션 ID가 없다면 아래 로직을 수행한다.
2. 토큰 생성
사용자가 보낸 아이디와 비밀번호를AuthenticationFilter가 받아서 UsernamePasswordAuthenticationToken 토큰(인증용 객체)을 생성한다.
AuthenticationFilter가 받고 아이디와 비번을 UsernamePasswordAuthenticationToken 토큰(인증용 객체)에 전달
3. 생성된UsernamePasswordAuthenticationToken는 AuthenticationManager의 인증 메서드를 호출하는 데 사용된다. 여기서 AuthenticationManager는 단순한 인터페이스이며 실제 구현은 ProviderManager이다.
ProviderManager 에는 사용자 요청을 인증에 필요한 AuthenticationProvdier 목록이 있다.
ProviderManager는 제공된 각 AuthenticationProvdier를 살펴보고 전달된 인증 개체(UPAT)를 기반으로 사용자 인증을 시도한다.
4. 토큰을 처리할 수 있는 AuthenticationProvider 선택 AuthenticationManager는 List 형태로 AuthenticationProvider들을 가지고 있는데, 실제로 인증을 할 AuthenticationProvider에게 인증용 객체를 다시 위임한다. 예시) AuthenticationManager는 인증을 담당하는 클래스이지만, 진짜는 AuthenticationManager가 가지고 있는 AuthenticationProvider 인터페이스들한테 "이거(인증용 객체) 줄테니 한번 인증처리가 가능한 AuthenticationProvider는 인증처리 해줘" 라고 또 떠넘기는 방식이다.
AuthenticationProvider의 리스트 중 일부
CasAuthenticationProvider
JaasAuthenticationProvider
DaoAuthenticationProvider
OpenIDAuthenticationProvider
RememberMeAuthenticationProvider
LdapAuthenticationProvider
2-2 . Spring Security 동작원리 상세 (5~7)
동작원리의 5~7까지 흐름
5. AuthenticationProvider는 사용자의 이름(username)을 기반으로 사용자의 세부정보를 검색하기위해 UserDetailsService를 사용할 수 있다.
5-1. AuthenticationProvider 인터페이스에서는 authticate() 메서드를 오버라이딩해 인증용객체를 파라미터로 받아 로그인페이지에서 입력한 사용자의 정보를 들고 올 수 있다.
UserDetailsService는 Spring Security의 인터페이스 이며 이를 구현한 서비스는 직접개발 해야한다.
즉, 아래 코드의 loadUserByUsername메소드를 오버라이딩해DB와 비교하는 로직을 직접개발 해야한다.
public interface UserDetailService {
UserDetail loadUserByUsername(String username) throws UsernameNotFoundException;
}
8. AuthenticationProvider인터페이스에 의해 사용자가 성공적으로 인증되면, 완전히 채워진 인증개체가 반환된다. 인증에 실패하면 AuthenticaionException이 발생한다. AuthenticaionException이 발생하면 인증 메커니즘을 지원하는 AuthenticationEntryPoint에 의해 처리된다.
사용자의 정보가 존재하지 않는경우 예외를 던진다고한다. (아래 코드 참고)
9. 인증완료
AuthenticationManager는 획득한 완전히 채워진 인증개체를 관련 인증 필터(AuthenticationFilter)로 다시 반환한다
10. SecurityContext에서 인증 개체 설정
AuthenticationFilter는 향후 필터 사용을 위해 획득한 인증 개체를 SecurityContext에 저장한다.
SecurityContext는 1번 로직에서 설명 했었다. 이 SecurityContext에 인증개체가 있는지를 확인하고,
인증되지 않은 사용자와 인증된 사용자의 요청을 감시하고, AuthenticationManager에게 인증처리를 맡김 인증이 성공한다면 인증용 객체를 AuthenticationContext 객체에 저장하고 AuthenticationSuccessHandler 실행 실패 시 AuthenticationFailureHandler 실행
AuthenticationProvider
로그인페이지에서 입력한 정보와 DB에 저장된 정보를 비교 일치하다면 Authentication객체로 담아 AuthenticationManager에게 전달 만약 DB에 저장된 정보들 중 로그인페이지에서 입력한 정보와 일치하는 정보가 없다면 UsernameNotFoundException예외 처리
UserDetailsService
DB에서 유저정보를 가져오는 역할담당
UserDetails
사용자의 정보를 담은 인터페이스이며 직접 상속받아 사용한다. 유저객체에서 UserDetails를 상속받게 되면 구현해야할 메서드들이 있으며, 각 메서드들의 정의는 다음과 같다.