์ด๋ฒ์ JWT ์ธ์ฆ ๋ฐฉ์๋ฅผ Redis๋ฅผ ์ด์ฉํด์ ๊ตฌํํ๋ ๊ฒ์ ๋ชฉํ๋ก ํ๊ธฐ ๋๋ฌธ์ JWT์ ๋ํ ์ ๋ณด๋ ์์ฑํ์ง ์์ผ๋ ค๊ณ ํ๋ค. JWT์ ๋ํด์ ์ ํ ๋ชจ๋ฅด๋ ๊ฒฝ์ฐ ๊ตฌ๊ธ์ ๊ฒ์ํ๋ฉด ๊ต์ฅํ ๋ง์ ์ ๋ณด๊ฐ ๋์ค๋ฏ๋ก ์ฐธ๊ณ ํ๋ฉด ์ข์ ๊ฒ ๊ฐ๋ค.
๋ฉ๋ชจ๋ฆฌ ๊ธฐ๋ฐ์ โํค-๊ฐโ ๊ตฌ์กฐ ๋ฐ์ดํฐ ๊ด๋ฆฌ ์์คํ ์ด๋ฉฐ, ๋ชจ๋ ๋ฐ์ดํฐ๋ฅผ ๋ฉ๋ชจ๋ฆฌ์ ์ ์ฅํ๊ณ ์กฐํํ๊ธฐ์ ๋น ๋ฅธ Read, Write ์๋๋ฅผ ๋ณด์ฅํ๋ ๋น ๊ด๊ณํ ๋ฐ์ดํฐ๋ฒ ์ด์ค์ด๋ค. REDIS์ ๋ํ ์ฅ๋จ์ ์ ์งง๊ฒ ๋งํ์๋ฉด ์ฅ์ ์ ๋น ๋ฅธ ์ฒ๋ฆฌ ์๋์ด๊ณ , ๋จ์ ์ ์ ์ฅ ๊ณต๊ฐ ์ ์ฝ์ด๋ค.
Spring Date Redis๋ฅผ ํตํด Lettuce, Jedis๋ผ๋ ๊ตฌํ์ฒด๊ฐ ์กด์ฌํ๋ค.
๋ ์ค Lettuce๊ฐ ์ฝ๋๋ ๊ฐ๋จํ๊ณ ๋ ํผ๋ฐ์ค๋ ๋ง์ผ๋ฉฐ ์ฑ๋ฅ๋ ์ข๊ธฐ ๋๋ฌธ์ Lettuce๋ฅผ ์ฌ์ฉํ๋ค.
Lettuce๋ ๋ณ๋์ ์ค์ ์์ด ์ฌ์ฉํ ์ ์์ผ๋ฉฐ Jedis๋ฅผ ์ฌ์ฉํ๊ณ ์ ํ๋ฉด ๋ณ๋์ ์์กด์ฑ์ ํ์๋ก ํ๋ค. Lettuce๋ฅผ ์ฌ์ฉํ ๊ฒ ์ด๋ฏ๋ก ๋ณ๋์ ์์กด์ฑ ์ค์ ์ ํ์ง ์๊ณ ์งํํ๋ค.
plugins {
id 'java'
id 'org.springframework.boot' version '3.2.1'
id 'io.spring.dependency-management' version '1.1.4'
}
group = 'com.example'
version = '0.0.1-SNAPSHOT'
java {
sourceCompatibility = '17'
}
configurations {
compileOnly {
extendsFrom annotationProcessor
}
}
repositories {
mavenCentral()
}
dependencies {
implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-web'
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
compileOnly 'org.projectlombok:lombok'
runtimeOnly 'com.mysql:mysql-connector-j'
annotationProcessor 'org.projectlombok:lombok'
testImplementation 'org.springframework.boot:spring-boot-starter-test'
}
tasks.named('test') {
useJUnitPlatform()
}
server:
port: 8081
spring:
# Redis
cache:
type: redis
data:
redis:
host: localhost
port: 6379
# DataSource Setting
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/jwt?useSSL=false&allowPublicKeyRetrieval=true&characterEncoding=UTF-8&serverTimezone=Asia/Seoul
username:
password:
# JPA Setting
jpa:
hibernate:
ddl-auto: none
naming:
physical-strategy: org.hibernate.boot.model.naming.PhysicalNamingStrategyStandardImpl
# JWT secretKey 64์๋ฆฌ
jwt:
secretKey: testsecrettestsecrettestsecrettestsecrettestsecrettestsecretKey1
Redis๋ฅผ ํธ๋ค๋ง ํ๊ธฐ ์ํด Redis Template๊ณผ JPA์ ์ ์ฌํ ๋ฐฉ์์ผ๋ก ๊ฐ์ฒด๋ฅผ ํตํด Redis๋ฅผ ์ ์ดํ ์ ์๋ Redis Repository ๋๊ฐ์ง ๋ฐฉ๋ฒ์ด ์๋ค.
๊ฐ๋จํ ๊ธฐ๋ฅ์ด ํ์ํ๊ณ ๋ณต์กํ ๋ฐ์ดํฐ ์ ํ์ ๋ค๋ฃจ์ง ์๋๋ค๋ฉด Redis Repository๊ฐ ํธ๋ฆฌํ ์ ์๋ค. ๊ทธ๋ฌ๋ ๋ ๋ง์ ์ ์ด์ ์ ์ฐ์ฑ์ด ํ์ํ๊ฑฐ๋ ํน์ ๋ฐ์ดํฐ ์ ํ์ ๋ค๋ค์ผ ํ๋ ๊ฒฝ์ฐ์๋ RedisTemplate์ ์ฌ์ฉํ๋ ๊ฒ์ด ๋ ์ ํฉํ ์ ์๋ค.
RedisTemplate๋ฅผ ์ฌ์ฉํด ๋ณด๋๋ก ํ์!
์์ธํ ๋ด์ฉ์ ์๋ ์์ค๋ฅผ ์ฐธ๊ณ ํ์.
@Service
@RequiredArgsConstructor
public class RedisService {
private static final long REDIS_TOKEN_EXPIRATION = 3 * 24 * 60 * 60 * 1000; // 3 days
private final RedisTemplate<String, String> redisTemplate;
public void saveTokensToRedis(String accessToken, String refreshToken) {
// Redis์ ํ ํฐ ์ ์ฅ
redisTemplate.opsForValue().set(accessToken, refreshToken, REDIS_TOKEN_EXPIRATION, TimeUnit.MILLISECONDS);
}
public void deleteTokensFromRedis(String accessToken) {
// Redis์์ ํ ํฐ ์ญ์
redisTemplate.delete(accessToken);
}
// accessToken ์ ๋ฌด ์ฒดํฌ
public boolean isTokenExists(String accessToken) {
return redisTemplate.hasKey(accessToken);
}
// refreshToken ๊ฐ์ ธ์ค๊ธฐ
public String getRefreshToken(String key) {
return redisTemplate.opsForValue().get(key);
}
}
์ฒ์์ JWT์ ๋ํ ์ ๋ณด๋ฅผ ์์ฑํ์ง ์๋๋ค๊ณ ํ์ง๋ง ์์ฃผ ๊ฐ๋ตํ๊ฒ ์์๋ณด์.
์ธํฐ๋ท์ ๋ง์ ์ ๋ณด๋ค์ด ์กด์ฌํ๋ ๋์์ ๋ฐ์ผ๋ฉด ์ข๊ฒ ๋ค. ์ฌ๊ธฐ์๋ ์ด์ ๋๋ง ์ค๋ช ํ๊ณ ๋์ด๊ฐ๊ฒ ๋ค.
๐ ๊นํ๋ธ์๋ ์ฃผ์์ผ๋ก ์ฝํ๋ฆฐ ๋ฒ์ ๋ ์์ฑํด ๋์๋ค.
@Service
@RequiredArgsConstructor
public class LoginService {
private static final long ACCESS_TOKEN_EXPIRATION = 15 * 60 * 1000; // 15 minutes
private static final long REFRESH_TOKEN_EXPIRATION = 3 * 24 * 60 * 60 * 1000; // 3 days
private final UserRepository userRepository;
private final RedisService redisService;
@Value("${jwt.secretKey}")
private String jwtSecretKey;
public HashMap<String, Object> login(LoginDto loginDto) {
User loginData = userRepository.findByUserId(loginDto.getUserId()).orElseThrow(
() -> new ApiError("401", "์
๋ ฅํ ํ์์ ๋ณด๋ ์ผ์นํ์ง ์์ต๋๋ค."));
if (loginData.getUserPwd().equals(loginDto.getUserPwd())) {
JwtResultResponse jwtToken = JwtUtils.createJwtToken(jwtSecretKey, ACCESS_TOKEN_EXPIRATION, loginData);
JwtResultResponse jwtRefreshToken = JwtUtils.createJwtToken(jwtSecretKey, REFRESH_TOKEN_EXPIRATION, loginData);
HashMap<String, Object> loginHashMap = new HashMap<>();
loginHashMap.put("jwtToken", jwtToken.getToken());
loginHashMap.put("jwtRefreshToken", jwtRefreshToken.getToken());
redisService.saveTokensToRedis(jwtToken.getToken(), jwtRefreshToken.getToken());
return loginHashMap;
} else throw new ApiError("401", "์
๋ ฅํ ํ์์ ๋ณด๋ ์ผ์นํ์ง ์์ต๋๋ค.");
}
public HashMap<String, Object> reToken(User user) {
JwtResultResponse jwtToken = JwtUtils.createJwtToken(jwtSecretKey, ACCESS_TOKEN_EXPIRATION, user);
JwtResultResponse jwtRefreshToken = JwtUtils.createJwtToken(jwtSecretKey, REFRESH_TOKEN_EXPIRATION, user);
HashMap<String, Object> loginHashMap = new HashMap<>();
loginHashMap.put("jwtToken", jwtToken.getToken());
loginHashMap.put("jwtRefreshToken", jwtRefreshToken.getToken());
redisService.saveTokensToRedis(jwtToken.getToken(), jwtRefreshToken.getToken());
return loginHashMap;
}
}
Github : https://github.com/Jungmin-Dev/jwt
์ถ์ฒ : https://byungil.tistory.com/309 - [Redis) ์ฑ๊ธ๋ฒ๊ธ Refresh Token์ Redis์ ์ ์ฅํ๊ณ ์ฌ์ฉํด๋ณด์]
์ถ์ฒ : https://velog.io/@turtledev/Spring-Redis-Redis-Template%EA%B3%BC-Redis-Repository - [Spring, Redis] Redis Template๊ณผ Redis Repository