[JWT] SpringSecurity + JWT를 사용하여 로그인, 회원가입 구현 - 3

Donghoon Jeong·2024년 2월 24일
0

JWT

목록 보기
4/4
post-thumbnail

이번 포스팅에선 데이터베이스에 저장하던 Refresh Token을 인 메모리 데이터베이스인 Redis에 저장하고, Refresh Token을 사용하여 Access Token을 발급하는 과정을 살펴보겠습니다.

1. 의존성 추가

build.gradle

dependencies {
 
    ...
    
   	// Redis
	implementation 'org.springframework.boot:spring-boot-starter-data-redis'
}

2. Redis 설정

application.yml

spring:
  redis:
    data:
      host: localhost
      port: 6379

3. Refresh Token 생성

RefreshToken.java

@Getter
@AllArgsConstructor
@RedisHash(value = "refreshToken", timeToLive = 60 * 60 * 24 * 7)
public class RefreshToken {

    @Id
    private String refreshToken;
    private Long memberId;
}

Redis에 저장할 Refresh Token을 정의합니다.

Refresh Token은 Redis에 저장하여 JPA 의존성이 필요하지 않기 때문에, @Id 어노테이션은 java.persistence.id가 아닌 opg.springframework.data.annotation.Id를 import 합니다.

Redis 데이터의 유효시간은, timetoLive 옵션으로 refresh Token과 같은 시간인 7일로 지정하였습니다.


4. Repository 생성

RefreshTokenRepository.java

@Repository
public interface RefreshTokenRepository extends CrudRepository<RefreshToken,String> {

}

Repository 또한 JPA 의존성이 필요하지 않기 때문에 JpaRepository가 아닌 CrudRepository를 상속 받습니다.


5. RedisConfig 설정

RedisConfig.java

@Configuration
public class RedisConfig {

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

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

6. AuthService 구현

AuthService.java

@Service
@RequiredArgsConstructor
public class AuthService {
    private final AuthenticationManagerBuilder authenticationManagerBuilder;
    private final MemberRepository memberRepository;
    private final PasswordEncoder passwordEncoder;
    private final TokenProvider tokenProvider;
    private final RefreshTokenRepository refreshTokenRepository;

    @Transactional
    public MemberResponseDTO signup(MemberRequestDTO memberRequestDTO) {
        if (memberRepository.existsByUsername(memberRequestDTO.getUsername())) {
            throw new CustomException(ErrorCode.DUPLICATE_USER_ID);
        }

        Member member = memberRequestDTO.toMember(passwordEncoder);
        return MemberResponseDTO.of(memberRepository.save(member));
    }

    @Transactional
    public TokenDTO login(MemberRequestDTO memberRequestDTO) {
        // 1. Login ID/PW 를 기반으로 AuthenticationToken 생성
        UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(memberRequestDto.getUsername(), memberRequestDto.getPassword());

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

        Member member = memberRepository.findByUsername(memberRequestDTO.getUsername())
                .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));

        // 3. 인증 정보를 기반으로 JWT 토큰 생성
        String accessToken = tokenProvider.generateAccessToken(authentication);
        String refreshToken = tokenProvider.generateRefreshToken(authentication);

        // 4. Redis에  RefreshToken 저장
        refreshTokenRepository.save(
                new RefreshToken(refreshToken, member.getId())
        );

        // 5. 토큰 발급
        return TokenDTO.builder()
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .memberId(member.getId())
                .build();
    }


    @Transactional
    public TokenDTO reissue(RequestTokenDTO requestTokenDTO) {
        // 1. Redis에 Refresh Token이 저장되어 있는지 확인
        RefreshToken foundTokenInfo = refreshTokenRepository.findById(requestTokenDTO.getRefreshToken())
                .orElseThrow(() -> new CustomException(ErrorCode.TOKEN_NOT_FOUND));
        
        Member member = memberRepository.findById(foundTokenInfo.getMemberId())
                .orElseThrow(() -> new CustomException(ErrorCode.MEMBER_NOT_FOUND));

        String refreshToken = foundTokenInfo.getRefreshToken();
        tokenProvider.validateToken(refreshToken);

        // 2. Refresh Token으로 부터 인증 정보를 꺼냄
        Authentication authentication = tokenProvider.getAuthentication(refreshToken);

        // 3. 새로운 Access Token 생성
        String accessToken = tokenProvider.generateAccessToken(authentication);

        // Token 재발급
        return TokenDTO.builder()
                .accessToken(accessToken)
                .refreshToken(refreshToken)
                .memberId(member.getId())
                .build();
    }

}

  • login()
    사용자의 로그인 요청을 처리하고, 입력된 ID와 비밀번호로 UsernamePasswordAuthenticationToken을 생성하여 Spring Security의 AuthenticationManager를 통해 실제로 검증합니다.
    authenticate 메서드가 실행이 될 때 CustomUserDetailsService에서 만들었던 loadUserByUsername 메서드가 실행되므로 검증이 성공하면 해당 인증 정보를 기반으로 JWT 토큰을 생성하고, Refresh Token을 생성하여 Redis에 저장합니다.

  • reissue()
    Access Token을 사용하여 Redis에 저장된 Refresh Token의 유효성 검사 후, Access Token을 재발급하는 메서드입니다.
    먼저 Redis에 Refresh Token이 저장되어 있는지 확인하고, Refresh Token으로 부터 인증 정보를 꺼내 새로운 Access Token을 발급하고 Redis에도 업데이트를 해줍니다.


7. 테스트

Redis 실행

설치  
$ docker pull redis  
  
실행
$ docker run --name redis -p 6379:6379 -d redis

Redis-cli 접속
$ docker exec -it redis redis-cli

Redis를 실행하기 위해 다음과 같은 Docker 명령어를 입력해 줍니다.

로그인을 통해 AccessToken, Refresh Token 발급

로그인을 진행하였을 때, Access Token과 Refresh Token이 정상적으로 발급된 것을 확인할 수 있습니다.

또한 Redis에도 Token이 저장된 것을 확인할 수 있습니다.

AccessToken 만료 시, RefresToken을 통해 재발급

Access Token이 만료되었을 경우, Refresh Token을 사용하여 재발급 또한 정상적으로 이루어지는 것을 확인할 수 있습니다.

profile
정신 🍒 !

0개의 댓글