[Spring Boot] Access token과 Refresh token으로 자동 로그인 구현하기

smlee·2022년 8월 20일
0

SpringBoot - JPA

목록 보기
4/8
post-thumbnail

자동로그인은 거의 모든 애플리케이션에서 빠지지 않는 기능이다. 이러한 자동로그인을 Access token과 Refresh token으로 자동로그인을 구현할 예정이다.

1. build.gradle에 라이브러리 추가

compile 'io.jsonwebtoken:jjwt:0.9.1'

build.gradle의 dependency 블록 내에 io.jsonwebtoken::jjwt:0.9.1을 추가한다.

2. Access token과 Refresh token 생성

자동로그인, 로그인 과정에서 내가 생각한 시나리오는 다음과 같았다.

(1) 아이디와 비밀번호를 입력 후 회원이 맞으면 refresh token과 access token이 발행된다.
(2) 발행된 토큰들 중 refresh token은 DB에 저장된다.
(3) access token의 유효기간은 2시간으로 짧게 둔다. 모든 요청 시 access token을 헤더에 담아서 보낸다.
(4) access token이 만료되었으면 refresh token을 헤더에 담아 보내서 access token을 재발급한 후 응답을 보낸다.
(5) 만약 refresh token도 만료되었다면, 자동로그인은 되지 않고 로그인 화면으로 넘어가서 refresh token과 access token을 모두 재발급 받게 한다.

(1) Access token 생성 메서드

토큰을 관리하는 JwtService.java 파일을 생성하고 가장 먼저 access token을 생성하는 메서드인 createAccessToken을 만들었다.

public String createAccessToken(long userId) {
        Date now = new Date();
        Date expiration = new Date(now.getTime() + Duration.ofHours(2).toMillis());
        return Jwts.builder()
                .claim("userId", userId)
                .setIssuedAt(now)
                .setExpiration(expiration)
                .signWith(SignatureAlgorithm.HS256, Secrets.JWT_ACCESS_SECRET_KEY)
                .compact();
    }

현재 시간을 now라는 변수에 담는다. 그리고 access token의 유효 기간은 2시간이므로 expiration이라는 변수에 2시간을 세팅해준다. claims에 userId만 저장을 한다. 그리고, .signWith에 어떤 알고리즘을 사용할지 정해주고, Access token을 암호화하는 비밀 키를 넣어준다.

access token의 payload에 담을 정보는 유저의 PK이므로 claim에 userId만 세팅해준다.

(2) Refresh token 생성 메서드

refresh token 메서드 역시 JwtService.java에 생성하였다.

public String createRefreshJwt(long userId) {
        Date now = new Date();
        Date expiration = new Date(now.getTime() + Duration.ofDays(30).toMillis());
        return Jwts.builder()
                .claim("userId", userId)
                .setIssuedAt(now)
                .setExpiration(expiration)
                .signWith(SignatureAlgorithm.HS256, Secrets.JWT_REFRESH_SECRET_KEY)
                .compact();
    }

위의 메서드는 refresh token을 생성하는 메서드이다. access token을 생성하는 메서드와 달라진 부분은 만료기간과 비밀 키 부분 쪽이다.

refresh token은 넉넉하게 30일로 잡았다.

3. token 검증

JwtProvider.java에 유효 기간을 검증하는 메서드를 만들었다.

public boolean  validateAccessTokenExpiration(String  token, boolean isAccessToken) throws BaseException{
        try{
            // 토큰 파싱
            Jws<Claims> claims = Jwts.parser()
                    .setSigningKey(isAccessToken? Secrets.JWT_ACCESS_SECRET_KEY:Secrets.JWT_REFRESH_SECRET_KEY)
                    .parseClaimsJws(token);

            return claims.getBody().getExpiration().before(new Date()); // 현재보다 만료가 이전인지 확인
        }
        catch (ExpiredJwtException ignored){
            return  true;
        }catch (Exception e){
            throw   new BaseException(BaseResponseStatus.INVALID_JWT); // 만약 올바르지 않은 토큰이라면 에러
        }
    }

문자열로 된 access token이나 refresh token을 첫번째 매개변수로 받고, 두번째로는 isAccessToken이라는 값을 받는다. 이 변수는 boolean형으로, 첫번째로 들어온 변수가 access token이면 true이고 refresh token이면 false가 들어온다.

isAccessToken을 받은 이유는 토큰을 파싱할 때 암호화할 때 사용했던 비밀 키가 필요한데, isAccessToken 값에 따라 setSigningKey에 들어가는 키가 달라지게 했다.

4. Controller 작성

@PostMapping("")
@ResponseBody
public BaseResponse<PostAutoLoginRes>       autoLogin(@RequestHeader(value = "X-ACCESS-TOKEN") String accessToken,
                                                          @RequestHeader(value = "X-REFRESH-TOKEN") String refreshToken){
        String      message;
        long        studentId;
        if(!jwtProvider.validateAccessTokenExpiration(accessToken, true)){
            // access token이 만료되지 않았다면

            studentId = jwtService.getUserIdx();
            message = "access token이 검증되었습니다.";

            return  new BaseResponse<PostAutoLoginRes>(new PostAutoLoginRes(studentId, accessToken, refreshToken);
        }

        else if(!jwtProvider.validateAccessTokenExpiration(refreshToken, false)){
            // refresh token이 만료되지 않았다면 accesToken 재발급

            studentId = jwtService.getUserIdxByRefresh();

            if(studentProvider.retrieveRefreshToken(studentId)){
                // db에 저장된 refresh token과 클라이언트가 보내온 refresh token이 일치하는 경우

                message = "access token이 재발급되었습니다.";

                String  newAccessToken = jwtService.createAccessToken(studentId);

                return  new BaseResponse<PostAutoLoginRes>(new PostAutoLoginRes(studentId, newAccessToken, refreshToken);
            }
        }
        // refresh token도 만료되었으면 EXPIRED_REFRESH_TOKEN 에러를 반환한다. : 로그인 화면으로 넘어간다.
        throw   new BaseException(BaseResponseStatus.EXPIRED_REFRESH_TOKEN);
    }

헤더로 access token과 refresh token을 받은 뒤 jwtProvider에서 작성한 코드로 토큰들을 검증하였다.

(1) access token 유효 -> 로그인 완료 response
(2) access token 만료, refresh token 유효 -> access token 재발급 후 로그인 성공 (재발급된 access token은 response로 보내짐)
(3) access token, refresh token 모두 만료 -> EXPIRED_REFRESH_TOKEN 에러 발생 : 재로그인이 필요

0개의 댓글