[Spring Security, JWT, Redis] Refresh Token 활용 Token 재발급 (2) - DB 활용 (RDBMS)

3Beom's 개발 블로그·2023년 8월 24일
1

프로젝트에서 Spring Security + JWT + Redis 방식으로 인증/인가 로직을 구현하였고, 전체 과정을 기록으로 남겨두려 한다.


이전 포스트에서 Refresh Token을 활용한 Token 재발급 로직을 구현하는 두가지 방법을 정리했었다.

👉 [Spring Security, JWT, Redis] Refresh Token 활용 Token 재발급 (1) - DB와 Redis

본 포스트에서는 그 두가지 방법 중 DB를 활용하는 방법에 대해 다뤄보려 한다. 활용된 기술 스택은 다음과 같다.

  • Spring Boot
  • JPA
  • MariaDB

DB를 활용한 Refresh Token 관리

DB를 활용하여 Refresh Token을 관리할 경우, 구현해야 하는 내용은 다음과 같다.

  • DB의 사용자 테이블에 Refresh Token 컬럼 추가
  • 로그인, 회원가입 시 Refresh Token DB에 저장
  • 토큰 재발급 api 구현 (Controller, Service, Repository)

Refresh Token 컬럼 추가해서 저장해두고 Request Header에 담겨오는 Refresh Token과 비교해서 일치하면 재발급 과정을 진행하면 된다.

사용자 테이블에 Refresh Token 추가

이번에 진행한 프로젝트에서는 Users 테이블에 사용자 정보를 저장해 두었다. 다음과 같이 Users 테이블과 매핑된 User Entity에 Refresh Token 필드를 추가하고, 이를 수정할 수 있는 Setter 메서드를 추가한다.

(Users 테이블의 pk 외 다른 필드 관련 코드들은 모두 생략했다)

@Entity
@Table(name = "USERS")
@Getter
@NoArgsConstructor(access = AccessLevel.PROTECTED)
public class User extends BaseEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long userId;

...

    private String refreshToken;
    
...
    
    @Builder
    public User(Long userId, ..., String refreshToken) {
        this.userId = userId;
        
        ...
        
        this.refreshToken = refreshToken;
    }
    
...

    public void setRefreshToken(String refreshToken) {
        this.refreshToken = refreshToken;
    }

...

}

이제 로그인, 회원가입 api에 Refresh Token을 저장하는 로직을 추가한다.

로그인, 회원가입에 Refresh Token 저장 로직 추가

사실 이전 포스트에서 로그인, 회원가입 api를 만들었었고, 두 api 모두 Service 계층에서 Refresh Token을 DB에 저장하고 있었다.

[로그인]

// UserServiceImpl

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserServiceImpl implements UserService {

...

    @Override
    @Transactional
    public AuthenticatedResponseDto login(LoginRequestDto loginRequestDto) {
        TokenInfo tokenInfo = setFirstAuthentication(loginRequestDto.getId(),
            loginRequestDto.getPassword());
        log.debug("login token : {}", tokenInfo);

        User user = userRepository.findById(loginRequestDto.getId()).get();
        user.setRefreshToken(tokenInfo.getRefreshToken());

        return AuthenticatedResponseDto.builder()
            .tokenInfo(tokenInfo)
            .user(user)
            .build();
    }

...

}

[회원가입]

// UserServiceImpl

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserServiceImpl implements UserService {

...

    @Override
    @Transactional
    public AuthenticatedResponseDto signUp(SignUpRequestDto signUpRequestDto) {
        validateSignUpUserInfo(signUpRequestDto);

        User user = signUpRequestDto.toUser(
            passwordEncoder.encode(signUpRequestDto.getPassword()),
            userRepository.findOneCountry(signUpRequestDto.getCountryId()));

		...

        userRepository.insert(user);

        log.debug("user : {}", user);

        TokenInfo tokenInfo = setFirstAuthentication(signUpRequestDto.getId(),
            signUpRequestDto.getPassword());
        log.debug("token : {}", tokenInfo);

        user.setRefreshToken(tokenInfo.getRefreshToken());

        return AuthenticatedResponseDto.builder()
            .tokenInfo(tokenInfo)
            .user(user)
            .build();
    }

...
}

(본 프로젝트에서 Repository 계층을 JpaRepository 를 implements 하지 않고 EntityManager로 직접 다 구현하여 저장 메서드 이름이 다르다. 위 코드에서는 insert()라고 되어있지만 save() 라고 생각하면 된다.)

자세한 내용은 아래 글을 확인하자.

👉 [Spring Security, JWT, Redis] 로그인, 회원가입 구현 (UserDetails, UserDetailsService)

Token 재발급 api 구현

Frontend 측에서 Access Token이 만료되었다는 응답을 받게 되면 Refresh Token을 보내 재발급 요청을 보낼 것이다. 이 때 해당 api로 요청하면 된다.

[Dto]

// UserRequestDto
public class UserRequestDto {

...

    @Data
    @Builder
    @NoArgsConstructor
    @AllArgsConstructor
    public static class ReissueTokensRequestDto {

        private String id;
    }
    
...

Response Dto는 이전에 만들어뒀던 TokenInfo를 그대로 활용한다.

// TokenInfo

@Builder
@Data
@AllArgsConstructor
public class TokenInfo {

    private String grantType;
    private String accessToken;
    private String refreshToken;

}

[Service]

// UserServiceImpl

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional(readOnly = true)
public class UserServiceImpl implements UserService {

...

    @Override
    @Transactional
    public TokenInfo reissueTokens(String refreshToken,
        ReissueTokensRequestDto reissueTokensRequestDto) {
        User user = userRepository.findById(reissueTokensRequestDto.getId())
            .orElseThrow(() -> new UserNotFoundException(USER_NOT_FOUND.getMessage()));

        if (jwtTokenProvider.validateToken(refreshToken) &&
            refreshToken.equals(user.getRefreshToken())) {
            TokenInfo tokenInfo = reissueTokensFromUser(user);
            user.setRefreshToken(tokenInfo.getRefreshToken());
            return tokenInfo;
        }

        throw new NotMatchedTokenException(NOT_MATCHED_TOKEN.getMessage());
    }
    
    private TokenInfo reissueTokensFromUser(User user) {
        CustomUserDetails customUserDetails = new CustomUserDetails(user);

        return jwtTokenProvider.generateToken(customUserDetails.getAuthorities(),
            customUserDetails.getUsername());
    }
 
...

}

위 코드의 순서는 다음과 같다.

  1. User 정보를 조회한다.
  2. Request Header로 전달된 Refresh Token의 유효성 검사를 진행한다.
  3. 유효할 경우 해당 Refresh Token과 User의 Refresh Token을 비교한다.
  4. Refresh Token이 일치할 경우 Token을 새로 발급하고 DB에도 새로 발급된 Refresh Token을 저장한다.

reissueTokensFromUser() 메서드의 경우, CustomeUserDetails 객체를 만들어 활용했는데, 사실 Token을 생성할 때 Payload에 사용자의 권한 정보와 아이디 정보만 포함되면 되기 때문에 굳이 이렇게 객체를 생성하지 않아도 된다.
(구현하기 나름!)

[Controller]

// UserApiController

@Slf4j
@RestController
@RequestMapping("/api/user")
@RequiredArgsConstructor
public class UserApiController {

...

    @PostMapping("/auth")
    public ResponseEntity<ResponseDto<?>> reissueTokens(
        @RequestHeader("refreshToken") String refreshToken,
        @RequestBody ReissueTokensRequestDto reissueTokensRequestDto
    ) {
        return ResponseEntity.status(HttpStatus.CREATED).body(
            ResponseDto.create(
                REISSUE_TOKENS_SUCCESS.getMessage(),
                userService.reissueTokens(refreshToken, reissueTokensRequestDto)
            )
        );
    }

...

}

예시에서 보다 명확하게 표현하기 위해 Request Header에 Refresh Token을 담는 필드 이름을 'refreshToken' 이라고 지었다.

이렇게 DB를 활용하여 Token을 재발급 받는 api를 구현할 수 있다.

profile
경험과 기록으로 성장하기

0개의 댓글