Programming/Spring Boot

security + jwt + redis 로그인 기능 구현 (최종)

Jan92 2021. 9. 23. 00:57

Security + JWT + JPA + Redis

이전 Spring Security + JWT 로그인 구현에서 마지막으로 Refresh Token 저장을 위한 Redis를 추가하고, 토큰 재발급 기능을 추가하여 로그인 기능을 완성하였습니다.

 

 

 

Spring Security + JWT 로그인 기능 파헤치기 - 1

로그인 기능은 거의 대부분의 애플리케이션에서 기본적으로 사용됩니다. 추가로 요즘은 웹이 아닌 모바일에서도 사용 가능하다는 장점과 Stateless 한 서버 구현을 위해 JWT를 사용하는 경우를 많

wildeveloperetrain.tistory.com

앞서 포스팅 한 Security + JWT 로그인 구현 과정입니다. 이어지는 내용이기 때문에 참고 부탁드리겠습니다.

 

 

 

Spring Boot Redis 두 가지 사용 방법 RedisTemplate, RedisRepository

https://wildeveloperetrain.tistory.com/21 Redis란? 레디스의 기본적인 개념 (인메모리 데이터 구조 저장소) Redis란? Key, Value 구조의 비정형 데이터를 저장하고 관리하기 위한 오픈 소스 기반의 비관계형..

wildeveloperetrain.tistory.com

Redis 사용법에 대한 내용입니다. 해당 로그인 기능에서는 RedisTemplate을 사용하여 기능을 구현하였습니다.

 

 

* 해당 두 포스팅은 내용 이해에 필요한 부분이라 생각하여 링크로 올려놨으며, GitHub 전체 코드 및 다른 추가적으로 참고할 수 있는 내용은 포스팅 맨 하단에 링크해두겠습니다.

 

 


 

 

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

Gradle 의존성 추가

 

 

Spring Boot 에서는 Spring Data Redis를 통해 Lettuce, Jedis라는 두 가지 오픈소스 라이브러리를 사용할 수 있습니다. 의존성을 추가할 때 별도의 설정을 하지 않으면 Lettuce가 기본으로 적용됩니다.

 

'Redis 사용법' 포스팅에 조금 더 자세한 내용이 담겨있지만, 해당 코드에서는 RedisTemplate을 이용하여 Redis를 사용합니다.

 

 

@RequiredArgsConstructor
@Configuration
@EnableRedisRepositories
public class RedisRepositoryConfig {

    private final RedisProperties redisProperties;

    // lettuce
    @Bean
    public RedisConnectionFactory redisConnectionFactory() {
        return new LettuceConnectionFactory(redisProperties.getHost(), redisProperties.getPort());
    }

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

RedisConnctionFactory 인터페이스를 통해 LettuceConnectionFactory를 생성하여 반환합니다.

setKeySerializer, setValueSerializer 설정을 함으로써 redis-cli을 통해 직접 데이터를 볼 수 있습니다.

 

 

# Redis
spring.redis.host=localhost
spring.redis.port=6379

RedisProperties를 통해서 properties에 저장한 host, port를 가지고 와서 연결합니다.

 

 


 

 

    public ResponseEntity<?> login(UserRequestDto.Login login) {

        if (usersRepository.findByEmail(login.getEmail()).orElse(null) == null) {
            return response.fail("해당하는 유저가 존재하지 않습니다.", HttpStatus.BAD_REQUEST);
        }

        // 1. Login ID/PW 를 기반으로 Authentication 객체 생성
        // 이때 authentication 는 인증 여부를 확인하는 authenticated 값이 false
        UsernamePasswordAuthenticationToken authenticationToken = login.toAuthentication();

        // 2. 실제 검증 (사용자 비밀번호 체크)이 이루어지는 부분
        // authenticate 매서드가 실행될 때 CustomUserDetailsService 에서 만든 loadUserByUsername 메서드가 실행
        Authentication authentication = authenticationManagerBuilder.getObject().authenticate(authenticationToken);

        // 3. 인증 정보를 기반으로 JWT 토큰 생성
        UserResponseDto.TokenInfo tokenInfo = jwtTokenProvider.generateToken(authentication);

        // 4. RefreshToken Redis 저장 (expirationTime 설정을 통해 자동 삭제 처리)
        redisTemplate.opsForValue()
                .set("RT:" + authentication.getName(), tokenInfo.getRefreshToken(), tokenInfo.getRefreshTokenExpirationTime(), TimeUnit.MILLISECONDS);

        return response.success(tokenInfo, "로그인에 성공했습니다.", HttpStatus.OK);
    }

Login 과정의 Service단 로직입니다.

나머지 코드는 기존에 구현된 부분이고 '4. RefreshToken Redis 저장' 부분이 해당 포스팅에서 추가된 부분인데요.

 

 

 

redisTemplate set method

RefreshToken 저장을 위해 사용한 set 메서드는 key, value 값 외에 long type의 timeout, TimeUnit type의 unit을 인자로 받습니다.

해당 메서드의 용도는 'Set the value and expiration timeout for key.' refresh token의 유효시간 정보를 함께 저장해서 토큰이 만료되었을 때 value 값을 자동으로 삭제하는 기능을 위해 사용하였습니다.

 

 

 

Redis Refresh Token

 

이렇게 로그인이 성공했을 때 redis에 refresh token이 정상적으로 저장되는 것을 볼 수 있습니다. 또한 TTL (key) 명령어를 통해 해당 key 값의 유효시간도 정상적으로 저장된 것을 확인할 수 있습니다.

 

 


 

 

    public ResponseEntity<?> reissue(UserRequestDto.Reissue reissue) {
        // 1. Refresh Token 검증
        if (!jwtTokenProvider.validateToken(reissue.getRefreshToken())) {
            return response.fail("Refresh Token 정보가 유효하지 않습니다.", HttpStatus.BAD_REQUEST);
        }

        // 2. Access Token 에서 User email 를 가져옵니다.
        Authentication authentication = jwtTokenProvider.getAuthentication(reissue.getAccessToken());

        // 3. Redis 에서 User email 을 기반으로 저장된 Refresh Token 값을 가져옵니다.
        String refreshToken = (String)redisTemplate.opsForValue().get("RT:" + authentication.getName());
        if(!refreshToken.equals(reissue.getRefreshToken())) {
            return response.fail("Refresh Token 정보가 일치하지 않습니다.", HttpStatus.BAD_REQUEST);
        }

        // 4. 새로운 토큰 생성
        UserResponseDto.TokenInfo tokenInfo = jwtTokenProvider.generateToken(authentication);

        // 5. RefreshToken Redis 업데이트
        redisTemplate.opsForValue()
                .set("RT:" + authentication.getName(), tokenInfo.getRefreshToken(), tokenInfo.getRefreshTokenExpirationTime(), TimeUnit.MILLISECONDS);

        return response.success(tokenInfo, "Token 정보가 갱신되었습니다.", HttpStatus.OK);
    }

Service단에 있는 토큰이 재발급 기능을 하는 reissue() 메서드입니다.

 

 

    @Getter
    @Setter
    public static class Reissue {
        @NotEmpty(message = "accessToken 을 입력해주세요.")
        private String accessToken;

        @NotEmpty(message = "refreshToken 을 입력해주세요.")
        private String refreshToken;
    }

토큰 갱신을 위해서는 accessToken, refreshToken 값이 필요합니다.

 

 

갱신 과정은

  1. Refresh Token이 유효한지 검증합니다.
  2. Access Token에서 Authentication 객체를 가지고 와서 저장된 name(email)을 가지고 옵니다.
  3. email을 가지고 Redis에 저장된 Refresh Token을 가지고 와서 입력받은 Refresh Token 값과 비교합니다.
  4. Authentication 객체를 가지고 새로운 토큰을 생성합니다.
  5. Redis에 새로 생성된 Refresh Token을 저장합니다.
  6. 클라이언트에게 새로 발급된 토큰 정보를 내려줍니다.

 

 

* 해당 포스팅은 JwtTokenProvider class의 메서드들과 같이 이전 포스팅에서 나왔던 부분의 자세한 설명을 하지 않아 이해가 어려울 수 있습니다. 아래 전체 코드와 이전 Secutiry + JWT 로그인 구현 포스팅의 내용을 참고하시면 이해에 도움이 될 것 같습니다.

 

 

GitHub 전체 코드

 

GitHub - JianChoi-Kor/Login: Login (Spring Security, JWT, Redis, JPA )

Login (Spring Security, JWT, Redis, JPA ). Contribute to JianChoi-Kor/Login development by creating an account on GitHub.

github.com

 

 

함께 보면 좋은 자료

 

Spring Security + JWT 로그인 기능 파헤치기 - 1

로그인 기능은 거의 대부분의 애플리케이션에서 기본적으로 사용됩니다. 추가로 요즘은 웹이 아닌 모바일에서도 사용 가능하다는 장점과 Stateless 한 서버 구현을 위해 JWT를 사용하는 경우를 많

wildeveloperetrain.tistory.com

 

 

Spring Security 시큐리티 동작 원리 이해하기 - 1

스프링 시큐리티 (Spring Security)는 스프링 기반 어플리케이션의 보안(인증과 권한, 인가)을 담당하는 스프링 하위 프레임워크입니다. 보안과 관련해서 체계적으로 많은 옵션들을 제공해주기 때문

wildeveloperetrain.tistory.com

 

 

JWT 토큰 기반 인증 시스템 (JSON Web Token)

Spring Boot + Security + JWT + Redis 를 기본으로한 RESTful API를 구현하기로 계획하며 토큰 기반 인증 시스템 JWT에 대해서 다시 한번 정리합니다. 토큰 기반 인증 시스템이란, 먼저 웹 보안은 요청하는 사

wildeveloperetrain.tistory.com

 

 

참고 자료

 

Spring Security 와 JWT 겉핥기

Introduction 이 글에서는 Spring Boot + JWT + Security 를 사용해서 회원가입/로그인 로직을 구현했습니다. JWT 와 Spring Security 코드는 인프런 Spring Boot JWT Tutorial (정은구) 강의를 수강하면서 만들고..

bcp0109.tistory.com