스프링 공부/게시판 프로젝트 만들기

[스프링] 8. JWT를 구현하기. 1단계: 일단 JWT를 위한 토큰을 만들자. (2)

장아장 2023. 1. 6. 21:10

이전에 정리해두었던 TokenProvider의 기능들을 정리해보려고 한다. 

 

일단 생성자!

public TokenProvider(@Value("${jwt.secret}") String secretKey) {
    byte[] keyBytes = Decoders.BASE64.decode(secretKey);
    this.key = Keys.hmacShaKeyFor(keyBytes);
}

@Value에서 jwt.secret는 application.yml에서 가져오는 것이다.

jwt:
  secret:

이런식으로 되어있을 때, secret: 뒤에 사용할 키를 적으면 된다. 

이전 페이지에서 정리를 간단히 했던 것 같은데, 그게 전부라 설명은 스킵!

 

public TokenDto generateTokenDto(Authentication authentication) {
    return new TokenDto(authentication, key);
}

이전 페이지에서 TokenDto에 대한 생성자를 열심히 만들어두었다. 그걸 토대로 generateTokenDto를 해준 것이다. 

 

private Claims parseClaims(String accessToken){
    try{
        return Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(accessToken).getBody();
    } catch (ExpiredJwtException exception){
        return exception.getClaims();
    }
}

액세스토큰을 가져왔을 때, 토큰을 복호화하고, 필요한 정보를 빼내야 한다. 

이 과정에서 ExpiredJwtException는 JWT에서 지원하는 예외처리로, 액세스 토큰이 만기되었을 때, 만기된 채로 Claims를 반환하게 해주었다. 

왜냐하면, 그냥 예외처리시키는 것이 아니라, 우리는 이걸 집어서 Refresh Token을 받아와 액세스 토큰을 다시 만들어주어야 하기 때문이다. 

 

public boolean validateToken(String token) {
    try {
        Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(token);
        return true;
    } catch (io.jsonwebtoken.security.SecurityException | MalformedJwtException e) {
        log.info("잘못된 JWT 서명입니다.");
    } catch (ExpiredJwtException e) {
        log.info("만료된 JWT 토큰입니다.");
    } catch (UnsupportedJwtException e) {
        log.info("지원되지 않는 JWT 토큰입니다.");
    } catch (WrongTokenException e) {
        log.info("JWT 토큰이 잘못되었습니다.");
    }
    return false;
}

여기에서는 JWT예외가 발생했을 때, 예외처리된 내용들을 INFO레벨로 로그 라이브러리에 저장해주기 위해 만들었다. 

만약 토큰이 사용가능한 토큰이라면 참을 반환하겠지만, 그렇지 않다면 어떤 부분에서 문제가 있었는지 로그처리하고 거짓을 반환하게 했다. 

SecurityException과 MalformedJwtException은 JWT에서 제공하는 예외처리로, 기본적인 Security의 Exception 발생이나, jwt가 정상적으로 생성되지 않았을 때의 예외를 확인해주는 기능을 한다. 

WrongTokenException은 IllegalArgumentException이고, 나머지는 모두 RuntimeException으로 처리되어야 하는 것을 extends해서 따로 만들어두었다. 

 

public Authentication getAuthentication(String accessToken) {
    Claims claims = parseClaims(accessToken);
    if (claims.get(AUTHORITIES_KEY.getComponent()) == null) {
        throw new NotAuthenticationInfoException();
    }
    Collection<? extends GrantedAuthority> authorities =
            Arrays.stream(claims.get(AUTHORITIES_KEY.getComponent()).toString().split(","))
                    .map(SimpleGrantedAuthority::new)
                    .collect(Collectors.toList());
    UserDetails principal = new User(claims.getSubject(), "", authorities);
    return new UsernamePasswordAuthenticationToken(principal, "", authorities);
}

이전에 액세스토큰을 만들 때,

.claim(AUTHORITIES_KEY.getComponent(), getAuthorities(authentication))

이렇게 claims를 설정해주었다. 이 때, "auth" : "~~~"이런 식으로 물결에 내용이 나온다. 이를 가져왔을 때, 값이 존재하지 않는다면, 권한이 맞지 않는 것으로 예외처리하게된다. 

또한, 

authentication.getAuthorities().stream()
        .map(GrantedAuthority::getAuthority)
        .collect(Collectors.joining(","));

여기에서 우리는 authentication에서 권한 정보들을 모두 ","로 연결해 가져왔기 때문에, 이걸로 다시 나누어주면 각 정보가 나오게 된다. 

여기서 나오는 데이터들을 SimpleGrantedAuthority로 매핑했다. 

이 놈은 Authentication에서 나오는 정보들을 문자열로 반환할 수 있게 저장해주는 역할을 한다. 

루크 테일러 아저씨가 그랬댔어...암튼 그랬댔어...

토큰을 만들 때, 우리는 사용자명을 "Subject"에 담았다. 

이걸 불러와

이런 구조로 되어있는 Security파일에 있는 User객체로 생성해준다. 

이걸 이용해 

이런 형태로 만들어준다. 왜 얘냐?

신뢰성있는 Authentication 토큰을 만들어주기 때문이다. 

 

이렇게 하면 Token을 제공하는 입장에서 만들 수 있는 것은 모두 끝났다. 

우리가 사용할 JWT token도 만들고, Authentication 토큰도 만들어준다. 이를 나중에 JwtFilter에서 사용하게 되는데, 이 전에 짚고가야 할 예외들을 만들어주고 넘어가야 할 것 같다.