From 9832336b8908f6a273479fcfb155c9bea848b799 Mon Sep 17 00:00:00 2001 From: tongtongstudio Date: Sat, 2 Aug 2025 10:04:18 +0800 Subject: [PATCH] =?UTF-8?q?=E6=97=A0=E6=B3=95=E8=BF=90=E8=A1=8C=E3=80=82?= =?UTF-8?q?=E5=A2=9E=E5=8A=A0=E6=B3=A8=E5=86=8C=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pom.xml | 33 ++++ .../videotablet/config/CommonConfig.java | 14 ++ .../{redis => config}/RedisConfig.java | 2 +- .../videotablet/config/SecurityConfig.java | 40 +++++ .../controller/HelloController.java | 6 +- .../controller/LoginController.java | 150 ++++++++++++++++++ .../videotablet/controller/SmsController.java | 37 +++-- .../com/ttstd/videotablet/dto/TokenPair.java | 92 +++++++++++ .../com/ttstd/videotablet/entity/User.java | 109 +++++++++++++ .../repository/UserRepository.java | 13 ++ .../videotablet/service/UserService.java | 79 +++++++++ .../com/ttstd/videotablet/utils/JwtUtil.java | 57 +++++++ .../resources/application-debug.properties | 8 +- 13 files changed, 621 insertions(+), 19 deletions(-) create mode 100644 src/main/java/com/ttstd/videotablet/config/CommonConfig.java rename src/main/java/com/ttstd/videotablet/{redis => config}/RedisConfig.java (96%) create mode 100644 src/main/java/com/ttstd/videotablet/config/SecurityConfig.java create mode 100644 src/main/java/com/ttstd/videotablet/controller/LoginController.java create mode 100644 src/main/java/com/ttstd/videotablet/dto/TokenPair.java create mode 100644 src/main/java/com/ttstd/videotablet/entity/User.java create mode 100644 src/main/java/com/ttstd/videotablet/repository/UserRepository.java create mode 100644 src/main/java/com/ttstd/videotablet/service/UserService.java create mode 100644 src/main/java/com/ttstd/videotablet/utils/JwtUtil.java 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 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