먼저 프로젝트 설정을 추가해야한다.
JWT dependencey 추가한다.
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'
application.properties에 jwt.secret.key 값을 추가하자. (임의로 정함)
jwt.secret.key=7Iqk7YyM66W07YOA7L2U65Sp7YG065+9U3ByaW5n6rCV7J2Y7Yqc7YSw7LWc7JuQ67mI7J6F64uI64ukLg==
하나의 모듈로서 동작하는 클래스
(JWT 관련 기능들을 가진 JwtUtil이라는 클래스를 만들어서 JWT 관련 기능을 수행)
JwtUtil 클래스 안에 5가지의 기능을 넣을 것임
일단 먼저 JwtUtil에서 필요한 JWT 데이터를 선언할 것이다.
// Header KEY 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자 Bearer 란 JWT 혹은 OAuth에 대한 토큰을 사용한다는 표시
public static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간
public final long TOKEN_TIME = 60 * 60 * 1000L;
// application.properties 에 있는 secretKey 값을 가져오는 방법
@Value("${jwt.secret.key}")
private String secretKey;
private Key key;
// 암호화 알고리즘 HS256으로 사용할 것임
private final SignatureAlgorithm signatureAlgorithm = SignatureAlgorithm.HS256;
// 로그 찍는 것
public static final Logger logger = LoggerFactory.getLogger("JWT 관련 로그");
// @PostConstruct는 딱 한 번만 받아오면 되는 값을 사용 할 때마다 요청을 새로 호출하는 실수를 방지하기 위해 사용
@PostConstruct
public void init() {
byte[] bytes = Base64.getDecoder().decode(secretKey);
key = Keys.hmacShaKeyFor(bytes); // key값에 우리가 사용할 secret 값이 담겨진다.
}
이렇게 미리 데이터를 선언한다.
public String createToken(String username, UserRoleEnum role) {
Date date = new Date();
// 이 코드를 통해서 jwt 토큰을 생성할 수 있다.
return BEARER_PREFIX + Jwts.builder()
.setSubject(username) // 사용자 식별자값(ID) (pk든 아이디값이든 유저이름이든 상관없음)
.claim(AUTHORIZATION_KEY, role) // 사용자 권한 앞에는 key, 뒤에는 value 권한값
.setExpiration(new Date(date.getTime() + TOKEN_TIME)) // 만료시간
.setIssuedAt(date) // 발급입
.signWith(key,signatureAlgorithm) // 암호화 알고리즘 -> 암호화 알고리즘과 키를 넣어줌
.compact();
// 이 암호화 코드는 기호에 맞게 옵션을 부여하여 설정하면 된다.
}
JWT 토큰을 생성할 때, Jwts 클래스 안에서 builder 를 통해서 식별자값, 권한, 만료시간, 발급일, 암호화 값을 넣어주었다.
하지만 이렇게 하나하나 넣어줄 필요없이 나중에 프로젝트에 맞게 옵션을 부여하여 설정하면 된다.
public void addJwtToCookie(String token, HttpServletResponse response) {
try {
// 공백 불가능 하기 때문에 공백을 %20으로 바꿔주고 인코딩하여 token으로 처리 (쿠키는 공백이 불가능)
token = URLEncoder.encode(token, "utf-8").replaceAll("\\+","%20");
// 쿠키를 설정하여 Name-Value 값으로 쿠키 생성 (헤더값과 url 토큰값을 넣음)
Cookie cookie = new Cookie(AUTHORIZATION_HEADER,token);
cookie.setPath("/"); // path도 이렇게 한번 줘봄
response.addCookie(cookie); // Response 객체에 Cookie를 추가
} catch (UnsupportedEncodingException e) {
logger.error(e.getMessage());
}
}
쿠키에 JWT 토큰을 저장할 때, 처음 JWT 데이터를 만들 때
public static final String BEARER_PREFIX = "Bearer "
이렇게 설정한 적이 있다. 위에서 createToken 메서드에서 해당 토큰의 값 안에는 띄어쓰기가 포함되어 있는데, 쿠키에는 공백이 들어있으면 안되기 때문에 해당 공백을 %20으로 바꿔주었다.
파라미터 값으로 들어온 HttpServletResponse 값에 가공된 쿠키의 값을 넣어주면 된다.
public String substringToken(String tokenValue) {
// Cookie에 있는 토큰의 값을 가져와서 검사를 한다
if(StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
return tokenValue.substring(7); // BAERER_PREFIX 숫자의 길이가 7이므로 그 이후의 값을 짤라서 가져옴
}
logger.error("Not Found Token");
throw new NullPointerException("Not Found Token");
}
나중에 쿠키의 값을 가져와서 체크를 할 때 사용될 메서드이다.
해당 쿠키 안에 JWT 토큰이 있을 때, if문을 통해 해당 토큰이 있는지 확인하고, BEARER_PREFIX 값으로 시작되는지 확인하면 해당 토큰의 값을 짤라서 반환한다.
왜냐하면 BEARER_PREFIX 값이 "Bearer " 이므로 7글자이기 때문에 그 뒤의 토큰값을 가져오는 형식이다.
그 외에는 예외처리한다.
public boolean validateToken(String token) {
try {
// Jwts.parserBuilder() 를 사용하여 JWT를 파싱, JWT 가 위변조되지 않았는지 secretKey(key)값을 넣어 확인
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;
}
위에서 Cookie에 들어있던 JWT 토큰을 잘라서 반환된 값을 받아와서 검증한다.
Jwts.parserBuilder()를 사용해서 JWT를 파싱한다.
이 검증 메서드 자체는 따로 커스텀해서 더 좋게 만들 수 있다.
public Claims getUserInfoFromToken(String token) {
// Jwts.parserBuilder() 와 secretKey를 사용하여 JWT의 Claims를 가져와 담겨 있는 사용자의 정보를 사용
return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}
JWT의 구조 중 Payload 부분에는 토큰에 담긴 정보가 들어있다.
여기에 담긴 정보의 한 '조각'을 클레임(claim)이라고 부르고, 이는 key-value 의 한쌍으로 이루어져있다.
토큰에는 여러개의 클레임 들을 넣을 수 있다.
이렇게 JWT 를 다루는 방법에 대해 작성했지만 커스텀을 통해 더 좋게 만들 수 있다.
@GetMapping("/create-jwt")
public String createJwt(HttpServletResponse res) {
// Jwt 생성
String token = jwtUtil.createToken("Robbie", UserRoleEnum.USER);
// Jwt 쿠키 저장
jwtUtil.addJwtToCookie(token, res);
return "createJwt : " + token;
}
@GetMapping("/get-jwt")
public String getJwt(@CookieValue(JwtUtil.AUTHORIZATION_HEADER) String tokenValue) {
// JWT 토큰 substring
String token = jwtUtil.substringToken(tokenValue);
// 토큰 검증
if(!jwtUtil.validateToken(token)){
throw new IllegalArgumentException("Token Error");
}
// 토큰에서 사용자 정보 가져오기
Claims info = jwtUtil.getUserInfoFromToken(token);
// 사용자 username
String username = info.getSubject();
System.out.println("username = " + username);
// 사용자 권한
String authority = (String) info.get(JwtUtil.AUTHORIZATION_KEY);
System.out.println("authority = " + authority);
return "getJwt : " + username + ", " + authority;
}
토큰을 생성해서 쿠키에 저장하고 반환해주는 메서드가 createJwt 메서드이다.
HttpServletResponse 객체에 쿠키 값이 저장되어서 클라이언트에게 반환이 된다.
그리고 getJwt 메서드는 해당 쿠키 값을 읽어와서 검증하고 토큰에서 사용자의 정보를 가져오는 방법이다.
@CookieValue 어노테이션을 통해서 쿠키를 전달받을 수 있으며, value 속성을 전달 받을 쿠키의 이름을 지정하면 된다.
위에서 토큰을 생성했을 때,
.setSubject(username)
유저이름을 저장했으니
Claims info = jwtUtil.getUserInfoFromToken(token);
String username = info.getSubject();
Claim 객체를 통해 해당 유저의 이름을 반환 받을 수 있고 사용자 권한 또한 가져올 수 있다.