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

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

장아장 2023. 1. 6. 20:50

기본적인 스웨거와 CORS에 대한 처리를 완료했다. 이제 JWT(Json Web Token)를 만들어주고, 멤버 로직을 짜기 시작해야 한다. 

JWT를 정리한 내 순서는

  1. TokenProvider, TokenDto
  2. 예외처리들
  3. JwtFilter
  4. Filter를 SecurityConfig에 넣기

의 과정으로 정리할 것이며, 일단 토큰에 대해서 코드를 정리해봐야 겠다. 

 

그럴려면, 일단 토큰이 있어야지!

@Getter
@NoArgsConstructor
@AllArgsConstructor
@Builder
public class TokenDto {

    private String grantType;
    private String accessToken;
    private String refreshToken;
    private Date accessTokenExpiresIn;

토큰의 인스턴스들이다. 

grantType으로 권한 타입을 잡고, 토큰을 억세스토큰과 리프레쉬토큰으로 나누었다. 

하나의 토큰으로 접근하는게 로직을 짜기 더 수월하긴 하지만, 이전에 해보았었고, 이번엔 보안상 더 안전한 방법으로 처리를 하기로 했다. 

기본적인 접근은 억세스 토큰으로 처리하지만, 토큰의 만기 기간을 정해두고, 기간이 지나면 리프레쉬 토큰을 이용해 억세스 토큰을 다시 받아오는 방식으로 만들려고 했다.

 

토큰 생성에 필요한 요소들은 enum클래스로 분리했다. 

아무래도, 상수가 많아지다보니, enum으로 정리해두는게 더 낫지 않았나 하는 생각이 들었었다. 

(반박시 다 맞습니다ㅎㅎㅎ)

public enum TokenGeneratingComponent {

    AUTHORITIES_KEY("auth"),
    BEARER_TYPE("bearer"),
    ACCESS_TOKEN_EXPIRE_TIME(Long.toString(1000 * 60 * 60 * 24)),
    REFRESH_TOKEN_EXPIRE_TIME(Long.toString(1000 * 60 * 60 * 24 * 7));

    private final String component;

    private TokenGeneratingComponent(String component){
        this.component = component;
    }

    public String getComponent(){
        return this.component;
    }
}

요소들을 다 묶어두기 위해 Long타입으로 나와야 할 시간을 String으로 파싱해서 담아두었다. 

 

이렇게 토큰을 만들어두고, 토큰을 생성하는 방법을 생각해보아야 한다. 

토큰은 인증 정보와, 암호화 키를 이용해 정보에서 사용해도 위험이 적은(즉, 노출되어도 위험이 적은) 정보들을 암호화시켜 사용한다. 

(토큰이 어떻게 생겼는지 궁금하다면 위의 링크를 들어가보자!)

 

기본적으로 토큰의 각 인스턴스가 어떻게 생겼는지를 정리하면 아래와 같다. 

  • grantType : enum의 타입을 가져온다. 
  • accessTokenExpiresIn : 현재의 시간을 가져온 뒤, 토큰의 만료시간을 더해주면 된다. 
  • accessToken : payload에 사용자명, getAuthorities를 통해 가져온 사용자의 권한, accessTokenExpiresIn을 가져와 HS512알고리즘으로 암호화시킨다. 
    • getAuthorities : Authentication에서 Authorities를 가져온다. 이를 문자열로 묶어 반환한다.
  • refreshToken : 키를 가져와 signature를 만든다. 이 때, 만료시간만 등록시켰다. 현재시간에 enum에 있는 refresh token의 만료시간을 더했다. 

 

이 구조를 TokenProvider라는 클래스를 만들어 그 안에 넣을까 했지만, 생각해보면 결국 TokenDto가 생성되는 부분이기 때문에, TokenDto 생성자에 기능들을 넣게 되었다. 

 

private String getAuthorities(Authentication authentication){
    return authentication.getAuthorities().stream()
            .map(GrantedAuthority::getAuthority)
            .collect(Collectors.joining(","));
}

Authentication에서 권한만을 가져와 ,로 연결해 문자열로 반환시키게 했다. 

 

private String generateAccessToken(Authentication authentication, Key key){
    return Jwts.builder()
            .setSubject(authentication.getName())       // payload "sub": "name"
            .claim(AUTHORITIES_KEY.getComponent(), getAuthorities(authentication))        // payload "auth": "ROLE_USER"
            .setExpiration(accessTokenExpiresIn)        // payload "exp": 1516239022 (예시)
            .signWith(key, SignatureAlgorithm.HS512)    // header "alg": "HS512"
            .compact();
}

이를 받아와 accessToken을 만들었다. 

Authentication, Key를 입력변수로 받아 처리를 했으며,

setSubject(대상 설정)에 사용자명, claims에 사용자의 권한, 만료기간을 아까 말한 로직으로 정리했다. 

this.accessTokenExpiresIn = new Date((new Date()).getTime() + Long.parseLong(ACCESS_TOKEN_EXPIRE_TIME.getComponent()));

이런 식이었다. 

header.payload.signature에서 헤더 부분에 signWith으로 서명 알고리즘을 헤더에 두게 했다. 

compact라는 메서드는 이를 컴팩트하고, 안전한 문자열로 만들게 해준다. (자세한 방식은 여기에)

 

private String generateRefreshToken(Key key){
    return Jwts.builder()
            .setExpiration(new Date((new Date()).getTime() + Long.parseLong(REFRESH_TOKEN_EXPIRE_TIME.getComponent())))
            .signWith(key, SignatureAlgorithm.HS512)
            .compact();
}

refresh token은 만료기간과 암호화 알고리즘만 등록시켰다. 

 

public TokenDto(Authentication authentication, Key secretKey){
    this.accessTokenExpiresIn = new Date((new Date()).getTime() + Long.parseLong(ACCESS_TOKEN_EXPIRE_TIME.getComponent()));
    this.grantType = BEARER_TYPE.getComponent();
    this.accessToken = generateAccessToken(authentication, secretKey);
    this.refreshToken = generateRefreshToken(secretKey);
}

이를 생성자에 넣어, TokenProvider에서는 생성자 호출만 하면 되게 만들어두었다. 

 

이제, TokenProvider를 정리해보자.

기능별로 정리를 해보자면, 

  • 생성자 : secret key를 가져와 바이트 배열로 decode해서 받고, 이걸 HMAC알고리즘에 넣는다. 여기서 이 바이트를 위한 암호키를 만들게 한다. 
  • TokenDto를 생성하는 기능
  • 액세스 토큰을 가져와 복호화시킨 후, 기간 만료된 토큰이라면 예외처리를 던지며, 아니면 토큰의 Claims를 반환한다. 
  • 토큰이 사용가능한 상태가 아니면, 각 상태에 따라 예외를 로그 처리하고 거짓을 반환하며, 문제가 없다면 참을 반환한다.
  • 액세스토큰에서 Authentication을 가져온다.

 

TokenProvider에 대한 코드 설명은 다음 게시글로 넘겨야 할 것 같다. 

(페이지가 너무 길면 읽기 재미없잖아요?><)