1. RefreshToken의 필요성
- AccessToken(JWT)를 통한 인증 방식의 문제는 만일 제 3자에게 탈취당할 경우 보안에 취약하다.
- 유효기간이 짧은 Token의 경우 그만큼 사용자는 로그인을 자주 해서 새롭게 Token을 발급받아야 하므로 불편하다.
- AccessToken을 짧게 설정하고 RefreshToken을 사용함으로서 사용자가 자주 로그인하는 불편함을 해결할 수 있다.
2. RefreshToken의 이해
- 로그인을 완료했을 때 AccessToken과 동시에 발급되는 RefreshToken은 긴 유효기간을 가지면서, AccessToken 만료 시 새로 발급해준다.
- AccessToken 유효기간 : 1시간
- RefreshToken 유효기간 : 2주
1. 사용자가 아이디, 비밀번호를 통해 로그인을 한다.
2. 서버에서는 회원 DB에서 값을 비교한다.
3. 4. 로그인인 완료되면 AccessToken, RefreshToken을 발급한다. 이때 회원 DB에 RefreshToken을 저장한다.
5. 사용자는 RefreshToken을 안전한 저장소에 저장 후, AccessToken을 헤더에 실어 요청을 보낸다.
6. 7. AccessToken을 검증하여 이에 맞는 데이터를 보낸다.
8. 시간이 지나 AccessToken 만료된 경우
9. 사용자는 이전과 동일하게 AccessToken을 헤더에 실어 요청을 보낸다.
10. 11. 서버는 AccessToken이 만료됨을 확인하고 권한없음을 반환한다.
12. 사용자는 RefreshToken과 AccessToken을 함께 서버로 보낸다.
13. 서버는 받은 AccessToken이 조작되지 않았는지 확인한후, RefreshToken과 사용자의 DB에 저장되어 있던 RefreshToken을 비교한다. 모든 검증을 통과하면 새로운 AccessToken을 재발급한다.
3. RefreshToken을 통한 AccessToken 재발급 구현
(1) RefreshToken.class (Entity)
@Getter
@Entity
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class RefreshToken extends BaseDateEntity {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "token", nullable = false, columnDefinition = "varchar(500)")
private String token;
@OneToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "users_id")
private User user;
@Builder
public RefreshToken(Long id, String token, User user) {
this.id = id;
this.token = token;
this.user = user;
}
public void changeToken(String token) {
this.token = token;
}
}
(2) RefreshTokenController
@GetMapping("/api/refresh-token")
public ResponseEntity<AccessTokenResponseDto> reissueAccessTokenByRefreshToken(
@RequestHeader(value = "Access-Token") String accessToken,
@RequestHeader(value = "Refresh-Token") String refreshToken
) {
AccessTokenResponseDto responseDto =
refreshTokenService.reissueAccessToken(accessToken, refreshToken);
return ResponseEntity.ok().body(responseDto);
}
- 클라이언트에게 해당 API URL로 헤더에 만료된 AccessToken과 RefreshToken을 전달받는다.
(3) RefreshTokenService
public class RefreshTokenService {
private final Jwt jwt;
private final RefreshTokenRepository refreshTokenRepository;
private final UserRepository userRepository;
@Transactional
public AccessTokenResponseDto reissueAccessToken(String accessToken, String refreshToken) {
if (jwt.isExpiredToken(accessToken)) {
log.info("AccessToken not Expired, AccessToken reissue denied");
throw new IllegalTokenException();
}
try {
Claims claims = jwt.verify(refreshToken);
User user = userRepository.findByEmail(claims.getEmail())
.orElseThrow(IllegalTokenException::new);
RefreshToken findRefreshToken = refreshTokenRepository.findByUserId(user.getId())
.orElseThrow(IllegalTokenException::new);
if (!findRefreshToken.getToken().equals(refreshToken)) {
throw new IllegalTokenException();
}
String newAccessToken = jwt.createAccessToken(claims);
return AccessTokenResponseDto.fromEntity(newAccessToken, user);
} catch (TokenExpiredException e) {
throw new RefreshTokenExpiredException();
} catch (JWTVerificationException e) {
log.warn("Jwt processing failed: ", e);
throw new IllegalTokenException();
}
}
}
- 매개변수로 넘어온 AccessToken이 만료되었는지, 또한 RefreshToken이 유효한지 검증한다.
- 추가적으로 매개변수 RefreshToken과 DB에 저장된 RefreshToken이 일치하는지 검증한다.
- 모든 검증에 통과하면 새로운 AccessToken을 생성하여 반환한다.
자세한 코드 위 링크에서 보실 수 있습니다.
Reference
반응형
'프로젝트 > LinkBook' 카테고리의 다른 글
AccessToken 로그인 로직 (feat. Spring Security) (0) | 2022.08.23 |
---|---|
토큰(JWT)기반 인증 도입 (0) | 2022.08.23 |
회원가입 이메일 인증 구현 (0) | 2022.08.21 |
Batch Fetch Size를 통해 페이징 문제 해결 (0) | 2022.08.20 |
계층형 Category 구현 (Enum으로의 전환) (0) | 2022.08.19 |
댓글