JWT(Json Web Token)
: Json 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token으로, 토큰의 한 종류이다.
(보통 쿠키 저장소에 담겨서 위에서 배운 ‘저장된 쿠키’라고 생각하시면 좋습니다.)
대용량 트래픽 처리를 위해 서버가 2대 이상 필요한 경우가 있다.
이때 Session 마다 다른 Client 로그인 정보를 갖고 있을 수 있다.
🤔❓ 문제
: Client 1
의 로그인 정보를 가지고 있지 않은 Sever2
에게 API 요청을 하게된다면?!
🤓 해결
1. Sticky Session: Client 마다 요청 Server 고정
2. 👍 세션 저장소 생성 or JWT 사용
JWT
로그인 정보를 Server 에 저장하지 않고, Client 에 로그인정보를 JWT 로 암호화하여 저장
→ JWT 통해 인증/인가
모든 서버에서 동일한 Secret Key 소유
Secret Key를 통한 암호화 / 위조 검증 (복호화시)
JWT 장/단점
추가 공부) Session VS Jwt 어떤게 더 좋을까?
Client 로그인 성공
a. 로그인 정보를 JWT 로 암호화
b. JWT 를 Client 응답에 전달
Authorization: BEARER eyJ0eXAiO~~
c. Client 에서 JWT 저장 (쿠기, Local storage 등)
Client 에서 JWT 를 통해 인증하는 방법
a. Client : JWT 를 API 요청 시마다 Header 에 포함
Content-Type: application/json Authorization: Bearer <JWT> ...
b. Server
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.2'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.2'
spring:
...
jwt:
secret:
key=🔒
// Header Key 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 Key
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자
/* Bearer : OAuth 2.0 인증 프로토콜에서 사용하는 인증 토큰의 유형을 명시하는 문자열
Bearer 다음에 오는 토큰 값은 하나의 문자열로 인식되어야 하기 때문에,
Bearer 와 토큰 값 사이에는 한 칸 띄어쓰기가 필요
*/
private static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료 시간
private static final long TOKEN_TIME = 60 * 60 * 1000L;
// JWT 의 비밀 키 저장
@Value("${jwt.secret.key}")
private String secretKey;
// JWT 서명에 사용할 비밀 키를 저장
private Key key;
// JWT 서명 알고리즘을 정의하는 열거형 변수 <HS256 (HMAC with SHA-256) 알고리즘>
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 애플리케이션 시작 시 비밀 키를 디코딩하고 Key 객체로 변환하는 초기화 작업을 수행
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
@Value("${jwt.secret.key}") private String secretKey;
- Spring 의존성 주입을 위해 사용됨
:application.yml
(애플리케이션 설정 파일) 에 정의된 속성 이름으로 JWT 의 비밀 키를 저장
JWT 서명에 필요한 키를 초기화하고, 해당 서명 알고리즘을 설정하는 데 사용
@PostConstruct public void init() { byte[] bytes = Base64.getDecoder().decode(secretKey); key = Keys.hmacShaKeyFor(bytes); }
@PostConstruct
: 종속성 주입이 완료된 후 실행되어야 하는 메서드에 사용init()
: secretKey로부터 바이트 배열을 디코딩하고, 이 바이트 배열을 사용하여 Keys.hmacShaKeyFor() 메서드를 호출하여 Key 객체를 생성
// header 토큰을 가져오기
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(7);
}
return null;
}
Authorization
값을 읽어온 후 7개 문자열(=Bearer
)을 제거한다. // 토큰 생성
public String createToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username)
.claim(AUTHORIZATION_KEY, role)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date)
.signWith(key, signatureAlgorithm)
.compact();
}
- setSubject : JWT 의 주체 설정
- claim : 역할 정보를 claim으로 추가
- setExpriation : JWT 만료 시간 설정
- setIssuedAt : JWT 발급 시간
- signWith : JWT를 서명하는데 사용할 키와 알고리즘
- 서명 : JWT 의 무결성을 보장
- compact() : JWT 를 문자열로 변환 후 반환
// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
log.info("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
claim
정보를 가져온다. // 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
claim
정보를 추출하고 해당 정보를 Claims
객체로 반환package <com.example.demo.domain.utility.jwt;
import com.example.demo.domain.member.entity.UserRoleEnum;
import io.jsonwebtoken.*;
import io.jsonwebtoken.security.Keys;
import io.jsonwebtoken.security.SecurityException;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.annotation.PostConstruct;
import javax.servlet.http.HttpServletRequest;
import java.security.Key;
import java.util.Base64;
import java.util.Date;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtUtil {
/**
* 토큰 생성에 필요한 값들
*/
/* Header Key 값 */
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 Key
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자
// Bearer : OAuth 2.0 인증 프로토콜에서 사용하는 인증 토큰의 유형을 명시하는 문자열
// Bearer 다음에 오는 토큰 값은 하나의 문자열로 인식되어야 하기 때문에,
// Bearer 와 토큰 값 사이에는 한 칸 띄어쓰기가 필요
private static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료 시간
private static final long TOKEN_TIME = 60 * 60 * 1000L;
// JWT 의 비밀 키 저장
@Value("${jwt.secret.key}")
private String secretKey;
// JWT 서명에 사용할 비밀 키를 저장
private Key key;
// JWT 서명 알고리즘을 정의하는 열거형 변수 <HS256 (HMAC with SHA-256) 알고리즘>
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 애플리케이션 시작 시 비밀 키를 디코딩하고 Key 객체로 변환하는 초기화 작업을 수행
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
/**
* Header 에서 Token 가져오기
*/
// header 토큰을 가져오기
public String resolveToken(HttpServletRequest request) {
String bearerToken = request.getHeader(AUTHORIZATION_HEADER);
if (StringUtils.hasText(bearerToken) && bearerToken.startsWith(BEARER_PREFIX)) {
return bearerToken.substring(7);
}
return null;
}
/**
* JWT 생성
*/
// 토큰 생성
public String createToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username)
.claim(AUTHORIZATION_KEY, role)
.setExpiration(new Date(date.getTime() + TOKEN_TIME))
.setIssuedAt(date)
.signWith(key, signatureAlgorithm)
.compact();
}
/**
* JWT 검증
*/
// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException e) {
log.info("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
log.info("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
log.info("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
log.info("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
/**
* JWT 에서 사용자 정보 가져오기
*/
// 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
}