Refresh Token, Access Token 을 활용한 로그인 코드구현

msung99·2022년 12월 17일
5
post-thumbnail

이전 JWT 설명에서 Access Token에 이은 Refresh Token 을 활용한 코드 구현방법을 다루겠다고 말씀드렸었습니다. 현 포스팅에서 그들의 코드 구현을 알아보도록 하겠습니다.

지난 포스팅 내용을 참고하실 분들은 아래 링크를 참고하시면 좋을듯합니다.

Refresh-token,Access-token이란? 구현방법은?

지난 제 포스팅 내용을 보시고 JWT가 무엇인지 학습을 하셨거나, 또는 이미 JWT 인증방식을 이론적으로 잘 알고 있다는 가정하에 진행되므로 유의바랍니다. 만일 선수지식이 없으시다면 이번 내용이 잘 이해가 안되실겁니다.

또한 이번 포스팅은 이론 설명 위주가 아닌 코드가 중심으로 내용이 구성될 것이므로 이 또한 참고 바랍니다.


1. Login 구현

우선 로그인 API 입니다. login API 를 호출함으로써 저희는 AccessToken 과 RefreshToken 을 발급받을 수 있습니다.

 @ResponseBody
    @PostMapping("/login")
    public  BaseResponse<LoginUserRes>loginUser(@RequestBody LoginUserReq loginUserReq){
        try {
            LoginUserRes loginUserRes = userProvider.loginUser(loginUserReq);
            return new BaseResponse<>(loginUserRes);
        } catch(BaseException baseException){
            return new BaseResponse<>(baseException.getStatus());
        }
    }

2. loginUser (UserService 부분)

이어서 로그인 API 를 호출시 Service 단으로 넘어오는 코드 부분입니다.
로그인 API 임에 따라 클라이언트로부터 로그인 시도를 위한 회원 계정정보(email, password) 를 입력받을텐데, DB에 회원정보가 존재하는지 조회합니다.

조회에 성공하면 해당 회원에 대한 JWT(Access, Refresh Token)를 발급해주는 기능입니다.

// 로그인 (password 검사)
    public LoginUserRes loginUser(LoginUserReq loginUserReq) throws BaseException {

        // 회원가입시 저장한 회원 계정의 비밓번호와, 현재 로그인 창에서 입력된 비밀번호(loginUserSomeField가 동일한지 검증한다.
        LoginUserSomeField loginUserSomeField = userDao.getSomeInfo_WhenLogin(loginUserReq); // 회원가입 때 입력받은 비밀번호를 DB에 그냥 저장한 것이 아니라 암호화해서
        
        // DB에 저장된 회원의 비밀번호(password)와, 현재 로그인 창에서 입력한 비밀번호(loginUserSomeField.getPassword())가
        // 일치하면 해당 사용자 계정에 대한 userIdx 를 가져온다. 또한 jwt 를 발급해준다
        if(loginUserReq.getPassword().equals(password)){
            int userIdx = userDao.getSomeInfo_WhenLogin(loginUserReq).getUserIdx();
            String email = userDao.getSomeInfo_WhenLogin(loginUserReq).getEmail();
            String nickname = userDao.getSomeInfo_WhenLogin(loginUserReq).getNickname();
            String[] tokenList = jwtService.createTokenWhenLogin(userIdx); // access, refresh token 생성
            return new LoginUserRes(userIdx, email, nickname, tokenList[1], tokenList[0]);  // JWT 토큰을 클라이언트에게 Response로 발급해준다.
        }
        // 비밀번호가 일치하지 않는다면 로그인에 실패한것
        else{
            throw new BaseException(BaseResponseStatus.LOGIN_FAILURE);
        }
    }

3. 로그인 API 호출시 JWT 발급받기

로그인을 시도한다면, 당연히 RefreshToken 과 AccessToken 을 모두 발급받아야 합니다. 이때 발급받은 RefreshToken 은 지난 포스팅에서 말씀드렸듯이, 클라이언트에 발급하는 것 외에도 서버의 DB 에 별도로 저장해놓아야 합니다.

DB에 저장된 RefreshToken 은 추후에 AccessToken 토큰이 만료기간이 지나서(expired) 새롭게 발급해야 할 때 검증(Validation) 을 위해 사용됩니다.

 public String[] createTokenWhenLogin(int userIdx){
        String RefreshToken = createRefreshToken(userIdx);
        String AccessToken = createAccessToken(userIdx);
        // 발급받은 RefreshToken 은 DB 에 저장
        jwtRepository.saveRefreshToken(RefreshToken, userIdx);
        String[] tokenList = {RefreshToken, AccessToken};
        return tokenList;
    }

4. Access, Refresh Token 생성

앞서 토큰 생성을 위한 createTokenWhenLogin 메소드를 호출시 아래 2가지 메소드를 호출해서 두 종류의 토큰을 생성합니다.

이떄 jwt 타입임을 명시해주고, 서명값(Signature) 생성시 필요한 암호화 알고리즘은 HS256 을 사용했습니다.

또 이떄 가장 중욯란 것은 AccessToken 과 RefreshToken 의 Expire 만료시간 설정입니다. 지난 포스팅에서 언급드렸듯이, Access Token 은 보통 30분, Refresh Token 은 2주~1달정도를 설정해놓는다 했는데 기억하실지 모르겠네요..!

저는 만료기간을 각각 30분, 1주일로 설정 해주었습니다.

 public String createAccessToken(int userIdx){
        byte[] keyBytes = Decoders.BASE64.decode(Secret.ACCESS_TOKEN_SECRET_KEY);
        Key key = Keys.hmacShaKeyFor(keyBytes);
        Date now = new Date();
        return Jwts.builder()
                .setHeaderParam("type","jwt") // Header 의 type 에다 해당 토큰의 타입을 jwt로 명시
                .claim("userIdx",userIdx) // claim 에 userIdx 할당
                .setIssuedAt(now) // 언제 발급되었는지를 현재 시간으로 넣어줌
                .setExpiration(new Date(System.currentTimeMillis()+1*(1000*60*30))) // Access Token 만료기간은 30분으로 설정
                .signWith(key, SignatureAlgorithm.HS256) // 서명(Signature) 를 할떄는 HS256 알고리즘 사용하며,  Secret.JWT_SECRET_KEY 라는 비밀키(Secret key) 를 가지고 Signature 를 생성한다.
                .compact();                                   //  Secret.JWT_SECRET_KEY 는 비밀키로써 .gitignore 로 절대 노출시키지 말것! 이 비밀키를 통해 내가 밝급한건인지 아닌지를 판별할 수 있으므로
    }

    // Refresh Token 생성
    public String createRefreshToken(int userIdx){
        byte[] keyBytes = Decoders.BASE64.decode(Secret.REFRESH_TOKEN_SECRET_KEY);
        Key key = Keys.hmacShaKeyFor(keyBytes);
        Date now = new Date();
        return Jwts.builder()
                .setHeaderParam("type", "jwt")
                .claim("userIdx", userIdx)
                .setIssuedAt(now)
                .setExpiration(new Date(System.currentTimeMillis()+1*(1000*60*60*24*7))) // Refresh Token 만료기간은 일주일로 설정
                .signWith(key, SignatureAlgorithm.HS256) // 1*(1000*60*60*24*30) : 만료기간을 365일(1년)으로 설정하는 경우
                .compact();
    }

5. AccessToken 만료시 재발급받기

AccessToken 이 만료될때마다 새로운 토큰을 발급시켜줘야 한다고 했었죠? 이를 위한 기능입니다. 이 부분은 이해가 잘 안가실 수 있으니, 조금 상세하게 과정을 설명드리겠습니다.

  1. HTTP Header 로 부터 refreshToken 을 추출해옵니다. (클라이언트로 부터 refreshToken 을 얻어오는 과정)

  2. 앞서 DB에 저장해두었던 refreshToken 과 대조.비교하며 유효성을 검사합니다. 만일 refreshToken 이 탈취되어 해커가 임의로 변경한 RefreshToken 을 Header 에 실어서 보낸다면 DB 에 저장된 토큰값과 일치하지 않아서 적절한 예외처리가 이루어질 것입니다.

  3. 그 외에도 여러 검증(Validation) 을 걸쳐서 최종적으로 정상으로 판단된 토큰임이 판단된다면, 앞서 생성한 createAccessToken() 메소드를 통해 새로운 AccessToken 을 생성하고 리턴해줍니다.

public String ReCreateAccessToken() throws BaseException {
        // Http Header 로 부터 추출
        String refreshToken = getRefreshToken();
        String dbRefreshToken;

        // refresh token 유효성 검증1 : DB조회
        try {
            RefreshToken dbRefreshTokenObj = jwtRepository.getRefreshToken(refreshToken);
            dbRefreshToken = dbRefreshTokenObj.getRefreshToken();
        } catch(Exception ignored){  // DB에 RefreshToken 이 존재하지 않는경우
            throw new BaseException(BaseResponseStatus.NOT_DB_CONNECTED_TOKEN);
        }

        // DB에 RefreshToken이 존재하지 않거나(null), 전달받은 refeshToken 이 DB에 있는 refreshToken 과 일치하지 않는 경우
        if(dbRefreshToken == null || !dbRefreshToken.equals(refreshToken)){
            throw new BaseException(BaseResponseStatus.NOT_MATCHING_TOKEN);
        }

        // Refresh Token 에 대한 DB 조회에 성공한 경우
        // refresh token 유효성 검증2 : 형태 유효성
        Jws<Claims> claims;
        try {
            claims = Jwts.parserBuilder()
                    .setSigningKey(Secret.REFRESH_TOKEN_SECRET_KEY)
                    .build()
                    .parseClaimsJws(refreshToken);
        }  catch (io.jsonwebtoken.security.SignatureException signatureException){
            throw new BaseException(BaseResponseStatus.INVALID_TOKEN);
        }
        catch (io.jsonwebtoken.ExpiredJwtException expiredJwtException) { // refresh token 이 만료된 경우
            throw new BaseException(BaseResponseStatus.REFRESH_TOKEN_EXPIRED); // 새롭게 로그인읋 시도하라는 Response 를 보낸다.
        } catch (Exception ignored) { // Refresh Token이 유효하지 않은 경우 (만료여부 외의 예외처리)
            throw new BaseException(BaseResponseStatus.REFRESH_TOKEN_INVALID);
        }

        int userIdx = claims.getBody().get("userIdx", Integer.class);
        return createAccessToken(userIdx);
    }

마치며

이렇게 JWT 관련 핵심적인 코드를 중심으로 어떻게 구현하는지에 대해 간단히 알아봤습니다. JWT 가장 보편적으로 많이 쓰이는 Authorization 방식이므로 잘 익혀두시는 것을 권장드립니다.
잘 이해가 안가시는 부분이 있거나 궁금하신게 있다면 편하게 댓글을 남겨주시면 알려 드리겠습니다.


참고

jwt.io
auth0 JWT Documents
StackOverFlow : Where do I need to use JWT? (Question)
라인 Developers Document : About Login Authentication

1개의 댓글

comment-user-thumbnail
2024년 1월 7일

리프레쉬 토큰을 디비에 저장하는 방법에 대해 문의드립니다.
1) 별도의 테이블을 만드나요?
2) 속성은 오직 리프레쉬 토큰 하나인가요?
3) 리프레쉬 토큰에 별다른 처리없이 그대로 저장하나요?

답글 달기