JWT 란?
JSON 포맷을 이용하여 사용자에 대한 속정을 저장하는 Claim 기반의 Web Token 이다.
일반적으로 쿠키 저장소를 사용하여 JWT 를 저장한다. ( Header )
세션ID 로 해도 되는데 왜 JWT?
먼저 세션 ID 로 관리할때 만약 서버를 여러대 두고 사용하면 세션을 관리할때 서로 서버의 세션들이 있을텐데 어떻게 관리할까...
- 해결 방법 : Sticky Session : Client 마다 요청 Server 고정.
또는 세션 저장소 생성하여 모든 세션을 저장.
하지만
JWT 사용하면
로그인 정보를 Server 에 저장하지 않고 Client 에 로그인 정보를 JWT 암호화 저장 -> JWT 통해 인증 / 인가
모든 서버에서 동일한 Secret Key 소유
Secret Key 통한 암호화 / 위조 검증
또한 세션ID 로 구현한다면 서버에 저장하기 때문에 자원이 소요되지만 JWT 는 SecretKey 만 소요 하고 있다면 되므로 자원을 세션 ID 보다 더 좋음.
JWT 장단점
장점
- 동시 접속자가 많을 때 서버 측 부하 낮춤
- Client Server 가 다른 도메인을 사용할 때.
: 카카오 OAuth2 로그인시
단점
- 구현의 복잡도 증가
- JWT 에 담는 내용이 클수록 네트워크 비용 증가
- 키 새성된 JWT 를 일부만 만료시킬 방법이 없음
- Secret key 유출 시 JWT 조작 가능
JWT 구조
서버에서 직접 쿠키를 생성해 JWT 담아 Client 응답에 전달
- Header 에 JWT 전달.
Client 에서 JWT 통해 인증 방법
- 서버에서 API 요청시마다 쿠키에 포함된 JWT를 찾아서 사용
Server
- Client 가 전달한 JWT 위조 여부 검증 ( Secret Key 사용 )
- JWT 유효기간이 지나지 않았는지 검증
- 검증 성공시
- JWT -> 에서 사용자 정보를 가져와 확인
JWT 는 누구나 평문으로 복호화 가능합니다. 하지만 Secret Key 가 없으면 JWT 수정 불가능하다.
결국 JWT 는 Read Only 데이터 이다.
Bearer 란?
우리가 궁금해하던 bearer는 형식에서 type에 해당. 토큰에는 많은 종류가 있고 서버는 다양한 종류의 토큰을 처리하기 위해 전송받은 type에 따라 토큰을 다르게 처리합니다.
아래는 인증 타입의 종류에서 나타난 것 처럼, bearer는 JWT와 OAuth를 타나내는 인증 타입입니다.
인증 타입
Basic
사용자 아이디와 암호를 Base64로 인코딩한 값을 토큰으로 사용한다. (RFC 7617)
Bearer
JWT 혹은 OAuth에 대한 토큰을 사용한다. (RFC 6750)
Digest
서버에서 난수 데이터 문자열을 클라이언트에 보낸다. 클라이언트는 사용자 정보와 nonce를 포함하는 해시값을 사용하여 응답한다 (RFC 7616)
HOBA
전자 서명 기반 인증 (RFC 7486)
Mutual
암호를 이용한 클라이언트-서버 상호 인증 (draft-ietf-httpauth-mutual)
실제로 작성해보기
build.gradle
// JWT
compileOnly group: 'io.jsonwebtoken', name: 'jjwt-api', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-impl', version: '0.11.5'
runtimeOnly group: 'io.jsonwebtoken', name: 'jjwt-jackson', version: '0.11.5'
Util
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;
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes);
}
// 토큰 생성
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 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) {
log.error(e.getMessage());
}
}
// JWT 토큰 substring
public String substringToken(String tokenValue) {
if (StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7);
}
log.error("Not Found Token");
throw new NullPointerException("Not Found Token");
}
// 토큰 검증
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;
}
// 토큰에서 사용자 정보 가져오기
public Claims getUserInfoFromToken(String token) {
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
권한 enum class
@Getter
@RequiredArgsConstructor
public enum UserRoleEnum {
USER(Authority.USER), // 사용자 권한
ADMIN(Authority.ADMIN); // 관리자 권한
private final String authority;
public static class Authority {
public static final String USER = "ROLE_USER";
public static final String ADMIN = "ROLE_ADMIN";
}
}
'Spring' 카테고리의 다른 글
Filter (0) | 2023.06.26 |
---|---|
Spring Security (3) (0) | 2023.06.26 |
@Configuration 쓰고 안쓰고의 차이점 (0) | 2023.06.24 |
Spring Retry (0) | 2023.06.19 |
JPA Dirty Checking (0) | 2023.06.17 |