⏰ 2024. 05. 31 금
✔ 스프링 이론 강의를 듣고 정리하면서 작성했습니다.
JWT 란?
JWT(Json Web Token)은 JSON 형식을 이용하여 사용자에 대한 속성을 저장하는 Claim
기반의 Web Token
이고, 일반적으로 Web Token
을 쿠키 저장소를 사용하여 JWT를 저장한다.
일반적으로 JWT를 쿠키에 저장해 클라이언트에 반환하지만, JWT를 HTTP ResponseHeader
에 담아 클라이언트에 반환하는 방법도 있다.
로그인 정보를 Server에 저장하지 않고, Client에 로그인 정보를 JWT로 암호화하여 저장하고, 모든 서버에 동일한 Secret Key
를 소유해 암호화와 위조를 검증한다.
JWT 특징과 장단점
JWT 사용 흐름
Client가 username, password를 입력해 로그인 시도
DB에서 로그인 시 입력된 정보를 DB에서 확인해 로그인 성공 시
서버에서 "로그인 정보"를 가져와 Secret Key
사용해서 암호화한 JWT 발급
서버에서 직접 쿠키
를 생성해 JWT를 쿠키에 담아 Client 응답에 JWT 전달
또는, 쿠키를 생성하지 않고 ResponseHeader
에 담아 Client 응답에 JWT 전달
클라이언트에서 데이터와 함께 API 요청 시, 쿠키
나 ResponseHeader
에 포함된 JWT를 가져오기
클라이언트에서 전달된 Secret Key
를 사용해 JWT의 위조 여부와 유효기간을 검증
검증 성공 시, JWT에서 사용자 정보
를 가져와 확인하고 요청에 맞는 응답을 클라이언트에 전달
JWT 구조
jwt.io
홈페이지를 통해 누구나 평문으로 복호화 가능해 JWT의 정보를 확인할 수 있다.Secret Key
가 없으면 JWT를 사용하고 수정하는 행위는 불가능하다.Header
, Payload
, Signature
로 구성됩니다.Header
토큰의 유형(타입)
과 해싱 알고리즘
을 지정Payload
클레임(claim)
을 포함하며, 클라이언트와 서버 간에 공유되는 정보Signature
Header
와 Payload
를 인코딩한 후 비밀 키를 사용하여 서명된 값으로, 토큰의 유효성을 검사하는 데 사용JWT 다루기
특정 매개 변수(파라미터)에 대한 작업을 수행하는 메서드들이 존재하는 클래스로, 쉽게 말하면 다른 객체에 의존하지 않고 하나의 모듈로서 동작하는 클래스를 의미한다.
만약 JWT 관련 특정 기능들만 모아놓은 Util 클래스를 JwtUtil
클래스라고 명명하고, JWT를 사용하기위해 구현해야한다.
// Header KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자
public static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간
private final long TOKEN_TIME = 60 * 60 * 1000L; // 60분
@Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey
private String secretKey;
private Key key;
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256; // JWT 암호화 알고리즘
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
@Value
를 통해 properties
에 작성해둔 Secret Key를 가져온다.Secret Key
는 인코딩 되어있기 때문에 디코딩해서 Key 객체로 담아야 한다.@PostConstruct
을 사용해 값을 주입한다.Bearer
은 JWT 또는 OAuth 토큰이라는 것을 표시하는데 사용한다.// 토큰 생성
public String createToken(String username, UserRoleEnum role) {
Date date = new Date();
return BEARER_PREFIX +
Jwts.builder()
.setSubject(username) // 사용자 식별자값(ID)
.claim(AUTHORIZATION_KEY, role) // 사용자 권한
.setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료 시간
.setIssuedAt(date) // 발급일
.signWith(key, signatureAlgorithm) // 키와 암호화 알고리즘
.compact();
}
subject
에 사용자의 정보를 담기 위해 userID
를 넣는다.사용자 권한
정보를 키, 벨류 형식으로 넣는다.issuedAt
에 발급일을 넣는다.signWith
에 Secret Key
값을 담고있는 key와 암호화 알고리즘을 값을 넣는다.compact
을 하면 JWT 토큰이 String
타입으로 생성된다.// JWT Cookie 에 저장
public void addJwtToCookie(String token, HttpServletResponse res) {
try {
token = URLEncoder.encode(token, "utf-8").replaceAll("\\+", "%20"); // Cookie Value 에는 공백이 불가능해서 encoding 진행
Cookie cookie = new Cookie(AUTHORIZATION_HEADER, token); // Name-Value
cookie.setPath("/");
res.addCookie(cookie); // Response 객체에 Cookie 추가
} catch (UnsupportedEncodingException e) {
logger.error(e.getMessage());
}
}
// HttpServletRequest 에서 Cookie Value : JWT 가져오기
public String getTokenFromRequest(HttpServletRequest req) {
Cookie[] cookies = req.getCookies();
if(cookies != null) {
for (Cookie cookie : cookies) {
if (cookie.getName().equals(AUTHORIZATION_HEADER)) {
try {
return URLDecoder.decode(cookie.getValue(), "UTF-8"); // Encode 되어 넘어간 Value 다시 Decode
} catch (UnsupportedEncodingException e) {
return null;
}
}
}
}
return null;
}
// JWT 토큰 substring
public String substringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7);
}
logger.error("Not Found Token");
throw new NullPointerException("Not Found Token");
}
StringUtils.hasText
를 사용하여 공백, null을 확인하고 startsWith
을 사용하여 토큰의 시작값이 Bearer이 맞는지 확인한다.substring
을 사용하여 Bearer을 잘라낸다.// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException | SignatureException e) {
log.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
log.error("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
log.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
log.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
Jwts.parserBuilder()
를 사용하여 JWT를 파싱해 검증에 사용한다.setSigningKey(key)
를 통해 확인한다.// 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
클레임(Claims)
타입으로 되어 있고, 이는 key-value 의 한 쌍으로 이뤄져있다.claims
에서 getSubject()
메서드를 통해, JWT 생성 시 setSubject
로 넣은 username
을 가져와 JPA을 통해 유저 정보를 얻을 수 있다.