基本实现手机验证码,账号密码登录。腾讯云短信后台显示成功,但是收不到,也没有报错
This commit is contained in:
20
pom.xml
20
pom.xml
@@ -54,13 +54,31 @@
|
||||
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
|
||||
<version>2.1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-security</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
<artifactId>spring-boot-starter-oauth2-client</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-oauth2-resource-server</artifactId>
|
||||
</dependency>
|
||||
<!-- 支持 JWT 令牌解析 -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.security</groupId>
|
||||
<artifactId>spring-security-oauth2-jose</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.security.oauth</groupId>
|
||||
<artifactId>spring-security-oauth2</artifactId>
|
||||
<version>2.5.2.RELEASE</version> <!-- 推荐 2.x 最新版本 -->
|
||||
</dependency>
|
||||
|
||||
<dependency>
|
||||
|
||||
@@ -2,22 +2,25 @@ package com.onekeycall.videotablet.controller;
|
||||
|
||||
import com.onekeycall.videotablet.dto.TokenPair;
|
||||
import com.onekeycall.videotablet.entity.User;
|
||||
import com.onekeycall.videotablet.result.Result;
|
||||
import com.onekeycall.videotablet.service.UserService;
|
||||
import com.onekeycall.videotablet.utils.JwtUtil;
|
||||
import com.onekeycall.videotablet.utils.TextUtils;
|
||||
import jakarta.servlet.http.HttpServletRequest;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.security.authentication.AuthenticationManager;
|
||||
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
|
||||
import org.springframework.security.core.Authentication;
|
||||
import org.springframework.security.core.userdetails.UserDetails;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.Map;
|
||||
import java.util.Objects;
|
||||
import java.util.*;
|
||||
|
||||
@RestController
|
||||
public class LoginController {
|
||||
@@ -47,9 +50,21 @@ public class LoginController {
|
||||
}
|
||||
|
||||
@PostMapping("/public/login")
|
||||
public ResponseEntity<?> login() {
|
||||
// 登录逻辑由Spring Security自动处理
|
||||
return ResponseEntity.ok("Login successful");
|
||||
public ResponseEntity<?> login(
|
||||
@RequestParam(value = "user_id") String userId, @RequestParam String password,
|
||||
@RequestParam(value = "device_id", required = false) String deviceId) {
|
||||
// 1. 创建认证令牌
|
||||
Authentication authenticationToken = new UsernamePasswordAuthenticationToken(userId, password);
|
||||
|
||||
// 2. 使用 AuthenticationManager 进行认证(核心步骤)
|
||||
Authentication authentication = authenticationManager.authenticate(authenticationToken);
|
||||
|
||||
// 3. 认证成功后生成 JWT
|
||||
UserDetails userDetails = (UserDetails) authentication.getPrincipal();
|
||||
TokenPair tokenPair = jwtUtil.generateTokenPair(userDetails.getUsername(), deviceId);
|
||||
|
||||
// 4. 返回 Token
|
||||
return ResponseEntity.ok(Collections.singletonMap("token", tokenPair.toMap()));
|
||||
}
|
||||
|
||||
// 注册请求参数类
|
||||
@@ -77,84 +92,123 @@ public class LoginController {
|
||||
}
|
||||
|
||||
@PostMapping("/public/registerByPhone")
|
||||
public ResponseEntity<?> registerByPhone(
|
||||
public Result registerByPhone(
|
||||
@RequestParam String phone, @RequestParam String code,
|
||||
@RequestParam(value = "verify_key") String verifyKey, @RequestParam(value = "device_id") String deviceId) {
|
||||
|
||||
if (TextUtils.isEmpty(verifyKey)) {
|
||||
return new ResponseEntity<>("verify key is empty", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
//
|
||||
// if (TextUtils.isEmpty(verifyKey)) {
|
||||
// return Result.error().message("verify key is empty", HttpStatus.BAD_REQUEST);
|
||||
// }
|
||||
Map<String, Object> map = (Map<String, Object>) redisTemplate.opsForValue().get(phone);
|
||||
if (map != null) {
|
||||
String redisVerifyKey = (String) map.get("verifyKey");
|
||||
if (!Objects.equals(redisVerifyKey, verifyKey)) {
|
||||
return new ResponseEntity<>("verify key is not same", HttpStatus.BAD_REQUEST);
|
||||
return Result.error().message("verify key is not same");
|
||||
}
|
||||
String redisCode = map.get("code").toString();
|
||||
if (!Objects.equals(redisCode, code)) {
|
||||
return new ResponseEntity<>("code is not same", HttpStatus.BAD_REQUEST);
|
||||
return Result.error().message("code is not same");
|
||||
}
|
||||
try {
|
||||
User user = userService.registerByPhone(phone, code, deviceId, new Date());
|
||||
TokenPair tokenPair = jwtUtil.generateTokenPair(user.getUserId(), deviceId);
|
||||
//返回给app保存,access_token用来加入header请求接口,refresh_token用来更换access_token
|
||||
return new ResponseEntity<>(tokenPair, HttpStatus.CREATED);
|
||||
Map<String, Object> tokenMap = new HashMap<>();
|
||||
tokenMap.put("new_user", user.isNewUser());
|
||||
tokenMap.put("user_id", user.getUserId());
|
||||
tokenMap.put("has_password", user.isHasPassword());
|
||||
tokenMap.put("token", tokenPair.toMap());
|
||||
return Result.ok().data(tokenMap);
|
||||
} catch (RuntimeException e) {
|
||||
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
|
||||
return Result.error().message(e.getMessage());
|
||||
} finally {
|
||||
redisTemplate.delete(phone);
|
||||
}
|
||||
} else {
|
||||
return new ResponseEntity<>("verify key is expired", HttpStatus.BAD_REQUEST);
|
||||
return Result.error().message("verify key is expired");
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/public/loginByPhone")
|
||||
public ResponseEntity<?> loginByPhone(@RequestBody PhoneRequest request) {
|
||||
String requestVerifyKey = request.getVerifyKey();
|
||||
if (TextUtils.isEmpty(requestVerifyKey)) {
|
||||
return new ResponseEntity<>("verify key is empty", HttpStatus.BAD_REQUEST);
|
||||
}
|
||||
try {
|
||||
User user = userService.loginByPhone(request.getPhone(), request.getCode());
|
||||
// 生成并返回JWT令牌(实际项目中需要实现JWT逻辑)
|
||||
|
||||
return ResponseEntity.ok("Login successful: " + user.getUsername());
|
||||
} catch (RuntimeException e) {
|
||||
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
|
||||
public Result loginByPhone(
|
||||
@RequestParam String phone, @RequestParam String code,
|
||||
@RequestParam(value = "verify_key") String verifyKey, @RequestParam(value = "device_id") String deviceId) {
|
||||
Map<String, Object> map = (Map<String, Object>) redisTemplate.opsForValue().get(phone);
|
||||
if (map != null) {
|
||||
String redisVerifyKey = (String) map.get("verifyKey");
|
||||
if (!Objects.equals(redisVerifyKey, verifyKey)) {
|
||||
return Result.error().message("verify key is not same");
|
||||
}
|
||||
String redisCode = map.get("code").toString();
|
||||
if (!Objects.equals(redisCode, code)) {
|
||||
return Result.error().message("code is not same");
|
||||
}
|
||||
try {
|
||||
User user = userService.loginByPhone(phone, code);
|
||||
// 生成并返回JWT令牌(实际项目中需要实现JWT逻辑)
|
||||
TokenPair tokenPair = jwtUtil.generateTokenPair(user.getUserId(), deviceId);
|
||||
Map<String, Object> tokenMap = new HashMap<>();
|
||||
tokenMap.put("new_user", user.isNewUser());
|
||||
tokenMap.put("user_id", user.getUserId());
|
||||
tokenMap.put("has_password", user.isHasPassword());
|
||||
tokenMap.put("token", tokenPair.toMap());
|
||||
return Result.ok().data(tokenMap);
|
||||
} catch (RuntimeException e) {
|
||||
return Result.error().message(e.getMessage());
|
||||
}
|
||||
} else {
|
||||
return Result.error().message("verify key is expired");
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
public static class PhoneRequest {
|
||||
private String phone;
|
||||
private String code;
|
||||
private String verifyKey;
|
||||
|
||||
// Getters and Setters
|
||||
public String getPhone() {
|
||||
return phone;
|
||||
}
|
||||
|
||||
public void setPhone(String phone) {
|
||||
this.phone = phone;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getVerifyKey() {
|
||||
return verifyKey;
|
||||
}
|
||||
|
||||
public void setVerifyKey(String verifyKey) {
|
||||
this.verifyKey = verifyKey;
|
||||
@PostMapping("/public/setPasswordByPhone")
|
||||
public Result setPasswordByPhone(
|
||||
HttpServletRequest request, @RequestParam(value = "user_id") String userId,
|
||||
@RequestParam String password, @RequestParam(value = "verify_password") String verifyPassword,
|
||||
@RequestParam(value = "device_id") String deviceId) {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||
String token = authHeader.substring(7); // 提取真正的Token
|
||||
if (StringUtils.equals(password, verifyPassword)) {
|
||||
if (jwtUtil.validateAccessToken(userId, token, deviceId)) {
|
||||
userService.setPasswordByUserId(userId, password);
|
||||
return Result.ok().message("set password success");
|
||||
} else {
|
||||
return Result.error().message("token is not same");
|
||||
}
|
||||
} else {
|
||||
return Result.error().message("password is not same");
|
||||
}
|
||||
} else {
|
||||
return Result.error().message("Authorization header is incorrect");
|
||||
}
|
||||
}
|
||||
|
||||
@PostMapping("/public/changePassword")
|
||||
public Result changePassword(
|
||||
HttpServletRequest request,
|
||||
@RequestParam(value = "user_id") String userId,
|
||||
@RequestParam(value = "old_password") String oldPassword,
|
||||
@RequestParam String password, @RequestParam(value = "verify_password") String verifyPassword,
|
||||
@RequestParam(value = "device_id") String deviceId) {
|
||||
String authHeader = request.getHeader("Authorization");
|
||||
if (authHeader != null && authHeader.startsWith("Bearer ")) {
|
||||
String token = authHeader.substring(7);
|
||||
if (StringUtils.equals(password, verifyPassword)) {
|
||||
if (!oldPassword.equals(password)) {
|
||||
if (jwtUtil.validateAccessToken(userId, token, deviceId)) {
|
||||
return userService.changePassword(userId, oldPassword, password);
|
||||
} else {
|
||||
return Result.error().message("token is not same");
|
||||
}
|
||||
} else {
|
||||
return Result.error().message("The old password and the new password are the same");
|
||||
}
|
||||
} else {
|
||||
return Result.error().message("password is not same");
|
||||
}
|
||||
} else {
|
||||
return Result.error().message("Authorization header is incorrect");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -167,7 +167,7 @@ public class TencentSmsController {
|
||||
map.put("sentAt", now);
|
||||
map.put("expireAt", expireAt);
|
||||
Map<String, Object> codeMap = new HashMap<>(map);
|
||||
codeMap.put("code", randomString);
|
||||
codeMap.put("code", code);
|
||||
if (!sent) {
|
||||
//4.保存验证码到Redis,并且设置有效期5分钟
|
||||
redisTemplate.opsForValue().set(phone, codeMap, Duration.ofMinutes(5));
|
||||
|
||||
@@ -2,6 +2,9 @@ package com.onekeycall.videotablet.dto;
|
||||
|
||||
import lombok.Data;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 双Token认证令牌对(AccessToken + RefreshToken)
|
||||
* 设计要点:
|
||||
@@ -20,31 +23,33 @@ public class TokenPair {
|
||||
// RefreshToken过期时间戳(毫秒)
|
||||
private long refresh_expires;
|
||||
// 关联设备指纹(防御中间人攻击)[1](@ref)
|
||||
private String deviceId;
|
||||
private String device_id;
|
||||
|
||||
/**
|
||||
* 全参数构造器(安全增强版)
|
||||
* @param access_token JWT格式访问令牌
|
||||
* @param refresh_token JWT格式刷新令牌
|
||||
* @param accessExpireMs AccessToken有效期(毫秒)
|
||||
*
|
||||
* @param access_token JWT格式访问令牌
|
||||
* @param refresh_token JWT格式刷新令牌
|
||||
* @param accessExpireMs AccessToken有效期(毫秒)
|
||||
* @param refreshExpireMs RefreshToken有效期(毫秒)
|
||||
* @param deviceId 客户端设备指纹
|
||||
* @param device_id 客户端设备指纹
|
||||
*/
|
||||
public TokenPair(String access_token, String refresh_token,
|
||||
long accessExpireMs, long refreshExpireMs,
|
||||
String deviceId) {
|
||||
String device_id) {
|
||||
this.access_token = access_token;
|
||||
this.refresh_token = refresh_token;
|
||||
this.access_expires = System.currentTimeMillis() + accessExpireMs;
|
||||
this.refresh_expires = System.currentTimeMillis() + refreshExpireMs;
|
||||
this.deviceId = deviceId;
|
||||
this.device_id = device_id;
|
||||
}
|
||||
|
||||
/**
|
||||
* 快速创建方法(推荐)
|
||||
* @param accessToken 访问令牌
|
||||
*
|
||||
* @param accessToken 访问令牌
|
||||
* @param refreshToken 刷新令牌
|
||||
* @param deviceId 设备指纹
|
||||
* @param deviceId 设备指纹
|
||||
* @return 初始化过期时间的TokenPair
|
||||
*/
|
||||
public static TokenPair create(String accessToken, String refreshToken, String deviceId) {
|
||||
@@ -59,6 +64,7 @@ public class TokenPair {
|
||||
|
||||
/**
|
||||
* 检查AccessToken是否过期
|
||||
*
|
||||
* @return true=已过期,false=有效
|
||||
*/
|
||||
public boolean isAccessExpired() {
|
||||
@@ -67,6 +73,7 @@ public class TokenPair {
|
||||
|
||||
/**
|
||||
* 检查RefreshToken是否过期
|
||||
*
|
||||
* @return true=已过期,false=有效
|
||||
*/
|
||||
public boolean isRefreshExpired() {
|
||||
@@ -75,7 +82,8 @@ public class TokenPair {
|
||||
|
||||
/**
|
||||
* 安全刷新令牌(生成新TokenPair)
|
||||
* @param newAccessToken 新访问令牌
|
||||
*
|
||||
* @param newAccessToken 新访问令牌
|
||||
* @param newRefreshToken 新刷新令牌
|
||||
* @return 更新后的TokenPair(保留原设备ID)
|
||||
*/
|
||||
@@ -85,7 +93,17 @@ public class TokenPair {
|
||||
newRefreshToken,
|
||||
this.access_expires - System.currentTimeMillis(), // 剩余时间延续
|
||||
this.refresh_expires - System.currentTimeMillis(),
|
||||
this.deviceId // 保持设备一致性
|
||||
this.device_id // 保持设备一致性
|
||||
);
|
||||
}
|
||||
|
||||
public Map<String, Object> toMap() {
|
||||
Map<String, Object> map = new HashMap<>();
|
||||
map.put("access_token", access_token);
|
||||
map.put("refresh_token", refresh_token);
|
||||
map.put("access_expires", String.valueOf(access_expires));
|
||||
map.put("refresh_expires", String.valueOf(refresh_expires));
|
||||
map.put("device_id", device_id);
|
||||
return map;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,6 +12,11 @@ import java.util.Date;
|
||||
@Table(name = "users")
|
||||
public class User implements UserDetails {
|
||||
|
||||
@Transient // 关键注解:声明此字段不映射到数据库
|
||||
private boolean newUser;
|
||||
@Transient // 关键注解:声明此字段不映射到数据库
|
||||
private boolean hasPassword;
|
||||
|
||||
@Id
|
||||
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||
@Column(name = "id")
|
||||
@@ -66,6 +71,22 @@ public class User implements UserDetails {
|
||||
return true;
|
||||
}
|
||||
|
||||
public boolean isNewUser() {
|
||||
return newUser;
|
||||
}
|
||||
|
||||
public void setNewUser(boolean newUser) {
|
||||
this.newUser = newUser;
|
||||
}
|
||||
|
||||
public boolean isHasPassword() {
|
||||
return hasPassword;
|
||||
}
|
||||
|
||||
public void setHasPassword(boolean hasPassword) {
|
||||
this.hasPassword = hasPassword;
|
||||
}
|
||||
|
||||
// Getters and Setters for id, username, password, email
|
||||
public Long getId() {
|
||||
return id;
|
||||
|
||||
@@ -9,4 +9,5 @@ public interface UserRepository extends JpaRepository<User, Long> {
|
||||
boolean existsByPhone(String phone);
|
||||
Optional<User> findByUserId(String userId);
|
||||
boolean existsByUserId(String userId);
|
||||
boolean existsPasswordByUserId(String userId);
|
||||
}
|
||||
@@ -2,6 +2,7 @@ package com.onekeycall.videotablet.service;
|
||||
|
||||
import com.onekeycall.videotablet.entity.User;
|
||||
import com.onekeycall.videotablet.repository.UserRepository;
|
||||
import com.onekeycall.videotablet.result.Result;
|
||||
import com.onekeycall.videotablet.utils.SecureIdGenerator;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.data.redis.core.RedisTemplate;
|
||||
@@ -49,19 +50,23 @@ public class UserService implements UserDetailsService {
|
||||
|
||||
// 2. 检查手机号是否已注册
|
||||
if (userRepository.existsByPhone(phone)) {
|
||||
throw new RuntimeException("Phone number already registered");
|
||||
}
|
||||
User user = userRepository.findByPhone(phone).get();
|
||||
user.setNewUser(false);
|
||||
return user;
|
||||
} else {
|
||||
// 3. 创建新用户
|
||||
User user = new User();
|
||||
user.setNewUser(true);
|
||||
user.setPhone(phone);
|
||||
user.setCreatTime(createTime);
|
||||
user.setLastLoginTime(createTime);
|
||||
user.setUpdateTime(createTime);
|
||||
user.setUserId(SecureIdGenerator.generateSecureId(12));
|
||||
user.setUsername(SecureIdGenerator.generateSecureUserName(8));
|
||||
user.setDeviceId(deviceId);
|
||||
|
||||
// 3. 创建新用户
|
||||
User user = new User();
|
||||
user.setPhone(phone);
|
||||
user.setCreatTime(createTime);
|
||||
user.setLastLoginTime(createTime);
|
||||
user.setUpdateTime(createTime);
|
||||
user.setUserId(SecureIdGenerator.generateSecureId(12));
|
||||
user.setUsername(SecureIdGenerator.generateSecureUserName(8));
|
||||
user.setDeviceId(deviceId);
|
||||
return userRepository.save(user);
|
||||
return userRepository.save(user);
|
||||
}
|
||||
}
|
||||
|
||||
public User loginByPhone(String phone, String code) {
|
||||
@@ -81,4 +86,23 @@ public class UserService implements UserDetailsService {
|
||||
return userRepository.findByUserId(userId)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found with userId: " + userId));
|
||||
}
|
||||
|
||||
public void setPasswordByUserId(String userId, String password) {
|
||||
User user = userRepository.findByUserId(userId)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found with userId: " + userId));
|
||||
user.setPassword(passwordEncoder.encode(password));
|
||||
userRepository.save(user);
|
||||
}
|
||||
|
||||
public Result changePassword(String userId, String oldPassword, String newPassword) {
|
||||
User user = userRepository.findByUserId(userId)
|
||||
.orElseThrow(() -> new UsernameNotFoundException("User not found with userId: " + userId));
|
||||
if (!passwordEncoder.matches(oldPassword, user.getPassword())) {
|
||||
return Result.error().message("Old password is incorrect");
|
||||
} else {
|
||||
user.setPassword(passwordEncoder.encode(newPassword));
|
||||
userRepository.save(user);
|
||||
return Result.ok().message("change password success");
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1,12 +1,15 @@
|
||||
package com.onekeycall.videotablet.utils;
|
||||
|
||||
import com.onekeycall.videotablet.dto.TokenPair;
|
||||
import io.jsonwebtoken.Claims;
|
||||
import io.jsonwebtoken.JwtException;
|
||||
import io.jsonwebtoken.Jwts;
|
||||
import io.jsonwebtoken.security.Keys;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.data.redis.core.StringRedisTemplate;
|
||||
import org.springframework.stereotype.Component;
|
||||
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
|
||||
|
||||
import java.util.Date;
|
||||
import java.util.UUID;
|
||||
@@ -25,10 +28,15 @@ public class JwtUtil {
|
||||
@Value("${jwt.refresh-expire}")
|
||||
private Long refreshExpire;
|
||||
|
||||
/**
|
||||
* @param userId 统一使用user_id
|
||||
* @param deviceId
|
||||
* @return
|
||||
*/
|
||||
// 生成双Token(关联设备指纹)
|
||||
public TokenPair generateTokenPair(String username, String deviceId) {
|
||||
public TokenPair generateTokenPair(String userId, String deviceId) {
|
||||
String accessToken = Jwts.builder()
|
||||
.subject(username)
|
||||
.subject(userId)
|
||||
.claim("type", "ACCESS")
|
||||
.claim("deviceId", deviceId) // 绑定设备
|
||||
.issuedAt(new Date())
|
||||
@@ -38,16 +46,16 @@ public class JwtUtil {
|
||||
|
||||
String refreshId = UUID.randomUUID().toString();
|
||||
String refreshToken = Jwts.builder()
|
||||
.subject(username)
|
||||
.subject(userId)
|
||||
.claim("type", "REFRESH")
|
||||
.claim("refreshId", refreshId)
|
||||
.expiration(new Date(System.currentTimeMillis() + refreshExpire))
|
||||
.signWith(Keys.hmacShaKeyFor(secret.getBytes()))
|
||||
.compact();
|
||||
|
||||
// Redis存储Refresh Token(key: user:refresh:<username>, value: refreshId)
|
||||
// Redis存储Refresh Token(key: user:refresh:<userId>, value: refreshId)
|
||||
redisTemplate.opsForValue().set(
|
||||
"user:refresh:" + username,
|
||||
"user:refresh:" + userId,
|
||||
refreshId,
|
||||
refreshExpire,
|
||||
TimeUnit.MILLISECONDS
|
||||
@@ -56,5 +64,86 @@ public class JwtUtil {
|
||||
return new TokenPair(accessToken, refreshToken, accessExpire, refreshExpire, deviceId);
|
||||
}
|
||||
|
||||
// Token解析与校验(略)
|
||||
// 解析Token并返回Claims
|
||||
public Claims parseToken(String token) {
|
||||
try {
|
||||
return Jwts.parser()
|
||||
.verifyWith(Keys.hmacShaKeyFor(secret.getBytes()))
|
||||
.build()
|
||||
.parseSignedClaims(token)
|
||||
.getPayload();
|
||||
} catch (JwtException | IllegalArgumentException e) {
|
||||
throw new InvalidTokenException("Token解析失败: " + e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
// 校验Access Token有效性(签名+过期时间+设备绑定)
|
||||
public boolean validateAccessToken(String userId, String accessToken, String deviceId) {
|
||||
Claims claims = parseToken(accessToken);
|
||||
|
||||
// 0. 验证用户名一致性(新增)
|
||||
String tokenUserId = claims.getSubject();
|
||||
if (!userId.equals(tokenUserId)) {
|
||||
throw new SecurityException("用户身份不匹配");
|
||||
}
|
||||
|
||||
// 1. 验证Token类型
|
||||
if (!"ACCESS".equals(claims.get("type", String.class))) {
|
||||
throw new InvalidTokenException("非Access Token类型");
|
||||
}
|
||||
|
||||
// 2. 验证设备ID一致性
|
||||
String tokenDeviceId = claims.get("deviceId", String.class);
|
||||
if (!deviceId.equals(tokenDeviceId)) {
|
||||
throw new SecurityException("设备ID不匹配");
|
||||
}
|
||||
|
||||
// 3. 验证过期时间(JWT解析时自动校验)
|
||||
return true; // 通过所有校验
|
||||
}
|
||||
|
||||
// 校验Refresh Token有效性(签名+Redis一致性)
|
||||
public boolean validateRefreshToken(String refreshToken, String username) {
|
||||
Claims claims = parseToken(refreshToken);
|
||||
|
||||
// 1. 验证Token类型
|
||||
if (!"REFRESH".equals(claims.get("type", String.class))) {
|
||||
throw new InvalidTokenException("非Refresh Token类型");
|
||||
}
|
||||
|
||||
// 2. 从Redis获取存储的refreshId
|
||||
String redisKey = "user:refresh:" + username;
|
||||
String storedRefreshId = redisTemplate.opsForValue().get(redisKey);
|
||||
if (storedRefreshId == null) {
|
||||
throw new InvalidTokenException("Refresh Token已吊销");
|
||||
}
|
||||
|
||||
// 3. 比对refreshId一致性
|
||||
String tokenRefreshId = claims.get("refreshId", String.class);
|
||||
if (!storedRefreshId.equals(tokenRefreshId)) {
|
||||
throw new SecurityException("Refresh Token无效");
|
||||
}
|
||||
|
||||
return true; // 通过所有校验
|
||||
}
|
||||
|
||||
// 使用Refresh Token刷新Access Token
|
||||
public TokenPair refreshAccessToken(String refreshToken, String deviceId) {
|
||||
Claims claims = parseToken(refreshToken);
|
||||
String userId = claims.getSubject();
|
||||
|
||||
// 1. 校验Refresh Token有效性
|
||||
validateRefreshToken(refreshToken, userId);
|
||||
|
||||
// 2. 生成新Token(复用原设备ID)
|
||||
return generateTokenPair(userId, deviceId);
|
||||
}
|
||||
|
||||
// 登出时吊销Refresh Token
|
||||
public void revokeRefreshToken(String username) {
|
||||
String redisKey = "user:refresh:" + username;
|
||||
redisTemplate.delete(redisKey);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
Reference in New Issue
Block a user