added refresh token
This commit is contained in:
@@ -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(),
|
||||
|
||||
@@ -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]);
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user