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

msung99·2022년 12월 17일
8
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

profile
블로그 이전했습니다 🙂 : https://haon.blog

1개의 댓글

comment-user-thumbnail
2024년 1월 7일

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

답글 달기

관련 채용 정보