[Spring Security6] Json Web Token
JsonWebToken(JWT) 는 JSON 객체를 사용해 정보를 안전하게 전송하는 방법을 의미한다.
주로 권한 부여에 사용되고, 기존 JSESSIONID 쿠키와 세션 기반 사용자 인증의 단점을 극복하기 위해 도입됐다.
기존 세션 기반 인증을 사용하는 경우 서버 측에서 사용자에 대한 정보를 세션에 저장한다.
따라서 서버를 여러 개 분산해서 사용하는 경우 확장성에 문제가 발생할 수 있고, 사용자가 많아질 경우 세션 정보 관리의 오버헤드가 커진다.
JWT 기반 인증을 사용할 경우 서버는 사용자의 상태를 세션에 저장할 필요가 없다.
클라이언트의 요청마다 토큰이 포함되고, 이 토큰에는 모든 사용자 정보가 들어있다.
서버에서 세션을 저장하지 않기에 서버를 분산해도 세션 관련 문제가 발생하지 않고 서비스 간 상태를 공유하지 않는다. ( stateless)
JWT는 Header / Payload / Signature 세 부분으로 구성되고, 각 부분은 . 으로 구분되며 Base64URL으로 인코딩 한 후 전송된다.
// JWT
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.
SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c
// Header
{
"alg": "HS256",
"typ": "JWT"
}
//Payload
{
"sub": "1234567890",
"name": "John Doe",
"iat": 1516239022
}
//Signature
HMACSHA256(
base64UrlEncode(header) + "." +
base64UrlEncode(payload),
secret
)
Header : 토큰의 시그니처를 생성하고 검증할 때 사용되는 알고리즘과 토큰 타입을 정의한다.
Payload : 클레임이라는 명세된 데이터를 통해 발행자, 만료 시간 등을 표현한다. 사용자와 관련된 정보를 포함할 수 있다.
Signature : 토큰의 무결성을 보증하기 위해 사용된다. 헤더에 명시된 알고리즘을 사용해 서버에만 저장되는 비밀 키를 생성한다.
사용자가 인증을 마친 후 서버는 JWT를 발행한다.
토큰을 발행할 때 토큰이 조작됨을 방지하기 위해 Signature 부분에 비밀 키를 추가한다. (비밀 키는 서버에서만 저장한다)
발행된 JWT는 클라이언트에게 HTTP의 Authorization 헤더를 통해 전송되고, 클라이언트는 다음 요청에서 JWT를 서버에 보낸다.
서버는 클라이언트로부터 받은 JWT에서 헤더와 페이로드를 추출한다.
헤더와 페이로드를 합치고 같은 비밀 키를 사용해 다시 시그니처를 계산하는데, 여기서 서버가 보관하고 있는 비밀 키를 사용한다.
클라이언트가 페이로드를 조작했다면 시그니처가 일치하지 않고 토큰이 조작됐다고 판단해 요청을 거부한다.
public class JWTTokenGeneratorFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
if (null != authentication) {
SecretKey key = Keys.hmacShaKeyFor(SecurityConstants.JWT_KEY.getBytes(StandardCharsets.UTF_8));
String jwt = Jwts.builder().setIssuer("MyProject").setSubject("JWT Token")
.claim("username", authentication.getName())
.claim("authorities", populateAuthorities(authentication.getAuthorities()))
.setIssuedAt(new Date())
.setExpiration(new Date((new Date()).getTime() + 30000000))
.signWith(key).compact();
response.setHeader(SecurityConstants.JWT_HEADER, jwt);
}
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return !request.getServletPath().equals("/user");
}
private String populateAuthorities(Collection<? extends GrantedAuthority> collection) {
Set<String> authoritiesSet = new HashSet<>();
for (GrantedAuthority authority : collection) {
authoritiesSet.add(authority.getAuthority());
}
return String.join(",", authoritiesSet);
}
}
public interface SecurityConstants {
public static final String JWT_KEY = "jxgEQeXHuPq8VdbyYFNkANdudQ53YUn4";
public static final String JWT_HEADER = "Authorization";
}
사용자가 로그인에 성공한 후 서버에서만 저장하는 비밀 키를 통해 JWT 토큰을 발행한다. (실제 운영 환경에서는 환경 변수로 분리하는 편이다)
setIssuer : JWT의 발행자를 설정한다.
claim : 로그인한 사용자의 이름과 사용자의 권한을 설정한다. (보안을 위해 비밀번호는 설정하지 말자)
setIssuedAt : 토큰을 발행한 날짜를 설정한다.
setExpiration : 토큰의 만료 날짜를 설정한다.
signWith : 생성한 비밀 키를 사용해 JWT에 서명한다.
발행된 JWT를 HTTP 응답의 헤더에 추가해 클라이언트에게 전달한다.
이 때 클라이언트가 헤더를 읽을 수 있도록 CORS 설정을 고려해야한다.
JWT 토큰 발행 필터는 로그인 한 경우에만 작동해야 한다.
shouldNotFilter 메서드로 해당 부분을 설정해준다.
public class JWTTokenValidatorFilter extends OncePerRequestFilter {
@Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
throws ServletException, IOException {
String jwt = request.getHeader(SecurityConstants.JWT_HEADER);
if (null != jwt) {
try {
SecretKey key = Keys.hmacShaKeyFor(
SecurityConstants.JWT_KEY.getBytes(StandardCharsets.UTF_8));
Claims claims = Jwts.parserBuilder()
.setSigningKey(key)
.build()
.parseClaimsJws(jwt)
.getBody();
String username = String.valueOf(claims.get("username"));
String authorities = (String) claims.get("authorities");
Authentication auth = new UsernamePasswordAuthenticationToken(username, null,
AuthorityUtils.commaSeparatedStringToAuthorityList(authorities));
SecurityContextHolder.getContext().setAuthentication(auth);
} catch (Exception e) {
throw new BadCredentialsException("Invalid Token received!");
}
}
filterChain.doFilter(request, response);
}
@Override
protected boolean shouldNotFilter(HttpServletRequest request) {
return request.getServletPath().equals("/user");
}
}
JWT 토큰을 검증하는 필터를 작성하자.
요청 헤더에서 JWT 토큰을 추출하고 파싱 후 검증한다.
검증 과정에서 토큰을 발행할 때 사용한 비밀 키를 사용한다.
토큰 검증이 끝나면 SecurityContextHolder에 해당 객체를 설정해 다른 부분에서 사용자의 권한 정보를 바탕으로 권한 검사를 수행한다.
'Spring > Spring Security' 카테고리의 다른 글
[Spring Security6] OAuth2와 OpenID Connect (0) | 2023.08.13 |
---|---|
[Spring Security 6] 메서드 단위 권한 요청 (0) | 2023.08.12 |
[Spring Security6] Authorization과 Filter (0) | 2023.08.09 |
[Spring Security6] CORS, CSRF (0) | 2023.08.08 |
[Spring Security6] 사용자 인증 (0) | 2023.08.06 |
댓글
이 글 공유하기
다른 글
-
[Spring Security6] OAuth2와 OpenID Connect
[Spring Security6] OAuth2와 OpenID Connect
2023.08.13 -
[Spring Security 6] 메서드 단위 권한 요청
[Spring Security 6] 메서드 단위 권한 요청
2023.08.12 -
[Spring Security6] Authorization과 Filter
[Spring Security6] Authorization과 Filter
2023.08.09 -
[Spring Security6] CORS, CSRF
[Spring Security6] CORS, CSRF
2023.08.08