지난 포스트에서는 Netflix Zuul의 ZuulFilter를 활용해 토큰을 검증하는 과정까지 설명하였습니다.
하지만 토큰을 검증하기 전에는 당연히 토큰을 발급하는 과정이 필요한데, 이번 포스트에선 그 과정에 대해 다루겠습니다.
Spring에서 JWT를 사용하기 위한 라이브러리 입니다.
JWT에 대한 사전 지식이 부족할 경우 JWT 포스트를 참조해주세요.
groovy
1 dependencies { 2 implementation 'io.jsonwebtoken:jjwt:0.9.0' 3 }
사용하기 위해선 gradle이나 maven에 의존성을 추가해야 합니다.
jjwt를 활용해서 JWT를 생성하거나 혹은 분석할 수 있습니다.
로그인 컨트롤러를 구현합니다.
화면단에서 넘어오는 requestBody와 추후에 refresh-token 파트에서 사용할 HttpServletRequest를 받아와줍니다.
java
1 @RestController 2 @RequiredArgsConstructor 3 public class SignController { 4 private final SignService signService; 5 6 @PostMapping("/auth/sign/in") 7 public ResponseEntity<SignInResponse> signIn(HttpServletRequest request, @RequestBody SignInRequest requestBody) { 8 return ResponseEntity.ok(signService.signIn(request, requestBody)); 9 } 10 }
requestDto에는 로그인시에 입력하는 아이디와 비밀번호가 포함되어 있습니다.
java
1 @Getter 2 @AllArgsConstructor 3 public class SignInRequest { 4 private String id; 5 6 private String password; 7 }
responseDto에는 프론트엔드로 보내줄 token값과, 상태관리에서 사용할 멤버 이름이나 아이디, 권한 등을 보내줄 수 있습니다.
해당 데이터들은 Redux와 같은 상태 관리를 통해 클라이언트에서 사용이 가능합니다.
java
1 @NoArgsConstructor 2 @AllArgsConstructor(access = Accesslevel.PRIVATE) 3 @Builder 4 @Getter 5 public class SignInResponse { 6 private String memberName; 7 8 private String token; 9 }
서비스에서는 decrypt과정을 통해 암호를 해독하여 일치하는지 확인합니다.
그 후 access token을 만들어 보내줍니다.
java
1 @Service 2 @RequiredArgsConstructor 3 public class SignService { 4 5 private final Logger logger = (Logger) LoggerFactory.getLogger(this.getClass()); 6 private static final String AUTH_TOKEN_HEADER_PREFIX = "Bearer "; 7 8 private final BCryptPasswordEncoder passwordEncoder; 9 private final JwtUtil jwtUtil; 10 private final UserRepository userRepository; 11 12 @Transactional 13 public SignResponse signIn(HttpServletRequest request, SignInRequest requestBody) { 14 User loginUser = userDao 15 .findById(requestBody.getId()) 16 .filter(user -> passwordEncoder.matches(AesCryptUtil.decrypt(requestBody.getPassword()), user.getPassword())) 17 .updateLoginTime(); 18 19 String accessToken = jwtUtil.createAccessToken(); 20 21 logger.info("{} login success", requestBody.getId()); 22 23 return SignInResponse.builder() 24 .memberName(loginUser.getMemberName()) 25 .token(accessToken).build(); 26 } 27 }
JWT를 생성할 때는 어플리케이션의 성격에 맞게 claim값들을 설정할 수 있습니다.
등록된 클레임(Registered Claim)은 Jwts객체에 setter가 존재하여 이를 통해 설정할 수 있습니다.
만료 시간(Expiration Time) 클레임은 설정해주어서 만료 시간이 지날 경우 토큰이 폐기되도록 하는 것이 좋습니다.
jwtUtil에서는 이들을 통해 access-token을 생성해줍니다.
HashMap을 이용해 body claim을 넣어줄 수 있습니다.
java
1 @Component 2 public class JwtUtil { 3 4 @Value("${jwt.signKey}") 5 private String jwtSignKey; 6 7 public String createAccessToken() { 8 Map<String, Object> body = new HashMap<>(); 9 body.put(PublicClaimKey.NAME.getValue(), user.getName()); 10 body.put(PrivateClaimKey.USER_ID.getValue(), user.getId()); 11 12 return this.createToken("accessToken", body, 30); 13 } 14 15 public String createToken(String subject, Map<String, Object> body, int expirationTime) { 16 LocalDateTime currentTime = LocalDateTime.now(); 17 18 Claims claims = Jwts.claims(body); 19 20 return Jwts.builder() 21 .setClaims(claims) 22 .setSubject(subject) 23 .setIssuer("https://oms-tech-blog.netlify.app") 24 .setIssuedAt(Date.from(currentTime.atZone(ZoneId.systemDefault()).toInstant())) 25 .setExpiration(Date.from(currentTime.plusMinutes(expirationTime).atZone(ZoneId.systemDefault()).toInstant())) 26 .signWith(SignatureAlgorithm.HS512, jwtSignKey) 27 .compact(); 28 } 29 }
공캐 클레임(Public Claim)이나 비공개 클레임(Private Claim)들은 enum등을 활용해서 구현 할 수 있습니다.
TokenField interface를 이용하면 Claim을 parse하거나 set해줄 때 TokenField 타입을 활용할 수 있습니다.
java
1 public interface TokenField { 2 String getValue(); 3 }
java
1 public enum PublicClaimKey implements TokenField { 2 NAME("name"), 3 NICKNAME("nickname"); 4 5 private String value; 6 7 PublicClaimKey(String value) { 8 this.value = value; 9 } 10 11 public String getValue() { 12 return value; 13 } 14 }
java
1 public enum PrivateClaimKey implements TokenField { 2 USER_ID("userId"); 3 4 private String value; 5 6 PrivateClaimKey(String value) { 7 this.value = value; 8 } 9 10 public String getValue() { 11 return value; 12 } 13 }
원래 refresh token 발급 까지 설명하고 구현하려 하였으나 생각보다 양이 많아져서 한번 더 나누게 되었습니다.
다음 포스터에서는 access token의 한계점과 refresh token에 대해서 설명하고 구현하도록 하겠습니다.