Photo by JuniperPhoton on Unsplash

Refresh Token Rotation với Spring Boot và WebFlux - (part 2)



Nếu bạn chưa đọc phần 1 của bài viết thì bạn có thể đọc lại ở đây. Trong phần 2 này, mình sẽ trình bày cách để mình implement Refresh Token Rotation với Spring Boot.

Chuẩn bị

  • Java (8+)
  • Neo4j (Trong bài này mình dùng Neo4j làm DB chính, bạn cũng có thể chọn một DB khác gần gũi hơn để thực hiện)

Sơ chế

Trong phần này bạn sẽ cần tạo sẵn những thành phần cần có cho Spring Boot gồm controller, service, model, repository.

Controller

Trong Controller ta sẽ cần chuẩn bị sẳn 2 router để cho User Sign In và Sign Up ngoài ra cần thêm 1 API để FE gọi khi cần dùng Refresh Token

@Slf4j
@RestController
@RequestMapping("/api")
public class AuthServiceController {

@Autowired
private AuthService authService;

@PostMapping("/v1.0/auth/sign-in")
@ResponseStatus(code = HttpStatus.OK)
public Mono<ResponseEntity<ResponseModel>> signIn(@Valid @RequestBody SignInRequestDto request) {
    try {
        return authService.signIn(request)
            .map(response -> {
                return ResponseEntity.ok(
                    new ResponseModel(200, ResponseMessage.SUCCESSFUL, response)
                );
            })
            .switchIfEmpty(Mono.just(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()));
    } catch (Exception error) {
        log.error("Can not sign in", error);
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Can not auth user", error);
    }
}

@PostMapping("/v1.0/auth/sign-up")
@ResponseStatus(code = HttpStatus.OK)
public Mono<ResponseEntity<ResponseModel>> signUp(@Valid @RequestBody SignUpRequestDto request) {
    try {
        authService.signUp(request);

        return Mono.just(ResponseEntity.ok(
            new ResponseModel(200, ResponseMessage.SUCCESSFUL, null)
        ));
    } catch (Exception error) {
        log.error("Can not sign in", error);
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Can not auth user", error);
    }
}

@PostMapping("/v1.0/auth/refresh-token")
@ResponseStatus(code = HttpStatus.OK)
public Mono<ResponseEntity<ResponseModel>> refreshToken(@RequestBody HashMap<String, Object> request) {
    try {
            return authService.createRefreshToken(UUID.fromString((String) request.get("refreshToken")))
                .map(response -> {
                    return ResponseEntity.ok(new ResponseModel(200, ResponseMessage.SUCCESSFUL, response));
                })
            .switchIfEmpty(Mono.just(ResponseEntity.status(HttpStatus.UNAUTHORIZED).build()));
    } catch (Exception error) {
        log.error("Can not sign in", error);
        throw new ResponseStatusException(HttpStatus.NOT_FOUND, "Can not auth user", error);
    }
}

Model

@Node
@Data
public class UserModel {

    @Id
    @GeneratedValue
    private Long id;

    private String username;
    private String email;
    private String password;
    private String avatar;
    private String profile;
}

Repository

public interface AuthRepository extends Neo4jRepository<UserModel, Long> {

    UserModel findByEmail(String mail);

    UserModel findByUsername(String username);
}

Service

public interface AuthService {

    void signUp(SignUpRequestDto request) throws Exception;

    Mono<HashMap<String, Object>> signIn(SignInRequestDto request) throws Exception;

    Mono<HashMap<String, Object>> createRefreshToken(UUID uuid) throws Exception;
}

Implementation

@Service
public class AuthServiceImpl implements AuthService {

    @Autowired
    private AuthRepository authRepository;

    @Autowired
    private RefreshTokenRepository refreshTokenRepository;

    @Autowired
    private BCryptPasswordEncoder passwordEncoder;

    @Autowired
    private JwtUtil jwtUtil;

    @Override
    public void signUp(SignUpRequestDto request) throws Exception {
        String username = request.getUsername();
        UserModel existUserName = authRepository.findByUsername(username);
        if (existUserName != null) {
            throw new Exception("Username exist");
        }

        String email = request.getEmail();
        UserModel existUser = authRepository.findByEmail(email);
        if (existUser != null) {
            throw new Exception("User exist");
        }

        UserModel userModel = new UserModel();
        userModel.setUsername(username);
        userModel.setEmail(email);
        userModel.setAvatar("");
        userModel.setProfile("");

        String password = request.getPassword();
        String hashPassword = passwordEncoder.encode(password);
        userModel.setPassword(hashPassword);

        authRepository.save(userModel);
    }

    @Override
    public Mono<HashMap<String, Object>> signIn(SignInRequestDto request) throws Exception {
        String email = request.getEmail();
        UserModel userModel = authRepository.findByEmail(email);
        if (userModel == null) {
            throw new Exception("Wrong username or password");
        }

        if (!passwordEncoder.matches(request.getPassword(), userModel.getPassword())) {
            throw new Exception("Wrong username or password");
        }

        User user = new User();
        user.setMail(email);
        user.setPassword(userModel.getPassword());

        UUID refreshTokenUuid = UUID.randomUUID();

        RefreshTokenModel refreshTokenModel = new RefreshTokenModel();
        refreshTokenModel.setEmail(email);
        refreshTokenModel.setToken(refreshTokenUuid);
        refreshTokenModel.setExpiryDate(Instant.now().plusSeconds(3600));

        refreshTokenRepository.save(refreshTokenModel);

        HashMap<String, Object> response = new HashMap<>();
        response.put("accessToken", jwtUtil.generateToken(user));
        response.put("refreshToken", refreshTokenUuid);

        return Mono.just(response);
    }

    private Boolean verifyExpiration(RefreshTokenModel refreshToken) {
        if (refreshToken.getExpiryDate().compareTo(Instant.now()) < 0) {
            return false;
        }

        return true;
    }

    @Override
    public Mono<HashMap<String, Object>> createRefreshToken(UUID refreshToken) throws Exception {
        if (refreshToken == null) {
            throw new Exception("Empty refresh token");
        }

        RefreshTokenModel refreshTokenModel = refreshTokenRepository.findByToken(refreshToken);
        if (refreshTokenModel == null) {
            refreshTokenRepository.deleteByOldToken(refreshToken.toString());
            throw new Exception("Refresh token not found");
        }

        if (!verifyExpiration(refreshTokenModel)) {
            refreshTokenRepository.deleteByToken(refreshToken.toString());
            throw new Exception("Refresh token is expired");
        }

        String email = refreshTokenModel.getEmail();
        if (email == null) {
            throw new Exception("User not found");
        }

        User user = new User();
        user.setMail(email);

        List<UUID> oldRefreshToken = refreshTokenModel.getOldToken();
        if (oldRefreshToken == null) {
            oldRefreshToken = new ArrayList<>();
        }

        oldRefreshToken.add(refreshToken);

        UUID newRefreshToken = UUID.randomUUID();
        refreshTokenModel.setToken(newRefreshToken);
        refreshTokenModel.setExpiryDate(Instant.now().plusSeconds(3600));
        refreshTokenModel.setOldToken(oldRefreshToken);
        refreshTokenRepository.save(refreshTokenModel);

        HashMap<String, Object> response = new HashMap<>();
        response.put("accessToken", jwtUtil.generateToken(user));
        response.put("refreshToken", newRefreshToken);

        return Mono.just(response);
    }
}

Bạn có thể xem toàn bộ source code ở đây .