基本实现手机验证码,账号密码登录。腾讯云短信后台显示成功,但是收不到,也没有报错
This commit is contained in:
20
pom.xml
20
pom.xml
@@ -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>
|
||||||
|
|||||||
@@ -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) {
|
||||||
try {
|
String redisVerifyKey = (String) map.get("verifyKey");
|
||||||
User user = userService.loginByPhone(request.getPhone(), request.getCode());
|
if (!Objects.equals(redisVerifyKey, verifyKey)) {
|
||||||
// 生成并返回JWT令牌(实际项目中需要实现JWT逻辑)
|
return Result.error().message("verify key is not same");
|
||||||
|
}
|
||||||
return ResponseEntity.ok("Login successful: " + user.getUsername());
|
String redisCode = map.get("code").toString();
|
||||||
} catch (RuntimeException e) {
|
if (!Objects.equals(redisCode, code)) {
|
||||||
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
|
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");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@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);
|
||||||
public void setPhone(String phone) {
|
return Result.ok().message("set password success");
|
||||||
this.phone = phone;
|
} else {
|
||||||
}
|
return Result.error().message("token is not same");
|
||||||
|
}
|
||||||
public String getCode() {
|
} else {
|
||||||
return code;
|
return Result.error().message("password is not same");
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
public void setCode(String code) {
|
return Result.error().message("Authorization header is incorrect");
|
||||||
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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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));
|
||||||
|
|||||||
@@ -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,31 +23,33 @@ 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 refresh_token JWT格式刷新令牌
|
* @param access_token JWT格式访问令牌
|
||||||
* @param accessExpireMs AccessToken有效期(毫秒)
|
* @param refresh_token JWT格式刷新令牌
|
||||||
|
* @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 设备指纹
|
||||||
* @return 初始化过期时间的TokenPair
|
* @return 初始化过期时间的TokenPair
|
||||||
*/
|
*/
|
||||||
public static TokenPair create(String accessToken, String refreshToken, String deviceId) {
|
public static TokenPair create(String accessToken, String refreshToken, String 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,7 +82,8 @@ 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;
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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;
|
||||||
|
|||||||
@@ -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);
|
||||||
}
|
}
|
||||||
@@ -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,19 +50,23 @@ 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. 创建新用户
|
||||||
|
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. 创建新用户
|
return userRepository.save(user);
|
||||||
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);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
public User loginByPhone(String phone, String code) {
|
public User loginByPhone(String phone, String code) {
|
||||||
@@ -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");
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
@@ -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 Token(key: user:refresh:<username>, value: refreshId)
|
// Redis存储Refresh Token(key: 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);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
}
|
}
|
||||||
Reference in New Issue
Block a user