이전 JWT 설명에서 Access Token에 이은 Refresh Token 을 활용한 코드 구현방법을 다루겠다고 말씀드렸었습니다. 현 포스팅에서 그들의 코드 구현을 알아보도록 하겠습니다.
지난 포스팅 내용을 참고하실 분들은 아래 링크를 참고하시면 좋을듯합니다.
Refresh-token,Access-token이란? 구현방법은?
지난 제 포스팅 내용을 보시고 JWT가 무엇인지 학습을 하셨거나, 또는 이미 JWT 인증방식을 이론적으로 잘 알고 있다는 가정하에 진행되므로 유의바랍니다. 만일 선수지식이 없으시다면 이번 내용이 잘 이해가 안되실겁니다.
또한 이번 포스팅은 이론 설명 위주가 아닌 코드가 중심으로 내용이 구성될 것이므로 이 또한 참고 바랍니다.
우선 로그인 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());
}
}
이어서 로그인 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);
}
}
로그인을 시도한다면, 당연히 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;
}
앞서 토큰 생성을 위한 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();
}
AccessToken 이 만료될때마다 새로운 토큰을 발급시켜줘야 한다고 했었죠? 이를 위한 기능입니다. 이 부분은 이해가 잘 안가실 수 있으니, 조금 상세하게 과정을 설명드리겠습니다.
HTTP Header 로 부터 refreshToken 을 추출해옵니다. (클라이언트로 부터 refreshToken 을 얻어오는 과정)
앞서 DB에 저장해두었던 refreshToken 과 대조.비교하며 유효성을 검사합니다. 만일 refreshToken 이 탈취되어 해커가 임의로 변경한 RefreshToken 을 Header 에 실어서 보낸다면 DB 에 저장된 토큰값과 일치하지 않아서 적절한 예외처리가 이루어질 것입니다.
그 외에도 여러 검증(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) 별도의 테이블을 만드나요?
2) 속성은 오직 리프레쉬 토큰 하나인가요?
3) 리프레쉬 토큰에 별다른 처리없이 그대로 저장하나요?