JWT란 무엇일까?
JWT(Json Web Token): JSON 포맷을 이용하여 사용자에 대한 속성을 저장하는 Claim 기반의 Web Token 입니다. 즉, 토큰의 한 종류라고 생각하시면 됩니다. 일반적으로 쿠키 저장소를 사용하여 JWT를 저장합니다.
JWT 장/단점
- 장점
- 동시 접속자가 많을 때 서버 측 부하 낮춤
- Client, Sever 가 다른 도메인을 사용할 때
- 예) 카카오 OAuth2 로그인 시 JWT Token 사용
- 단점
- 구현의 복잡도 증가
- JWT에 담는 내용이 커질 수록 네트워크 비용 증가 (클라이언트 → 서버)
- 기 생성된 JWT 를 일부만 만료시킬 방법이 없음
- SecretKey 유출 시 JWT 조작 가능
JWT 사용 흐름
- Client 가 username, password 로 로그인 성공 시
- 서버에서 "로그인 정보" → JWT 로 암호화 (Secret Key 사용)
- 서버에서 직접 쿠키를 생성해 JWT를 담아 Client 응답에 전달
- 브라우저 쿠키 저장소에 자동으로 JWT 저장됨
- Client 에서 JWT 통해 인증방법
- 서버에서 API 요청 시마다 쿠키에 포함된 JWT를 찾아서 사용 - 쿠키에 담긴 정보가 여러 개일 수 있기 때문에 그 중 이름이 JWT가 담긴 쿠키의 이름과 동일한지 확인하여 JWT를 가져옵니다.
- Server
- Client 가 전달한 JWT 위조 여부 검증 (Secret Key 사용)
- JWT 유효기간이 지나지 않았는지 검증
- JWT에서 사용자 정보를 가져와 확인
JWT 생성 및 활용
0. 토큰 생성에 필요한 데이터
import java.util.Base64;
// 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;
// 로그 설정
public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
- Base64로 Encode된 Secret Key를 properties에 작성해두고 @Value를 통해 가져옵니다.
- JWT를 생성할 때 가져온 Secret Key로 암호화합니다.
- 이때 Encode된 Secret Key를 Decode 해서 사용합니다.
- Key는 Decode된 Secret Key를 담는 객체입니다.
- @PostConstruct는 딱 한 번만 받아오면 되는 값을 사용 할 때마다 요청을 새로 호출하는 실수를 방지하기 위해 사용됩니다.
- 해당 클래스의 생성자 호출 이후에 실행되어 Key 필드에 값을 주입 해줍니다.
- 암호화 알고리즘은 HS256 알고리즘을 사용합니다.
- Bearer 이란 JWT 혹은 OAuth에 대한 토큰을 사용한다는 표시입니다.
1. JWT 생성
// 토큰 생성
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();
}
- JWT의 subject에 사용자의 식별값 즉, ID를 넣습니다.
- JWT에 사용자의 권한 정보를 넣습니다. key-value 형식으로 key 값을 통해 확인할 수 있습니다.
- 토큰 만료시간을 넣습니다. ms 기준입니다.
- issuedAt에 발급일을 넣습니다.
- signWith에 secretKey 값을 담고있는 key와 암호화 알고리즘을 값을 넣어줍니다.
- key와 암호화 알고리즘을 사용하여 JWT를 암호화합니다.
2. JWT Cookie에 저장
import java.io.UnsupportedEncodingException;
import java.net.URLEncoder;
// 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("/");
// Response 객체에 Cookie 추가
res.addCookie(cookie);
} catch (UnsupportedEncodingException e) {
logger.error(e.getMessage());
}
}
3.받아온 Cookie의 Value인 JWT 토큰 substring
// 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이 맞는지 확인합니다. 맞다면 순수 JWT를 반환하기 위해 substring을 사용하여 'Bearer '을 잘라냅니다.
4. JWT 검증
import java.security.SignatureException;
// 토큰 검증
public boolean validateToken(String token) {
try {
Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
return true;
} catch (SecurityException | MalformedJwtException | SignatureException e) {
logger.error("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
} catch (ExpiredJwtException e) {
logger.error("Expired JWT token, 만료된 JWT token 입니다.");
} catch (UnsupportedJwtException e) {
logger.error("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
} catch (IllegalArgumentException e) {
logger.error("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
}
return false;
}
Jwts.parserBuilder() 를 사용하여 JWT를 파싱할 수 있습니다. JWT가 위변조되지 않았는지 secretKey(key)값을 넣어 확인합니다.
5.JWT에서 사용자 정보 가져오기
// 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
JWT의 구조 중 Payload 부분에는 토큰에 담긴 정보가 들어있습니다. 여기에 담긴 정보의 한 ‘조각’ 을 클레임(claim) 이라고 부르고, 이는 key-value의 한 쌍으로 이뤄져있습니다. 토큰에는 여러 개의 클레임들을 넣을 수 있습니다. Jwts.parserBuilder() 와 secretKey를 사용하여 JWT의 Claims를 가져와 담겨 있는 사용자의 정보를 사용합니다.
'Spring > 팀스파르타' 카테고리의 다른 글
30. Spring Security 로그인 (1) | 2024.11.20 |
---|---|
28. 필터란 무엇일까? (0) | 2024.11.16 |
26. 쿠키와 세션이란 무엇일까? (0) | 2024.11.13 |
25. 인증과 인가란 무엇일까? (1) | 2024.11.12 |
24. Bean을 수동으로 등록하기 (1) | 2024.11.10 |