이번엔 JWT와 관련한 스프링 코드를 리뷰하는 시간을 가져볼까한다.
예제는 MSA관련하여 공유받은 예제에 GateWay 관련 코드이다.
코드를 모두 작성한 상태는 아니지만 JWT와 관련한 코드의 구조는 아래와 같다
우선 DTO 쪽을 먼저 살펴보자
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@Builder
public class MsgDTO {
private int result; // 결과 코드
private String msg; // 결과 메시지
}
result
=> 인증 요청에 대한 결과
msg
=> 결과에 대한 메시지
MsgDTO는handler
패키지에ErrorMsg
클래스와 함께 인증 에러 혹은 인가 에러가 발생했을 때의 상태를 관리하기도 하고, 인증에 성공했을 때 넘겨주는 값이 되어주기도 한다.
import com.fasterxml.jackson.annotation.JsonInclude;
import lombok.Builder;
import lombok.Getter;
import lombok.Setter;
@Getter
@Setter
@JsonInclude(JsonInclude.Include.NON_DEFAULT)
@Builder
public class TokenDTO {
private String userId; // 회원 아이디
private String role; // 토큰에 저장되는 권한
}
Token에 담겨 있을 유저의 데이터를 담는 역할을 해준다.
아직까진 위 둘이 정확히 무슨 일을 해주는지 감이 잘 안잡힌다.
코드를 좀 더 둘러보자
public enum JwtStatus {
ACCESS, // 유효한 토큰
DENIED, // 유효하지 않은 토큰
EXPIRED // 만료된 토큰(재발행 등 활용)
}
보이는 것 그대로 토큰의 상태를 관리하기 위해 생성한 Enum이다
public enum JwtTokenType {
ACCESS_TOKEN,
REFRESH_TOKEN
}
얘도 굳이 설명할 필요 없어보인다.
import io.jsonwebtoken.*;
import io.jsonwebtoken.io.Decoders;
import kopo.poly.dto.TokenDTO;
import kopo.poly.util.CmmUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.HttpHeaders;
import org.springframework.beans.factory.annotation.Value;
import io.jsonwebtoken.security.Keys;
import org.springframework.http.HttpCookie;
import org.springframework.http.server.reactive.ServerHttpRequest;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import javax.crypto.SecretKey;
import java.util.Date;
import java.util.HashSet;
import java.util.Set;
@Slf4j
@Component
@RequiredArgsConstructor
public class JwtTokenProvider {
@Value("${jwt.secret.key}")
private String secretKey;
@Value("${jwt.token.creator}")
private String creator;
@Value("${jwt.token.access.valid.time}")
private long accessTokenValidTime;
@Value("${jwt.token.access.name}")
private String accessTokenName;
@Value("${jwt.token.refresh.name}")
private String refreshTokenName;
public static final String HEADER_PREFIX = "Bearer"; //Bearer 토큰 사용을 위한 선언 값
/*************이 아래에 메소드들을 append 시키면 된다.*****************/
/**
*
* @param userId 회원 아이디
* @param roles 회원 권한
* @return 인증 처리한 정보(로그인 성공, 실패)
*/
public String createToken(String userId, String roles){
log.info(this.getClass().getName() + ".createToken Start!!!!!");
log.info("userId : " + userId);
Claims claims = Jwts.claims()
.setIssuer(creator) // JWT 토큰 생성자
.setSubject(userId); // 회원아이디 저장 : PK 저장(userId)
claims.put("roles", roles);
Date now = new Date();
log.info(this.getClass().getName() + ".createToken End!!!!");
return Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.setExpiration(new Date(now.getTime() + (accessTokenValidTime * 1000)))
.signWith(SignatureAlgorithm.HS256, secretKey)
.compact();
}
createToken()
=> 함수명 그대로 토큰을 생성하기 위한 메소드이다.
io.jsonwebtoken.Claims는 여러개의 클레임을 관리하는데 사용되는 인터페이스이고
Jwts.claims()는 새로운 Claims를 만드는 객체다.
매개변수로 부터 넘겨받은 userId값과 application.yml에 미리 정의해둔 creator 값을 통해Claim
을 생성하고
put()
을 통해 키와 값을 클레임에 추가시킨 다음
Jwts.builder()를 통해 빌더패턴을 이용하여
JWT를 생성하고 .compact()를 통해 문자열을 생성하여 리턴한다.이전 장에서도 설명했지만,
alg
<->Header
|claim
<->Payload
의 관계를 가지며, Register Claim에는발급시간
,만료시간
등을 담을 수 있다(자세한건 JWT 자세히 보기)
리턴하는 값을 JSON 구조로 본다면 대략적으로 아래와 같을 것이다.
{
"Header": {
"alg": "HS256",
"typ": "JWT"
},
"Payload": {
"iss": creator(에 정의된 값),
"sub": userId (에 정의된 값),
"roles": roles(에 정의된 값),
"iat": 1636582148,
"exp": 1636582189
},
"Signature": "5e9IsjmNOnfJqvpU18x7HtZZRDmuU6zFWXG5eXh9a5U"
}
/**
* JWT토큰 저장된 값 가져오기
*
* @param token 토큰
* @return 회원 아이디
*/
public TokenDTO getTokenInfo(String token){
log.info(this.getClass().getName() + ".getTokenInfo Start!!!");
// 보안키 문자들을 JWT Key 형태로 변경하기
SecretKey secret = Keys.hmacShaKeyFor(Decoders.BASE64URL.decode(secretKey));
// SecretKey secret = Keys.hmacShaKeyFor(Decoders.BASE64.decode(secretKey));
// JWT 토큰 정보
Claims claims = Jwts.parser().setSigningKey(secret).parseClaimsJws(token).getBody();
String userId = CmmUtil.nvl(claims.getSubject());
String role = CmmUtil.nvl((String) claims.get("roles"));
log.info("userId : " + userId);
log.info("role : " + role);
TokenDTO rDTO = TokenDTO.builder().userId(userId).role(role).build();
log.info(this.getClass().getName() + ".getTokenInfo End!!!");
return rDTO;
}
JWT 토큰에 저장된 값을 가져오기 위한 함수이다.
configFile에서 미리 정의해둔 jwt의 시크릿 키 값을
Decoders.BASE64URL.decode를 통해 시크릿 키를 디코딩하고
Keys.hmacShaKeyFor
에 디코딩된 바이트 배열을 전달하여서 HMAC-SHA (Hash-based Message Authentication Code - Secure Hash Algorithm) 알고리즘에 사용될 SecretKey를 생성하고
Claims에 담에 getSubject()를 통해 userId를 받고 HashMap 으로 저장되어 있는 claims에서 get("roles")로 권한을 가져온다.
사실 이해 안가는 부분이 있어 교수님께 여쭤보려했으나..
(교수님께서 주신 예제에서는 보안키 문자들을 BASE64로 변환했는데 내가 저번에 작성한 글에서 보면 JWT는 BASE64URL를 사용한다.. 그래서 교수님이 주신 예제코드를 주석 처리 때렸다.ㅋㅋ)
지금부터 JWT랑 SpringSecurity를 공부해서 MSA를 구현하기엔 마감기한까지 힘들 것 같다는 피드백을 받았다.
JWT랑 SpringSecurity가 1학년 학생이 쉽게 이해할 수 내용들이 아니기 때문에 Redis와 Spring Session을 이용하라는 피드백을 주셨기 때문에... 당분간은 Redis쪽을 공부할 예정이라 이 글은 당분간 작성을 중단해야할 것 같다.