[Spring] Redis (JWT access/refresh token)

๊น€์ •๋ฏผยท2024๋…„ 4์›” 23์ผ
2
post-thumbnail

์ด๋ฒˆ์— JWT ์ธ์ฆ ๋ฐฉ์‹๋ฅผ Redis๋ฅผ ์ด์šฉํ•ด์„œ ๊ตฌํ˜„ํ•˜๋Š” ๊ฒƒ์„ ๋ชฉํ‘œ๋กœ ํ•˜๊ธฐ ๋•Œ๋ฌธ์— JWT์— ๋Œ€ํ•œ ์ •๋ณด๋Š” ์ž‘์„ฑํ•˜์ง€ ์•Š์œผ๋ ค๊ณ ํ•œ๋‹ค. JWT์— ๋Œ€ํ•ด์„œ ์ „ํ˜€ ๋ชจ๋ฅด๋Š” ๊ฒฝ์šฐ ๊ตฌ๊ธ€์— ๊ฒ€์ƒ‰ํ•˜๋ฉด ๊ต‰์žฅํžˆ ๋งŽ์€ ์ •๋ณด๊ฐ€ ๋‚˜์˜ค๋ฏ€๋กœ ์ฐธ๊ณ ํ•˜๋ฉด ์ข‹์„ ๊ฒƒ ๊ฐ™๋‹ค.

๐Ÿ’ก REDIS(REmote Dictionary Server) ?

๋ฉ”๋ชจ๋ฆฌ ๊ธฐ๋ฐ˜์˜ โ€œํ‚ค-๊ฐ’โ€ ๊ตฌ์กฐ ๋ฐ์ดํ„ฐ ๊ด€๋ฆฌ ์‹œ์Šคํ…œ์ด๋ฉฐ, ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅํ•˜๊ณ  ์กฐํšŒํ•˜๊ธฐ์— ๋น ๋ฅธ Read, Write ์†๋„๋ฅผ ๋ณด์žฅํ•˜๋Š” ๋น„ ๊ด€๊ณ„ํ˜• ๋ฐ์ดํ„ฐ๋ฒ ์ด์Šค์ด๋‹ค. REDIS์— ๋Œ€ํ•œ ์žฅ๋‹จ์ ์„ ์งง๊ฒŒ ๋งํ•˜์ž๋ฉด ์žฅ์ ์€ ๋น ๋ฅธ ์ฒ˜๋ฆฌ ์†๋„์ด๊ณ , ๋‹จ์ ์€ ์ €์žฅ ๊ณต๊ฐ„ ์ œ์•ฝ์ด๋‹ค.

๐Ÿ”‘ ๋ ˆ๋””์Šค ์„ค์ •

Lettuce vs Jedis

Spring Date Redis๋ฅผ ํ†ตํ•ด Lettuce, Jedis๋ผ๋Š” ๊ตฌํ˜„์ฒด๊ฐ€ ์กด์žฌํ•œ๋‹ค.
๋‘˜ ์ค‘ Lettuce๊ฐ€ ์ฝ”๋“œ๋„ ๊ฐ„๋‹จํ•˜๊ณ  ๋ ˆํผ๋Ÿฐ์Šค๋„ ๋งŽ์œผ๋ฉฐ ์„ฑ๋Šฅ๋„ ์ข‹๊ธฐ ๋•Œ๋ฌธ์— Lettuce๋ฅผ ์‚ฌ์šฉํ•œ๋‹ค.

Lettuce๋Š” ๋ณ„๋„์˜ ์„ค์ • ์—†์ด ์‚ฌ์šฉํ•  ์ˆ˜ ์žˆ์œผ๋ฉฐ Jedis๋ฅผ ์‚ฌ์šฉํ•˜๊ณ ์ž ํ•˜๋ฉด ๋ณ„๋„์˜ ์˜์กด์„ฑ์„ ํ•„์š”๋กœ ํ•œ๋‹ค. Lettuce๋ฅผ ์‚ฌ์šฉํ•  ๊ฒƒ ์ด๋ฏ€๋กœ ๋ณ„๋„์˜ ์˜์กด์„ฑ ์„ค์ •์€ ํ•˜์ง€ ์•Š๊ณ  ์ง„ํ–‰ํ•œ๋‹ค.

build.gradle

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()
}

application.yml


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

๐Ÿงฒ RedisTemplate, RedisRepository

Redis๋ฅผ ํ•ธ๋“ค๋ง ํ•˜๊ธฐ ์œ„ํ•ด Redis Template๊ณผ JPA์™€ ์œ ์‚ฌํ•œ ๋ฐฉ์‹์œผ๋กœ ๊ฐ์ฒด๋ฅผ ํ†ตํ•ด Redis๋ฅผ ์ œ์–ดํ•  ์ˆ˜ ์žˆ๋Š” Redis Repository ๋‘๊ฐ€์ง€ ๋ฐฉ๋ฒ•์ด ์žˆ๋‹ค.

๊ฐ„๋‹จํ•œ ๊ธฐ๋Šฅ์ด ํ•„์š”ํ•˜๊ณ  ๋ณต์žกํ•œ ๋ฐ์ดํ„ฐ ์œ ํ˜•์„ ๋‹ค๋ฃจ์ง€ ์•Š๋Š”๋‹ค๋ฉด Redis Repository๊ฐ€ ํŽธ๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค. ๊ทธ๋Ÿฌ๋‚˜ ๋” ๋งŽ์€ ์ œ์–ด์™€ ์œ ์—ฐ์„ฑ์ด ํ•„์š”ํ•˜๊ฑฐ๋‚˜ ํŠน์ • ๋ฐ์ดํ„ฐ ์œ ํ˜•์„ ๋‹ค๋ค„์•ผ ํ•˜๋Š” ๊ฒฝ์šฐ์—๋Š” RedisTemplate์„ ์‚ฌ์šฉํ•˜๋Š” ๊ฒƒ์ด ๋” ์ ํ•ฉํ•  ์ˆ˜ ์žˆ๋‹ค.

RedisTemplate๋ฅผ ์‚ฌ์šฉํ•ด ๋ณด๋„๋ก ํ•˜์ž!

๐Ÿ’ก RedisTemplate

RedisTemplate์„ ์ด์šฉํ•ด์„œ Redis์— ์ ‘๊ทผํ•˜๋Š” Service๋ฅผ ๋งŒ๋“ค์–ด ๊ด€๋ฆฌํ•  ์ˆ˜ ์žˆ๋‹ค.

์ž์„ธํ•œ ๋‚ด์šฉ์€ ์•„๋ž˜ ์†Œ์Šค๋ฅผ ์ฐธ๊ณ ํ•˜์ž.

@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 ์ธ์ฆ

์ฒ˜์Œ์— JWT์— ๋Œ€ํ•œ ์ •๋ณด๋ฅผ ์ž‘์„ฑํ•˜์ง€ ์•Š๋Š”๋‹ค๊ณ  ํ–ˆ์ง€๋งŒ ์•„์ฃผ ๊ฐ„๋žตํ•˜๊ฒŒ ์•Œ์•„๋ณด์ž.

  • ๋กœ๊ทธ์ธ ์‹œ ์œ ํšจ ๊ธฐ๊ฐ„์ด ๋งค์šฐ ์งง์€ Access Token๊ณผ ์œ ํšจ ๊ธฐ๊ฐ„์ด ๊ธด Refresh Token์„ ํ•จ๊ป˜ ๋ฐœ๊ธ‰.
  • Access Token์ด ๋งŒ๋ฃŒ๋˜๋ฉด Refresh Token์„ ํ†ตํ•ด ์ƒˆ Access Token๊ณผ ์ƒˆ Refresh Token์„ ์žฌ๋ฐœ๊ธ‰
  • ์ƒˆ๋กœ๊ณ ์นจ์œผ๋กœ Access Token ๊ฐ’์ด ์—†์–ด์ง€๋ฉด Refresh Token์„ ํ†ตํ•ด ์ƒˆ Access Token๊ณผ ์ƒˆ Refresh Token์„ ๋ฐœ๊ธ‰
  • ๋กœ๊ทธ์•„์›ƒ ์‹œ Access Token๊ณผ Refresh Token ๋ชจ๋‘ ๋ฌดํšจํ™” ์‹œํ‚จ๋‹ค.

์ธํ„ฐ๋„ท์— ๋งŽ์€ ์ •๋ณด๋“ค์ด ์กด์žฌํ•˜๋‹ˆ ๋„์›€์„ ๋ฐ›์œผ๋ฉด ์ข‹๊ฒ ๋‹ค. ์—ฌ๊ธฐ์„œ๋Š” ์ด์ •๋„๋งŒ ์„ค๋ช…ํ•˜๊ณ  ๋„˜์–ด๊ฐ€๊ฒ ๋‹ค.

๋กœ๊ทธ์ธ์— ๋Œ€ํ•œ ์ธ์ฆ ๋ฐฉ๋ฒ•์„ ์ž‘์„ฑํ•œ Service๋ฅผ ๋งŒ๋“ค์–ด ๋ณด์•˜๋‹ค. JwtUtils์— ๋Œ€ํ•œ ์ž์„ธํ•œ ์ •๋ณด ๋ฐ ๋‹ค๋ฅธ ์†Œ์Šค๋“ค์€ ์•„๋ž˜ ๊นƒํ—ˆ๋ธŒ์— ์ž‘์„ฑํ•ด๋‘์—ˆ๋‹ค.

๐ŸŽ€ ๊นƒํ—ˆ๋ธŒ์—๋Š” ์ฃผ์„์œผ๋กœ ์ฝ”ํ‹€๋ฆฐ ๋ฒ„์ „๋„ ์ž‘์„ฑํ•ด ๋‘์—ˆ๋‹ค.


@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;
  }
}

๋งˆ๋ฌด๋ฆฌ

์ฒ˜์Œ์— ๋ชจ๋“  ๋ฐ์ดํ„ฐ๋ฅผ ๋ฉ”๋ชจ๋ฆฌ์— ์ €์žฅํ•˜๊ณ  ์กฐํšŒํ•˜๊ธฐ ๋•Œ๋ฌธ์— ๋น ๋ฅด๊ณ  ์ข‹๋‹ค๋Š” redis๋ฅผ ์•Œ๊ฒŒ ๋˜์—ˆ์„ ๋•Œ ์–ด๋–ป๊ฒŒ ๋™์ž‘๋˜๋Š” ๊ฑด์ง€ ๊ฐ์ด ์˜ค์ง€ ์•Š์•˜๋‹ค. ํ•˜์ง€๋งŒ ์ง์ ‘ ๊ตฌํ˜„ํ•ด ๋ณด๋ฉด์„œ ์–ด๋Š ์ •๋„ ์ดํ•ด๋ฅผ ํ•˜๊ฒŒ ๋˜์—ˆ๊ณ  ์ถ”ํ›„ ๋Œ€์šฉ๋Ÿ‰ ํŠธ๋ž˜ํ”ฝ์— ๋Œ€์‘ํ•  ์ˆ˜ ์žˆ๋Š” ๋Œ€์•ˆ์ด ๋  ๊ฒƒ์ด๋ผ๊ณ  ์ƒ๊ฐ์ด ๋œ๋‹ค. ์•„์ง ๋Œ€์šฉ๋Ÿ‰ ํŠธ๋ž˜ํ”ฝ์„ ๋ฐ›์•„ ์ฒ˜๋ฆฌํ•  ํ™˜๊ฒฝ๋„ ์•„๋‹ˆ๊ณ  ์‹ค๋ ฅ๋„ ์•„๋‹ˆ๋ผ๊ณ  ์ƒ๊ฐํ•˜์ง€๋งŒ ์–ธ์  ๊ฐ€ ๊ทธ๋ ‡๊ฒŒ ์‚ฌ์šฉํ•ด ๋ณผ ์ˆ˜ ์žˆ๋Š” ๋‚ ์ด ์™”์œผ๋ฉด ์ข‹๊ฒ ๋‹ค.


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

0๊ฐœ์˜ ๋Œ“๊ธ€