Trong bài viết trước, chúng ta đã cùng tìm hiểu về cách xây dựng ứng dụng Spring security với JWT để xác thực và phân quyền. Và như mọi người đã biết thì Access Token sẽ hết hạn sau một khoảng thời gian, vậy làm sao để tạo lại token mới thì trong bài viết này chúng ta sẽ tiếp tục tìm hiểu về JWT Refresh Token.
Mọi người có thể tìm đọc các bài viết liên quan tại đây!
Spring Boot Refresh Token Flow
Dựa trên source code phát triển từ bài viết trước Spring Boot Security - JWT Authentication chúng ta thêm thông tin refreshToken
trả về khi login thành công.
Thông thường chúng ta thường cấu hình thời gian hết hạn của Refresh Token sẽ dài hơn của Access Token.
Khi truy cập Server với Access Token đã hết hạn, Client sẽ được yêu cầu lấy lại Access Token mới dựa trên thông tin Refresh Token được cấp trước đó.
Refresh Token Request and Response
Requests
- TokenRefreshRequest: { refreshToken }
Responses
- JwtResponse: { accessToken, type, refreshToken, id, username, email, roles }
- MessageResponse: { message }
- TokenRefreshResponse: { accessToken, type, refreshToken }
TokenRefreshRequest
package tiendv.example.payload.request;
// imports
public class TokenRefreshRequest {
@NotBlank
private String refreshToken;
public String getRefreshToken() {
return refreshToken;
}
public void setRefreshToken(String refreshToken) {
this.refreshToken = refreshToken;
}
}
JwtResponse
// imports
import java.util.List;
public class JwtResponse {
private String token;
private String type = "Bearer";
private String refreshToken;
private Long id;
private String username;
private String email;
private List<String> roles;
public JwtResponse(String accessToken, String refreshToken, Long id, String username, String email, List<String> roles) {
this.token = accessToken;
this.refreshToken = refreshToken;
this.id = id;
this.username = username;
this.email = email;
this.roles = roles;
}
public String getToken() {
return token;
}
// getter/setter
}
TokenRefreshResponse
package tiendv.example.payload.response;
public class TokenRefreshResponse {
private String accessToken;
private String refreshToken;
private String tokenType = "Bearer";
public TokenRefreshResponse(String accessToken, String refreshToken) {
this.accessToken = accessToken;
this.refreshToken = refreshToken;
}
// getter/setter
}
Renew
Trong AuthController
:
- update trong api
/signin
thêm thông tin Refresh Token - thêm api
refreshToken
tạo mới Access Token từ Refresh Token
AuthController
package tiendv.example.controller;
// imports
@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
@RequestMapping("/api/auth")
public class AuthController {
...
@PostMapping("/signin")
public ResponseEntity<?> login(@Valid @RequestBody LoginRequest loginRequest) {
Authentication authentication = authenticationManager.authenticate(
new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword()));
SecurityContextHolder.getContext().setAuthentication(authentication);
UserDetailsImpl userDetails = (UserDetailsImpl) authentication.getPrincipal();
String jwt = jwtUtils.generateJwtToken(userDetails);
List<String> roles = userDetails.getAuthorities().stream()
.map(item -> item.getAuthority())
.collect(Collectors.toList());
RefreshToken refreshToken = refreshTokenService.createRefreshToken(userDetails.getId());
return ResponseEntity.ok(new JwtResponse(
jwt,
refreshToken.getToken(),
userDetails.getId(),
userDetails.getUsername(),
userDetails.getEmail(),
roles));
}
@PostMapping("/refreshtoken")
public ResponseEntity<?> refreshtoken(@Valid @RequestBody TokenRefreshRequest request) {
String requestRefreshToken = request.getRefreshToken();
return refreshTokenService.findByToken(requestRefreshToken)
.map(refreshTokenService::verifyExpiration)
.map(RefreshToken::getUser)
.map(user -> {
String token = jwtUtils.generateTokenFromUsername(user.getUsername());
return ResponseEntity.ok(new TokenRefreshResponse(token, requestRefreshToken));
})
.orElseThrow(() -> new TokenRefreshException(requestRefreshToken,
"Refresh token is not in database!"));
}
}
Trong phương thức refreshtoken
:
- Đầu tiên, lấy Refresh Token từ HTTP Request
- Tiếp theo lấy
RefreshToken(id, User, token, expiryDate)
trong DB để thực hiện validate, verify. - Tiếp tục lấy thông tin
User
từRefreshToken
để tạo mới Access Token và trả vềTokenRefreshResponse
- Nếu có lỗi xảy ra throw
TokenRefreshException
RefreshToken Service
RefreshToken
Class này quan hệ 1-1 với User
.
package tiendv.example.model;
// imports
@Entity(name = "refreshtoken")
public class RefreshToken {
@Id
@GeneratedValue(strategy = GenerationType.AUTO)
private long id;
@OneToOne
@JoinColumn(name = "user_id", referencedColumnName = "id")
private User user;
@Column(nullable = false, unique = true)
private String token;
@Column(nullable = false)
private Instant expiryDate;
public RefreshToken() {
}
// getter/setter
}
RefreshTokenRepository
package tiendv.example.repository;
// imports
@Repository
public interface RefreshTokenRepository extends JpaRepository<RefreshToken, Long> {
Optional<RefreshToken> findByToken(String token);
@Modifying
int deleteByUser(User user);
}
RefreshTokenService
package tiendv.example.security.service;
// imports
@Service
public class RefreshTokenService {
@Value("${jwt.app.jwtRefreshExpirationMs}")
private Long refreshTokenDurationMs;
@Autowired
private RefreshTokenRepository refreshTokenRepository;
@Autowired
private UserRepository userRepository;
public Optional<RefreshToken> findByToken(String token) {
return refreshTokenRepository.findByToken(token);
}
public RefreshToken createRefreshToken(Long userId) {
RefreshToken refreshToken = new RefreshToken();
refreshToken.setUser(userRepository.findById(userId).get());
refreshToken.setExpiryDate(Instant.now().plusMillis(refreshTokenDurationMs));
refreshToken.setToken(UUID.randomUUID().toString());
refreshToken = refreshTokenRepository.save(refreshToken);
return refreshToken;
}
public RefreshToken verifyExpiration(RefreshToken token) {
if (token.getExpiryDate().compareTo(Instant.now()) < 0) {
refreshTokenRepository.delete(token);
throw new TokenRefreshException(token.getToken(), "Refresh token was expired. Please make a new signin request");
}
return token;
}
@Transactional
public int deleteByUserId(Long userId) {
return refreshTokenRepository.deleteByUser(userRepository.findById(userId).get());
}
}
Handle Exception
Tạo class TokenRefreshException
kế thừa class RuntimeException
.
TokenRefreshException
package tiendv.example.exception;
// imports
@ResponseStatus(HttpStatus.FORBIDDEN)
public class TokenRefreshException extends RuntimeException {
private static final long serialVersionUID = 1L;
public TokenRefreshException(String token, String message) {
super(String.format("Failed for [%s]: %s", token, message));
}
}
Cuối cùng là tạo class TokenControllerAdvice
gắn annotation RestControllerAdvice
để hande exception trong ứng dụng:
TokenRefreshException
package tiendv.example.advice;
// imports
@RestControllerAdvice
public class TokenControllerAdvice {
@ExceptionHandler(value = TokenRefreshException.class)
@ResponseStatus(HttpStatus.FORBIDDEN)
public ErrorMessage handleTokenRefreshException(TokenRefreshException ex, WebRequest request) {
return new ErrorMessage(
HttpStatus.FORBIDDEN.value(),
new Date(),
ex.getMessage(),
request.getDescription(false));
}
}
Run & Test
Signin (trả về thêm thông tin Refresh Token)
Khi Access Token hết hạn
Lấy lại Access Token từ Refresh Token
Khi Refresh Token hết hạn, yêu cầu login (signin) lại.
Tổng kết
Trên đây là hướng dẫn về cấu hình Spring security (Refresh Token) để xác thực và phân quyền người dùng. Hy vọng mọi người sẽ hiểu được ý tưởng tổng thể của bài viết và áp dụng nó vào dự án của mọi các bạn một cách thoải mái.
Nguồn: https://thenewstack.wordpress.com/2021/11/24/spring-security-spring-boot-security-refresh-token/
Follow me: thenewstack.wordpress.com