9.3 토큰 API 구현

SummerToday·2024년 3월 8일
1
post-thumbnail
post-custom-banner

토큰 서비스 추가

리프레시 토큰을 전달 받아 토큰 제공자를 사용해 새로운 엑세스 토큰을 만드는 토큰 서비스 클래스를 생성한다.

  • 전달받은 유저 ID로 유저를 검색해서 전달하는 findById() 메서드 추가

    // service - UserService.java
    
    @RequiredArgsConstructor
    @Service
    public class UserService {
       
         ~ 생략 ~
    
       public User findById(Long userId) {
           return userRepository.findById(userId)
                   .orElseThrow(()->new IllegalArgumentException("Unexpected user"));
       }
    
         ~ 생략 ~
         
    }

  • 전달 받은 리프레시 토큰으로 리프레시 토큰 객체를 검색해서 전달하는 findByRefreshToken() 메서드 추가

    // service - RefreshTokenService.java
    
    @RequiredArgsConstructor
    @Service
    public class RefreshTokenService {
       private final RefreshTokenRepository refreshTokenRepository;
    
       public RefreshToken findByRefreshToken(String refreshToken) {
           return refreshTokenRepository.findByRefreshToken(refreshToken)
                   .orElseThrow(() -> new IllegalArgumentException("Unexpected token"));
       }
    }
    

  • 전달 받은 리프레시 토큰으로 토큰 유효성 검사를 진행하고, 유효한 토큰일 때 리프레시 토큰으로 사용자 ID를 찾은 후 해당 ID로 사용자를 찾은 후 TokenProvider의 generateToken()메서드를 호출해서 새로운 엑세스 토큰을 생성한다.

    // service - TokenService.java
    
    @RequiredArgsConstructor
    @Service
    public class TokenService {
    
      private final TokenProvider tokenProvider;
      private final RefreshTokenService refreshTokenService;
      private final UserService userService;
    
      public String createNewAccessToken(String refreshToken) {
          // 토큰 유효성 검사에 실패하면 예외 발생
          if(!tokenProvider.validToken(refreshToken)) {
              throw new IllegalArgumentException("Unexpected token");
          }
    
          Long userId = refreshTokenService.findByRefreshToken(refreshToken).getUserId();
          User user = userService.findById(userId);
    
          return tokenProvider.generateToken(user, Duration.ofHours(2));
      }
    }  

컨트롤러 추가

  • 토큰 생성 요청 및 응답을 담당할 DTO 클래스들을 작성한다.

    // dto - CreateAccessTokenRequest.java
    
    @Getter
    @Setter
    public class CreateAccessTokenRequest {
       private String refreshToken;
    }
    
    // dto -  CreateAccessTokenResponse.java
    
    @AllArgsConstructor
    @Getter
    public class CreateAccessTokenResponse {
       private String accessToken;
    }

  • /api/token POST 요청이 오면 토큰 서비스에서 리프레시 토큰을 기반으로 새로운 엑세스 토큰을 만들어주는 컨트롤러를 생성한다.

    // controller - TokenApiController.java
    
    @RequiredArgsConstructor
    @RestController
    public class TokenApiController {
    
       private final TokenService tokenService;
    
       @PostMapping("/api/token")
       public ResponseEntity<CreateAccessTokenResponse> createNewAccessToken(@RequestBody CreateAccessTokenRequest request) {
           String newAccessToken = tokenService.createNewAccessToken(request.getRefreshToken());
    
           return ResponseEntity.status(HttpStatus.CREATED)
                   .body(new CreateAccessTokenResponse(newAccessToken));
       }
    }
    • .body(new CreateAccessTokenResponse(newAccessToken))
      ResponseEntity의 본문(body)을 설정한다. 이 경우 CreateAccessTokenResponse 객체를 생성하여 이 객체를 본문으로 설정합니다. 생성된 access token을 CreateAccessTokenResponse 객체에 포함시킨다. 이렇게 함으로써 클라이언트가 새로운 access token을 수신할 수 있게 된다.


테스트 코드 작성

// test - controller - TokenApiController.java 

@SpringBootTest
@AutoConfigureMockMvc
class TokenApiControllerTest {

    @Autowired
    protected MockMvc mockMvc;

    @Autowired
    protected ObjectMapper objectMapper;

    @Autowired
    private WebApplicationContext context;

    @Autowired
    JwtProperties jwtProperties;

    @Autowired
    UserRepository userRepository;

    @Autowired
    RefreshTokenRepository refreshTokenRepository;

    @BeforeEach
    public void mockMvcSetUp() {
        this.mockMvc = MockMvcBuilders.webAppContextSetup(context)
                .build();
        userRepository.deleteAll();
    }

    @DisplayName("createNewAccessToken: 새로운 액세스 토큰을 발급한다.")
    @Test
    public void createNewAccessToken() throws Exception {
        // given
        final String url = "/api/token";

        User testUser = userRepository.save(User.builder()
                .email("user@gmail.com")
                .password("test")
                .build());

        String refreshToekn = JwtFactory.builder()
                .claims(Map.of("id", testUser.getId()))
                .build()
                .createToken(jwtProperties);

        refreshTokenRepository.save(new RefreshToken(testUser.getId(), refreshToekn));

        CreateAccessTokenRequest request = new CreateAccessTokenRequest();
        request.setRefreshToken(refreshToekn);
        final String requestBody = objectMapper.writeValueAsString(request);

        // when
        ResultActions resultActions = mockMvc.perform(post(url)
                .contentType(MediaType.APPLICATION_JSON_VALUE)
                .content(requestBody));

        // then
        resultActions
                .andExpect(status().isCreated())
                .andExpect(jsonPath("$.accessToken").isNotEmpty());
    }

}
  • Given
    테스트 유저를 생성하고, jjwt 라이브러리를 이요해 리프레시 토큰을 만들어 데이터베이스에 저장한다.
    토큰 생성 API의 요청 본문에 리프레시 토큰을 포함하여 요청 객체를 생성한다.

    • ObjectMapper
      Jackson 라이브러리에서 제공하는 클래스로, Java 객체와 JSON 데이터 간의 변환을 담당한다.

    • writeValueAsString()
      ObjectMapper의 메서드 중 하나로, 주어진 객체를 JSON 문자열로 변환한다. 이 메서드는 CreateAccessTokenRequest 객체(request)를 JSON 형식의 문자열로 변환하여 반환한다. 이렇게 변환된 JSON 문자열은 HTTP 요청의 본문(body)에 포함되어 서버에 전송된다.

  • When
    토큰 추가 API에 요청을 보낸다.이때 요청 타입은 JSON이며, given절에서 미리 만들어둔 객체를 요청 본문으로 함께 보낸다.


  • Then
    응답 코드가 201 Created인지 확인하고 응답으로 온 엑세스 토큰이 비어있지 않은지 확인한다.

    • andExpect(jsonPath("$.accessToken").isNotEmpty())
      이 부분은 JSON 응답의 accessToken 필드가 비어 있지 않음을 검증한다. 이는 새로운 액세스 토큰이 유효하게 생성되어 있음을 확인한다.
      만약 accessToken이 비어 있다면, 토큰 생성에 실패했거나, 반환된 데이터에 문제가 있음을 의미한다.



해당 글은 다음 도서의 내용을 정리하고 참고한 글임을 밝힙니다.
신선영, ⌜스프링 부트 3 벡엔드 개발자 되기 - 자바 편⌟, 골든래빗(주), 2023, 384쪽
profile
IT, 개발 관련 정보들을 기록하는 장소입니다.
post-custom-banner

0개의 댓글