프로젝트를 진행하면서, 사용자의 인증 여부를 확인하기 위해 JWT 토큰 기반의 방식을 선택했다.
보안과 JWT를 공부하면서 refresh 토큰을 제대로 사용해본 적이 없어 2024년 여름방학 때 확실하게 공부해야겠다는 생각이 들었다.
처음 프로젝트 때 카카오 소셜 로그인을 사용하면서 access token 만으로 인증 방식을 진행했지만, access token을 탈취당했을 상황의 문제점에 대해 고려하면서 refresh token을 도입하여 보완하고, 로그아웃 이후의 탈취상황을 고려해 블랙리스트(Blacklist)라는 개념을 찾게 되어 사용해보고자 도입해봤다.
Refresh Token 로직
- 클라이언트 로그인 요청
- 서버에서 access token, refresh token 생성 및 반환
access token의 유효기간을 짧게, refresh token을 길게 가져간다.
이때, refresh token을 redis에 저장한다. - 인증이 필요한 요청시 기존과 동일하게 access token을 header에 담아서 요청한다.
refresh token을 Redis에 저장하므로써 독립적으로 유효기간을 설정해서 관리할 수 있고 유저 식별자를 키-값으로 저장하여 쉽게 저장할 수 있다는 장점을 얻었다.
이후 access token이 만료됐다면, refresh token으로 access token 재발급을 요청한다.
또 refresh token이 만료됐다면, 로그인을 재요청한다.
JWT BlackList 사용
Jwt BlackList를 사용하게 되는 경우, 로그아웃 기능을 적용할 수 있다.
1. 사용자가 access token을 바디에 담아서 로그아웃 요청을 보낸다.
// 로그아웃
@Transactional
public void logout(TokenDTO tokenDTO) {
String accessToken = tokenDTO.getAccessToken();
String email = jwtProvider.getEmailFromAccessToken(accessToken);
log.info("access token: {}", accessToken);
log.info("email : {}", email);
// redis에서 refresh Token 제거
redisService.deleteRefreshToken(email);
// 해당 엑세스 토큰의 남은 유효시간을 얻어서 Access Token blacklist에 등록하여 만료시키기
Long tokenExpiration = jwtProvider.getTokenExpiration(accessToken);
redisService.setBlackList(accessToken, "logout", tokenExpiration);
}
액세스 토큰에서 email 값을 추출하여 redisService에서 이를 통해 refreshToken을 제거한 뒤, 남아있는 유효시간을 가져와서JwtBlackList를 생성하여 redis에 삽입한다.
2. 이후, API 요청을 보낼 때마다 서버에서 Jwt 토큰 유효성 검사를 진행할 때, 해당 토큰이 redis 내 BlackList로 등록되어있다면 로그아웃 처리된 토큰이므로 재발급하라는 응답을 보낸다.
```JwtProvider.java
...
public boolean validate(String token) {
try {
JWT.require(algorithm).build().verify(token);
if (redisService.hasKeyBlackList(token)) { // JWT BlackList
return false;
}
return true;
} catch (JWTVerificationException e) {
return false;
}
}
RedisService.java
import lombok.RequiredArgsConstructor;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;
import java.util.concurrent.TimeUnit;
@Service
@RequiredArgsConstructor
public class RedisService {
private final RedisTemplate<String, String> redisTemplate;
private final RedisTemplate<String, String> redisBlackListTemplate;
@Value("${jwt.refresh-token.expiration}")
private Long refreshTokenExpiration;
public void saveRefreshToken(String key, String value) {
redisTemplate.opsForValue().set(key, value, refreshTokenExpiration, TimeUnit.HOURS);
}
public String getRefreshToken(String key) {
return redisTemplate.opsForValue().get(key);
}
public void deleteRefreshToken(String key) {
redisTemplate.delete(key);
}
public void setBlackList(String key, String o, Long milliSeconds) {
redisBlackListTemplate.opsForValue().set(key, o, milliSeconds, TimeUnit.HOURS);
}
public Object getBlackList(String key) {
return redisBlackListTemplate.opsForValue().get(key);
}
public Boolean deleteBlackList(String key) {
return redisBlackListTemplate.delete(key);
}
public Boolean hasKeyBlackList(String key) {
return redisBlackListTemplate.hasKey(key);
}
}
refresh token을 저장하기 위한 RedisTemplate 외에, JWT BlackList를 저장하기 위한 RedisTemplate를 생성한다.
참고
https://velog.io/@boo105/Redis-%EB%A5%BC-%ED%86%B5%ED%95%9C-JWT-Blacklist-%EA%B5%AC%ED%98%84
Redis 를 통한 JWT Blacklist 구현
Redis, Jwt, Logout
velog.io
'Back-end > Spring' 카테고리의 다른 글
[Spring] @RequestPart vs @RequestParam vs @RequestBody (1) | 2024.10.11 |
---|---|
[Spring] Spring Boot에서 Google Meet API 적용해보기 (10) | 2024.10.06 |
[Spring] Query Parameter vs Path Variable (2) | 2024.10.02 |
[Spring] Security + JWT + Redis를 활용한 로그인 구현 (2) (0) | 2024.08.30 |
[Spring] @NotNull vs @Column(nullable = false) (0) | 2024.08.19 |