SpringBoot + jwt + redis를 통한 회원 인증, 인가 설정

청포도봉봉이·2023년 11월 23일
1

Spring

목록 보기
28/35
post-thumbnail

개요


개인 프로젝트를 하면서 refreshToken에 대한 정보를 redis에 저장시켜 refreshToken 만료 시 만료되었다는 response를 내려주는 방법에 대해 정리했다.

이 글은 로컬에 redis가 깔려있는 상태에서 작성했습니다.

Mac(m2) 로컬에 redis 설치



Redis 설정


//redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

build.gradle 파일에 추가해주고 빌드 해준다.


spring:
  redis:
    host: localhost
    port: 6379

application.yml 파일에 local의 redis 정보를 입력해준다.



RedisConfig


package study.till.back.config.redis;

import org.springframework.beans.factory.annotation.Value;
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.lettuce.LettuceConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Value("${spring.data.redis.host}")
    private String host;

    @Value("${spring.data.redis.port}")
    private int port;

    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(host, port);
    }

    @Bean
    public RedisTemplate<String, String> redisTemplate() {
        RedisTemplate<String, String> redisTemplate = new RedisTemplate<>();
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        redisTemplate.setValueSerializer(new StringRedisSerializer());
        redisTemplate.setConnectionFactory(redisConnectionFactory());

        return redisTemplate;
    }
}

위 코드에 대한 설명을 하자면

  1. 클래스 선언 : @Configuration 어노테이션을 통해 이 클래스가 스프링 설정 클래스임을 명시한다.

  2. 속성 값 주입 : @Value 어노테이션을 통해 Redis 서버의 호스트 이름과 포트 번호를 프로퍼티 파일에서 가져와 속성에 주입한다.

  3. Redis 연결 생성 : redisConnectionFactory() 메소드에서 LettuceConnectionFactory 인스턴스를 생성해 Redis 서버와의 연결 관리한다.

  4. RedisTemplate 설정 : redisTemplate() 메소드에서 RedisTemplate 인스턴스를 생성하고, 키와 값의 직렬화 방식을 StringRedisSerializer로 설정한다.

  5. RedisTemplate에 연결 설정 : 앞서 생성한 redisConnectionFactory()RedisTemplate의 연결 팩토리로 설정한다.

이렇게 설정된 RedisTemplate은 애플리케이션의 다른 부분에서 주입받아 Redis 데이터베이스와의 데이터 교환을 수행하는 데 사용된다.



JwtTokenProvider


@Slf4j
@Component
public class JwtTokenProvider {

    private final Key key;

    @Value("${jwt.accessExpirationTime}")
    long accessExpirationTime;

    @Value("${jwt.refreshExpirationTime}")
    long refreshExpirationTime;
  
    private final RedisTemplate<String, String> redisTemplate;

    public TokenInfo generateToken(String memberPk, List<String> roles) {
        long now = (new Date()).getTime();
        Date accessTokenExpiresIn = new Date(now + accessExpirationTime);

        // Access Token 생성
        Claims claims = Jwts.claims().setSubject(String.valueOf(memberPk));
        claims.put("roles", roles);
        String accessToken = Jwts.builder()
                .setClaims(claims)
                .setExpiration(accessTokenExpiresIn)
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        // Refresh Token 생성
        String refreshToken = Jwts.builder()
                .setClaims(claims)
                .setExpiration(new Date(now + refreshExpirationTime))
                .signWith(key, SignatureAlgorithm.HS256)
                .compact();

        redisTemplate.opsForValue().set(
                refreshToken,
                memberPk,
                refreshExpirationTime,
                TimeUnit.MILLISECONDS
        );

        return TokenInfo.builder()
                .grantType("Bearer")
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .build();
    }


}

기존 토큰 생성 로직에

redisTemplate.opsForValue().set(
	refreshToken,
	memberPk,
	refreshExpirationTime,
	TimeUnit.MILLISECONDS
);

redisTemplate.opsForValue().set(key, value, timeout, unit) 메소드는 Redis의 String 데이터 타입을 다루는데 사용되는 명령어이다.

를 추가하여 Key, Value로 이루어진 redis에 Key에는 refreshToken 정보, Value에는 사용자 ID값을 저장하도록 했다.

토큰 발행은 로그인 시 해주기 때문에 테스트 해보았다.



검증


로그인에 성공했으니 redis에 refreshToken 정보가 저장되어야 한다.

정상적으로 로그인 후 redis KeyrefreshToken 정보, Value에 사용자 ID가 저장된걸 확인했다.



accessToken 재발급 API 수정


클라이언트쪽에서 헤더 정보에 token 정보를 담아 API 요청을 할때 accessToken이 만료되었을때 accessToken을 재발급해주는 API가 있다. 이제 그 코드에 redis에 refreshToken에 대한 값이 없다면 그에 맞는 response를 내려주는 코드를 짰다.

public ResponseEntity<TokenResponse> refreshToken(TokenRequest tokenRequest) {
    String refreshToken = tokenRequest.getRefreshToken();

    ValueOperations<String, String> stringValueOperations = redisTemplate.opsForValue();
    String redisValue = stringValueOperations.get(refreshToken);

    if (redisValue != null && jwtTokenProvider.validateToken(refreshToken)) {
        Claims claims = jwtTokenProvider.parseClaims(refreshToken);
        String newAccessToken = jwtTokenProvider.createAccessToken(claims);

        TokenResponse tokenResponse = TokenResponse.builder()
                .newAccessToken(newAccessToken)
                .build();
        return ResponseEntity.ok(tokenResponse);
    }
    else {
        throw new ExpiredRefreshTokenException();
    }
}
package study.till.back.service;

import io.jsonwebtoken.Claims;
import lombok.RequiredArgsConstructor;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import study.till.back.config.jwt.JwtTokenProvider;
import study.till.back.dto.token.TokenInfo;
import study.till.back.dto.token.TokenRequest;
import study.till.back.dto.token.TokenResponse;
import study.till.back.exception.token.ExpiredRefreshTokenException;

@Service
@RequiredArgsConstructor
public class TokenService {

    private final JwtTokenProvider jwtTokenProvider;
    private final RedisTemplate<String, String> redisTemplate;

    public ResponseEntity<TokenResponse> refreshToken(TokenRequest tokenRequest) {
        String refreshToken = tokenRequest.getRefreshToken();

        ValueOperations<String, String> stringValueOperations = redisTemplate.opsForValue();
        String redisValue = stringValueOperations.get(refreshToken);

        if (redisValue != null && jwtTokenProvider.validateToken(refreshToken)) {
            Claims claims = jwtTokenProvider.parseClaims(refreshToken);
            String newAccessToken = jwtTokenProvider.createAccessToken(claims);

            TokenResponse tokenResponse = TokenResponse.builder()
                    .newAccessToken(newAccessToken)
                    .build();
            return ResponseEntity.ok(tokenResponse);
        }
        else {
            throw new ExpiredRefreshTokenException();
        }
    }
}

ValueOperations 인터페이스는 Redis의 String 데이터 타입을 다루는 메소드들을 제공하고, 이를 통해 Redis의 String 데이터에 대한 CRUD(Create, Read, Update, Delete) 작업을 수행할 수 있다.

이 코드를 살펴보면

redisTemplateopsForValue() 메소드를 호출하여 ValueOperations 인스턴스를 가져오고 있고 redisValue 변수에 대한 null 체크를 하여 null이라면 예외처리를 해주었다.

profile
서버 백엔드 개발자

0개의 댓글