<이전글>
<JWT 개념 및 설명>
<JWT 서비스 구현>
1. 리프레시 토큰 서비스 구현
1-1. 리프레시 토큰 도메인 구현하기
리프레시 토큰은 데이터베이스에 저장하는 정보이므로 엔티티와 리포지터리를 추가해야 한다.
만들 엔티티와 매핑되는 테이블 구조는 다음과 같다
컬럼명 | 자료형 | null 허용 | 키 | 설명 |
id | BIGINT | N | 기본키 | 일렬번호, 기본키 |
user_id | BIGINT | N | 유저 ID | |
refresh_token | VARCHAR(255) | N | 토큰값 |
◆ domain/RefreshToken.java
@NoArgsConstructor(access = AccessLevel.PROTECTED)
@Getter
@Entity
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id", updatable = false)
private Long id;
@Column(name = "user_id", nullable = false, unique = true)
private Long userId;
@Column(name = "refresh_token", nullable = false)
private String refreshToken;
public RefreshToken(Long userId, String refreshToken){
this.userId = userId;
this.refreshToken = refreshToken;
}
public RefreshToken update(String newRefreshToken){
this.refreshToken = newRefreshToken;
return this;
}
}
◆ repository/RefreshTokenRepository.java
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByUserId(Long userId);
Optional<RefreshToken> findByRefreshToken(String refreshToken);
}
1-2. 토큰 필터 구현하기
필터는 실제로 각종 요청을 처리하기 위한 로직으로 전달되기 전후에 URL 패턴에 맞는 모든 요청을 처리하는 기능을 제공한다.
요청이 오면 헤더값을 비교해서 토큰이 있는지 확인하고,
유효 토큰이라면 시큐리티 콘텍스트 홀더 (security context holder)에 인증 정보를 저장한다.
시큐리티 컨텍스트: 인증 객체가 저장되는 보관소이다.
여기서 인증 정보가 필요할 때 언제든지 인증 객체를 꺼내 사용할 수 있다.
이 클래스는 스레드마다 공간을 할당하는 즉, 스레드 로컬에 저장되므로 코드의 아무 곳에서나 참조할 수 있고,
다른 스레드와 공유하지 않으므로 독립적으로 사용할 수 있다.
그리고 이러한 시큐리티 컨텍스트 객체를 저장하는 객체가 시큐리티 컨텍스트 홀더이다.
◆ config/TokenAuthenticationFilter.java
이 필터는 액세스 토큰 값이 담긴 Authorization 헤더값을 가져온 뒤 액세스 토큰이 유효하다면 인증 정보를 설정한다.
@RequiredArgsConstructor
public class TokenAuthenticationFilter extends OncePerRequestFilter {
private final TokenProvider tokenProvider;
private final static String HEADER_AUTHORIZATION = "Authorization";
private final static String TOKEN_PREFIX = "Bearer ";
@Override
protected void doFilterInternal(
HttpServletRequest request,
HttpServletResponse response,
FilterChain filterChain) throws ServletException, IOException{
// 요청 헤더의 Authorization 키의 값 조회
String authorizationHeader = request.getHeader(HEADER_AUTHORIZATION);
// 가져온 값에서 접두사 제거
String token = getAccessToken(authorizationHeader);
// 가져온 토큰이 유효한지 확인하고, 유효한 때는 인증 정보 설정
if (tokenProvider.validToken(token)){
Authentication authentication = tokenProvider.getAuthentication(token);
SecurityContextHolder.getContext().setAuthentication(authentication);
}
filterChain.doFilter(request, response);
}
private String getAccessToken(String authorizationHeader){
if(authorizationHeader != null && authorizationHeader.startsWith(TOKEN_PREFIX)){
return authorizationHeader.substring(TOKEN_PREFIX.length());
}
return null;
}
}
요청 헤더에서 키가 'Authorization'인 필드의 값을 가져온 다음 토큰의 접두사 Bearer를 제외한 값을 얻는다.
만약 값이 null이거나 Bearer로 시작하지 않으면 null을 반환한다.
이어서 가져온 토큰이 유효한지 확인하고, 유효하다면 인증 정보를 관리하는 시큐리티 컨텍스트에 인증 정보를 설정한다.
위에서 작성한 코드가 실행되며 인증 정보가 설정된 이후에 컨텍스트 홀더에서 getAuthentication() 메서드를 사용해 인증 정보를 가져오면 유저 객체가 반환된다.
유저 객체에는 유저 이름(username)과 권한 목록(authorities)과 같은 인증 정보가 포함된다.
2. 리프레시 토큰 API 구현하기
2-1. 토큰 서비스 추가하기
◆ UserService.java
@RequiredArgsConstructor
@Service
public class UserService {
--- 생략 ---
// 메서드 추가
public User findById(Long userId){
return userRepository.findById(userId)
.orElseThrow(() -> new IllegalArgumentException("Unexpected user"));
}
}
UserService.java에 전달받은 유저 ID로 유저를 검색해서 전달하는 findById() 메서드를 추가로 구현한다.
◆ 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"));
}
}
전달받은 리프레시 토큰으로 리프레시 토큰 객체를 검색해서 전달하는 findByRefreshToken() 메서드 구현
◆ 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));
}
}
토큰 서비스 클래스
- createNewAccessToken()
전달받은 리프레시 토큰으로 토큰 유효성 검사를 진행하고,
유효한 토큰인 때 리프레시 토큰으로 사용자 ID를 찾는다. - generateToken()
사용자 ID로 사용자를 찾은 후에 새로운 액세스 토큰을 생성함
2-2. 컨트롤러 추가하기
실제로 토큰을 발급받는 API를 생성
◆ dto/CreateAccessTokenRequest.java
@Getter
@Setter
public class CreateAccessTokenRequest {
private String refreshToken;
}
◆ dto/CreateAccessTokenResponse.java
@AllArgsConstructor
@Getter
public class CreateAccessTokenResponse {
private String accessToken;
}
토큰 생성 요청 및 응답을 담당할 DTO
- CreateAccessTokenRequest
- CreateAccessTokenResponse
◆ 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));
}
}
/api/token POST 요청이 오면 토큰 서비스에서 리프레시 토큰을 기반으로 새로운 액세스 토큰을 만들어준다.
2-3. 테스트 코드 작성
◆ test/.../controller/TokenApiControllerTest.java
@SpringBootTest
@AutoConfigureMockMvc
public 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@email.com")
.password("test")
.build());
String refreshToken = JwtFactory.builder()
.claims(Map.of("id", testUser.getId()))
.build()
.createToken(jwtProperties);
refreshTokenRepository.save(new RefreshToken(testUser.getId(), refreshToken));
CreateAccessTokenRequest request = new CreateAccessTokenRequest();
request.setRefreshToken(refreshToken);
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의 요청 본문에 리프레시 토큰을 포함하여 요청 객체를 생성한다. |
When | 토큰 추가 API에 요청을 보낸다. 이때 요청 타입은 JSON이며, given절에서 미리 만들어둔 객체를 요청 본문으로 함께 보낸다. |
Then | 응답 코드가 201 Created인지 확인하고 응답으로 온 액세스 토큰이 비어 있지 않은지 확인한다. |
출처:https://github.com/shinsunyoung/springboot-developer
'JAVA > SpringBoot 3' 카테고리의 다른 글
[스프링 부트 3] OAuth2 - 2 (스프링 시큐리티로 OAuth2 서비스 구현, 로직 세팅) (2) | 2024.08.27 |
---|---|
[스프링 부트 3] OAuth2 - 1 (OAuth란?, 권한 부여 코드 타입 승인의 흐름, 쿠키란?, 토큰 발급받기) (0) | 2024.08.23 |
[스프링 부트 3] JWT - 2 (JWT 서비스, 토큰 생성자 구현, 로그인/로그아웃) (0) | 2024.08.22 |
[스프링 부트 3] JWT - 1 (토큰 기반 인증, JWT, 액세스 토큰, 리프레시 토큰) (0) | 2024.08.18 |
[스프링 부트 3] 스프링 시큐리티 - 2 (로그인/로그아웃, 회원가입 구현하기) (0) | 2024.08.16 |