JWT 로그인 방식 구현

2023-05-15
ꞏ
유영진
ꞏ
show5116

지난 포스트에서는 Netflix Zuul의 ZuulFilter를 활용해 토큰을 검증하는 과정까지 설명하였습니다.
하지만 토큰을 검증하기 전에는 당연히 토큰을 발급하는 과정이 필요한데, 이번 포스트에선 그 과정에 대해 다루겠습니다.

jjwt

Spring에서 JWT를 사용하기 위한 라이브러리 입니다.
JWT에 대한 사전 지식이 부족할 경우 JWT 포스트를 참조해주세요.

groovy
1dependencies {
2 implementation 'io.jsonwebtoken:jjwt:0.9.0'
3}

사용하기 위해선 gradle이나 maven에 의존성을 추가해야 합니다.
jjwt를 활용해서 JWT를 생성하거나 혹은 분석할 수 있습니다.

로그인 구현

controller

로그인 컨트롤러를 구현합니다.
화면단에서 넘어오는 requestBody와 추후에 refresh-token 파트에서 사용할 HttpServletRequest를 받아와줍니다.

java
1@RestController
2@RequiredArgsConstructor
3public 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

requestDto에는 로그인시에 입력하는 아이디와 비밀번호가 포함되어 있습니다.

java
1@Getter
2@AllArgsConstructor
3public class SignInRequest {
4 private String id;
5
6 private String password;
7}

responseDto

responseDto에는 프론트엔드로 보내줄 token값과, 상태관리에서 사용할 멤버 이름이나 아이디, 권한 등을 보내줄 수 있습니다.
해당 데이터들은 Redux와 같은 상태 관리를 통해 클라이언트에서 사용이 가능합니다.

java
1@NoArgsConstructor
2@AllArgsConstructor(access = Accesslevel.PRIVATE)
3@Builder
4@Getter
5public class SignInResponse {
6 private String memberName;
7
8 private String token;
9}

Service

서비스에서는 decrypt과정을 통해 암호를 해독하여 일치하는지 확인합니다.
그 후 access token을 만들어 보내줍니다.

java
1@Service
2@RequiredArgsConstructor
3public 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}

JwtUtil

JWT를 생성할 때는 어플리케이션의 성격에 맞게 claim값들을 설정할 수 있습니다.
등록된 클레임(Registered Claim)은 Jwts객체에 setter가 존재하여 이를 통해 설정할 수 있습니다.
만료 시간(Expiration Time) 클레임은 설정해주어서 만료 시간이 지날 경우 토큰이 폐기되도록 하는 것이 좋습니다.
jwtUtil에서는 이들을 통해 access-token을 생성해줍니다.
HashMap을 이용해 body claim을 넣어줄 수 있습니다.

java
1@Component
2public 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}

enum

공캐 클레임(Public Claim)이나 비공개 클레임(Private Claim)들은 enum등을 활용해서 구현 할 수 있습니다.
TokenField interface를 이용하면 Claim을 parse하거나 set해줄 때 TokenField 타입을 활용할 수 있습니다.

java
1public interface TokenField {
2 String getValue();
3}
java
1public 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
1public 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에 대해서 설명하고 구현하도록 하겠습니다.