[JWT] JSON WEB TOKEN - 토큰 발급하기, 검증하기, 갱신하기

leeng·2023년 4월 1일
0

JWT

목록 보기
1/2

처음에는 프로젝트를 만들면서 내게 익숙한 session을 이용해서 로그인을 구현했었다. 그런데 세션을 사용하는 것은 stateful한 방식이고, 보다 restful한 프로그램을 위해서는 stateless 방식을 지향해야한다고 들었다. 그래서 stateless한 통신을 위해 JWT를 사용해보기로 했다.

JWT란?

먼저 JWT란 JSON Web Token의 약자로, 인증에 필요한 정보들을 토큰에 담아 암호화하여 인증에 사용하는 인터넷 표준 인증 방식이다.

JWT의 구조는 아래와 같다.

HEADER에는 서명에 사용된 알고리즘과 토큰의 타입의 타입을, PAYLOAD에는 사용자나 토큰에 대한 정보를, 마지막으로 SIGNATURE는 헤더와 페이로드를 비밀키로 암호화한 결과가 담겨있다.
이렇게 생성된 토큰을 서버가 클라이언트에게 넘겨주고, 클라이언트는 서버와 통신할 때 이 토큰을 이용하여 인증을 시도하면 된다.

들어가기 전에...

사실 JWT를 Spring Security와 함께 사용하는 것이 일반적인데, 본인은 하나씩 개념을 익히면서 구현해나가고 싶어 일단 JWT 인증 방식을 사용해 본 후 Spring Security를 다시 적용해보기로 했다.

추가로, JWT 토큰을 사용하기 위한 라이브러리들이 여러 가지 있는데 여기서는 auth0 라이브러리를 사용하였다.
auth0 라이브러리를 사용하기 위해서는 build.gradle에 implementation 'com.auth0:java-jwt:3.18.1'를 추가하고jwt를 사용할 파일에서 'import com.auth0.jwt.JWT;' 를 해주면 된다.

JWT를 활용하기 위해 JwtTokenService 클래스를 만들어주었다. 해당 서비스에는 토큰 발급, 토큰 검증하기(정보 확인하기), 토큰 갱신하기 기능을 구현하였고 차례로 살펴보기로 하자.

1. 토큰 발급하기

토큰 생성을 위해서는 JWT 클래스의 create() 메소드를 호출하여 빌더패턴으로 토큰에 필요한 클레임(정보)들을 설정해준 후 암호화 알고리즘과 비밀키로 서명을 하면 된다.

보통 sub 클레임에는 식별 가능한 id를 넣어주고, exp 클레임에는 토큰 만료 시간을, 다른 클레임에는 상황에 따라 필요한 정보를 넣어준다.

    public String createToken(Long id, String nickname, String socialAccessToken, int expMinutes, String tokenType) {
        Date accessTokenExp = new Date(System.currentTimeMillis() + (60000 * expMinutes));

        String createdToken = JWT.create()
                .withSubject(String.valueOf(id))
                .withClaim("nickname", nickname)
                .withClaim("socialAccessToken", socialAccessToken)
                .withClaim("tokenType", tokenType)
                .withExpiresAt(accessTokenExp)
                .sign(Algorithm.HMAC512(JwtTokenConstants.SECRET_KEY));

        return createdToken;
    }

본인은 로그인 처리할 때 accessToken과 refreshToken을 함께 생성해주기 위해 아래와 같은 메소드를 하나 더 추가하였다. 이건 본인의 필요에 따라 다르게 구현하면 될 것 같다.

    public Jwt createTokens(Long id, String nickname, String socialAccessToken) {
        String accessToken = createToken(id, nickname, socialAccessToken, accessTokenExpMinutes, "accessToken");
        String refreshToken = createToken(id, nickname, socialAccessToken, refreshTokenExpMinutes, "refreshToken");

        return Jwt.builder().
                accessToken(accessToken)
                .refreshToken(refreshToken)
                .accessTokenExp(new Date(System.currentTimeMillis() + 60000 * accessTokenExpMinutes))
                .refreshTokenExp(new Date(System.currentTimeMillis() + 60000 * refreshTokenExpMinutes))
                .build();
    }

2. 토큰 검증하기

토큰 정보 확인 겸 토큰을 검증하는 메서드이다. 토큰이 유효하지 않을 경우 InvalidJwtTokenException을 발생시키고, 유효한 경우 액세스 토큰과 토큰의 만료 시간, 사용자의 id, 소셜액세스토큰(현 시점에서는 카카오 로그인 액세스 토큰)을 리턴해준다.

    public Jwt getAccessTokenInfo(String receivedToken) {
        String accessToken = null;
        Date accessTokenExp = null;
        String socialAccessToken = null;
        Long id = null;

        try {
            jwtTokenVerifier = new JwtTokenVerifier(JwtTokenConstants.SECRET_KEY);
            DecodedJWT decodedJWT = jwtTokenVerifier.verify(receivedToken); 

            if (decodedJWT.getClaim("nickname") != null) {
                accessToken = receivedToken;
                accessTokenExp = decodedJWT.getExpiresAt();
                id = Long.valueOf(decodedJWT.getSubject());
                socialAccessToken = decodedJWT.getClaim("socialAccessToken").asString();
            } else {
                throw new InvalidJwtTokenException(receivedToken);
            }
        }catch (JWTVerificationException ex) {
            throw new InvalidJwtTokenException(receivedToken);
        }

        return Jwt.builder().accessToken(accessToken).accessTokenExp(accessTokenExp).loginId(id).socialAccessToken(socialAccessToken).build();
    }

참고로 위 코드에서 사용한 JwtTokenVerifier는 아래와 같이 작성되어 있다.

public class JwtTokenVerifier {
    private final JWTVerifier verifier;

    public JwtTokenVerifier(String secret) {
        Algorithm algorithm = Algorithm.HMAC512(secret);
        verifier = JWT.require(algorithm).build();
    }

    public DecodedJWT verify(String token) throws JWTVerificationException {
        return verifier.verify(token);
    }
}

3. 토큰 갱신하기

리프레쉬 토큰을 검증하여 새로운 액세스 토큰을 발급하는 로직이다.
리프레쉬 토큰을 decode한 후, nickname이라는 클레임이 존재하고 tokenType이 refreshToken일 경우 유효한 토큰이다.
만약 유효한 경우, 리프레쉬 토큰이 가지고 있는 정보를 바탕으로 새로운 액세스 토큰을 발급해준다.

    public Jwt refreshAccessToken(String refreshToken) {
        String accessToken = null;
        jwtTokenVerifier = new JwtTokenVerifier(JwtTokenConstants.SECRET_KEY);
        DecodedJWT decodedJWT = jwtTokenVerifier.verify(refreshToken);

        // 리프레쉬 토큰 검증 후 액세스 토큰 새로 발급
        if (decodedJWT.getClaim("nickname") != null && decodedJWT.getClaim("tokenType").asString().equals("refreshToken")) {
            accessToken = createToken(Long.valueOf(decodedJWT.getSubject()), decodedJWT.getClaim("nickname").asString()
                    , decodedJWT.getClaim("socialAccessToken").asString(), accessTokenExpMinutes, "accessToken");
        } else {
            throw new InvalidJwtTokenException(refreshToken);
        }

        return Jwt.builder().accessToken(accessToken)
                .accessTokenExp(new Date(System.currentTimeMillis() + 60000 * accessTokenExpMinutes)).build();
    }

토큰 생성, 검증, 갱신하기까지 알아보았으니 다음에는 실제 프로젝트에 적용해보도록 하자!

profile
기술블로그보다는 기록블로그

0개의 댓글