无法运行。增加注册逻辑
This commit is contained in:
33
pom.xml
33
pom.xml
@@ -54,6 +54,14 @@
|
|||||||
<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-security</artifactId>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>org.springframework.boot</groupId>
|
||||||
|
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<dependency>
|
<dependency>
|
||||||
<groupId>org.projectlombok</groupId>
|
<groupId>org.projectlombok</groupId>
|
||||||
@@ -76,6 +84,12 @@
|
|||||||
<scope>runtime</scope>
|
<scope>runtime</scope>
|
||||||
<version>8.0.33</version>
|
<version>8.0.33</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
<!--添加数据库驱动依赖(根据实际数据库选择)-->
|
||||||
|
<dependency>
|
||||||
|
<groupId>com.h2database</groupId>
|
||||||
|
<artifactId>h2</artifactId>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
|
||||||
<!--异步处理-->
|
<!--异步处理-->
|
||||||
<dependency>
|
<dependency>
|
||||||
@@ -90,6 +104,25 @@
|
|||||||
<artifactId>dysmsapi20170525</artifactId>
|
<artifactId>dysmsapi20170525</artifactId>
|
||||||
<version>3.1.1</version>
|
<version>3.1.1</version>
|
||||||
</dependency>
|
</dependency>
|
||||||
|
|
||||||
|
<!-- JJWT (最新版) -->
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-api</artifactId>
|
||||||
|
<version>0.12.3</version>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-impl</artifactId>
|
||||||
|
<version>0.12.3</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
|
<dependency>
|
||||||
|
<groupId>io.jsonwebtoken</groupId>
|
||||||
|
<artifactId>jjwt-jackson</artifactId>
|
||||||
|
<version>0.12.3</version>
|
||||||
|
<scope>runtime</scope>
|
||||||
|
</dependency>
|
||||||
</dependencies>
|
</dependencies>
|
||||||
|
|
||||||
<build>
|
<build>
|
||||||
|
|||||||
14
src/main/java/com/ttstd/videotablet/config/CommonConfig.java
Normal file
14
src/main/java/com/ttstd/videotablet/config/CommonConfig.java
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package com.ttstd.videotablet.config;
|
||||||
|
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
public class CommonConfig {
|
||||||
|
@Bean
|
||||||
|
public PasswordEncoder passwordEncoder() {
|
||||||
|
return new BCryptPasswordEncoder();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,4 +1,4 @@
|
|||||||
package com.ttstd.videotablet.redis;
|
package com.ttstd.videotablet.config;
|
||||||
|
|
||||||
import org.springframework.context.annotation.Bean;
|
import org.springframework.context.annotation.Bean;
|
||||||
import org.springframework.context.annotation.Configuration;
|
import org.springframework.context.annotation.Configuration;
|
||||||
@@ -0,0 +1,40 @@
|
|||||||
|
package com.ttstd.videotablet.config;
|
||||||
|
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.context.annotation.Bean;
|
||||||
|
import org.springframework.context.annotation.Configuration;
|
||||||
|
import org.springframework.security.authentication.AuthenticationManager;
|
||||||
|
import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
|
||||||
|
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
|
||||||
|
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.web.SecurityFilterChain;
|
||||||
|
|
||||||
|
@Configuration
|
||||||
|
@EnableWebSecurity
|
||||||
|
public class SecurityConfig {
|
||||||
|
|
||||||
|
private final UserDetailsService userDetailsService;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public SecurityConfig(UserDetailsService userDetailsService) {
|
||||||
|
this.userDetailsService = userDetailsService;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Bean
|
||||||
|
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
|
||||||
|
http.csrf(csrf -> csrf.disable())
|
||||||
|
.authorizeHttpRequests(auth -> auth
|
||||||
|
.requestMatchers("/public/**").permitAll()
|
||||||
|
.requestMatchers("/admin/**").hasRole("ADMIN")
|
||||||
|
.anyRequest().authenticated()
|
||||||
|
);
|
||||||
|
return http.build();
|
||||||
|
}
|
||||||
|
|
||||||
|
// 添加AuthenticationManager bean定义
|
||||||
|
@Bean
|
||||||
|
public AuthenticationManager authenticationManager(AuthenticationConfiguration authConfig) throws Exception {
|
||||||
|
return authConfig.getAuthenticationManager();
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -14,7 +14,7 @@ public class HelloController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private StringRedisTemplate stringRedisTemplate;
|
private StringRedisTemplate stringRedisTemplate;
|
||||||
|
|
||||||
@GetMapping("/")
|
@GetMapping("/public/hello")
|
||||||
public Result getMethodName() {
|
public Result getMethodName() {
|
||||||
return Result.ok().message("Welcome to Yijiantong");
|
return Result.ok().message("Welcome to Yijiantong");
|
||||||
}
|
}
|
||||||
@@ -24,7 +24,7 @@ public class HelloController {
|
|||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@PostMapping("/set")
|
@PostMapping("/public/set")
|
||||||
public Result setRedis(@RequestParam(value = "username") String username) {
|
public Result setRedis(@RequestParam(value = "username") String username) {
|
||||||
//存储 key-value 键值对: "username"-"jaychou"
|
//存储 key-value 键值对: "username"-"jaychou"
|
||||||
stringRedisTemplate.opsForValue().set("username", username);
|
stringRedisTemplate.opsForValue().set("username", username);
|
||||||
@@ -36,7 +36,7 @@ public class HelloController {
|
|||||||
*
|
*
|
||||||
* @return
|
* @return
|
||||||
*/
|
*/
|
||||||
@GetMapping("/get")
|
@GetMapping("/public/get")
|
||||||
public Result getRedis(@RequestParam(value = "username") String username) {
|
public Result getRedis(@RequestParam(value = "username") String username) {
|
||||||
//通过 key 值读取 value
|
//通过 key 值读取 value
|
||||||
String result = stringRedisTemplate.opsForValue().get(username);
|
String result = stringRedisTemplate.opsForValue().get(username);
|
||||||
|
|||||||
@@ -0,0 +1,150 @@
|
|||||||
|
package com.ttstd.videotablet.controller;
|
||||||
|
|
||||||
|
import com.ttstd.videotablet.entity.User;
|
||||||
|
import com.ttstd.videotablet.service.UserService;
|
||||||
|
import com.ttstd.videotablet.utils.TextUtils;
|
||||||
|
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.web.bind.annotation.PostMapping;
|
||||||
|
import org.springframework.web.bind.annotation.RequestBody;
|
||||||
|
import org.springframework.web.bind.annotation.RestController;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
|
import java.util.Objects;
|
||||||
|
|
||||||
|
@RestController
|
||||||
|
public class LoginController {
|
||||||
|
|
||||||
|
private final UserService userService;
|
||||||
|
private final AuthenticationManager authenticationManager;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public LoginController(UserService userService, AuthenticationManager authenticationManager) {
|
||||||
|
this.userService = userService;
|
||||||
|
this.authenticationManager = authenticationManager;
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/public/register")
|
||||||
|
public ResponseEntity<?> registerUser(@RequestBody RegisterRequest registerRequest) {
|
||||||
|
try {
|
||||||
|
userService.registerUser(registerRequest.getUsername(), registerRequest.getPassword());
|
||||||
|
return new ResponseEntity<>("User registered successfully", HttpStatus.CREATED);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/public/login")
|
||||||
|
public ResponseEntity<?> login() {
|
||||||
|
// 登录逻辑由Spring Security自动处理
|
||||||
|
return ResponseEntity.ok("Login successful");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 注册请求参数类
|
||||||
|
public static class RegisterRequest {
|
||||||
|
private String username;
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
@PostMapping("/public/registerByPhone")
|
||||||
|
public ResponseEntity<?> registerByPhone(@RequestBody PhoneRequest request) {
|
||||||
|
String requestVerifyKey = request.getVerifyKey();
|
||||||
|
if (TextUtils.isEmpty(requestVerifyKey)) {
|
||||||
|
return new ResponseEntity<>("verify key is empty", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
String phone = request.getPhone();
|
||||||
|
Map<String, Object> map = (Map<String, Object>) redisTemplate.opsForValue().get(phone);
|
||||||
|
if (map != null) {
|
||||||
|
String verifyKey = (String) map.get("verifyKey");
|
||||||
|
if (!Objects.equals(verifyKey, requestVerifyKey)) {
|
||||||
|
return new ResponseEntity<>("verify key is not same", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
String code = map.get("code").toString();
|
||||||
|
if (!Objects.equals(code, request.getCode())) {
|
||||||
|
return new ResponseEntity<>("code is not same", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
User user = userService.registerByPhone(request.getPhone(), request.getCode(), new Date());
|
||||||
|
return new ResponseEntity<>(user, HttpStatus.CREATED);
|
||||||
|
} catch (RuntimeException e) {
|
||||||
|
return new ResponseEntity<>(e.getMessage(), HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return new ResponseEntity<>("verify key is expired", HttpStatus.BAD_REQUEST);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@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 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;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
@@ -16,7 +16,6 @@ import org.springframework.web.bind.annotation.RestController;
|
|||||||
import java.time.Duration;
|
import java.time.Duration;
|
||||||
import java.util.HashMap;
|
import java.util.HashMap;
|
||||||
import java.util.Map;
|
import java.util.Map;
|
||||||
import java.util.concurrent.TimeUnit;
|
|
||||||
|
|
||||||
@RestController
|
@RestController
|
||||||
public class SmsController {
|
public class SmsController {
|
||||||
@@ -29,21 +28,31 @@ public class SmsController {
|
|||||||
@Autowired
|
@Autowired
|
||||||
private RedisTemplate<String, Object> redisTemplate;
|
private RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
@GetMapping("/verify_code")
|
@GetMapping("/public/verify_code")
|
||||||
public Result getMethodName(@RequestParam("phone") String phone) {
|
public Result getMethodName(@RequestParam("phone") String phone) {
|
||||||
if (TextUtils.isEmpty(phone)) {
|
if (TextUtils.isEmpty(phone)) {
|
||||||
return Result.error().message("phone number is empty");
|
return Result.error().message("phone number is empty");
|
||||||
}
|
}
|
||||||
String oldCode = stringRedisTemplate.opsForValue().get(phone);
|
Map<String, Object> map = (Map<String, Object>) redisTemplate.opsForValue().get(phone);
|
||||||
if (TextUtils.isEmpty(oldCode)) {
|
if (map != null) {
|
||||||
String code = SendSms.generatedcode(6);
|
String oldCode = (String) map.get("code");
|
||||||
return sendCode(phone, code);
|
long sentTime = (Long) map.get("sentAt");
|
||||||
|
if (System.currentTimeMillis() - sentTime < Duration.ofMinutes(1).toMillis()) {
|
||||||
|
return Result.error().message("code has been sent, please try again after 1 minute");
|
||||||
|
}
|
||||||
|
if (TextUtils.isEmpty(oldCode)) {
|
||||||
|
String code = SendSms.generatedcode(6);
|
||||||
|
return sendCode(phone, code, false);
|
||||||
|
} else {
|
||||||
|
return sendCode(phone, oldCode, true);
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
return sendCode(phone, oldCode);
|
String code = SendSms.generatedcode(6);
|
||||||
|
return sendCode(phone, code, false);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
private Result sendCode(String phone, String code) {
|
private Result sendCode(String phone, String code, boolean sent) {
|
||||||
try {
|
try {
|
||||||
com.aliyun.dysmsapi20170525.Client client = createClient();
|
com.aliyun.dysmsapi20170525.Client client = createClient();
|
||||||
com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = new com.aliyun.dysmsapi20170525.models.SendSmsRequest()
|
com.aliyun.dysmsapi20170525.models.SendSmsRequest sendSmsRequest = new com.aliyun.dysmsapi20170525.models.SendSmsRequest()
|
||||||
@@ -54,20 +63,20 @@ public class SmsController {
|
|||||||
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
|
com.aliyun.teautil.models.RuntimeOptions runtime = new com.aliyun.teautil.models.RuntimeOptions();
|
||||||
// 复制代码运行请自行打印 API 的返回值
|
// 复制代码运行请自行打印 API 的返回值
|
||||||
SendSmsResponse sendSmsResponse = client.sendSmsWithOptions(sendSmsRequest, runtime);
|
SendSmsResponse sendSmsResponse = client.sendSmsWithOptions(sendSmsRequest, runtime);
|
||||||
// System.out.println(sendSmsResponse.body.toMap());
|
|
||||||
// System.out.println(sendSmsResponse.getBody().getMessage());
|
|
||||||
// System.out.println(sendSmsResponse.getBody().getCode());
|
|
||||||
if ("OK".equals(sendSmsResponse.getBody().getCode())) {
|
if ("OK".equals(sendSmsResponse.getBody().getCode())) {
|
||||||
String randomString = RandomStringUtils.randomAlphanumeric(32);
|
String randomString = RandomStringUtils.randomAlphanumeric(32);
|
||||||
long expireAt = System.currentTimeMillis() + Duration.ofMinutes(5).toMillis();
|
long now = System.currentTimeMillis();
|
||||||
|
long expireAt = now + Duration.ofMinutes(LOGIN_CODE_TTL).toMillis();
|
||||||
Map<String, Object> map = new HashMap<>();
|
Map<String, Object> map = new HashMap<>();
|
||||||
map.put("phone", phone);
|
|
||||||
map.put("code", code);
|
map.put("code", code);
|
||||||
map.put("sms", sendSmsResponse.getBody().getMessage());
|
map.put("sms", sendSmsResponse.getBody().getMessage());
|
||||||
map.put("verifyKey", randomString);
|
map.put("verifyKey", randomString);
|
||||||
|
map.put("sentAt", now);
|
||||||
map.put("expireAt", expireAt);
|
map.put("expireAt", expireAt);
|
||||||
//4.保存验证码到Redis,并且设置有效期5分钟
|
//4.保存验证码到Redis,并且设置有效期5分钟
|
||||||
redisTemplate.opsForValue().set(phone, map, Duration.ofMinutes(5));
|
if (!sent) {
|
||||||
|
redisTemplate.opsForValue().set(phone, map, Duration.ofMinutes(5));
|
||||||
|
}
|
||||||
return Result.ok().data(map);
|
return Result.ok().data(map);
|
||||||
} else {
|
} else {
|
||||||
return Result.error().message(sendSmsResponse.getBody().getMessage());
|
return Result.error().message(sendSmsResponse.getBody().getMessage());
|
||||||
|
|||||||
92
src/main/java/com/ttstd/videotablet/dto/TokenPair.java
Normal file
92
src/main/java/com/ttstd/videotablet/dto/TokenPair.java
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package com.ttstd.videotablet.dto;
|
||||||
|
|
||||||
|
import lombok.Data;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 双Token认证令牌对(AccessToken + RefreshToken)
|
||||||
|
* 设计要点:
|
||||||
|
* 1. 访问令牌短期有效(30分钟),刷新令牌长期有效(7天)
|
||||||
|
* 2. 绑定设备ID防止跨设备滥用[1](@ref)
|
||||||
|
* 3. 精确控制双Token过期时间
|
||||||
|
*/
|
||||||
|
@Data
|
||||||
|
public class TokenPair {
|
||||||
|
// 访问令牌(用于API请求认证)
|
||||||
|
private String accessToken;
|
||||||
|
// 刷新令牌(用于获取新AccessToken)
|
||||||
|
private String refreshToken;
|
||||||
|
// AccessToken过期时间戳(毫秒)
|
||||||
|
private long accessExpiresAt;
|
||||||
|
// RefreshToken过期时间戳(毫秒)
|
||||||
|
private long refreshExpiresAt;
|
||||||
|
// 关联设备指纹(防御中间人攻击)[1](@ref)
|
||||||
|
private String deviceId;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全参数构造器(安全增强版)
|
||||||
|
* @param accessToken JWT格式访问令牌
|
||||||
|
* @param refreshToken JWT格式刷新令牌
|
||||||
|
* @param accessExpireMs AccessToken有效期(毫秒)
|
||||||
|
* @param refreshExpireMs RefreshToken有效期(毫秒)
|
||||||
|
* @param deviceId 客户端设备指纹
|
||||||
|
*/
|
||||||
|
public TokenPair(String accessToken, String refreshToken,
|
||||||
|
long accessExpireMs, long refreshExpireMs,
|
||||||
|
String deviceId) {
|
||||||
|
this.accessToken = accessToken;
|
||||||
|
this.refreshToken = refreshToken;
|
||||||
|
this.accessExpiresAt = System.currentTimeMillis() + accessExpireMs;
|
||||||
|
this.refreshExpiresAt = System.currentTimeMillis() + refreshExpireMs;
|
||||||
|
this.deviceId = deviceId;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 快速创建方法(推荐)
|
||||||
|
* @param accessToken 访问令牌
|
||||||
|
* @param refreshToken 刷新令牌
|
||||||
|
* @param deviceId 设备指纹
|
||||||
|
* @return 初始化过期时间的TokenPair
|
||||||
|
*/
|
||||||
|
public static TokenPair create(String accessToken, String refreshToken, String deviceId) {
|
||||||
|
return new TokenPair(
|
||||||
|
accessToken,
|
||||||
|
refreshToken,
|
||||||
|
30 * 60 * 1000, // 30分钟有效期
|
||||||
|
7 * 24 * 60 * 60 * 1000, // 7天有效期
|
||||||
|
deviceId
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查AccessToken是否过期
|
||||||
|
* @return true=已过期,false=有效
|
||||||
|
*/
|
||||||
|
public boolean isAccessExpired() {
|
||||||
|
return System.currentTimeMillis() > accessExpiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 检查RefreshToken是否过期
|
||||||
|
* @return true=已过期,false=有效
|
||||||
|
*/
|
||||||
|
public boolean isRefreshExpired() {
|
||||||
|
return System.currentTimeMillis() > refreshExpiresAt;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 安全刷新令牌(生成新TokenPair)
|
||||||
|
* @param newAccessToken 新访问令牌
|
||||||
|
* @param newRefreshToken 新刷新令牌
|
||||||
|
* @return 更新后的TokenPair(保留原设备ID)
|
||||||
|
*/
|
||||||
|
public TokenPair refresh(String newAccessToken, String newRefreshToken) {
|
||||||
|
return new TokenPair(
|
||||||
|
newAccessToken,
|
||||||
|
newRefreshToken,
|
||||||
|
this.accessExpiresAt - System.currentTimeMillis(), // 剩余时间延续
|
||||||
|
this.refreshExpiresAt - System.currentTimeMillis(),
|
||||||
|
this.deviceId // 保持设备一致性
|
||||||
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
109
src/main/java/com/ttstd/videotablet/entity/User.java
Normal file
109
src/main/java/com/ttstd/videotablet/entity/User.java
Normal file
@@ -0,0 +1,109 @@
|
|||||||
|
package com.ttstd.videotablet.entity;
|
||||||
|
|
||||||
|
import jakarta.persistence.*;
|
||||||
|
import org.springframework.security.core.GrantedAuthority;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
|
||||||
|
import java.util.Collection;
|
||||||
|
import java.util.Collections;
|
||||||
|
import java.util.Date;
|
||||||
|
|
||||||
|
@Entity
|
||||||
|
@Table(name = "users")
|
||||||
|
public class User implements UserDetails {
|
||||||
|
|
||||||
|
@Id
|
||||||
|
@GeneratedValue(strategy = GenerationType.IDENTITY)
|
||||||
|
@Column(name = "id")
|
||||||
|
private Long id;
|
||||||
|
|
||||||
|
@Column(unique = true)
|
||||||
|
private String username;
|
||||||
|
|
||||||
|
@Column()
|
||||||
|
private String password;
|
||||||
|
|
||||||
|
@Column(unique = true)
|
||||||
|
private String email;
|
||||||
|
|
||||||
|
@Column(unique = true, nullable = false)
|
||||||
|
private String phone;
|
||||||
|
|
||||||
|
@Column(name = "create_time",unique = true, nullable = false)
|
||||||
|
private Date creatTime;
|
||||||
|
|
||||||
|
// Getters and Setters
|
||||||
|
@Override
|
||||||
|
public Collection<? extends GrantedAuthority> getAuthorities() {
|
||||||
|
return Collections.emptyList();
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAccountNonExpired() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isAccountNonLocked() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isCredentialsNonExpired() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public boolean isEnabled() {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Getters and Setters for id, username, password, email
|
||||||
|
public Long getId() {
|
||||||
|
return id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setId(Long id) {
|
||||||
|
this.id = id;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getUsername() {
|
||||||
|
return username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setUsername(String username) {
|
||||||
|
this.username = username;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPassword() {
|
||||||
|
return password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPassword(String password) {
|
||||||
|
this.password = password;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getEmail() {
|
||||||
|
return email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setEmail(String email) {
|
||||||
|
this.email = email;
|
||||||
|
}
|
||||||
|
|
||||||
|
public String getPhone() {
|
||||||
|
return phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setPhone(String phone) {
|
||||||
|
this.phone = phone;
|
||||||
|
}
|
||||||
|
|
||||||
|
public Date getCreatTime() {
|
||||||
|
return creatTime;
|
||||||
|
}
|
||||||
|
|
||||||
|
public void setCreatTime(Date creatTime) {
|
||||||
|
this.creatTime = creatTime;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,13 @@
|
|||||||
|
package com.ttstd.videotablet.repository;
|
||||||
|
|
||||||
|
import com.ttstd.videotablet.entity.User;
|
||||||
|
import org.springframework.data.jpa.repository.JpaRepository;
|
||||||
|
import java.util.Optional;
|
||||||
|
|
||||||
|
public interface UserRepository extends JpaRepository<User, Long> {
|
||||||
|
Optional<User> findByUsername(String username);
|
||||||
|
boolean existsByUsername(String username);
|
||||||
|
boolean existsByEmail(String email);
|
||||||
|
Optional<User> findByPhone(String phone);
|
||||||
|
boolean existsByPhone(String phone);
|
||||||
|
}
|
||||||
79
src/main/java/com/ttstd/videotablet/service/UserService.java
Normal file
79
src/main/java/com/ttstd/videotablet/service/UserService.java
Normal file
@@ -0,0 +1,79 @@
|
|||||||
|
package com.ttstd.videotablet.service;
|
||||||
|
|
||||||
|
import com.ttstd.videotablet.entity.User;
|
||||||
|
import com.ttstd.videotablet.repository.UserRepository;
|
||||||
|
import org.springframework.beans.factory.annotation.Autowired;
|
||||||
|
import org.springframework.data.redis.core.RedisTemplate;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetails;
|
||||||
|
import org.springframework.security.core.userdetails.UserDetailsService;
|
||||||
|
import org.springframework.security.core.userdetails.UsernameNotFoundException;
|
||||||
|
import org.springframework.security.crypto.password.PasswordEncoder;
|
||||||
|
import org.springframework.stereotype.Service;
|
||||||
|
|
||||||
|
import java.util.Date;
|
||||||
|
import java.util.Map;
|
||||||
|
|
||||||
|
@Service
|
||||||
|
public class UserService implements UserDetailsService {
|
||||||
|
|
||||||
|
private final UserRepository userRepository;
|
||||||
|
private final PasswordEncoder passwordEncoder;
|
||||||
|
private final RedisTemplate<String, Object> redisTemplate;
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, RedisTemplate<String, Object> redisTemplate) {
|
||||||
|
this.userRepository = userRepository;
|
||||||
|
this.passwordEncoder = passwordEncoder;
|
||||||
|
this.redisTemplate = redisTemplate;
|
||||||
|
}
|
||||||
|
|
||||||
|
public User registerUser(String username, String password) {
|
||||||
|
if (userRepository.existsByUsername(username)) {
|
||||||
|
throw new RuntimeException("Username already exists");
|
||||||
|
}
|
||||||
|
|
||||||
|
User user = new User();
|
||||||
|
user.setUsername(username);
|
||||||
|
user.setPassword(passwordEncoder.encode(password));
|
||||||
|
|
||||||
|
return userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public User registerByPhone(String phone, String code, Date createTime) {
|
||||||
|
// 1. 验证验证码
|
||||||
|
Map<String, Object> codeMap = (Map<String, Object>) redisTemplate.opsForValue().get(phone);
|
||||||
|
if (codeMap == null || !code.equals(codeMap.get("code").toString())) {
|
||||||
|
throw new RuntimeException("Invalid verification code");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 检查手机号是否已注册
|
||||||
|
if (userRepository.existsByPhone(phone)) {
|
||||||
|
throw new RuntimeException("Phone number already registered");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 创建新用户
|
||||||
|
User user = new User();
|
||||||
|
user.setPhone(phone);
|
||||||
|
user.setCreatTime(createTime);
|
||||||
|
|
||||||
|
return userRepository.save(user);
|
||||||
|
}
|
||||||
|
|
||||||
|
public User loginByPhone(String phone, String code) {
|
||||||
|
// 1. 验证验证码
|
||||||
|
Map<String, Object> codeMap = (Map<String, Object>) redisTemplate.opsForValue().get(phone);
|
||||||
|
if (codeMap == null || !code.equals(codeMap.get("code").toString())) {
|
||||||
|
throw new RuntimeException("Invalid verification code");
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 查询用户
|
||||||
|
return userRepository.findByPhone(phone)
|
||||||
|
.orElseThrow(() -> new RuntimeException("User not found with phone: " + phone));
|
||||||
|
}
|
||||||
|
|
||||||
|
@Override
|
||||||
|
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
|
||||||
|
return userRepository.findByUsername(username)
|
||||||
|
.orElseThrow(() -> new UsernameNotFoundException("User not found with username: " + username));
|
||||||
|
}
|
||||||
|
}
|
||||||
57
src/main/java/com/ttstd/videotablet/utils/JwtUtil.java
Normal file
57
src/main/java/com/ttstd/videotablet/utils/JwtUtil.java
Normal file
@@ -0,0 +1,57 @@
|
|||||||
|
package com.ttstd.videotablet.utils;
|
||||||
|
|
||||||
|
import com.ttstd.videotablet.dto.TokenPair;
|
||||||
|
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 java.util.Date;
|
||||||
|
import java.util.UUID;
|
||||||
|
import java.util.concurrent.TimeUnit;
|
||||||
|
|
||||||
|
@Component
|
||||||
|
public class JwtUtil {
|
||||||
|
|
||||||
|
@Autowired
|
||||||
|
private StringRedisTemplate redisTemplate;
|
||||||
|
|
||||||
|
@Value("${jwt.secret}") private String secret;
|
||||||
|
@Value("${jwt.access-expire}") private Long accessExpire;
|
||||||
|
@Value("${jwt.refresh-expire}") private Long refreshExpire;
|
||||||
|
|
||||||
|
// 生成双Token(关联设备指纹)
|
||||||
|
public TokenPair generateTokenPair(String username, String deviceId) {
|
||||||
|
String accessToken = Jwts.builder()
|
||||||
|
.subject(username)
|
||||||
|
.claim("type", "ACCESS")
|
||||||
|
.claim("deviceId", deviceId) // 绑定设备
|
||||||
|
.issuedAt(new Date())
|
||||||
|
.expiration(new Date(System.currentTimeMillis() + accessExpire))
|
||||||
|
.signWith(Keys.hmacShaKeyFor(secret.getBytes()))
|
||||||
|
.compact();
|
||||||
|
|
||||||
|
String refreshId = UUID.randomUUID().toString();
|
||||||
|
String refreshToken = Jwts.builder()
|
||||||
|
.subject(username)
|
||||||
|
.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)
|
||||||
|
redisTemplate.opsForValue().set(
|
||||||
|
"user:refresh:" + username,
|
||||||
|
refreshId,
|
||||||
|
refreshExpire,
|
||||||
|
TimeUnit.MILLISECONDS
|
||||||
|
);
|
||||||
|
|
||||||
|
return new TokenPair(accessToken, refreshToken);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Token解析与校验(略)
|
||||||
|
}
|
||||||
@@ -21,4 +21,10 @@ spring.data.redis.lettuce.pool.min-idle=5
|
|||||||
spring.data.redis.lettuce.pool.max-idle=10
|
spring.data.redis.lettuce.pool.max-idle=10
|
||||||
spring.data.redis.lettuce.pool.max-active=8
|
spring.data.redis.lettuce.pool.max-active=8
|
||||||
spring.data.redis.lettuce.pool.max-wait=1ms
|
spring.data.redis.lettuce.pool.max-wait=1ms
|
||||||
spring.data.redis.lettuce.shutdown-timeout=100ms
|
spring.data.redis.lettuce.shutdown-timeout=100ms
|
||||||
|
|
||||||
|
# Hibernate配置
|
||||||
|
#spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MariaDBDialect
|
||||||
|
spring.jpa.properties.hibernate.dialect=org.hibernate.dialect.MySQL8Dialect
|
||||||
|
spring.jpa.hibernate.ddl-auto=update
|
||||||
|
spring.jpa.show-sql=true
|
||||||
Reference in New Issue
Block a user