Redis

wish17·2023년 5월 16일
0

ec2에 redis 설치

// 설치
sudo apt-get install redis-server

// 실행
sudo service redis-server start

//실행 확인
redis-cli ping

설치과정에서 Which services should be restarted?라는 메세지가 나오면 그냥 종료하면 된다. (굳이 재시작할 필요 x)


Redis 설정

local환경에서 redis를 실행하면 일반적으로 외부에서 접근할 수 없어 비밀번호 설정이 굳이 필요는 없지만 보안을 강화하려면 아래와 같이 접근 비밀번호를 설정할 수 있다.

//redis 설정파일 open
sudo nano /etc/redis/redis.conf

//설정파일에 아래 내용 추가
requirepass yourpassword

yml설정

spring:
  redis:
    host: localhost
    port: 6379

Lettuce vs Jedis

Lettuce와 Jedis는 모두 자바에서 레디스를 사용하기 위한 클라이언트 라이브러리다.

참고 블로그의 내용을 보면 아래와 같은 이유들로 Lettuce를 사용하는 것이 좋을 것 같다.

스레드 안전(Thread Safety)

  • Jedis는 기본적으로 스레드에 안전하지 않다.
  • 멀티 스레드 환경에서 동일한 Jedis 인스턴스를 여러 스레드에서 동시에 사용하면 문제가 발생할 수 있다.
    • 이를 해결하기 위해 Jedis Pool를 사용하여 Jedis 인스턴스를 관리할 수 있지만, 이는 추가적인 코드 작성이 필요
  • Lettuce는 내부적으로 Netty를 사용하여 비동기 및 스레드 안전한 연결을 제공한다.

비동기 처리

  • Lettuce는 비동기 연산을 지원한다.
    • 레디스 서버에 요청을 보낸 후, 응답이 올 때까지 기다리지 않고 다른 작업을 계속할 수 있음을 의미

자동 재연결

  • Lettuce는 Redis 서버와의 연결이 끊어진 경우 자동으로 재연결하는 기능을 제공한다. (장애 복구에 도움 됨)

센티넬, 클러스터 지원

  • Lettuce는 레디스 센티넬 및 클러스터를 지원한다.
    • Redis의 고가용성과 확장성을 높이는 데 필요한 기능

아래와 같이 redis 의존성을 추가하면 별도의 의존성 설정 없이 Lettuce를 사용할 수 있다. 반면 Jedis는 별도의 설정이 필요하다.

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

Redis Repository vs Redis Template

스프링부트에서 Redis를 사용하는 방법에는 Repository 인터페이스를 정의하는 방법Redis Template을 사용하는 방법 두가지가 있다.

  • Repository 인터페이스를 정의하는 방법
    • Spring Data JPA를 사용하는 것과 비슷한 방법
    • Hash 자료구조만 사용 가능
    • 객체를 Redis의 Hash 자료구조로 직렬화하여 스토리지에 저장할 수 있다.
  • Redis Template
    • Redis 서버에 커맨드를 수행하기 위한 고수준의 추상화(high-level abstraction)를 제공

Redis Repository 적용

RefreshToken

@RedisHash(value = "refreshToken", timeToLive = 25200)
public class RefreshToken {

    @Id
    private String rtk;
    private Long memberId;

    public RefreshToken(final String rtk, final Long memberId) {
        this.rtk = rtk;
        this.memberId = memberId;
    }

    public String getRefreshToken() {
        return rtk;
    }

    public Long getMemberId() {
        return memberId;
    }
}
  • timeToLive는 분단위가 아니라 초단위다.

RefreshTokenRepository

public interface RefreshTokenRepository extends CrudRepository<RefreshToken, String> {
    Optional<RefreshToken> findById(String refreshToken);
}
  • @ID애너테이션이 붙은 필드값을 기준으로 데이터를 가져오고자 할 때 컬럼명을 기준으로 메서드명을 작성하는게 아니라 ID를 기준으로 작성해야한다.
    • findByRtk X findById O

RefreshController

@Service
@RequiredArgsConstructor
public class TokenService {
    private final RefreshTokenRepository refreshTokenRepository;
    private final JwtTokenizer jwtTokenizer;

    // Access Token을 생성하는 구체적인 로직
    public String delegateAccessToken(Member member) {
        String email = member.getEmail();

        Map<String, Object> claims = new HashMap<>();
        claims.put("email", email);
        claims.put("roles", member.getRoles());
        claims.put("nickName", member.getNickName());

        Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getAccessTokenExpirationMinutes());

        String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

        String accessToken = jwtTokenizer.generateAccessToken(claims, email, expiration, base64EncodedSecretKey);

        return "Bearer " + accessToken;
    }

    // Refresh Token을 생성하는 구체적인 로직
    public String delegateRefreshToken(Member member) {
        String subject = member.getEmail();
        Date expiration = jwtTokenizer.getTokenExpiration(jwtTokenizer.getRefreshTokenExpirationMinutes());
        String base64EncodedSecretKey = jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey());

        String refreshToken = jwtTokenizer.generateRefreshToken(subject, expiration, base64EncodedSecretKey);

        RefreshToken rtk = new RefreshToken(refreshToken, member.getId());
        refreshTokenRepository.save(rtk);

        return "Bearer " + refreshToken;
    }
}
@RestController
@RequestMapping("/refresh")
@RequiredArgsConstructor
public class RefreshController {
    private final JwtTokenizer jwtTokenizer;
    private final MemberRepository memberRepository;
    private final TokenService tokenService;
    private final RefreshTokenRepository refreshTokenRepository;


    /**
     * 리프레쉬 토큰 받으면 엑세스 토큰 재발급
     */
    @PostMapping
    public ResponseEntity<String> refreshAccessToken(HttpServletRequest request) {
        String refreshTokenHeader = request.getHeader("Refresh");
        if (refreshTokenHeader != null && refreshTokenHeader.startsWith("Bearer ")) {
            String refreshToken = refreshTokenHeader.substring(7);
            try {
                Optional<RefreshToken> refreshTokenObj = refreshTokenRepository.findById(refreshToken);
                if (!refreshTokenObj.isPresent()) {
                    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token [redis]");
                }

                Jws<Claims> claims = jwtTokenizer.getClaims(refreshToken, jwtTokenizer.encodeBase64SecretKey(jwtTokenizer.getSecretKey()));

                String email = claims.getBody().getSubject();
                Optional<Member> optionalMember = memberRepository.findByEmail(email);

                if (optionalMember.isPresent()) {
                    Member member = optionalMember.get();
                    String accessToken = tokenService.delegateAccessToken(member);

                    return ResponseEntity.ok().header("Authorization", "Bearer " + accessToken).body("Access token refreshed");
                } else {
                    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid member email");
                }
            } catch (JwtException e) {
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token");
            }
        } else {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Missing refresh token");
        }
    }

    /**
     * 로그아웃할 때 리프래쉬 토큰을 삭제
     */
    @DeleteMapping
    public ResponseEntity<String> logout(HttpServletRequest request) {
        String refreshTokenHeader = request.getHeader("Refresh");
        if (refreshTokenHeader != null && refreshTokenHeader.startsWith("Bearer ")) {
            String refreshToken = refreshTokenHeader.substring(7);
            try {
                Optional<RefreshToken> refreshTokenObj = refreshTokenRepository.findById(refreshToken);
                if (!refreshTokenObj.isPresent()) {
                    return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid refresh token [redis]");
                }

                refreshTokenRepository.deleteById(refreshToken);
                return ResponseEntity.ok().body("Logged out successfully, refresh token deleted");

            } catch (Exception e) {
                return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).body("Error occurred while deleting refresh token");
            }
        } else {
            return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Missing refresh token");
        }
    }
}

0개의 댓글