내일배움캠프 25일차 TIL : Spring - JWT 다루기

김원기·2024년 5월 28일

TIL

목록 보기
29/42
post-thumbnail

내일배움캠프 25일차 TIL : Spring - JWT 다루기

슬슬 JWT 구현해봐야 하니까...

의존성 추가

// 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'

먼저 build.gradle에 의존성을 먼저 추가해줘야 한다.

의존성을 추가해줬다면 다음으로는 application.properties에 jwt.secret.key 값을 추가한다.

jwt.secret.key=7Iqk7YyM66W07YOA7L2U65Sp7YG065+9U3ByaW5n6rCV7J2Y7Yqc7YSw7LWc7JuQ67mI7J6F64uI64ukLg==

JWT 생성 전에

클래스에서 필요한 멤버들을 먼저 생성할 예정이다.

// Header KEY 값, Cookie의 name 값
public static final String AUTHORIZATION_HEADER = "Authorization";
// 사용자 권한 값의 KEY
public static final String AUTHORIZATION_KEY = "auth";
// Token 식별자
public static final String BEARER_PREFIX = "Bearer ";
// 토큰 만료시간 60분
private final long TOKEN_TIME = 60 * 60 * 1000L;

@Value("${jwt.secret.key}") // Base64 Encode 한 SecretKey, application.properties에 선언
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);
}

JWT 생성

public String generateToken(String username, UserRoleEnum role) {
        Date date = new Date();
        return BEARER_PREFIX + Jwts.builder()
                .setSubject(username)
                .setIssuedAt(date)
                .setExpiration(new Date(date.getTime() + TOKEN_TIME))
                .signWith(key, signatureAlgorithm)
                .claim(AUTHORIZATION_KEY, role)
                .compact();
    }

Jwts.builder()를 통해 JWT 토큰을 생성한다.

  • .setSubject(username)
    : 사용자를 식별하는 값으로 PK, Id, username 다 상관 없다.
  • .setIssuedAt(date)
    : 이슈를 설정하는 부분이다. 여기서는 발급일을 저장하며, 선택사항이다.
  • .setExpiration(new Date(date.getTime() + TOKEN_TIME))
    : 위에서 설정한 만료기간을 저장하는 부분이다.
  • .signWith(key, signatureAlgorithm)
    : 암호화 알고리즘과 SecretKey를 디코딩한 키 값으로 서명하여 JWT를 생성한다.
  • .claim(AUTHORIZATION_KEY, role)
    : 보통 Payload에 담겨지는 Key-value 값을 담당한다. 주로 사용자 ID, 권한, 만료 시간 등을 포함하며, 여기서는 권한을 담당하게 됬다.

해당 빌더에 들어간 것 외에 옵션을 더 추가하거나 프로젝트 설정에 맞게 뺄 수 있다.

토큰 나누기

JWT 토큰을 나누는 과정이다.

이 과정이 필요한 이유는 우리가 JWT를 빌드할 때 Bearer 뒤에 붙여놨기 때문이다.

물론 토큰을 직접 사용할 수 있나..?

Bearer 토큰은 OAuth 2.0 및 JWT 표준에서 정의된 방식으로 단순히 헤더에 토큰 값을 추가하는 방식으로 구현할 수 있다.

여튼 전달하기에는 편리한 방식이긴 하지만 실제로 발급받은 JWT 토큰은 Bearer 의 뒷 부분이기 때문에 일단 자르도록 하겠다.

public String substringToken(String tokenValue) {
    // 토큰의 값 검사
    if(StringUtils.hasText(tokenValue) && tokenValue.startsWith(BEARER_PREFIX)) {
        return tokenValue.substring(7); // BAERER_PREFIX 숫자의 길이가 7이므로 그 이후의 값을 짤라서 가져옴
    }
    System.out.println("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) {
        System.out.println("Invalid JWT signature, 유효하지 않는 JWT 서명 입니다.");
    } catch (ExpiredJwtException e) {
        System.out.println("Expired JWT token, 만료된 JWT token 입니다.");
    } catch (UnsupportedJwtException e) {
        System.out.println("Unsupported JWT token, 지원되지 않는 JWT 토큰 입니다.");
    } catch (IllegalArgumentException e) {
        System.out.println("JWT claims is empty, 잘못된 JWT 토큰 입니다.");
    }
    return false;
}
  • Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token):

이 부분은 주어진 토큰을 파싱하고, 서명을 확인하여 유효한 JWT인지 확인한다.
key는 토큰 서명에 사용되는 키이며, parseClaimsJws(token)는 토큰의 클레임(claim)을 추출한다.

그 다음 try catch를 통해 각 예외를 잡아 적절한 상황의 메시지를 출력하며
예외가 없이 정상적으로 작동하는 경우 true를 반환해준다.

검증 메서드는 따로 커스텀 하여 더 좋게 만들 수 있다.

쿠키에 저장

// 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) {
        System.out.println(e.getMessage());
    }
}

서버가 토큰을 발급받아서 클라이언트(브라우저 즉 사용자) 에게 전달을 해준다 하여도 저장해놓지 않으면 사용할 수 없기 때문에 쿠키에 저장하여 사용자에게 던져준다.

값을 사용하기 전에

public Claims getUserInfoFromToken(String token) {
        // 담겨 있는 사용자의 정보를 사용 Key - value
    return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token).getBody();
}

위에서 Claim은 Payload에 들어가는 값을 담당한다고 했고 해당 메서드를 통해 payload의 값을 가져와 사용할 수 있다.

토큰을 생성해보자

@GetMapping("/create-token")
public ResponseEntity<CustomResponse<?>> createJwt(HttpServletResponse res, @RequestBody UserLoginRequestDto loginRequestDto) {
    String token = jwtUtil.generateToken(loginRequestDto.getUsername(), UserRoleEnum.valueOf(loginRequestDto.getRole()));
    jwtUtil.addJwtToCookie(token, res);
    return ResponseEntity.ok().body(CustomResponse.makeResponse(token, HttpStatus.OK));
}

따로 컨트롤러에서 토큰을 생성하고 쿠키에 저장해준다.

토큰의 값을 가져와 보자

@GetMapping("/get-token")
public ResponseEntity<CustomResponse<?>>  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);

    String result = "getJwt : " + username + ", " + authority;
    return ResponseEntity.ok().body(CustomResponse.makeResponse(result, HttpStatus.OK));
}

끝!

profile
혼자 공부하는 블로그라 부족함이 많아요 https://www.notion.so/18067a27ac7e4f4790dde645fb3bf3d3?pvs=4

0개의 댓글