added refresh token

This commit is contained in:
SlimusMinus
2026-08-03 02:43:46 +03:00
parent d3a231f3fc
commit 7660128d96
21 changed files with 309 additions and 138 deletions

View File

@@ -1,7 +1,9 @@
package com.krylov.refound.service;
import com.krylov.refound.dto.AuthRequest;
import com.krylov.refound.dto.AuthResponse;
import com.krylov.refound.dto.CheckLoginDto;
import com.krylov.refound.dto.RefreshRequest;
import com.krylov.refound.dto.RegisterRequest;
import com.krylov.refound.dto.UserDto;
import com.krylov.refound.dto.UserRegisterDto;
@@ -12,6 +14,7 @@ import com.krylov.refound.repository.UserRepository;
import com.krylov.refound.security.JwtService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
@@ -23,8 +26,9 @@ public class AuthService {
private final UserRepository userRepository;
private final PasswordEncoder passwordEncoder;
private final JwtService jwtService;
private final RefreshTokenService refreshTokenService;
public UserRegisterDto registerUser(RegisterRequest request){
public UserRegisterDto registerUser(RegisterRequest request) {
if (userRepository.findByLogin(request.getLogin()).isPresent()) {
log.warn("registration failed: login {} already taken", request.getLogin());
throw new LoginAlreadyExistsException("Логин уже занят");
@@ -36,31 +40,54 @@ public class AuthService {
userRepository.save(user);
String token = jwtService.generateToken(user.getLogin());
String accessToken = jwtService.generateAccessToken(user.getLogin(), user.getRole().name());
String refreshToken = refreshTokenService.issue(user.getLogin());
log.info("Refresh token returned = {}", refreshToken);
UserDto userDto = getUserDto(user);
return new UserRegisterDto(userDto, token);
return new UserRegisterDto(userDto, accessToken, refreshToken);
}
public UserRegisterDto loginUser(AuthRequest request){
public UserRegisterDto loginUser(AuthRequest request) {
User user = userRepository.findByLogin(request.getLogin()).orElse(null);
if (user == null || !passwordEncoder.matches(request.getPassword(), user.getPassword())) {
log.warn("login for user {} or password is incorrect", request.getLogin());
log.warn("login failed: login or password incorrect for {}", request.getLogin());
throw new InvalidCredentialsException("Неверный логин или пароль");
}
String token = jwtService.generateToken(user.getLogin());
String accessToken = jwtService.generateAccessToken(user.getLogin(), user.getRole().name());
String refreshToken = refreshTokenService.issue(user.getLogin());
log.info("Refresh token returned = {}", refreshToken);
UserDto userDto = getUserDto(user);
return new UserRegisterDto(userDto, token);
return new UserRegisterDto(userDto, accessToken, refreshToken);
}
public boolean existsByLogin(CheckLoginDto request){
public boolean existsByLogin(CheckLoginDto request) {
return userRepository.existsByLogin(request.getLogin());
}
public AuthResponse refresh(RefreshRequest request) {
String login = refreshTokenService.validateAndGetLogin(request.getRefreshToken())
.orElseThrow(() -> new BadCredentialsException("Invalid or expired refresh token"));
User user = userRepository.findByLogin(login)
.orElseThrow(() -> new BadCredentialsException("User not found"));
// ротация: старый refresh-токен отзываем, выдаём новую пару
refreshTokenService.revoke(request.getRefreshToken());
String newAccessToken = jwtService.generateAccessToken(user.getLogin(), user.getRole().name());
String newRefreshToken = refreshTokenService.issue(user.getLogin());
log.info("Refresh token returned = {}", newRefreshToken);
return new AuthResponse(newAccessToken, newRefreshToken, getUserDto(user));
}
public void logout(RefreshRequest request) {
refreshTokenService.revoke(request.getRefreshToken());
}
private static UserDto getUserDto(User user) {
return new UserDto(
user.getId(),

View File

@@ -0,0 +1,97 @@
package com.krylov.refound.service;
import com.krylov.refound.security.JwtService;
import java.time.Duration;
import java.util.Optional;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
@Slf4j
@Service
@RequiredArgsConstructor
public class RefreshTokenService {
private static final String KEY_PREFIX = "refresh_token:";
private final StringRedisTemplate redisTemplate;
private final JwtService jwtService;
/**
* Создаёт refresh-токен, сохраняет его в Redis с TTL и привязкой к логину.
* В качестве ключа используем не сам JWT, а отдельный UUID —
* так тело токена не обязано быть валидным JWT для проверки существования в Redis,
* упрощает revoke без необходимости парсить токен.
*/
public String issue(String login) {
String tokenId = UUID.randomUUID().toString();
String refreshJwt = jwtService.generateRefreshToken(login);
log.info("Saving refresh token: key={}, login={}",
KEY_PREFIX + tokenId,
login);
redisTemplate.opsForValue().set(
KEY_PREFIX + tokenId,
login,
Duration.ofMillis(jwtService.getRefreshExpirationMillis())
);
// Кодируем tokenId в сам JWT через отдельный claim, чтобы при refresh
// можно было найти запись в Redis по этому id, а не по всему телу токена
return tokenId + "." + refreshJwt;
}
/**
* Проверяет refresh-токен: валиден ли JWT и существует ли запись в Redis.
* Возвращает login, если всё ок, иначе — пусто.
*/
public java.util.Optional<String> validateAndGetLogin(String rawToken) {
log.info("Incoming refresh token = {}", rawToken);
String[] parts = rawToken.split("\\.", 2);
log.info("parts.length = {}", parts.length);
if (parts.length == 2) {
log.info("tokenId={}", parts[0]);
log.info("jwt={}", parts[1]);
}
if (parts.length != 2) {
return Optional.empty();
}
boolean valid = jwtService.isTokenValid(parts[1]);
log.info("jwt valid={}", valid);
if (!valid) {
return Optional.empty();
}
String tokenId = parts[0];
String jwt = parts[1];
if (!jwtService.isTokenValid(jwt)) {
return java.util.Optional.empty();
}
String storedLogin = redisTemplate.opsForValue().get(KEY_PREFIX + tokenId);
String jwtLogin = jwtService.extractLogin(jwt);
if (storedLogin == null || !storedLogin.equals(jwtLogin)) {
return java.util.Optional.empty();
}
return java.util.Optional.of(jwtLogin);
}
/** Ротация: старый токен удаляется, выпускается новый. */
public void revoke(String rawToken) {
String[] parts = rawToken.split("\\.", 2);
if (parts.length == 2) {
redisTemplate.delete(KEY_PREFIX + parts[0]);
}
}
}