基本实现手机验证码,账号密码登录。腾讯云短信后台显示成功,但是收不到,也没有报错

This commit is contained in:
2025-08-06 01:31:16 +08:00
parent 66480121b4
commit 940f1d7bac
8 changed files with 314 additions and 89 deletions

20
pom.xml
View File

@@ -54,13 +54,31 @@
<artifactId>springdoc-openapi-starter-webmvc-ui</artifactId> <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
<version>2.1.0</version> <version>2.1.0</version>
</dependency> </dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-security</artifactId> <artifactId>spring-boot-starter-security</artifactId>
</dependency> </dependency>
<dependency> <dependency>
<groupId>org.springframework.boot</groupId> <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>
<dependency> <dependency>

View File

@@ -2,22 +2,25 @@ package com.onekeycall.videotablet.controller;
import com.onekeycall.videotablet.dto.TokenPair; import com.onekeycall.videotablet.dto.TokenPair;
import com.onekeycall.videotablet.entity.User; import com.onekeycall.videotablet.entity.User;
import com.onekeycall.videotablet.result.Result;
import com.onekeycall.videotablet.service.UserService; import com.onekeycall.videotablet.service.UserService;
import com.onekeycall.videotablet.utils.JwtUtil; 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.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.HttpStatus; import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; import org.springframework.http.ResponseEntity;
import org.springframework.security.authentication.AuthenticationManager; 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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody; import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController; import org.springframework.web.bind.annotation.RestController;
import java.util.Date; import java.util.*;
import java.util.Map;
import java.util.Objects;
@RestController @RestController
public class LoginController { public class LoginController {
@@ -47,9 +50,21 @@ public class LoginController {
} }
@PostMapping("/public/login") @PostMapping("/public/login")
public ResponseEntity<?> login() { public ResponseEntity<?> login(
// 登录逻辑由Spring Security自动处理 @RequestParam(value = "user_id") String userId, @RequestParam String password,
return ResponseEntity.ok("Login successful"); @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") @PostMapping("/public/registerByPhone")
public ResponseEntity<?> registerByPhone( public Result registerByPhone(
@RequestParam String phone, @RequestParam String code, @RequestParam String phone, @RequestParam String code,
@RequestParam(value = "verify_key") String verifyKey, @RequestParam(value = "device_id") String deviceId) { @RequestParam(value = "verify_key") String verifyKey, @RequestParam(value = "device_id") String deviceId) {
//
if (TextUtils.isEmpty(verifyKey)) { // if (TextUtils.isEmpty(verifyKey)) {
return new ResponseEntity<>("verify key is empty", HttpStatus.BAD_REQUEST); // return Result.error().message("verify key is empty", HttpStatus.BAD_REQUEST);
} // }
Map<String, Object> map = (Map<String, Object>) redisTemplate.opsForValue().get(phone); Map<String, Object> map = (Map<String, Object>) redisTemplate.opsForValue().get(phone);
if (map != null) { if (map != null) {
String redisVerifyKey = (String) map.get("verifyKey"); String redisVerifyKey = (String) map.get("verifyKey");
if (!Objects.equals(redisVerifyKey, 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(); String redisCode = map.get("code").toString();
if (!Objects.equals(redisCode, code)) { if (!Objects.equals(redisCode, code)) {
return new ResponseEntity<>("code is not same", HttpStatus.BAD_REQUEST); return Result.error().message("code is not same");
} }
try { try {
User user = userService.registerByPhone(phone, code, deviceId, new Date()); User user = userService.registerByPhone(phone, code, deviceId, new Date());
TokenPair tokenPair = jwtUtil.generateTokenPair(user.getUserId(), deviceId); TokenPair tokenPair = jwtUtil.generateTokenPair(user.getUserId(), deviceId);
//返回给app保存access_token用来加入header请求接口refresh_token用来更换access_token //返回给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) { } catch (RuntimeException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); return Result.error().message(e.getMessage());
} finally { } finally {
redisTemplate.delete(phone); redisTemplate.delete(phone);
} }
} else { } else {
return new ResponseEntity<>("verify key is expired", HttpStatus.BAD_REQUEST); return Result.error().message("verify key is expired");
} }
} }
@PostMapping("/public/loginByPhone") @PostMapping("/public/loginByPhone")
public ResponseEntity<?> loginByPhone(@RequestBody PhoneRequest request) { public Result loginByPhone(
String requestVerifyKey = request.getVerifyKey(); @RequestParam String phone, @RequestParam String code,
if (TextUtils.isEmpty(requestVerifyKey)) { @RequestParam(value = "verify_key") String verifyKey, @RequestParam(value = "device_id") String deviceId) {
return new ResponseEntity<>("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 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 { try {
User user = userService.loginByPhone(request.getPhone(), request.getCode()); User user = userService.loginByPhone(phone, code);
// 生成并返回JWT令牌实际项目中需要实现JWT逻辑 // 生成并返回JWT令牌实际项目中需要实现JWT逻辑
TokenPair tokenPair = jwtUtil.generateTokenPair(user.getUserId(), deviceId);
return ResponseEntity.ok("Login successful: " + user.getUsername()); 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) { } catch (RuntimeException e) {
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST); return Result.error().message(e.getMessage());
}
} else {
return Result.error().message("verify key is expired");
} }
} }
@PostMapping("/public/setPasswordByPhone")
public static class PhoneRequest { public Result setPasswordByPhone(
private String phone; HttpServletRequest request, @RequestParam(value = "user_id") String userId,
private String code; @RequestParam String password, @RequestParam(value = "verify_password") String verifyPassword,
private String verifyKey; @RequestParam(value = "device_id") String deviceId) {
String authHeader = request.getHeader("Authorization");
// Getters and Setters if (authHeader != null && authHeader.startsWith("Bearer ")) {
public String getPhone() { String token = authHeader.substring(7); // 提取真正的Token
return phone; 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 {
public void setPhone(String phone) { return Result.error().message("password is not same");
this.phone = phone;
} }
} else {
public String getCode() { return Result.error().message("Authorization header is incorrect");
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/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");
}
}
} }

View File

@@ -167,7 +167,7 @@ public class TencentSmsController {
map.put("sentAt", now); map.put("sentAt", now);
map.put("expireAt", expireAt); map.put("expireAt", expireAt);
Map<String, Object> codeMap = new HashMap<>(map); Map<String, Object> codeMap = new HashMap<>(map);
codeMap.put("code", randomString); codeMap.put("code", code);
if (!sent) { if (!sent) {
//4.保存验证码到Redis,并且设置有效期5分钟 //4.保存验证码到Redis,并且设置有效期5分钟
redisTemplate.opsForValue().set(phone, codeMap, Duration.ofMinutes(5)); redisTemplate.opsForValue().set(phone, codeMap, Duration.ofMinutes(5));

View File

@@ -2,6 +2,9 @@ package com.onekeycall.videotablet.dto;
import lombok.Data; import lombok.Data;
import java.util.HashMap;
import java.util.Map;
/** /**
* 双Token认证令牌对AccessToken + RefreshToken * 双Token认证令牌对AccessToken + RefreshToken
* 设计要点: * 设计要点:
@@ -20,28 +23,30 @@ public class TokenPair {
// RefreshToken过期时间戳毫秒 // RefreshToken过期时间戳毫秒
private long refresh_expires; private long refresh_expires;
// 关联设备指纹(防御中间人攻击)[1](@ref) // 关联设备指纹(防御中间人攻击)[1](@ref)
private String deviceId; private String device_id;
/** /**
* 全参数构造器(安全增强版) * 全参数构造器(安全增强版)
*
* @param access_token JWT格式访问令牌 * @param access_token JWT格式访问令牌
* @param refresh_token JWT格式刷新令牌 * @param refresh_token JWT格式刷新令牌
* @param accessExpireMs AccessToken有效期毫秒 * @param accessExpireMs AccessToken有效期毫秒
* @param refreshExpireMs RefreshToken有效期毫秒 * @param refreshExpireMs RefreshToken有效期毫秒
* @param deviceId 客户端设备指纹 * @param device_id 客户端设备指纹
*/ */
public TokenPair(String access_token, String refresh_token, public TokenPair(String access_token, String refresh_token,
long accessExpireMs, long refreshExpireMs, long accessExpireMs, long refreshExpireMs,
String deviceId) { String device_id) {
this.access_token = access_token; this.access_token = access_token;
this.refresh_token = refresh_token; this.refresh_token = refresh_token;
this.access_expires = System.currentTimeMillis() + accessExpireMs; this.access_expires = System.currentTimeMillis() + accessExpireMs;
this.refresh_expires = System.currentTimeMillis() + refreshExpireMs; this.refresh_expires = System.currentTimeMillis() + refreshExpireMs;
this.deviceId = deviceId; this.device_id = device_id;
} }
/** /**
* 快速创建方法(推荐) * 快速创建方法(推荐)
*
* @param accessToken 访问令牌 * @param accessToken 访问令牌
* @param refreshToken 刷新令牌 * @param refreshToken 刷新令牌
* @param deviceId 设备指纹 * @param deviceId 设备指纹
@@ -59,6 +64,7 @@ public class TokenPair {
/** /**
* 检查AccessToken是否过期 * 检查AccessToken是否过期
*
* @return true=已过期false=有效 * @return true=已过期false=有效
*/ */
public boolean isAccessExpired() { public boolean isAccessExpired() {
@@ -67,6 +73,7 @@ public class TokenPair {
/** /**
* 检查RefreshToken是否过期 * 检查RefreshToken是否过期
*
* @return true=已过期false=有效 * @return true=已过期false=有效
*/ */
public boolean isRefreshExpired() { public boolean isRefreshExpired() {
@@ -75,6 +82,7 @@ public class TokenPair {
/** /**
* 安全刷新令牌生成新TokenPair * 安全刷新令牌生成新TokenPair
*
* @param newAccessToken 新访问令牌 * @param newAccessToken 新访问令牌
* @param newRefreshToken 新刷新令牌 * @param newRefreshToken 新刷新令牌
* @return 更新后的TokenPair保留原设备ID * @return 更新后的TokenPair保留原设备ID
@@ -85,7 +93,17 @@ public class TokenPair {
newRefreshToken, newRefreshToken,
this.access_expires - System.currentTimeMillis(), // 剩余时间延续 this.access_expires - System.currentTimeMillis(), // 剩余时间延续
this.refresh_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;
}
} }

View File

@@ -12,6 +12,11 @@ import java.util.Date;
@Table(name = "users") @Table(name = "users")
public class User implements UserDetails { public class User implements UserDetails {
@Transient // 关键注解:声明此字段不映射到数据库
private boolean newUser;
@Transient // 关键注解:声明此字段不映射到数据库
private boolean hasPassword;
@Id @Id
@GeneratedValue(strategy = GenerationType.IDENTITY) @GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "id") @Column(name = "id")
@@ -66,6 +71,22 @@ public class User implements UserDetails {
return true; 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 // Getters and Setters for id, username, password, email
public Long getId() { public Long getId() {
return id; return id;

View File

@@ -9,4 +9,5 @@ public interface UserRepository extends JpaRepository<User, Long> {
boolean existsByPhone(String phone); boolean existsByPhone(String phone);
Optional<User> findByUserId(String userId); Optional<User> findByUserId(String userId);
boolean existsByUserId(String userId); boolean existsByUserId(String userId);
boolean existsPasswordByUserId(String userId);
} }

View File

@@ -2,6 +2,7 @@ package com.onekeycall.videotablet.service;
import com.onekeycall.videotablet.entity.User; import com.onekeycall.videotablet.entity.User;
import com.onekeycall.videotablet.repository.UserRepository; import com.onekeycall.videotablet.repository.UserRepository;
import com.onekeycall.videotablet.result.Result;
import com.onekeycall.videotablet.utils.SecureIdGenerator; import com.onekeycall.videotablet.utils.SecureIdGenerator;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.RedisTemplate;
@@ -49,11 +50,13 @@ public class UserService implements UserDetailsService {
// 2. 检查手机号是否已注册 // 2. 检查手机号是否已注册
if (userRepository.existsByPhone(phone)) { if (userRepository.existsByPhone(phone)) {
throw new RuntimeException("Phone number already registered"); User user = userRepository.findByPhone(phone).get();
} user.setNewUser(false);
return user;
} else {
// 3. 创建新用户 // 3. 创建新用户
User user = new User(); User user = new User();
user.setNewUser(true);
user.setPhone(phone); user.setPhone(phone);
user.setCreatTime(createTime); user.setCreatTime(createTime);
user.setLastLoginTime(createTime); user.setLastLoginTime(createTime);
@@ -61,8 +64,10 @@ public class UserService implements UserDetailsService {
user.setUserId(SecureIdGenerator.generateSecureId(12)); user.setUserId(SecureIdGenerator.generateSecureId(12));
user.setUsername(SecureIdGenerator.generateSecureUserName(8)); user.setUsername(SecureIdGenerator.generateSecureUserName(8));
user.setDeviceId(deviceId); user.setDeviceId(deviceId);
return userRepository.save(user); return userRepository.save(user);
} }
}
public User loginByPhone(String phone, String code) { public User loginByPhone(String phone, String code) {
// 1. 验证验证码 // 1. 验证验证码
@@ -81,4 +86,23 @@ public class UserService implements UserDetailsService {
return userRepository.findByUserId(userId) return userRepository.findByUserId(userId)
.orElseThrow(() -> new UsernameNotFoundException("User not found with userId: " + 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");
}
}
} }

View File

@@ -1,12 +1,15 @@
package com.onekeycall.videotablet.utils; package com.onekeycall.videotablet.utils;
import com.onekeycall.videotablet.dto.TokenPair; import com.onekeycall.videotablet.dto.TokenPair;
import io.jsonwebtoken.Claims;
import io.jsonwebtoken.JwtException;
import io.jsonwebtoken.Jwts; import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys; import io.jsonwebtoken.security.Keys;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value; import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
import org.springframework.security.oauth2.common.exceptions.InvalidTokenException;
import java.util.Date; import java.util.Date;
import java.util.UUID; import java.util.UUID;
@@ -25,10 +28,15 @@ public class JwtUtil {
@Value("${jwt.refresh-expire}") @Value("${jwt.refresh-expire}")
private Long refreshExpire; private Long refreshExpire;
/**
* @param userId 统一使用user_id
* @param deviceId
* @return
*/
// 生成双Token关联设备指纹 // 生成双Token关联设备指纹
public TokenPair generateTokenPair(String username, String deviceId) { public TokenPair generateTokenPair(String userId, String deviceId) {
String accessToken = Jwts.builder() String accessToken = Jwts.builder()
.subject(username) .subject(userId)
.claim("type", "ACCESS") .claim("type", "ACCESS")
.claim("deviceId", deviceId) // 绑定设备 .claim("deviceId", deviceId) // 绑定设备
.issuedAt(new Date()) .issuedAt(new Date())
@@ -38,16 +46,16 @@ public class JwtUtil {
String refreshId = UUID.randomUUID().toString(); String refreshId = UUID.randomUUID().toString();
String refreshToken = Jwts.builder() String refreshToken = Jwts.builder()
.subject(username) .subject(userId)
.claim("type", "REFRESH") .claim("type", "REFRESH")
.claim("refreshId", refreshId) .claim("refreshId", refreshId)
.expiration(new Date(System.currentTimeMillis() + refreshExpire)) .expiration(new Date(System.currentTimeMillis() + refreshExpire))
.signWith(Keys.hmacShaKeyFor(secret.getBytes())) .signWith(Keys.hmacShaKeyFor(secret.getBytes()))
.compact(); .compact();
// Redis存储Refresh Tokenkey: user:refresh:<username>, value: refreshId // Redis存储Refresh Tokenkey: user:refresh:<userId>, value: refreshId
redisTemplate.opsForValue().set( redisTemplate.opsForValue().set(
"user:refresh:" + username, "user:refresh:" + userId,
refreshId, refreshId,
refreshExpire, refreshExpire,
TimeUnit.MILLISECONDS TimeUnit.MILLISECONDS
@@ -56,5 +64,86 @@ public class JwtUtil {
return new TokenPair(accessToken, refreshToken, accessExpire, refreshExpire, deviceId); 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);
}
} }