일 | 월 | 화 | 수 | 목 | 금 | 토 |
---|---|---|---|---|---|---|
1 | 2 | 3 | 4 | 5 | ||
6 | 7 | 8 | 9 | 10 | 11 | 12 |
13 | 14 | 15 | 16 | 17 | 18 | 19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 |
27 | 28 | 29 | 30 | 31 |
- testcode
- 회고록
- github
- kafkaconsumer
- 공부기록
- git
- jwt
- 알고리즘
- 문자열압축
- 코딩테스트
- java
- 코테
- AWS
- c
- 자료구조
- IT
- 자바
- 기술블로그
- codingtest
- 기록
- 한이음
- 선택정렬
- SpringSecurity
- 트러블슈팅
- 문자열함수
- 시스템프로그래밍
- 백준
- Django
- 협업도구
- kafka
- Today
- Total
신뇽이 되어보자
[Spring] Spring Security + JWT 사용자 인증(3) - Refresh Token을 활용한 인증 갱신 본문
[Spring] Spring Security + JWT 사용자 인증(3) - Refresh Token을 활용한 인증 갱신
신뇽이되고싶은미뇽 2025. 2. 2. 21:17해당 코드는 직접 작성한 것이며, 블로그를 포스팅하면서 이상한 점을 계속해서 변경하는 중입니다(블로그 포스팅의 장점,,) 그러하니 제 코드를 신뢰하지 말아주세요..ㅎ 참고용으로 봐주시면 감사하겠습니다.
Access Token
AccessToken은 사용자의 정보를 담고 있는 토큰으로 , 사용자 인증/인가에 사용된다.
서버는 AccessToken을 통해 사용자가 누구인지 확인하고, 해당 사용자의 경로접근 권한을 판단한다.
일반적으로 jwt형식으로 사용되는데,
jwt 인증 방식은 비밀키로 서명되어 무결성이 보장되고,
서버에서 세션을 저장할 필요없어서 무상태성이고
인증 요청마다 db요청이 필요없다는 장점이 있지만
단점으로는
유효기간이 만료되기 전까지 토큰은 유효하다는 것이다.
그래서 만약 토큰이 탈취가 된다면 유효기간이 만료될때까지 악용될 위험성이 있다.
또한 일반적인 jwt는 세션을 저장하지 않아서 서버에서 특정 토큰을 강제로 폐기하기가 어렵다.
위의 단점을 해결하기 위해서 RefreshToken이라는 개념이 나오게 된 것이다.
accessToken의 짧은 만료시간 + refreshToken을 사용한 지속적인 재발급
Refresh Token 으로 Access Token 재발급 구현
방법 1: accessToken이 만료되었을 시 refreshToken을 가져와서 유효성 검사 후 AccessToken만 재발급하기
RefreshToken을 사용해서 AccessToken을 재갱신하는 것 까지는 좋다.
그러나 이 방법은 refreshToken을 탈취 당했을 때 굉장히 곤란해지는데
만약에 Refresh Token이 탈취 당하면, 공격자가 계속해서 AccessToken을 재발급할 수 있다는 문제가 있다.
refreshToken은 유효기간이 길어서 사용자 피해가 클 위험이 있다.
이를 방지하기 위해서 다른 방법을 사용하게 되는데 이는 바로
방법2: RTR 방식
Refresh Token이 한번 사용되면 새로운 Refresh Token이 발급되기 때문에 기존 Refresh Token을 다시 사용할 수 없다. 즉, 사용자가 로그아웃 후 기존 RT를 다시 사용하려고 해도 무효화되어 보안이 강화된 것이다.
만약에 공격자가 사용자의 Refresh Token을 탈취해서 먼저 사용했다면,
RTR 방식에서는 한 번 사용된 Refresh Token은 폐기 처리하기 때문에 탈취 여부를 감지할 수 있게 된다.
예를 들어 사용자의 Refresh Token이 탈취가 되어 공격자가 이를 이용해 새로운 AccessToken과 RefreshTokn을 재갱신했다고 하자.
탈취가 당한 뒤 사용자가 자신의 Refresh Token을 제출했는데 DB에 저장된 Refresh Token과 다르면 탈취 당했다고 간주하고 강제 로그아웃하는 보안 조취를 취하면 된다.
어라 DB에 저장한다고? 그럼 무상태성이 깨지네?
맞다. 완벽한 무상태성으로 구현한다면, 토큰이 탈취당하거나 로그아웃 할 시 강제 만료가 불가능하다.
이를 위해 Refresh Token을 DB에 저장하면 강제 만료를 할 수 있게 된다.
그러면 세션에서 굳이 jwt로 바꿔야만 했나? 세션을 쓰는거나 비슷하지 않은가?
세션 기반 인증은 서버 메모리에 사용자 정보를 저장한다.
인증할 때마다 DB에 접근을 하게 된다 이는 사용자가 많아지면 많아질 수록 부하가 증가하게 된다.
반면, JWT는 사용자 인증을 할 때 AccessToken만 수행을 하기 때문에 인증할 때마다 DB조회를 하지 않는다.
Access Token은 여전히 무상태성을 유지한다.
이는 완벽한 무상태성을 보장하지는 않지만, Stateless하다고 할 수 있다.
Access Token은 유효기간이 짧아서 탈취돼도 큰 피해는 없다.
또한 Refresh Token은 DB에 저장해서 강제 로그아웃이 가능하다.
구현
Entity
Refresh Token을 DB에 저장하기 위해서 Entity를 만들어줬다.
Controller
재발급 요청이 들어오면
- Refresh Token을 쿠키에서 꺼내온다.
- 쿠키 이름이 refreshToken인 값을 찾아서 변수에 할당한다.
- 유효성 검사를 한다.
- 유효하다면 새로운 Access Token을 발급한다.
- 사용자 정보를 가져오고
- Refresh Token도 갱신을 해준다.
@PostMapping("/refresh")
public ResponseEntity<?> refreshAccessToken(HttpServletRequest request, HttpServletResponse response) throws Exception {
String refreshToken = null;
Cookie[] cookies = request.getCookies();
if (cookies != null) {
for (Cookie cookie : cookies) {
if ("refreshToken".equals(cookie.getName())) {
refreshToken = cookie.getValue();
break;
}
}
}
// refreshToken이 없으면 예외 처리
if (refreshToken == null) {
throw new Exception("Refresh token이 없습니다.");
}
log.info("Received refresh token: " + refreshToken);
// 후속 과정으로 유효성 검사 등 계속 진행...
// refreshToken이 유효하면 새로운 access token 발급
String newAccessToken = null;
if (refreshTokenService.validateRefreshToken(refreshToken)) {
newAccessToken = jwtTokenUtil.refreshAccessToken(refreshToken);
}
String username = jwtTokenUtil.extractUsername(refreshToken);
UserDetails userDetails = userDetailsService.loadUserByUsername(username);
refreshTokenService.createOrUpdateRefreshToken(userDetails, refreshTokenExpirationTime, response);
// access token을 JSON 응답으로 반환
return ResponseEntity.ok(new AuthenticationResponse(newAccessToken));
}
}
컨트롤러에서 refreshToken을 검증하는 부분이다.
if (refreshTokenService.validateRefreshToken(refreshToken)) {
newAccessToken = jwtTokenUtil.refreshAccessToken(refreshToken);
}
refreshTokenService
validateRefreshToken method
: RefreshToken이 DB에 존재하는지 검사하는 메서드
나는 DB에 새롭게 갱신된 Refresh Token으로 업데이트를 해주기 때문에
만약에 쿠키에서 추출된 Refresh Token이 DB에 없다면, 탈취 되었을 가능성이 있다는 의미이다.
public boolean validateRefreshToken(String token) {
Optional<RefreshToken> refreshTokenOpt = refreshTokenRepository.findByToken(token.trim());
if (refreshTokenOpt.isEmpty()) {
log.info("Token not found in DB. Incoming token: " + token);
return false;
}
RefreshToken refreshToken = refreshTokenOpt.get();
log.info("Token from DB: " + refreshToken.getToken());
return !refreshToken.isExpired();
}
jwtUtil
refreshAccessToken method
refreshToken을 검증하고 만료시간을 확인 한 후 새로운 AccessToken을 만드는 메서드이다.
public String refreshAccessToken(String refreshToken) {
validateRefreshToken(refreshToken);
String username = extractUsername(refreshToken);
UserDetails userDetails = customUserDetailsService.loadUserByUsername(username);
return generateToken(userDetails);
}
validateRefreshToken method
: refreshToken의 유효성을 검증하여, 토큰 탈취나 변조된 토큰이 사용되지 않았는지 확인하는 메서드이다.
- 토큰의 서명 검증 : 비밀키로 토큰의 무결성을 확인한다.
- 발급 시간 검증 : 토큰이 발급된 시간이 현재 시간 이후가 아닌지 확인한다.
- 만료시간 검증: 토큰의 만료 시간이 현재 시간 이후인지 확인한다.
//refreshToken 유효성 검증
public void validateRefreshToken(String refreshToken) {
try {
Jwts.parserBuilder()
.setSigningKey(SECRET_KEY)
.build()
.parseClaimsJws(refreshToken);
Claims claims = extractAllClaims(refreshToken);
if (claims.getIssuedAt().after(new Date())) {
throw new CustomException(ExceptionStatus.PREMATURE_TOKEN);
}
Date expireAt = claims.getExpiration();
if (expireAt.before(new Date())) {
throw new CustomException(ExceptionStatus.EXPIRED_TOKEN);
}
} catch (JwtException | IllegalArgumentException e) {
throw new CustomException(ExceptionStatus.INVALID_TOKEN);
}
}
RefreshTokenService
createOrUpdateRefreshToken method
: Refresh Token을 생성하거나, 갱신하고 이를 HTTP응답 쿠키로 설정하는 메서드이다.
- 기존 refreshToken이 있을 경우: 기존 토큰을 갱신하여 DB에 저장하고 새로운 refreshToken을 쿠키에 설정한다.
- 기존 refreshToken이 없을 경우 : CustomException을 던져 사용자에게 다시 로그인을 유도한다.
//RefreshToken 저장(이미 있으면 갱신)
@Transactional
public RefreshToken createOrUpdateRefreshToken(UserDetails userDetails, long expirationTimeInSeconds, HttpServletResponse response) {
CustomUserDetails customUserDetails = (CustomUserDetails) userDetails;
Long userId = customUserDetails.getUser();
// UserDetails에서 username을 받아서 refresh 토큰을 생성
String refreshToken = jwtTokenUtil.generateRefreshToken(userDetails);
LocalDateTime expiryDate = LocalDateTime.now().plus(Duration.ofMillis(REFRESH_TOKEN_EXPIRATION));
log.info("만료기간:" + expiryDate);
// 기존 refreshToken이 있는지 확인
Optional<RefreshToken> existingToken = refreshTokenRepository.findByUserId(userId);
RefreshToken tokenToReturn;
if (existingToken.isPresent()) { // 기존 토큰이 있으면 갱신
RefreshToken token = existingToken.get();
token.updateToken(refreshToken, expiryDate);
tokenToReturn = refreshTokenRepository.save(token);
} else {
// refreshToken이 없으면 로그인 페이지로 리다이렉션 유도
throw new CustomException(ExceptionStatus.REFRESH_TOKEN_NOT_FOUND); // 예외 발생, 클라이언트는 로그인하도록 유도
}
Cookie cookie = new Cookie("refreshToken", tokenToReturn.getToken());
cookie.setHttpOnly(true); // JavaScript에서 접근 불가
cookie.setSecure(true); // HTTPS 환경에서만 전송되도록 설정 (필요한 경우)
cookie.setPath("/"); // 모든 경로에서 사용 가능하도록 설정
cookie.setMaxAge((int) REFRESH_TOKEN_EXPIRATION); // 유효 기간 설정
response.addCookie(cookie);
log.info("쿠키 설정: refreshToken={}, MaxAge={}", tokenToReturn, REFRESH_TOKEN_EXPIRATION);
return tokenToReturn;
}
나는 일단 refreshToken을 DB에 저장을 했는데
AccessToken 갱신 주기마다 DB에 접근을 해야한다는 점이 성능 상 좋다고 느끼지 못했다.
그리고 DB같은 경우 해당 토큰에 대한 TTL(Time To Live)을 자동으로 관리할 수 없기 때문에
인메모리 방식인 Redis에 저장을 해볼까 한다.
또한 RefreshToken을 http-only 속성이 부여된 쿠키에 집어넣었는데
해당 속성이 부여된 쿠키는 자바스크립트 환경에서는 접근할 수 없기 때문에 XSS(교차 사이트 스크립팅) 공격에 대한 방어 효과가 있다. 이에 대해서는 다음에 자세히 알아보겠다.
[Spring] Spring Security + JWT 사용자 인증(2) - Spring Security 설정 및 필터 적용
해당 코드는 직접 작성한 것이며, 블로그를 포스팅하면서 이상한 점을 계속해서 변경하는 중입니다(블로그 포스팅의 장점,,) 그러하니 제 코드를 신뢰하지 말아주세요..ㅎ 참고용으로 봐주시
dragonair148.tistory.com
[Spring] Spring Security + JWT 사용자 인증(1) - 로그인 & 토큰 발급
해당 코드는 직접 작성한 것이며, 블로그를 포스팅하면서 이상한 점을 계속해서 변경하는 중입니다(블로그 포스팅의 장점,,) 그러하니 제 코드를 신뢰하지 말아주세요..ㅎ 참고용으로 봐주시
dragonair148.tistory.com
https://dragonair148.tistory.com/entry/Spring-Spring-Security%EB%9E%80
[Spring] Spring Security란?
Spring Security란? Spring 기반의 어플리케이션의 보안(인증과, 권한, 인가 등)을 담당하는 스프링 하위 프레임워크이다. Spring Security는 보안과 관련해서 체계적으로 많은 옵션을 제공해주기 때문에,
dragonair148.tistory.com
https://dragonair148.tistory.com/entry/Server-JWT%EB%9E%80
[Server] JWT란?
서버 기반 인증 시스템 vs 토큰 기반 인증 시스템서버 기반 인증 시스템이란?(Session / Cookie)서버 측에서 사용자들의 정보를 기억하기 위해 세션을 유지하는데, 이는 메모리 디스크, 데이터베이
dragonair148.tistory.com
'Spring' 카테고리의 다른 글
[Spring] Spring Security + JWT 사용자 인증(2) - Spring Security 설정 및 필터 적용 (0) | 2025.02.01 |
---|---|
[Spring] Spring Security + JWT 사용자 인증(1) - 로그인 & 토큰 발급 (0) | 2025.01.31 |
[Spring] Spring Security란? (1) | 2025.01.30 |
[Spring] @RequiredArgsConstructor (0) | 2025.01.14 |
[Spring] DAO Test 하다가 실수하기 좋은 포인트 (0) | 2024.04.13 |