diff --git a/pom.xml b/pom.xml
index 746270a..cf53816 100644
--- a/pom.xml
+++ b/pom.xml
@@ -54,6 +54,14 @@
springdoc-openapi-starter-webmvc-ui
2.1.0
+
+ org.springframework.boot
+ spring-boot-starter-security
+
+
+ org.springframework.boot
+ spring-boot-starter-data-jpa
+
org.projectlombok
@@ -76,6 +84,12 @@
runtime
8.0.33
+
+
+ com.h2database
+ h2
+ runtime
+
@@ -90,6 +104,25 @@
dysmsapi20170525
3.1.1
+
+
+
+ io.jsonwebtoken
+ jjwt-api
+ 0.12.3
+
+
+ io.jsonwebtoken
+ jjwt-impl
+ 0.12.3
+ runtime
+
+
+ io.jsonwebtoken
+ jjwt-jackson
+ 0.12.3
+ runtime
+
diff --git a/src/main/java/com/ttstd/videotablet/config/CommonConfig.java b/src/main/java/com/ttstd/videotablet/config/CommonConfig.java
new file mode 100644
index 0000000..3c8af43
--- /dev/null
+++ b/src/main/java/com/ttstd/videotablet/config/CommonConfig.java
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ttstd/videotablet/redis/RedisConfig.java b/src/main/java/com/ttstd/videotablet/config/RedisConfig.java
similarity index 96%
rename from src/main/java/com/ttstd/videotablet/redis/RedisConfig.java
rename to src/main/java/com/ttstd/videotablet/config/RedisConfig.java
index ca62acd..da18221 100644
--- a/src/main/java/com/ttstd/videotablet/redis/RedisConfig.java
+++ b/src/main/java/com/ttstd/videotablet/config/RedisConfig.java
@@ -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.Configuration;
diff --git a/src/main/java/com/ttstd/videotablet/config/SecurityConfig.java b/src/main/java/com/ttstd/videotablet/config/SecurityConfig.java
new file mode 100644
index 0000000..980645f
--- /dev/null
+++ b/src/main/java/com/ttstd/videotablet/config/SecurityConfig.java
@@ -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();
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ttstd/videotablet/controller/HelloController.java b/src/main/java/com/ttstd/videotablet/controller/HelloController.java
index aeb2f88..8912fb7 100644
--- a/src/main/java/com/ttstd/videotablet/controller/HelloController.java
+++ b/src/main/java/com/ttstd/videotablet/controller/HelloController.java
@@ -14,7 +14,7 @@ public class HelloController {
@Autowired
private StringRedisTemplate stringRedisTemplate;
- @GetMapping("/")
+ @GetMapping("/public/hello")
public Result getMethodName() {
return Result.ok().message("Welcome to Yijiantong");
}
@@ -24,7 +24,7 @@ public class HelloController {
*
* @return
*/
- @PostMapping("/set")
+ @PostMapping("/public/set")
public Result setRedis(@RequestParam(value = "username") String username) {
//存储 key-value 键值对: "username"-"jaychou"
stringRedisTemplate.opsForValue().set("username", username);
@@ -36,7 +36,7 @@ public class HelloController {
*
* @return
*/
- @GetMapping("/get")
+ @GetMapping("/public/get")
public Result getRedis(@RequestParam(value = "username") String username) {
//通过 key 值读取 value
String result = stringRedisTemplate.opsForValue().get(username);
diff --git a/src/main/java/com/ttstd/videotablet/controller/LoginController.java b/src/main/java/com/ttstd/videotablet/controller/LoginController.java
new file mode 100644
index 0000000..7784fe9
--- /dev/null
+++ b/src/main/java/com/ttstd/videotablet/controller/LoginController.java
@@ -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 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 map = (Map) 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;
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/src/main/java/com/ttstd/videotablet/controller/SmsController.java b/src/main/java/com/ttstd/videotablet/controller/SmsController.java
index 930cb33..97c6af5 100644
--- a/src/main/java/com/ttstd/videotablet/controller/SmsController.java
+++ b/src/main/java/com/ttstd/videotablet/controller/SmsController.java
@@ -16,7 +16,6 @@ import org.springframework.web.bind.annotation.RestController;
import java.time.Duration;
import java.util.HashMap;
import java.util.Map;
-import java.util.concurrent.TimeUnit;
@RestController
public class SmsController {
@@ -29,21 +28,31 @@ public class SmsController {
@Autowired
private RedisTemplate redisTemplate;
- @GetMapping("/verify_code")
+ @GetMapping("/public/verify_code")
public Result getMethodName(@RequestParam("phone") String phone) {
if (TextUtils.isEmpty(phone)) {
return Result.error().message("phone number is empty");
}
- String oldCode = stringRedisTemplate.opsForValue().get(phone);
- if (TextUtils.isEmpty(oldCode)) {
- String code = SendSms.generatedcode(6);
- return sendCode(phone, code);
+ Map map = (Map) redisTemplate.opsForValue().get(phone);
+ if (map != null) {
+ String oldCode = (String) map.get("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 {
- 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 {
com.aliyun.dysmsapi20170525.Client client = createClient();
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();
// 复制代码运行请自行打印 API 的返回值
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())) {
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 map = new HashMap<>();
- map.put("phone", phone);
map.put("code", code);
map.put("sms", sendSmsResponse.getBody().getMessage());
map.put("verifyKey", randomString);
+ map.put("sentAt", now);
map.put("expireAt", expireAt);
//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);
} else {
return Result.error().message(sendSmsResponse.getBody().getMessage());
diff --git a/src/main/java/com/ttstd/videotablet/dto/TokenPair.java b/src/main/java/com/ttstd/videotablet/dto/TokenPair.java
new file mode 100644
index 0000000..f10d51f
--- /dev/null
+++ b/src/main/java/com/ttstd/videotablet/dto/TokenPair.java
@@ -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 // 保持设备一致性
+ );
+ }
+}
diff --git a/src/main/java/com/ttstd/videotablet/entity/User.java b/src/main/java/com/ttstd/videotablet/entity/User.java
new file mode 100644
index 0000000..44c6b84
--- /dev/null
+++ b/src/main/java/com/ttstd/videotablet/entity/User.java
@@ -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;
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ttstd/videotablet/repository/UserRepository.java b/src/main/java/com/ttstd/videotablet/repository/UserRepository.java
new file mode 100644
index 0000000..fcb5f05
--- /dev/null
+++ b/src/main/java/com/ttstd/videotablet/repository/UserRepository.java
@@ -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 {
+ Optional findByUsername(String username);
+ boolean existsByUsername(String username);
+ boolean existsByEmail(String email);
+ Optional findByPhone(String phone);
+ boolean existsByPhone(String phone);
+}
\ No newline at end of file
diff --git a/src/main/java/com/ttstd/videotablet/service/UserService.java b/src/main/java/com/ttstd/videotablet/service/UserService.java
new file mode 100644
index 0000000..aa75c1e
--- /dev/null
+++ b/src/main/java/com/ttstd/videotablet/service/UserService.java
@@ -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 redisTemplate;
+
+ @Autowired
+ public UserService(UserRepository userRepository, PasswordEncoder passwordEncoder, RedisTemplate 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 codeMap = (Map) 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 codeMap = (Map) 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));
+ }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ttstd/videotablet/utils/JwtUtil.java b/src/main/java/com/ttstd/videotablet/utils/JwtUtil.java
new file mode 100644
index 0000000..5f59422
--- /dev/null
+++ b/src/main/java/com/ttstd/videotablet/utils/JwtUtil.java
@@ -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:, value: refreshId)
+ redisTemplate.opsForValue().set(
+ "user:refresh:" + username,
+ refreshId,
+ refreshExpire,
+ TimeUnit.MILLISECONDS
+ );
+
+ return new TokenPair(accessToken, refreshToken);
+ }
+
+ // Token解析与校验(略)
+}
\ No newline at end of file
diff --git a/src/main/resources/application-debug.properties b/src/main/resources/application-debug.properties
index c9fdfc5..19d1ab6 100644
--- a/src/main/resources/application-debug.properties
+++ b/src/main/resources/application-debug.properties
@@ -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-active=8
spring.data.redis.lettuce.pool.max-wait=1ms
-spring.data.redis.lettuce.shutdown-timeout=100ms
\ No newline at end of file
+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
\ No newline at end of file