spring

Jlaner 개발기록 2 로그인 구성 개요 및 Redis 구성

HJHStudy 2024. 7. 30. 02:43
728x90

이번 포스팅에서는 소셜 로그인과 보안 설정을 구성할 것이다.

 

구성하기 앞서 필자가 사용하는 기술을 둘러보고 진행하도록 하자.

추가적으로 이후에 필요한 것은 필요한 게 있을 때 추가적으로 진행하도록 하겠다.

 

사용 기술 - springboot, spring security, springDataJpa, Oauth2, JWT, redis, h2, mysql

implementation 'org.springframework.boot:spring-boot-starter-data-jpa'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    implementation 'org.springframework.boot:spring-boot-starter-data-redis'

    testImplementation 'org.springframework.security:spring-security-test'
    implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
    implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
    implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
    implementation 'javax.xml.bind:jaxb-api:2.3.1'

    compileOnly 'org.projectlombok:lombok'
    annotationProcessor 'org.projectlombok:lombok'
    testImplementation 'org.projectlombok:lombok'
    testAnnotationProcessor 'org.projectlombok:lombok'

    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    testRuntimeOnly 'org.junit.platform:junit-platform-launcher'
//  runtimeOnly 'com.h2database:h2'
    runtimeOnly 'com.mysql:mysql-connector-j'

 

이제 보안 설정을 어떻게 해야할지 생각해야 하는데 이전에 쿠키부터 Oauth2 소셜로그인을 구현한 예시가 있다. 그 예시에서 Oauth2 소셜 로그인을 진행한 부분인 구글, 카카오, 네이버 로그인을 하던 예제를 기준으로 사용할 것이다.

어떤 예제를 기준으로 사용할 것인지 설명했으니 로그인을 어떤 식으로 구현할지 인증을 어떤 식으로 사용할지 포스팅해보도록 하겠다.

 

로그인 흐름은 Oauth2소셜 로그인을 토대로 로그인을 별도로 제공하지 않을 것이다.

로그인 진행시 DB에 사용자 정보를 저장하고 쿠키에 인증 요청을 저장하고 페이지 LocalStorage에 액세스 토큰을 저장할 것이다. 로그인 진행 후에는 home으로 이동하게 되는데 home 뒤 파라미터에 쿼리 파라미터로 액세스 토큰 값을 더해서 리다이렉트 하게 만들 것이다. 이처럼 토큰 값을 외부에 유출하게 되면 보안상 문제가 생길 수 있지만 액세스 토큰의 유효기간을 짧게 가져가고 리프레시 토큰을 길게 가져갈 것이고 리프레시 토큰의 발급은 레디스와 쿠키에 저장해서 사용할 것이다. 인증을 검증하는 필터에서 요청에 따라 액세스 토큰 값을 검사해서 액세스 토큰 값이 없거나 유효하지 않은 토큰이라면 쿠키의 리프레시 토큰을 검증하고 만약 쿠키 탈취나 삭제 염려가 있으니 보안의 추가로 레디스 저장소에서 액세스 토큰과 사용자에 값에 대해 리프레시 토큰이 있는지 검사하고 리프레시 토큰의 유효기간을 검증한다. 있다면 새로운 액세스 토큰을 생성해 반환해 주고 없다면 로그인을 다시 진행하게끔 하는 구성을 가진 보안 구성을 하려고 한다. 사용자의 모든 요청에는 js를 통해 헤더에 토큰 값을 가지고 진행하게 만들 예정이다.

기본적인 구성과 예시들은 다른 포스팅에 있으니 보안 로직을 구성하기 위해 필요한 부분만 개발하며 확인해보자.

 

Member.class

package com.jlaner.project.domain;

import jakarta.persistence.*;
import lombok.*;

import java.util.ArrayList;
import java.util.List;

@Entity
@Getter
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Member {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    @Column(name = "member_id")
    private Long id;
    private String loginId;
    private String name;

    private String email;

    @Enumerated(EnumType.STRING)
    private MemberRole role;

    private String provider;
    private String providerId;

}

 

사용자를 저장할 엔티티이다. 이름과 이메일을 저장할 것이다. 필요에 따라 각 소셜 로그인 키를  발급받을 때 필요한 부분을 추가하면 되며 구현에 필요한 부분은 이름과 이메일이면 될 것 같아 이렇게 구성했다. role은 각 사용자가 부여받을 권한이다.

 

MemberRole.class

package com.jlaner.project.domain;


public enum MemberRole {
    USER, ADMIN
}

 

간단히 USER와 ADMIN 권한으로 둘을 나누었다.

다음은 레디스에 리프레시 토큰과 액세스 토큰을 저장해서 레디스를 세션 저장소로 사용하듯 인증에 사용해야 하기 때문에 레디스 설정과 토큰을 저장할 클래스를 확인하자.

 

application.yml

redis:
  host: localhost
  port: 6379

RedisProperties.class

package com.jlaner.project.config.redis;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Data
@Component
@ConfigurationProperties(prefix = "spring.redis")
public class RedisProperties {
    private String host;
    private int port;
}

 

레디스 사용을 위한 application.yml에 redis를 사용하기 위한 포트와 호스트를 기입했고 설정 정보를 빈으로 사용하기 위해 클래스를 만들었다.

 

RedisConfig.class

package com.jlaner.project.config.redis;

import lombok.RequiredArgsConstructor;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.connection.RedisStandaloneConfiguration;
import org.springframework.data.redis.connection.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.repository.configuration.EnableRedisRepositories;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
@EnableRedisRepositories
@RequiredArgsConstructor
public class RedisConfig {

    private final RedisProperties redisProperties;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        RedisStandaloneConfiguration redisStandaloneConfiguration = new RedisStandaloneConfiguration();
        redisStandaloneConfiguration.setHostName(redisProperties.getHost());
        redisStandaloneConfiguration.setPort(redisProperties.getPort());
        return new LettuceConnectionFactory(redisStandaloneConfiguration);
    }

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashKeySerializer(new StringRedisSerializer());
        redisTemplate.setHashValueSerializer(new GenericJackson2JsonRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory());
        return redisTemplate;
    }
}

 

레디스를 사용하기 위한 ConnectionFactory와 RedisTemplate를 설정한 클래스이다.

특별히 봐야 할 점은 사용한 직렬화와 역직렬화인 Serializer만 확인하고 넘어가도록 하겠다.

직렬화는 객체를 바이트 스트림으로 변환하는 과정이고, 역직렬화는 바이트 스트림을 다시 객체로 변환하는 과정이다.

레디스 키를 직렬화하고 해시 구조 내 키를 직렬화하는 StringRedisSerializer를 사용했다.

GenericJackson2JsonRedisSerializer는 해시 값의 직렬화에 사용된다. jackson 라이브러리를 사용해 json 형식으로 직렬화하고 json 문자열을 객체로 역직렬화한다.

 

여기서 확인하고 넘어가고 싶은 부분이 있다.

레디스에서 데이터를 저장할 때 문자열로 저장하는 것과 해시로 저장하는 것에 대해서이다.

 

기존에 해시로 직렬화와 역직렬화를 할 때 아래와 같이 저장이 되었다.

127.0.0.1:6379> KEYS *
1) "jwtToken"
2) "jwtToken:abce2491-5efe-4c69-b79e-65803114d6e0"
127.0.0.1:6379> HGETALL jwtToken:abce2491-5efe-4c69-b79e-65803114d6e0
1) "memberId"
2) "1"
3) "accessToken"
4) "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxNjM2OTU5LCJleHAiOjE3MjE3MjMzNTksInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.W4i3mxQOmB9-Tiz93nD3Dn1BjM2q_insASL2tstL3XU"
5) "id"
6) "abce2491-5efe-4c69-b79e-65803114d6e0"
7) "_class"
8) "com.jlaner.project.domain.RefreshToken"
9) "refreshToken"
10) "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxNjM2OTU5LCJleHAiOjE3MjI4NDY1NTksInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ._vIb03vuhaHiol075f99srDcljegdmAT8LV3RzmcMMY"

 

저장된 값을 확인하면 키와 값으로 저장이 된 것을 확인할 수 있다. 그리고 문자열로 저장할 때를 확인해 보자.

127.0.0.1:6379> KEYS *
1) "1"
127.0.0.1:6379> GET 1
"{\"@class\":\"com.jlaner.project.domain.RefreshToken\",\"id\":\"1\",\"memberId\":1,\"accessToken\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxNjY1MDUwLCJleHAiOjE3MjE3NTE0NTAsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.lDOMHueDlj_yU2EZ-shpwefoIM9OT-J0HaojpNQ6kbs\",\"refreshToken\":\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxNjY1MDUwLCJleHAiOjE3MjI4NzQ2NTAsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.CgzONiBGezMOtNTzNrvaJmdWXLfsvvzvsYaGmxVAHmk\"}"

위를 보면 1이라는 키 값에 json형태로 저장되어 있는 것을 확인할 수 있다. 가시성으로 확인할 때 해시 형식인 키와 값으로 저장되는 것이 확인하기 편하지만 왜 json 형식을 사용해 문자열로 저장을 했는지 차이에 대해 확인하고 넘어가 보자.

특징 문자열(String) 해시(Hash)
데이터 구조 단순, 단일 값 복잡, 다중 필드
명령어 SET key value, GET key HSET, HGET, HGETALL
사용 단순한 키-값 쌍 (예: JSON 문자열, 단일 값) 복잡한 객체나 여러 필드를 가진 데이터 (예: 사용자 프로필, 여러 속성을 가진 엔티티)
장점 사용이 쉽고 단순한 데이터에 적합 복잡한 데이터 구조 표현에 적합, 데이터 구조가 유연하고, 필드별로 접근 가능, 여러 필드를 한 번에 저장 및 조회 가능
단점 복잡한 데이터를 표현하기 어려움 구조가 조금 더 복잡하며, 특정 필드에 접근할 때 약간의 오버헤드 존재
성능 작은 데이터에 매우 빠름 작은 필드의 경우 메모리 효율적, 필요한 데이터만 접근 가능
메모리 효율성 데이터 크기에 따라 다름 작은 필드와 값을 저장할 때 효율적

 

차이에 대해서 알아보았다. 필자는 해시 값을 가지고 사용했다.

액세스 토큰도 레디스에 저장하는데 저장하는 이유는 액세스 토큰을 검증할 때 로컬 스토리지에 저장한 값을 사용하고 URL 파라미터에도 추가하니 토큰 유효기간이 짧더라도 보안을 좀 더 강화하고 싶은 생각에 필자의 생각을 토대로 추가한 부분이다. 틀린 부분이 있다면 지적해 주시길 바란다.

 

이런 장단점을 통해 해시 값을 사용한 이유로는 복잡하거나 만약 많은 사용자가 있지 않다면 문자열보다 이점이 적을 것이다. 하지만 해시를 사용한 이유로는 새로운 액세스 토큰을 발급받게 될 때 액세스 토큰이나 리프레시 토큰을 업데이트해야 하고 사용자의 액세스 토큰값으로 레디스에서 검색해야 하기 때문에 특정 필드에 접근하기 위해서 해시 값을 사용하게 되었고 특정 필드에 접근해야 하기 때문에 선택한 것이 해시이다. 단점에서 특정 필드에 접근할 때 오버헤드가 존재할 수 있는 단점이 있는데 이 단점을 커버하고 성능상 더 나은 방향을 위해서 인덱스를 액세스 토큰 값으로 생성하기로 했다.

이를 통해 다음과 같은 구조를 가지게 되었다.

127.0.0.1:6379> KEYS *
 1) "refreshTokenIndex:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxOTExNTczLCJleHAiOjE3MjMxMjExNzMsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.8UwZHC_UAJLxCMaY9o2ZEmZyHSyOun6VARDc3CFoweU"
 2) "refreshTokenIndex:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxODMyNTk1LCJleHAiOjE3MjMwNDIxOTUsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.xpVHu13IwaYnXh9n2dfI7mXcIQOAJO5aBsnHVL_UXnc"
 3) "refreshTokenIndex:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxODE4MDUwLCJleHAiOjE3MjMwMjc2NTAsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.NiLo-cYcG7wReCO6S6RbN8AwyMcaFY94SdPe-sCZ7tY"
 4) "refreshTokenIndex:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIyMjcxMTc4LCJleHAiOjE3MjM0ODA3NzgsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.UDD2B1J9cygBpCfhLpjhs1A-VfRhjs9pWcfgPEt4YAU"
 5) "1"
 6) "refreshTokenIndex:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxODM2MDE0LCJleHAiOjE3MjE5MjI0MTQsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.7LxgEBJUSW7yccL9xogdXjYO3ToNqdzbl4oey-S94C8"
 7) "refreshTokenIndex:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxODc0MDE1LCJleHAiOjE3MjE5NjA0MTUsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.uOo_8mEdFeklY4a0LrFb6WIVqrUPtX3eVQYtPJKp8uA"
 8) "refreshTokenIndex:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxODM0NTIzLCJleHAiOjE3MjMwNDQxMjMsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.vibICzkn0NmymYgbLFWLU3BnGc3BGURryUg9IjN756Q"
 9) "refreshTokenIndex:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxODM1OTMyLCJleHAiOjE3MjMwNDU1MzIsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.fEqhygaulLRLhDVxowB483TXdQ2JGSmSrKmLeToINfE"
10) "refreshTokenIndex:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxODM0NTk2LCJleHAiOjE3MjMwNDQxOTYsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.KKLxEQeQW6P9iXuT3dws__IrKwA6LwZOTztP2k97I1I"
11) "refreshTokenIndex:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxODE2NTUwLCJleHAiOjE3MjMwMjYxNTAsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.WxXoOBdW2cLRB42zt88_3SGDLb0VyIcoBZnCQNC5v0s"
12) "refreshTokenIndex:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxODM2MDAzLCJleHAiOjE3MjMwNDU2MDMsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.E-tf0RE0CGI42LT8F1UJWFgb-61b-_5pa3FdmGcd_Kc"
13) "refreshTokenIndex:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxODE2MzUwLCJleHAiOjE3MjMwMjU5NTAsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.toSPZgL_K-1e5bpiWVsQmIRdPFFlx8gL01rR4KRk_OY"
14) "refreshTokenIndex:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxODM1OTQzLCJleHAiOjE3MjE5MjIzNDMsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.mcOlIV_bNS7tFBuRp3wPh8lg43ZNp_fm3vlw0vc7vMM"
15) "refreshTokenIndex:eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxODczOTM4LCJleHAiOjE3MjMwODM1MzgsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.SI_3Cannv5G6fLbg0d3gb1R8TuDEV1Qh8jlFRQtOcqQ"
127.0.0.1:6379> HGETALL 1
1) "accessToken"
2) "\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIxODE4MDUwLCJleHAiOjE3MjE4MTgwNjAsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.dGwemCYXST08ovDDUU8p5lCQTpYAFOAS6MvsNr541Lc\""
3) "id"
4) "\"1\""
5) "refreshToken"
6) "\"eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJpc3MiOiJ0ZXN0IiwiaWF0IjoxNzIyMjcxMTc4LCJleHAiOjE3MjM0ODA3NzgsInN1YiI6Iu2VnOyngO2biCIsImlkIjoxfQ.UDD2B1J9cygBpCfhLpjhs1A-VfRhjs9pWcfgPEt4YAU\""
7) "memberId"
8) "\"1\""

 

다음은 레디스를 저장하고 삭제하고 인덱스를 저장하는 service 로직을 확인해 보자.

package com.jlaner.project.service;

import com.jlaner.project.domain.RefreshToken;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.HashOperations;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

import java.util.Map;

@RequiredArgsConstructor
@Service
@Slf4j
public class RefreshTokenRedisService {
    private final RedisTemplate<String, Object> redisTemplate;
    //리프레시 토큰을 별도의 인덱스로 검색할 수 있게끔 하기 위한 스태틱 필드
    private static final String REFRESH_TOKEN_INDEX_PREFIX = "refreshTokenIndex:";
    private HashOperations<String, String, Object> hashOps() {
        return redisTemplate.opsForHash();
    }

    public void saveToken(RefreshToken refreshToken) {
        String key = String.valueOf(refreshToken.getMemberId());
        hashOps().put(key, "id", refreshToken.getId());
        //레디스에서는 Long을 저장할때 String으로 저장하지 않으면 Integer로 저장될 수 있기 때문에 ClassCastException이 발생할 수 있다.
        //그러므로 memberId를 String으로 저장하고 꺼내 사용할 때 Long으로 변환.
        hashOps().put(key, "memberId", String.valueOf(refreshToken.getMemberId()));
        hashOps().put(key, "accessToken", refreshToken.getAccessToken());
        hashOps().put(key, "refreshToken", refreshToken.getRefreshToken());

        // refreshToken을 키로 하는 인덱스 저장
        redisTemplate.opsForValue().set(REFRESH_TOKEN_INDEX_PREFIX + refreshToken.getRefreshToken(), key);
    }


    public RefreshToken findByMemberId(Long memberId) {
        // Redis의 키를 memberId로 설정하여 검색
        String key = String.valueOf(memberId);
        return toRefreshToken(key);
    }
    public RefreshToken findByRefreshToken(String refreshToken) {
        String memberIdKey = (String) redisTemplate.opsForValue().get(REFRESH_TOKEN_INDEX_PREFIX + refreshToken);
        if (memberIdKey == null) {
            return null;
        }
        return toRefreshToken(memberIdKey);
    }
    public void deleteByMemberId(Long memberId) {
        // Redis의 키를 memberId로 설정하여 삭제
        redisTemplate.delete(String.valueOf(memberId));
    }
    public void deleteByAccessToken(String accessToken) {
        // Redis의 키를 memberId로 설정하여 삭제
        redisTemplate.delete(accessToken);
    }

    private RefreshToken toRefreshToken(String key) {
        Map<Object, Object> entries = redisTemplate.opsForHash().entries(key);

        String id = (String) entries.get("id");
        Object memberIdObj = entries.get("memberId");
        Long memberId = null;
        if (memberIdObj != null) {
            if (memberIdObj instanceof Integer) {
                memberId = ((Integer) memberIdObj).longValue();
            } else if (memberIdObj instanceof Long) {
                memberId = (Long) memberIdObj;
            } else if (memberIdObj instanceof String) {
                memberId = Long.valueOf((String) memberIdObj); // String을 Long으로 변환
            }
        }
        String accessToken = (String) entries.get("accessToken");
        String refreshToken = (String) entries.get("refreshToken");

        if (id == null || memberId == null || accessToken == null || refreshToken == null) {
            return null;
        }

        return new RefreshToken(id, memberId, accessToken, refreshToken);
    }

}

 

캐스팅이 많은 부분이 마음에 들지는 않는다. 더 나은 방법을 찾아보았지만 적절한 것을 찾지 못했기 때문에 더 알아보고 나중에 리팩토링 할 기회가 온다면 리팩토링 해보도록 하겠다.

특정 부분에 주석을 달았기 때문에 주석을 통해 설명을 한 것으로 하겠다.

토큰을 저장할 객체를 확인해 보자.

package com.jlaner.project.domain;

import jakarta.persistence.*;
import lombok.AccessLevel;
import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.springframework.data.redis.core.RedisHash;

import java.io.Serializable;

@NoArgsConstructor(access = AccessLevel.PROTECTED)
@AllArgsConstructor
@Getter
@RedisHash(value = "jwtToken", timeToLive = 60 * 60 * 24 * 14)
public class RefreshToken implements Serializable {

    @Id
    private String id;

    private Long memberId;

    private String accessToken;
    private String refreshToken;


    public RefreshToken(Long memberId, String accessToken, String refreshToken) {
        this.memberId = memberId;
        this.accessToken = accessToken;
        this.refreshToken = refreshToken;
    }

    public RefreshToken refreshTokenUpdate(String newRefreshToken) {
        this.refreshToken = newRefreshToken;
        return this;
    }
    public RefreshToken accessTokenUpdate(String newAccessToken) {
        this.refreshToken = newAccessToken;
        return this;
    }
}

 

레디스 해시를 통해 저장할 객체로 timetolive는 레디스 안에 저장되어 있을 시간으로 리프레시 토큰의 유효기간과 맞췄다. 리프레시 토큰이 만료되면 다시 로그인을 해야 하니 위와 같이 구성했다.

 

다음 포스팅으로 Oauth2 구성과 토큰 발급 시큐리티 구성을 확인해보도록 하겠다.

 

 

 

728x90