앞서 7장까지는 세션을 활용하는 브라우저 기반 웹 서비스에 대해 Spring Security를 어떻게 활용하는지 확인해보았다. 지금부터는 SPA(Single Page Application), 안드로이드/아이폰 모바일 앱 등을 위해 API를 제공하는 API 서버에서 Spring Security를 어떻게 활용할 수 있는지 알아본다.
REST API
API 서비스 개발을 위한 가장 일반적인 접근 방법이며 (사실상) 표준
엄격하게 REST API 디자인 원칙을 따르는 것는 쉽지 않음
어쨋든 통상적으로 REST API 라고 부르고 있음 — 본 과정에서는 사회통념을 따르도록 함
Java, Python, Go 등 다양한 언어로 풍부한 레퍼런스가 존재 — 구현이 쉬움
HTTP 프로토콜 기반만 따르면 어떤 기술이든 응용 가능(XML, JSON)
앞에서 알아본 3-Tier Architecture 구조와 REST API 서비스를 궁합이 좋음
3-Tier Architecture의 장점 대부분은 REST API 서비스를 개발/운영할때 여전히 유효함
REST API 서비스에서 Session을 꼭 써야 하는가에 대해서는 서비스 성격에 따라 고민이 필요함
Stateful vs Stateless
Stateful 아키텍처의 장단점
일단 Session을 사용하고 있으면, Stateful 하다고 할 수 있음
앞서 살펴본 것처럼 수평확장(Scale-Out) 과정이 쉽지 않음 — Session Cluster 반드시 필요
Session Cluster의 장애 또는 성능 병목이 서비스 전체에 큰 영향을 줄수 있음
단일 사용자의 다중 로그인 컨트롤, 사용자 유효성 체크, 강제 로그아웃 기능 구현이 쉬움
Stateless 아키텍처의 장단점
Session을 전혀 사용하지 않아야함 — 사실 HTTP 프로토콜 자체가 Stateless
수평확장이 매우 쉬움 — Session Cluster 필요 없음
단일 사용자의 다중 로그인 컨트롤, 사용자 유효성 체크, 강제 로그아웃 기능 구현이 어려움
무엇보다 완전한 Stateless 아키텍처 기반으로 유의미한 서비스 개발이 어려움
완전한 Stateless 서비스는 정적 리소스(html, css, javascript, 이미지 등)를 서비스 하는데 적합함 — 예: AWS S3
JWT는 Stateless 상태를 유지하며, 서버에서 사용자를 식별할 수 있는 수단을 제공한다.
JWT 개요
JSON 포맷을 사용하여 데이터를 만들기 위한 웹 표준(open standard) — RFC 7519
JWT는 유저에 관한 자체적으로 필요한 모든 정보를 지니고 있다. [ Self-contained ] → db 쿼리를 한번 초과로 날릴 필요가 없음
토큰에 대한 메타정보 (토큰타입, 사용된 해시 알고리즘)
사용자 정의 데이터
토큰 유휴성 검증을 위한 데이터
인터넷상에서 쉽게 전달할 수 있음. [ Compact ]
URL-Safe 텍스트로 구성되기 때문에 HTTP 프로토콜의 어느 위치에든 들어갈 수 있음 (보통 HTTP 헤더에 들어감)
URL, POST parameter, HTTP header
위변조 검증 가능
토큰이 위변조되지 않았음을 증명하는 서명(Signature) 포함
JWT는 secret키(HMAC 알고리즘)나, public/private key pair (RSA 알고리즘)을 사용하여 서명될 수 있음
Signed tokens can verify the integrityof the claims contained within it, while encrypted tokenshidethose claims from other parties
JWT를 사용하는 이유
쿠키와 세션 →
쿠키는 http의 header에 포함되어 보내지는데, 이 http의 내용들은 패킷을 중간에 가로채서 가져가게 되면 내용이 그대로 다 보임(암호화 이런것들 전혀 안하니까)
개인 민감정보를 그대로 노출했을 때(쿠키)의 단점을 막아내기 위해서 나온 것이 바로 Session임(인증 정보자체를 주고 받지 말고 서버에 저장하자). 그러나 Session을 이용할 때, 수평확장을 하게 되면 Session Cluster라는 것을 사용해야 하고 이는 성능 병목이 될 수 있음(stateful의 단점) → stateless이면서 세션 저장소 조회 같은 DB작업도 없는 무언가 있을까?? → JWT
Single Sign On 에서 JWT를 많이 사용한다고 함 ( 적은 오버헤드와 다른 도메인의 시스템 사이에 쉽게 사용이 가능하기 때문에)
Information Exchange : JWT는 안전하게 정보를 주고 받는데 있어 좋은 방법임. 왜냐하면, 서명이 되어 있기 때문에(예를 들어, public/private key pair를 사용한다면) 전송자가 누구인지 알 수 있고 내용이 변조되지 않았음을 알 수 있음
내용이 변조되면 Signature 부분이 맞지 않을 것이기에. Signautre부분
JWT 작동원리 (Token-based Authentication 의 작동원리임)
서버에서 사용자가 성공적으로 인증되면 JWT를 반환함
클라이언트는 JWT를 로컬 영역에 저장하고(local storage 혹은 쿠키에 저장될 수도 있음) 이후 서버에 요청을 보낼 때 JWT를 HTTP 헤더에 포함시킴
서버는 클라이언트가 전달한 JWT를 통해 사용자를 식별할 수 있음
JWT 구조
Header, Payload, Signature 세 부분으로 구성됨
Header, Payload, Signature 세 부분을 Base64 Url-Safe 방식으로 인코딩하고 dot(.)을 구분자로 결합함
XML 기반의 표준인 SAML 에 비교해서 compact 함
Header — JWT를 검증하는데 필요한 두가지 정보를 담고 있음 (토큰 타입, 사용된 알고리즘)
알고리즘은 HMAC, RSA 방식을 지원
위 그림에서 HS512는 HMAC using SHA-512를 의미
HMAC 알고리즘에서 비밀키는 최소한 알고리즘의 서명 길이 만큼의 비트를 가지고 있어야함 (HS512 — 64byte)
Payload : JWT를 통해 전달하고자 하는 데이터
Claims는 entity(대개 user)에 대한 statements, 그리고 추가적인 메타데이터를 포함하고 잇음
Claim 자체는 쉽게 말해 Key-Value 데이터 쌍
JWT 자체는 암호화되는 것이 아니기 때문에 민감정보를 포함해서는 안됨
Claim은 3가지 타입(Reserved Claims, Public Claims, Custom Claims) 으로 구분됨
Reserved Claims : 미리 등록된 Claims. 필수적으로 사용할 필요는 없지만 사용을 권고함(이름은 compact하게 유지하기 위해 다 3글자)
iss — 토큰을 발급한 발급자 (Issuer)
exp — 만료시간이 지난 토큰은 사용불가
nbf — Not Before의 의미로 해당 시간 이전에는 토큰 사용불가
iat — 토큰이 발급된 시각
jti — JWT ID로 토큰에 대한 식별자
Public Claims : 사용자 마음대로 쓸 수 있으나 충돌 방지를 위해 미리 정의된(IANA JSON Web Token Registry) 이름으로 사용을 권고함
Custom Claims : 사용자 정의 Claims (Reserved, Public 에 정의된 이름과 중복되지 않도록함)
public static Claims from(String email, String… roles)
↔ String sign(Claims claims)
Jwt전용 Authentication 인터페이스 구현체 추가(JwtAuthenticationToken)
authorities
credentials
details
principal
authenticated
인증되기 전의 Token 생성자
로그인 시, JwtAutheticationFilter에서 사용
인증되고 난 후의 Token 생성자
재로그인 시, JwtAuthenticationFilter에서 사용
JwtAuthenticationProvider(로그인 시)에서 사용
Token에 들어갈 principal 구현
이거 구현할 시, Claims에 포함되어 있는 내용들만 여기에 들어갈 수 있음. 왜냐면 얘를 만드는 곳은 Filter기 때문에 Token의 내용들로만 값을 채워넣을 수 있음
JwtAuthenticationFilter
두가지 기능을 수행해야함(이건 나의 뇌피셜)
login endpoint로 들어올 시, Token 생성
JwtAuthenticationFilter : attemptAuthentication()
→ AuthenticationManager : authenticate()
→ JwtAuthenticationProvider : authenticate()
→ userService.login()
& token 생성, 반환
Authorization Header에 Token담겨 있을 시, 인증하여 SecurityContextHolder에 담기
getToken — 헤더에 값있는지 확인
Token validate 후 JwtAuthenticationToken 생성하여 SecurityContextHolder에 담기
JwtAuthenticationToken 타입을 처리할 수 있는 AuthenticationProvider 인터페이스 구현체를 추가 — JwtAuthenticationProvider
UserService 클래스를 이용해 로그인을 처리하고, JWT 토큰을 생성함
UserService 클래스는 더이상 UserDetailsService 인터페이스를 구현하지 않음
인증이 완료된 사용자의 JwtAuthenticationToken 을 반환함
principal 필드 — JwtAuthentication 객체
details 필드 — com.prgrms.devcourse.user.User 객체 (org.springframework.security.core.userdetails.User와 명백히 다름에 주목)
UserDetailsService에 의존하지 않는 UserService를 만들며, Spring Security의 인증 처리 기능 일부를 커스터마이징 해야함
💡
UsernamePasswordAuthenticationToken 타입을 처리할 수 있는 AuthenticationProvider 인터페이스 구현체로 DaoAuthenticationProvider 클래스가 있다. 또한 DaoAuthenticationProvider 구현체는 UserDetailsService 인터페이스에 의존한다. 8장까지는 Spring Security가 제공하는 기본 인프라스트럭쳐를 최대한 이용하려 했고, 이런 이유로 UserService 클래스가 UserDetailsService 인터페이스 구현체 역할을 했다.
JwtAuthenticationProvider Security Configuration에 설정(AuthenticationManager가 JwtAuthentication Provider를 찾을수 있도록 설정해주어야 함)JwtAuthenticationFilter 가 해야할 일을 SecurityContextPersistenceFilter의 SecurityContextRepository 커스텀 구현으로 옮기기(Optional)
JwtAuthenticationFilter의 핵심 역할은 HTTP 요청헤더에서 JWT 토큰을 확인하고, 검증하여 SecurityContext 를 생성하는 것
SecurityContextRepository는 HTTP 요청에서 필요한 데이터를 얻고, 이 데이터를 이용함
JwtAuthenticationFilter 구현 VS. SecurityContextRepository 커스텀 구현
JwtAuthenticationFilter
HTTP 헤더에서 JWT 토큰을 추출하고, 검증하여 SecurityContext를 생성할 수 있음
Security Filter 체인 상에서 어디에 위치하는지가 중요함
SecurityContextPersistenceFilter 바로 뒤에 또는 UsernamePasswordAuthenticationFilter 필터 전후로 위치하면 적당함
SecurityContextRepository 커스텀 구현
기본적으로 JwtAuthenticationFilter 구현과 유사함
그러나 SecurityContextRepository 인터페이스에 맞추어 부수적인 메소드 구현이 필요함
saveContext, containsContext 메소드
SecurityContextPersistenceFilter, SessionManagementFilter 2개의 필터에서 SecurityContextRepository 구현이 어떻게 사용되는지 잘 알고 있어야함
SecurityContextRepository 인터페이스 커스텀 구현 방식이 추가적으로 고려할 내용이 많고, Spring Security 전반에 걸쳐 끼치는 영향이 더 큼 → Filter구현 방식이 조금 더 낫다
특히 SessionManagementFilter를 사용할 경우 SecurityContextRepository 메소드 구현 방법에 따라 적절한 설정이 필요함
💡
sessionCreationPolicy가 STATELESS 이면서 (사실 JWT 토큰이 사용된다는 것 자체가 STATELESS임을 의미함) SecurityContextRepository의 containsContext 메소드가 false를 반환하는 경우, 불필요한 SessionFixationProtectionStrategy가 실행되지 않도록 NullAuthenticatedSessionStrategy 구현체를 설정하는 등 별도 처리가 필요하다.