diff --git a/sql/video_tablet_db.sql b/sql/video_tablet_db.sql index 5eb19e5..770e284 100644 --- a/sql/video_tablet_db.sql +++ b/sql/video_tablet_db.sql @@ -1,17 +1,17 @@ /* Navicat Premium Data Transfer - Source Server : local_mariadb - Source Server Type : MariaDB - Source Server Version : 110702 (11.7.2-MariaDB-ubu2404) - Source Host : localhost:3305 + Source Server : ttstd_tt + Source Server Type : MySQL + Source Server Version : 90400 (9.4.0) + Source Host : 139.199.77.221:13306 Source Schema : video_tablet_db - Target Server Type : MariaDB - Target Server Version : 110702 (11.7.2-MariaDB-ubu2404) + Target Server Type : MySQL + Target Server Version : 90400 (9.4.0) File Encoding : 65001 - Date: 11/08/2025 09:12:27 + Date: 17/08/2025 17:43:33 */ SET NAMES utf8mb4; @@ -22,7 +22,7 @@ SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- DROP TABLE IF EXISTS `devices_sn`; CREATE TABLE `devices_sn` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT, + `id` bigint NOT NULL AUTO_INCREMENT, `sn` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '设备唯一标识', `device_model` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '设备型号', `device_alias` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '绑定用户给设备的备注', @@ -30,16 +30,18 @@ CREATE TABLE `devices_sn` ( `bind_phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '绑定用户的手机', `add_time` datetime NOT NULL COMMENT '添加时间', `activation_time` datetime NULL DEFAULT NULL COMMENT '激活时间', - `bind_time` datetime(6) NULL DEFAULT NULL, + `bind_time` datetime(6) NULL DEFAULT NULL COMMENT '绑定时间', + `bind_sig` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '第一次绑定时生成的信令', + `token` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '绑定时生成的token', PRIMARY KEY (`id`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; +) ENGINE = InnoDB AUTO_INCREMENT = 2 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; -- ---------------------------- -- Table structure for ordinary_users -- ---------------------------- DROP TABLE IF EXISTS `ordinary_users`; CREATE TABLE `ordinary_users` ( - `id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'id自增', + `id` bigint NOT NULL AUTO_INCREMENT COMMENT 'id自增', `user_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '用户账号,类似于微信id', `phone` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL COMMENT '绑定手机', `password` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '密码', @@ -48,11 +50,11 @@ CREATE TABLE `ordinary_users` ( `create_time` datetime NOT NULL COMMENT '创建时间', `last_login_time` datetime(6) NULL DEFAULT NULL COMMENT '上次登录时间', `update_time` datetime(6) NULL DEFAULT NULL COMMENT '用户信息更新时间', - `gender` int(11) NULL DEFAULT NULL COMMENT '性别', + `gender` int NULL DEFAULT NULL COMMENT '性别', `avatar` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '头像', `wx_id` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT '绑定的微信id', PRIMARY KEY (`id`) USING BTREE, - UNIQUE INDEX `UKdu5v5sr43g5bfnji4vb8hg5s3`(`phone`) USING BTREE -) ENGINE = InnoDB AUTO_INCREMENT = 7 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic; + UNIQUE INDEX `UKdu5v5sr43g5bfnji4vb8hg5s3`(`phone` ASC) USING BTREE +) ENGINE = InnoDB AUTO_INCREMENT = 9 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = DYNAMIC; SET FOREIGN_KEY_CHECKS = 1; diff --git a/src/main/java/com/onekeycall/videotablet/config/SecurityConfig.java b/src/main/java/com/onekeycall/videotablet/config/SecurityConfig.java index c1c438a..673edb5 100644 --- a/src/main/java/com/onekeycall/videotablet/config/SecurityConfig.java +++ b/src/main/java/com/onekeycall/videotablet/config/SecurityConfig.java @@ -28,6 +28,8 @@ public class SecurityConfig { .requestMatchers("/ws/**").permitAll() .requestMatchers("/api/ws/**", "/topic/**").permitAll() .requestMatchers("/public/**").permitAll() + .requestMatchers("/sn/**").permitAll() + .requestMatchers("/user/**").permitAll() .requestMatchers("/admin/**").hasRole("ADMIN") // .requestMatchers("/user/**").hasAnyRole("USER", "ADMIN") .requestMatchers("/user/**").permitAll() diff --git a/src/main/java/com/onekeycall/videotablet/controller/AliyunSmsController.java b/src/main/java/com/onekeycall/videotablet/controller/AliyunSmsController.java index 1132080..c1e7cb0 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/AliyunSmsController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/AliyunSmsController.java @@ -6,6 +6,8 @@ import com.onekeycall.videotablet.result.Result; import com.onekeycall.videotablet.sms.SendSms; import com.onekeycall.videotablet.utils.TextUtils; import org.apache.commons.lang3.RandomStringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; @@ -20,6 +22,8 @@ import java.util.Map; public class AliyunSmsController { private static final int LOGIN_CODE_TTL = 5; + Logger logger = LoggerFactory.getLogger(AliyunSmsController.class); + //引入 redis @Autowired private StringRedisTemplate stringRedisTemplate; @@ -83,18 +87,18 @@ public class AliyunSmsController { } catch (TeaException error) { // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。 // 错误 message - System.out.println(error.getMessage()); + logger.error(error.getMessage()); // 诊断地址 - System.out.println(error.getData().get("Recommend")); + logger.error(error.getData().get("Recommend").toString()); com.aliyun.teautil.Common.assertAsString(error.message); return Result.error().data("sms", error.message); } catch (Exception _error) { TeaException error = new TeaException(_error.getMessage(), _error); // 此处仅做打印展示,请谨慎对待异常处理,在工程项目中切勿直接忽略异常。 // 错误 message - System.out.println(error.getMessage()); + logger.error(error.getMessage()); // 诊断地址 - System.out.println(error.getData().get("Recommend")); + logger.error(error.getData().get("Recommend").toString()); com.aliyun.teautil.Common.assertAsString(error.message); return Result.error().data("sms", error.message); } diff --git a/src/main/java/com/onekeycall/videotablet/controller/BindSnController.java b/src/main/java/com/onekeycall/videotablet/controller/BindSnController.java index 681e71a..a2f61cd 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/BindSnController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/BindSnController.java @@ -8,14 +8,27 @@ import com.onekeycall.videotablet.service.UserService; import com.onekeycall.videotablet.utils.JwtUtil; import com.onekeycall.videotablet.utils.PushUtils; import com.onekeycall.videotablet.utils.TextUtils; +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; +import io.jsonwebtoken.security.Keys; import org.apache.commons.lang3.RandomStringUtils; import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.web.bind.annotation.*; +import javax.crypto.SecretKey; +import java.util.Base64; import java.util.Date; +import java.util.Objects; +import java.util.UUID; +import java.util.concurrent.TimeUnit; @RestController -@RequestMapping("/public") +@RequestMapping("/sn") public class BindSnController { @Autowired @@ -25,17 +38,27 @@ public class BindSnController { @Autowired private DeviceSnService deviceSnService; + @Autowired + private RedisTemplate redisTemplate; + + /** + * 用户app发送绑定推送到手机 + * + * @param authHeader + * @param deviceId + * @param userId + * @param sn + * @return + */ @PostMapping("/bind_sn") public Result bindSn( @RequestHeader("Authorization") String authHeader, @RequestHeader("Device-ID") String deviceId, @RequestParam(value = "user_id") String userId, @RequestParam(value = "sn") String sn) { - // 1. 校验 Authorization 头 if (!authHeader.startsWith("Bearer ")) { return Result.error().message("Invalid Authorization header"); } String token = authHeader.substring(7); // 去掉 "Bearer " 前缀 - // 2. 校验 Token if (!jwtUtil.validateAccessToken(userId, token, deviceId)) { return Result.error().message("Invalid token"); } @@ -43,7 +66,6 @@ public class BindSnController { User user = userService.getUserByUserId(userId); String userPhone = user.getPhone(); - // 3. 校验 sn 是否存在 DeviceInfo deviceInfo = deviceSnService.findBySn(sn); if (deviceInfo == null) { return Result.error().message("sn not found"); @@ -55,9 +77,10 @@ public class BindSnController { try { - String randomString = RandomStringUtils.randomAlphanumeric(32); -// PushUtils.aliyunAsyncPush(randomString, userPhone, sn); - PushUtils.tpnsPush(randomString, userPhone, sn); + String verifyKey = RandomStringUtils.randomAlphanumeric(32); +// PushUtils.aliyunAsyncPush(verifyKey, userPhone, sn); + PushUtils.tpnsPush(verifyKey, userPhone, sn); + redisTemplate.opsForValue().set(sn, verifyKey, 1, TimeUnit.MINUTES); return Result.ok().message("send message success"); } catch (Exception e) { e.printStackTrace(); @@ -66,41 +89,63 @@ public class BindSnController { } + /** + * 平板根据返回的数据绑定手机 + * + * @param authHeader + * @param deviceId + * @param userId + * @param sn + * @param verifyKey + * @return + */ @PostMapping("/device_bind") public Result deviceBind( @RequestHeader("Authorization") String authHeader, @RequestHeader("Device-ID") String deviceId, @RequestParam(value = "user_id") String userId, @RequestParam(value = "sn") String sn, @RequestParam(value = "verify_key") String verifyKey) { - // 1. 校验 Authorization 头 + + String redisVerifyKey = (String) redisTemplate.opsForValue().get(sn); + if (redisVerifyKey == null) { + return Result.notFound().message("verify key not found"); + } + if (!Objects.equals(redisVerifyKey, verifyKey)) { + return Result.error().message("verify key is not same"); + } + if (!authHeader.startsWith("Bearer ")) { - return Result.error().message("Invalid Authorization header"); + return Result.unAuthorized().message("Invalid Authorization header"); } String token = authHeader.substring(7); // 去掉 "Bearer " 前缀 - // 2. 校验 Token if (!jwtUtil.validateAccessToken(userId, token, deviceId)) { - return Result.error().message("Invalid token"); + return Result.unAuthorized().message("Invalid token"); } User user = userService.getUserByUserId(userId); if (user == null) { - return Result.error().message("user not found"); + return Result.notFound().message("user not found"); } + String userPhone = user.getPhone(); - - - // 3. 校验 sn 是否存在 DeviceInfo oldDeviceInfo = deviceSnService.findBySn(sn); if (oldDeviceInfo == null) { - return Result.error().message("sn not found"); + return Result.notFound().message("sn not found"); } if (!TextUtils.isEmpty(oldDeviceInfo.getBindPhone())) { return Result.error().message("sn already bind"); } + String deviceSig = jwtUtil.generateDeviceSig(sn); + String deviceToken = jwtUtil.generateDeviceToken(sn, deviceId); + oldDeviceInfo.setBindPhone(userPhone); + oldDeviceInfo.setDeviceAlias(user.getNickname() + "的平板"); oldDeviceInfo.setBindTime(new Date()); + oldDeviceInfo.setDeviceModel(deviceId); + oldDeviceInfo.setBindSig(deviceSig); + oldDeviceInfo.setToken(deviceToken); oldDeviceInfo.setSn(sn); deviceSnService.save(oldDeviceInfo); @@ -113,4 +158,32 @@ public class BindSnController { } } + /** + * 获取平板sn绑定状态 + * + * @param Device_Token + * @param deviceId + * @param Device_Sig + * @param sn + * @return + */ + @GetMapping("/get_bind_statu") + public Result getBindStatus( + @RequestHeader("Device_Token") String Device_Token, @RequestHeader("Device-ID") String deviceId, + @RequestHeader("Device_Sig") String Device_Sig, @RequestParam(value = "sn") String sn) { + + + DeviceInfo deviceInfo = deviceSnService.findBySn(sn); + if (deviceInfo == null) { + return Result.notFound().message("sn not found"); + } + + if (TextUtils.isEmpty(deviceInfo.getBindPhone())) { + return Result.error().message("sn not bind"); + } + + return Result.ok().message("sn bind"); + } + + } diff --git a/src/main/java/com/onekeycall/videotablet/controller/DevicesController.java b/src/main/java/com/onekeycall/videotablet/controller/DevicesController.java new file mode 100644 index 0000000..0b1f07a --- /dev/null +++ b/src/main/java/com/onekeycall/videotablet/controller/DevicesController.java @@ -0,0 +1,24 @@ +package com.onekeycall.videotablet.controller; + +import com.onekeycall.videotablet.entity.DeviceInfo; +import com.onekeycall.videotablet.entity.User; +import com.onekeycall.videotablet.result.Result; +import com.onekeycall.videotablet.service.DeviceSnService; +import com.onekeycall.videotablet.service.UserService; +import com.onekeycall.videotablet.utils.JwtUtil; +import com.onekeycall.videotablet.utils.PushUtils; +import com.onekeycall.videotablet.utils.TextUtils; +import org.apache.commons.lang3.RandomStringUtils; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.web.bind.annotation.*; + +@RestController +@RequestMapping("/sn") +public class DevicesController { + @Autowired + private JwtUtil jwtUtil; + @Autowired + private DeviceSnService deviceSnService; + + +} diff --git a/src/main/java/com/onekeycall/videotablet/controller/LoginController.java b/src/main/java/com/onekeycall/videotablet/controller/LoginController.java index 5805dfd..5fdcf9d 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/LoginController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/LoginController.java @@ -5,6 +5,8 @@ import com.onekeycall.videotablet.entity.User; import com.onekeycall.videotablet.result.Result; import com.onekeycall.videotablet.service.UserService; import com.onekeycall.videotablet.utils.JwtUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.security.authentication.AuthenticationManager; @@ -26,6 +28,8 @@ public class LoginController { @Autowired private JwtUtil jwtUtil; + Logger logger = LoggerFactory.getLogger(LoginController.class); + @Autowired public LoginController(UserService userService, AuthenticationManager authenticationManager) { this.userService = userService; @@ -65,6 +69,8 @@ public class LoginController { public Result phoneLogin( @RequestHeader("Device-ID") String deviceId, @RequestParam String phone, @RequestParam String password) { + logger.info("phoneLogin: phone={}, password={}, deviceId={}", phone, password, deviceId); + User user = userService.getUserByPhone(phone); if (user == null) { return Result.error().message("手机号未注册"); @@ -93,7 +99,8 @@ public class LoginController { public Result registerByPhone( @RequestParam String phone, @RequestParam String code, @RequestParam(value = "verify_key") String verifyKey, @RequestParam(value = "device_id") String deviceId) { -// + logger.info("registerByPhone: phone={}, code={}, verifyKey={}, deviceId={}", phone, code, verifyKey, deviceId); + // if (TextUtils.isEmpty(verifyKey)) { // return Result.error().message("verify key is empty", HttpStatus.BAD_REQUEST); // } @@ -109,6 +116,7 @@ public class LoginController { } try { User user = userService.registerByPhone(phone, code, deviceId, new Date()); + logger.info("loginByPhoneCode: user={}", user.toString()); TokenPair tokenPair = jwtUtil.generateTokenPair(user.getUserId(), deviceId); //返回给app保存,access_token用来加入header请求接口,refresh_token用来更换access_token Map tokenMap = new HashMap<>(); @@ -122,7 +130,8 @@ public class LoginController { return Result.error().message(e.getMessage()); } } else { - return Result.error().message("verify key is expired"); +// return Result.error().message("verify key is expired"); + return Result.error().message("验证码已过期,请重新获取"); } } @@ -142,6 +151,7 @@ public class LoginController { } try { User user = userService.loginByPhone(phone, code); + logger.info("loginByPhoneCode: user={}", user); // 生成并返回JWT令牌(实际项目中需要实现JWT逻辑) TokenPair tokenPair = jwtUtil.generateTokenPair(user.getUserId(), deviceId); Map tokenMap = new HashMap<>(); @@ -158,9 +168,11 @@ public class LoginController { return Result.error().message("verify key is expired"); } } + // @PostMapping("/device_login") // public Result loginByDeviceSn(){ // // } + } \ No newline at end of file diff --git a/src/main/java/com/onekeycall/videotablet/controller/ManageSnController.java b/src/main/java/com/onekeycall/videotablet/controller/ManageSnController.java index f646e39..b11ee86 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/ManageSnController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/ManageSnController.java @@ -5,18 +5,24 @@ import com.onekeycall.videotablet.entity.User; import com.onekeycall.videotablet.result.Result; import com.onekeycall.videotablet.service.DeviceSnService; import com.onekeycall.videotablet.service.UserService; +import com.onekeycall.videotablet.utils.CXAESUtil; import com.onekeycall.videotablet.utils.JwtUtil; import com.onekeycall.videotablet.utils.PushUtils; import com.onekeycall.videotablet.utils.TextUtils; import org.apache.commons.lang3.RandomStringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import java.util.Date; +import java.util.HashMap; +import java.util.Map; @RestController @RequestMapping("/public") public class ManageSnController { + Logger logger = LoggerFactory.getLogger(ManageSnController.class); @Autowired private JwtUtil jwtUtil; @@ -50,8 +56,46 @@ public class ManageSnController { DeviceInfo deviceInfo = new DeviceInfo(); deviceInfo.setSn(sn); deviceInfo.setAddTime(new Date()); + deviceInfo.setDeviceModel(deviceId); deviceSnService.save(deviceInfo); return Result.ok(); } + + @GetMapping("/decode_sn") + public Result decodeSn( + @RequestHeader("Authorization") String authHeader, @RequestHeader("Device-ID") String deviceId, + @RequestParam(value = "user_id") String userId, @RequestParam(value = "encrypt_sn") String encryptSn) throws Exception { + + logger.info("Authorization = {}, Device-ID = {}, user_id = {}, encrypt_sn = {}", authHeader, deviceId, userId, encryptSn); + // 1. 校验 Authorization 头 + if (!authHeader.startsWith("Bearer ")) { + return Result.error().message("Invalid Authorization header"); + } + String token = authHeader.substring(7); // 去掉 "Bearer " 前缀 + + // 2. 校验 Token + if (!jwtUtil.validateAccessToken(userId, token, deviceId)) { + return Result.error().message("Invalid token"); + } + + // 3. 解密 sn + String sn = CXAESUtil.decrypt(CXAESUtil.key, encryptSn); + logger.info("sn = {}", sn); + if (TextUtils.isEmpty(sn)) { + return Result.error().message("sn decrypt failed"); + } + + DeviceInfo deviceInfo = deviceSnService.findBySn(sn); + if (deviceInfo == null) { + return Result.error().message("sn not found"); + } + if (!TextUtils.isEmpty(deviceInfo.getBindPhone())) { + return Result.ok().message("sn already bind"); + } + Map map = new HashMap<>(); + map.put("sn", sn); + + return Result.ok().data(map); + } } diff --git a/src/main/java/com/onekeycall/videotablet/controller/TencentSmsController.java b/src/main/java/com/onekeycall/videotablet/controller/TencentSmsController.java index 323a019..82eb680 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/TencentSmsController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/TencentSmsController.java @@ -12,6 +12,8 @@ import com.tencentcloudapi.sms.v20210111.models.SendSmsRequest; import com.tencentcloudapi.sms.v20210111.models.SendSmsResponse; import com.tencentcloudapi.sms.v20210111.models.SendStatus; import org.apache.commons.lang3.RandomStringUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; import org.springframework.data.redis.core.StringRedisTemplate; @@ -27,6 +29,8 @@ import java.util.Map; public class TencentSmsController { private static final int LOGIN_CODE_TTL = 5; + Logger logger = LoggerFactory.getLogger(TencentSmsController.class); + @Autowired private StringRedisTemplate stringRedisTemplate; @@ -144,10 +148,10 @@ public class TencentSmsController { SendSmsResponse res = client.SendSms(req); // 输出json格式的字符串回包 - System.out.println(SendSmsResponse.toJsonString(res)); + logger.info(SendSmsResponse.toJsonString(res)); // 也可以取出单个值,您可以通过官网接口文档或跳转到response对象的定义处查看返回字段的定义 - // System.out.println(res.getRequestId()); + // logger.info(res.getRequestId()); /* 当出现以下错误码时,快速解决方案参考 * [FailedOperation.SignatureIncorrectOrUnapproved](https://cloud.tencent.com/document/product/382/9558#.E7.9F.AD.E4.BF.A1.E5.8F.91.E9.80.81.E6.8F.90.E7.A4.BA.EF.BC.9Afailedoperation.signatureincorrectorunapproved-.E5.A6.82.E4.BD.95.E5.A4.84.E7.90.86.EF.BC.9F) @@ -168,7 +172,7 @@ public class TencentSmsController { map.put("expireAt", expireAt); Map codeMap = new HashMap<>(map); codeMap.put("code", code); - System.out.println(codeMap); + logger.info(codeMap.toString()); if (!sent) { //4.保存验证码到Redis,并且设置有效期5分钟 redisTemplate.opsForValue().set(phone, codeMap, Duration.ofMinutes(5)); diff --git a/src/main/java/com/onekeycall/videotablet/controller/UserController.java b/src/main/java/com/onekeycall/videotablet/controller/UserController.java new file mode 100644 index 0000000..88f1ae5 --- /dev/null +++ b/src/main/java/com/onekeycall/videotablet/controller/UserController.java @@ -0,0 +1,93 @@ +package com.onekeycall.videotablet.controller; + +import com.nimbusds.openid.connect.sdk.claims.UserInfo; +import com.onekeycall.videotablet.dto.TokenPair; +import com.onekeycall.videotablet.entity.DeviceInfo; +import com.onekeycall.videotablet.entity.User; +import com.onekeycall.videotablet.result.Result; +import com.onekeycall.videotablet.service.UserService; +import com.onekeycall.videotablet.utils.JwtUtil; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; +import org.springframework.beans.factory.annotation.Autowired; +import org.springframework.data.redis.core.RedisTemplate; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.web.bind.annotation.*; + +import java.util.HashMap; +import java.util.Map; + +@RestController +@RequestMapping("/user") +public class UserController { + + private final UserService userService; + private final AuthenticationManager authenticationManager; + + @Autowired + private RedisTemplate redisTemplate; + @Autowired + private JwtUtil jwtUtil; + + Logger logger = LoggerFactory.getLogger(LoginController.class); + + @Autowired + public UserController(UserService userService, AuthenticationManager authenticationManager) { + this.userService = userService; + this.authenticationManager = authenticationManager; + } + + + @PostMapping("/refresh_token") + public Result refreshToken( + @RequestHeader(value = "Authorization", required = false) String authHeader, @RequestHeader("Device-ID") String deviceId, + @RequestParam (value = "user_id") String userId, @RequestParam("refresh_token") String refreshToken) { + logger.info("refreshToken: Authorization={} userId={} deviceId={} refreshToken={}", authHeader, userId, deviceId, refreshToken); + + try { + // 验证refreshToken的有效性 + if (!jwtUtil.validateRefreshToken(refreshToken, userId)) { + return Result.error().message("无效的refresh token"); + } + + // 从refreshToken中获取用户ID + TokenPair tokenPair = jwtUtil.refreshAccessToken(refreshToken, deviceId); + + // 构建返回结果 + Map tokenMap = new HashMap<>(); + tokenMap.put("access_token", tokenPair.getAccess_token()); + + return Result.ok().data(tokenMap); + } catch (Exception e) { + logger.error("刷新token失败", e); + return Result.error().message("刷新token失败: " + e.getMessage()); + } + } + + @GetMapping("/get_user_info") + public Result getUserInfo( + @RequestHeader("Authorization") String authHeader, @RequestHeader("Device-ID") String deviceId, + @RequestParam(value = "user_id") String userId + ) { + + // 1. 校验 Authorization 头 + if (!authHeader.startsWith("Bearer ")) { + return Result.error().message("Invalid Authorization header"); + } + String token = authHeader.substring(7); // 去掉 "Bearer " 前缀 + + // 2. 校验 Token + if (!jwtUtil.validateAccessToken(userId, token, deviceId)) { + return Result.error().message("Invalid token"); + } + + User user = userService.getUserByUserId(userId); + + Map userInfo = new HashMap<>(); + userInfo.put("user_id", user.getUserId()); + userInfo.put("phone", user.getPhone()); + userInfo.put("nickname", user.getNickname()); + userInfo.put("avatar", user.getAvatar()); + return Result.ok().data("user_info", userInfo); + } +} diff --git a/src/main/java/com/onekeycall/videotablet/controller/UserPasswordController.java b/src/main/java/com/onekeycall/videotablet/controller/UserPasswordController.java index e5c834f..f851468 100644 --- a/src/main/java/com/onekeycall/videotablet/controller/UserPasswordController.java +++ b/src/main/java/com/onekeycall/videotablet/controller/UserPasswordController.java @@ -1,5 +1,6 @@ package com.onekeycall.videotablet.controller; +import com.onekeycall.videotablet.entity.User; import com.onekeycall.videotablet.result.Result; import com.onekeycall.videotablet.service.UserService; import com.onekeycall.videotablet.utils.JwtUtil; @@ -27,6 +28,32 @@ public class UserPasswordController { this.authenticationManager = authenticationManager; } + @PostMapping("/set_first_password") + public Result setFirstPassword(@RequestHeader("Authorization") String authHeader, @RequestHeader("Device-ID") String deviceId, + @RequestParam(value = "user_id") String userId, @RequestParam(value = "nick_name") String nickName, + @RequestParam String password, @RequestParam(value = "verify_password") String verifyPassword) { + if (!authHeader.startsWith("Bearer ")) { + return Result.error().message("Invalid Authorization header"); + } + User user = userService.getUserByUserId(userId); + if (user == null) { + return Result.error().message("user not found"); + } + + String token = authHeader.substring(7); // 去掉 "Bearer " 前缀 + if (!jwtUtil.validateAccessToken(userId, token, deviceId)) { + return Result.error().message("Invalid token"); + } + + + if (!StringUtils.equals(password, verifyPassword)) { + return Result.error().message("password is not same"); + } + + userService.setPasswordByUserId(userId, password); + return Result.ok().message("set first password success"); + } + @PostMapping("/phone_set_password") public Result setPasswordByPhone( @RequestHeader("Authorization") String authHeader, @RequestHeader("Device-ID") String deviceId, diff --git a/src/main/java/com/onekeycall/videotablet/entity/DeviceInfo.java b/src/main/java/com/onekeycall/videotablet/entity/DeviceInfo.java index 3df843a..1d91d1e 100644 --- a/src/main/java/com/onekeycall/videotablet/entity/DeviceInfo.java +++ b/src/main/java/com/onekeycall/videotablet/entity/DeviceInfo.java @@ -18,11 +18,10 @@ public class DeviceInfo { @Column(name = "id",unique = true, nullable = false) private Long id; - @Convert(converter = AesAttributeConverter.class) @Column(name = "sn", unique = true, nullable = false) private String sn; - @Column(name = "device_model") + @Column(name = "device_model", nullable = false) private String deviceModel; @Column(name = "device_alias") @@ -45,4 +44,9 @@ public class DeviceInfo { @Column(name = "activation_time") private Date activationTime; + @Column(name = "bind_sig") + private String bindSig; + + @Column(name = "token") + private String token; } diff --git a/src/main/java/com/onekeycall/videotablet/entity/User.java b/src/main/java/com/onekeycall/videotablet/entity/User.java index 6dc5a73..a188922 100644 --- a/src/main/java/com/onekeycall/videotablet/entity/User.java +++ b/src/main/java/com/onekeycall/videotablet/entity/User.java @@ -1,5 +1,6 @@ package com.onekeycall.videotablet.entity; +import com.google.gson.Gson; import com.onekeycall.videotablet.converter.AesAttributeConverter; import jakarta.persistence.*; import org.springframework.security.core.GrantedAuthority; @@ -51,6 +52,9 @@ public class User implements UserDetails { @Column(name = "device_id", nullable = false) private String deviceId; + @Column(name = "avatar") + private String avatar; + @Override public Collection getAuthorities() { return Collections.emptyList(); @@ -172,4 +176,17 @@ public class User implements UserDetails { public void setDeviceId(String deviceId) { this.deviceId = deviceId; } + + public String getAvatar() { + return avatar; + } + + public void setAvatar(String avatar) { + this.avatar = avatar; + } + + @Override + public String toString() { + return new Gson().toJson(this); + } } \ No newline at end of file diff --git a/src/main/java/com/onekeycall/videotablet/handler/GlobalExceptionHandler.java b/src/main/java/com/onekeycall/videotablet/handler/GlobalExceptionHandler.java index a84fa7f..b6ee9e2 100644 --- a/src/main/java/com/onekeycall/videotablet/handler/GlobalExceptionHandler.java +++ b/src/main/java/com/onekeycall/videotablet/handler/GlobalExceptionHandler.java @@ -6,6 +6,7 @@ import org.springframework.http.ResponseEntity; import org.springframework.security.oauth2.common.exceptions.InvalidTokenException; import org.springframework.validation.FieldError; import org.springframework.web.bind.MethodArgumentNotValidException; +import org.springframework.web.bind.MissingRequestHeaderException; import org.springframework.web.bind.MissingServletRequestParameterException; import org.springframework.web.bind.annotation.ControllerAdvice; import org.springframework.web.bind.annotation.ExceptionHandler; @@ -101,11 +102,19 @@ public class GlobalExceptionHandler { // } @ResponseBody - @ResponseStatus(HttpStatus.BAD_REQUEST) + @ResponseStatus(HttpStatus.UNAUTHORIZED) @ExceptionHandler(InvalidTokenException.class) public Result handleInvalidTokenException(InvalidTokenException e) { // 控制台打印异常 e.printStackTrace(); return Result.error().message(e.getMessage()); } + + @ResponseBody + @ExceptionHandler(MissingRequestHeaderException.class) + public Result handleMissingRequestHeaderException(MissingRequestHeaderException e) { + // 控制台打印异常 + e.printStackTrace(); + return Result.error().message(e.getMessage()); + } } \ No newline at end of file diff --git a/src/main/java/com/onekeycall/videotablet/result/Result.java b/src/main/java/com/onekeycall/videotablet/result/Result.java index 5c46d6d..1960568 100644 --- a/src/main/java/com/onekeycall/videotablet/result/Result.java +++ b/src/main/java/com/onekeycall/videotablet/result/Result.java @@ -60,6 +60,14 @@ public class Result { return r; } + public static Result unAuthorized() { + Result r = new Result(); + r.setSuccess(ResultCodeEnum.UNAUTHORIZED.getSuccess()); + r.setCode(ResultCodeEnum.UNAUTHORIZED.getCode()); + r.setMessage(ResultCodeEnum.UNAUTHORIZED.getMessage()); + return r; + } + public static Result setResult(ResultCodeEnum resultCodeEnum) { Result r = new Result(); r.setSuccess(resultCodeEnum.getSuccess()); diff --git a/src/main/java/com/onekeycall/videotablet/result/ResultCodeEnum.java b/src/main/java/com/onekeycall/videotablet/result/ResultCodeEnum.java index 7bfc412..1157ec5 100644 --- a/src/main/java/com/onekeycall/videotablet/result/ResultCodeEnum.java +++ b/src/main/java/com/onekeycall/videotablet/result/ResultCodeEnum.java @@ -11,7 +11,11 @@ public enum ResultCodeEnum { SUCCESS(true, 20000, "成功"), UNKNOWN_REASON(false, 20001, "未知错误"), - NOT_FOUND(true, 20004, "没有数据"); + NOT_FOUND(true, 20004, "没有数据"), + /** + * 未登录 + */ + UNAUTHORIZED(false, 20003, "未登录"); private final Boolean success; diff --git a/src/main/java/com/onekeycall/videotablet/service/UserService.java b/src/main/java/com/onekeycall/videotablet/service/UserService.java index 230f545..70e0520 100644 --- a/src/main/java/com/onekeycall/videotablet/service/UserService.java +++ b/src/main/java/com/onekeycall/videotablet/service/UserService.java @@ -56,6 +56,7 @@ public class UserService implements UserDetailsService { // 2. 检查手机号是否已注册 if (userRepository.existsByPhone(phone)) { User user = userRepository.findByPhone(phone).get(); + user.setUserId(deviceId); user.setNewUser(false); return user; } else { diff --git a/src/main/java/com/onekeycall/videotablet/sms/SendSms.java b/src/main/java/com/onekeycall/videotablet/sms/SendSms.java index 598c8bd..ddc7e78 100644 --- a/src/main/java/com/onekeycall/videotablet/sms/SendSms.java +++ b/src/main/java/com/onekeycall/videotablet/sms/SendSms.java @@ -9,10 +9,14 @@ import com.aliyun.sdk.service.dysmsapi20170525.models.SendSmsRequest; import com.aliyun.sdk.service.dysmsapi20170525.models.SendSmsResponse; import com.google.gson.Gson; import darabonba.core.client.ClientOverrideConfiguration; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.concurrent.CompletableFuture; public class SendSms { + static Logger logger = LoggerFactory.getLogger(SendSms.class); + public static void sendTest(String number) throws Exception { // HttpClient Configuration @@ -67,12 +71,12 @@ public class SendSms { CompletableFuture response = client.sendSms(sendSmsRequest); // Synchronously get the return value of the API request SendSmsResponse resp = response.get(); - System.out.println(new Gson().toJson(resp)); + logger.info(new Gson().toJson(resp)); // Asynchronous processing of return values /*response.thenAccept(resp -> { - System.out.println(new Gson().toJson(resp)); + logger.info(new Gson().toJson(resp)); }).exceptionally(throwable -> { // Handling exceptions - System.out.println(throwable.getMessage()); + logger.info(throwable.getMessage()); return null; });*/ diff --git a/src/main/java/com/onekeycall/videotablet/utils/CXAESUtil.java b/src/main/java/com/onekeycall/videotablet/utils/CXAESUtil.java new file mode 100644 index 0000000..abd6003 --- /dev/null +++ b/src/main/java/com/onekeycall/videotablet/utils/CXAESUtil.java @@ -0,0 +1,329 @@ +package com.onekeycall.videotablet.utils; + +import java.io.File; +import java.io.FileInputStream; +import java.io.FileNotFoundException; +import java.io.FileOutputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; +import java.nio.charset.StandardCharsets; +import java.security.InvalidAlgorithmParameterException; +import java.security.InvalidKeyException; +import java.security.NoSuchAlgorithmException; + +import javax.crypto.Cipher; +import javax.crypto.CipherInputStream; +import javax.crypto.CipherOutputStream; +import javax.crypto.NoSuchPaddingException; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.SecretKeySpec; + +public class CXAESUtil { + /** + * 加解密算法/工作模式/填充方式 + */ + private static final String CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding"; + private final static String HEX = "0123456789abcdef"; + private static final int keyLenght = 16; + private static final String defaultV = "0"; + public final static String key = "Ls0HSh9fNfeZ8Qu5"; + + + /** + * 加密 + * + * @param key 密钥 + * @param src 加密文本 + * @return + * @throws Exception + */ + public static String encrypt(String key, String src) { + // /src = Base64.encodeToString(src.getBytes(), Base64.DEFAULT); + byte[] rawKey = toMakekey(key, keyLenght, defaultV).getBytes();// key.getBytes(); + try { + byte[] result = encrypt(rawKey, src.getBytes(StandardCharsets.UTF_8)); + // result = Base64.encode(result, Base64.DEFAULT); + return toHex(result); + } catch (Exception e) { + e.printStackTrace(); + return ""; + } + } + + /** + * 加密 + * + * @param key + * 密钥 + * @param src + * 加密文本 + * @return + * @throws Exception + */ + public static String encrypt2Java(String key, String src) throws Exception { + // /src = Base64.encodeToString(src.getBytes(), Base64.DEFAULT); + byte[] rawKey = toMakekey(key, keyLenght, defaultV).getBytes();// key.getBytes(); + byte[] result = encrypt2Java(rawKey, src.getBytes("utf-8")); + // result = Base64.encode(result, Base64.DEFAULT); + return toHex(result); + } + + /** + * 解密 + * + * @param key + * 密钥 + * @param encrypted + * 待揭秘文本 + * @return + * @throws Exception + */ + public static String decrypt(String key, String encrypted) throws Exception { + byte[] rawKey = toMakekey(key, keyLenght, defaultV).getBytes();// key.getBytes(); + byte[] enc = toByte(encrypted); + // enc = Base64.decode(enc, Base64.DEFAULT); + byte[] result = decrypt(rawKey, enc); + // /result = Base64.decode(result, Base64.DEFAULT); + return new String(result, "utf-8"); + } + + /** + * 密钥key ,默认补的数字,补全16位数,以保证安全补全至少16位长度,android和ios对接通过 + * @param str + * @param strLength + * @param val + * @return + */ + private static String toMakekey(String str, int strLength, String val) { + int strLen = str.length(); + if (strLen < strLength) { + while (strLen < strLength) { + StringBuffer buffer = new StringBuffer(); + buffer.append(str).append(val); + str = buffer.toString(); + strLen = str.length(); + } + } + return str; + } + + /** + * 真正的加密过程 + * 1.通过密钥得到一个密钥专用的对象SecretKeySpec + * 2.Cipher 加密算法,加密模式和填充方式三部分或指定加密算 (可以只用写算法然后用默认的其他方式)Cipher.getInstance("AES"); + * @param key + * @param src + * @return + * @throws Exception + */ + private static byte[] encrypt(byte[] key, byte[] src) throws Exception { + SecretKeySpec skeySpec = new SecretKeySpec(key, "AES"); + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, skeySpec); + byte[] encrypted = cipher.doFinal(src); + return encrypted; + } + + /** + * 真正的加密过程 + * + * @param key + * @param src + * @return + * @throws Exception + */ + private static byte[] encrypt2Java(byte[] key, byte[] src) throws Exception { + SecretKeySpec skeySpec = new SecretKeySpec(key, "AES"); + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, skeySpec); + byte[] encrypted = cipher.doFinal(src); + return encrypted; + } + + /** + * 真正的解密过程 + * + * @param key + * @param encrypted + * @return + * @throws Exception + */ + private static byte[] decrypt(byte[] key, byte[] encrypted) throws Exception { + SecretKeySpec skeySpec = new SecretKeySpec(key, "AES"); + Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, skeySpec); + byte[] decrypted = cipher.doFinal(encrypted); + return decrypted; + } + + public static String toHex(String txt) { + return toHex(txt.getBytes()); + } + + public static String fromHex(String hex) { + return new String(toByte(hex)); + } + + + /** + * 把16进制转化为字节数组 + * @param hexString + * @return + */ + public static byte[] toByte(String hexString) { + int len = hexString.length() / 2; + byte[] result = new byte[len]; + for (int i = 0; i < len; i++) + result[i] = Integer.valueOf(hexString.substring(2 * i, 2 * i + 2), 16).byteValue(); + return result; + } + + + /** + * 二进制转字符,转成了16进制 + * 0123456789abcdefg + * @param buf + * @return + */ + public static String toHex(byte[] buf) { + if (buf == null) + return ""; + StringBuffer result = new StringBuffer(2 * buf.length); + for (int i = 0; i < buf.length; i++) { + appendHex(result, buf[i]); + } + return result.toString(); + } + + private static void appendHex(StringBuffer sb, byte b) { + sb.append(HEX.charAt((b >> 4) & 0x0f)).append(HEX.charAt(b & 0x0f)); + } + + /** + * 初始化 AES Cipher + * @param sKey + * @param cipherMode + * @return + */ + public static Cipher initAESCipher(String sKey, int cipherMode) { + // 创建Key gen + // KeyGenerator keyGenerator = null; + Cipher cipher = null; + try { + /* + * keyGenerator = KeyGenerator.getInstance("AES"); + * keyGenerator.init(128, new SecureRandom(sKey.getBytes())); + * SecretKey secretKey = keyGenerator.generateKey(); byte[] + * codeFormat = secretKey.getEncoded(); SecretKeySpec key = new + * SecretKeySpec(codeFormat, "AES"); cipher = + * Cipher.getInstance("AES"); //初始化 cipher.init(cipherMode, key); + */ + byte[] rawKey = toMakekey(sKey, keyLenght, defaultV).getBytes(); + SecretKeySpec skeySpec = new SecretKeySpec(rawKey, "AES"); + cipher = Cipher.getInstance(CIPHER_ALGORITHM); + cipher.init(cipherMode, skeySpec); + } catch (NoSuchAlgorithmException e) { + e.printStackTrace(); // To change body of catch statement use File | + // Settings | File Templates. + } catch (NoSuchPaddingException e) { + e.printStackTrace(); // To change body of catch statement use File | + // Settings | File Templates. + } catch (InvalidKeyException e) { + e.printStackTrace(); // To change body of catch statement use File | + // Settings | File Templates. + } + return cipher; + } + + /** + * 对文件进行AES加密 + * @param sourceFile + * @param fileType + * @param sKey + * @return + */ + public static File encryptFile(File sourceFile, String toFile, String dir, String sKey) { + // 新建临时加密文件 + File encrypfile = null; + InputStream inputStream = null; + OutputStream outputStream = null; + try { + inputStream = new FileInputStream(sourceFile); + encrypfile = new File(dir + toFile); + outputStream = new FileOutputStream(encrypfile); + Cipher cipher = initAESCipher(sKey, Cipher.ENCRYPT_MODE); + // 以加密流写入文件 + CipherInputStream cipherInputStream = new CipherInputStream(inputStream, cipher); + byte[] cache = new byte[1024]; + int nRead = 0; + while ((nRead = cipherInputStream.read(cache)) != -1) { + outputStream.write(cache, 0, nRead); + outputStream.flush(); + } + cipherInputStream.close(); + } catch (FileNotFoundException e) { + e.printStackTrace(); // To change body of catch statement use File | + // Settings | File Templates. + } catch (IOException e) { + e.printStackTrace(); // To change body of catch statement use File | + // Settings | File Templates. + } finally { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); // To change body of catch statement use + // File | Settings | File Templates. + } + try { + outputStream.close(); + } catch (IOException e) { + e.printStackTrace(); // To change body of catch statement use + // File | Settings | File Templates. + } + } + return encrypfile; + } + + /** + * AES方式解密文件 + * @param sourceFile + * @return + */ + public static File decryptFile(File sourceFile, String toFile, String dir, String sKey) { + File decryptFile = null; + InputStream inputStream = null; + OutputStream outputStream = null; + try { + decryptFile = new File(dir + toFile); + Cipher cipher = initAESCipher(sKey, Cipher.DECRYPT_MODE); + inputStream = new FileInputStream(sourceFile); + outputStream = new FileOutputStream(decryptFile); + CipherOutputStream cipherOutputStream = new CipherOutputStream(outputStream, cipher); + byte[] buffer = new byte[1024]; + int r; + while ((r = inputStream.read(buffer)) >= 0) { + cipherOutputStream.write(buffer, 0, r); + } + cipherOutputStream.close(); + } catch (IOException e) { + e.printStackTrace(); // To change body of catch statement use File | + // Settings | File Templates. + } finally { + try { + inputStream.close(); + } catch (IOException e) { + e.printStackTrace(); // To change body of catch statement use + // File | Settings | File Templates. + } + try { + outputStream.close(); + } catch (IOException e) { + e.printStackTrace(); // To change body of catch statement use + // File | Settings | File Templates. + } + } + return decryptFile; + } + +} diff --git a/src/main/java/com/onekeycall/videotablet/utils/JwtUtil.java b/src/main/java/com/onekeycall/videotablet/utils/JwtUtil.java index 06c9992..a11e3fc 100644 --- a/src/main/java/com/onekeycall/videotablet/utils/JwtUtil.java +++ b/src/main/java/com/onekeycall/videotablet/utils/JwtUtil.java @@ -1,16 +1,21 @@ package com.onekeycall.videotablet.utils; +import com.onekeycall.videotablet.controller.LoginController; import com.onekeycall.videotablet.dto.TokenPair; import io.jsonwebtoken.Claims; import io.jsonwebtoken.JwtException; import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.SignatureAlgorithm; import io.jsonwebtoken.security.Keys; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; 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 org.springframework.security.oauth2.common.exceptions.InvalidTokenException; +import java.util.Base64; import java.util.Date; import java.util.UUID; import java.util.concurrent.TimeUnit; @@ -18,6 +23,8 @@ import java.util.concurrent.TimeUnit; @Component public class JwtUtil { + Logger logger = LoggerFactory.getLogger(JwtUtil.class); + @Autowired private StringRedisTemplate redisTemplate; @@ -80,6 +87,7 @@ public class JwtUtil { .parseSignedClaims(token) .getPayload(); } catch (JwtException | IllegalArgumentException e) { + logger.error("parseToken Token解析失败: {}", e.getMessage()); throw new InvalidTokenException("Token解析失败: " + e.getMessage()); } } @@ -121,10 +129,10 @@ public class JwtUtil { * 校验Refresh Token有效性(签名+Redis一致性) * * @param refreshToken - * @param username + * @param userId * @return */ - public boolean validateRefreshToken(String refreshToken, String username) { + public boolean validateRefreshToken(String refreshToken, String userId) { Claims claims = parseToken(refreshToken); // 1. 验证Token类型 @@ -133,7 +141,7 @@ public class JwtUtil { } // 2. 从Redis获取存储的refreshId - String redisKey = "user:refresh:" + username; + String redisKey = "user:refresh:" + userId; String storedRefreshId = redisTemplate.opsForValue().get(redisKey); if (storedRefreshId == null) { throw new InvalidTokenException("Refresh Token已吊销"); @@ -174,4 +182,56 @@ public class JwtUtil { } + @Value("${jwt.tablet.secret}") + private String TABLET_SECRET; + + + /** + * 生成设备签名(首次绑定) + * @param sn 设备序列号 + * @return 设备唯一签名 + */ + public String generateDeviceSig(String sn) { + // 使用UUID+SN哈希生成唯一签名 + String rawSig = sn + UUID.randomUUID(); + return Base64.getEncoder().encodeToString( + Keys.hmacShaKeyFor(rawSig.getBytes()).getEncoded() + ); + } + + + /** + * 生成设备令牌(联合SN+deviceId) + * @param sn 设备序列号 + * @param deviceId 设备ID + * @return JWT格式令牌 + */ + public String generateDeviceToken(String sn, String deviceId) { + return Jwts.builder() + .setSubject(deviceId) + .claim("sn", sn) + .setIssuedAt(new Date()) + .signWith(Keys.hmacShaKeyFor(TABLET_SECRET.getBytes()), SignatureAlgorithm.HS256) + .compact(); + } + + /** + * 验证设备令牌 + * @param deviceToken 设备令牌 + * @return 验证结果 + */ + + public Claims validateDeviceToken(String deviceToken) { + try { + return Jwts.parser() + .verifyWith(Keys.hmacShaKeyFor(TABLET_SECRET.getBytes())) + .build() + .parseSignedClaims(deviceToken) + .getPayload(); + } catch (JwtException | IllegalArgumentException e) { + logger.error("validateDeviceToken Token解析失败: {}", e.getMessage()); + throw new InvalidTokenException("Token解析失败: " + e.getMessage()); + } + } + } \ No newline at end of file diff --git a/src/main/java/com/onekeycall/videotablet/utils/PushUtils.java b/src/main/java/com/onekeycall/videotablet/utils/PushUtils.java index 5c3f21c..def3de5 100644 --- a/src/main/java/com/onekeycall/videotablet/utils/PushUtils.java +++ b/src/main/java/com/onekeycall/videotablet/utils/PushUtils.java @@ -16,12 +16,14 @@ import com.tencent.xinge.push.app.PushAppRequest; import darabonba.core.client.ClientOverrideConfiguration; import org.glassfish.jaxb.core.v2.TODO; import org.json.JSONObject; - +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import java.util.ArrayList; import java.util.concurrent.CompletableFuture; import java.util.concurrent.ExecutionException; public class PushUtils { + static Logger logger = LoggerFactory.getLogger(PushUtils.class); public static void aliyunAsyncPush(String verifyKey, String phone, String sn) throws ExecutionException, InterruptedException { // HttpClient Configuration @@ -84,12 +86,12 @@ public class PushUtils { CompletableFuture response = client.push(pushRequest); // Synchronously get the return value of the API request PushResponse resp = response.get(); - System.out.println(new Gson().toJson(resp)); + logger.info(new Gson().toJson(resp)); // Asynchronous processing of return values /*response.thenAccept(resp -> { - System.out.println(new Gson().toJson(resp)); + logger.info(new Gson().toJson(resp)); }).exceptionally(throwable -> { // Handling exceptions - System.out.println(throwable.getMessage()); + logger.info(throwable.getMessage()); return null; });*/ @@ -133,7 +135,7 @@ public class PushUtils { pushAppRequest.setAccount_list(accountList); JSONObject ret = xingeApp.pushApp(pushAppRequest); - System.out.println(ret); + logger.info(ret.toString()); } } diff --git a/src/main/resources/application-debug.properties b/src/main/resources/application-debug.properties index 4cdcc29..c7fc142 100644 --- a/src/main/resources/application-debug.properties +++ b/src/main/resources/application-debug.properties @@ -3,7 +3,7 @@ server.port=8088 ## mysql 数据连接信息 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -spring.datasource.url=jdbc:mysql://139.199.77.221:13306/video_tablet_db?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true +spring.datasource.url=jdbc:mysql://127.0.0.1:3306/video_tablet_db?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true spring.datasource.username=tt spring.datasource.password=fanhuitong @@ -11,9 +11,9 @@ spring.datasource.password=fanhuitong # 0也是默认值,表示你要操控的 Redis 上的哪个数据库 spring.data.redis.database=0 # 6379也是默认值,表示 Redis 端口 -spring.data.redis.port=16379 +spring.data.redis.port=6379 # 这里填写你的服务器地址 -spring.data.redis.host=139.199.77.221 +spring.data.redis.host=127.0.0.1 spring.data.redis.password=fanhuitong # 可省略 spring.data.redis.lettuce.pool.min-idle=5 @@ -32,3 +32,23 @@ jwt.secret='wPQ1qRFo4YbuA849tmwKnDpQ8891vJBo' # 可选,根据你的需要设置过期时间 jwt.access-expire=86400000 jwt.refresh-expire=2592000000 + +jwt.tablet.secret='Your256BitSecretKeyMustBeAtLeast32BytesLong!' + +# 指定日志文件名(项目根目录生成) +logging.file.name=app.log +# 或指定日志目录(目录下生成 spring.log) +logging.file.path=/var/log/myapp + +# 设置日志级别 +logging.level.root=INFO//////////////////////////////////////////////////////////////////////////// + +logging.level.com.example=DEBUG + +# 自定义日志格式 +logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n +logging.pattern.file=%d{yyyy-MM-dd} [%thread] %-5level %logger - %msg%n + +# 日志文件切割(默认10MB分割,保留7天) +logging.logback.rollingpolicy.max-file-size=10MB +logging.logback.rollingpolicy.max-history=30 \ No newline at end of file diff --git a/src/main/resources/application-prod.properties b/src/main/resources/application-prod.properties index 4a374d0..b2aeafe 100644 --- a/src/main/resources/application-prod.properties +++ b/src/main/resources/application-prod.properties @@ -3,8 +3,7 @@ server.port=8088 ## mysql 数据连接信息 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -#spring.datasource.url=jdbc:mysql://139.199.77.221:3306/spring_boot?useUnicode=true&characterEncoding=utf8&useSSL=false -spring.datasource.url=jdbc:mysql://127.0.0.1:3305/video_tablet_db?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true +spring.datasource.url=jdbc:mysql://139.199.77.221:13306/video_tablet_db?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true spring.datasource.username=tt spring.datasource.password=fanhuitong @@ -12,9 +11,9 @@ spring.datasource.password=fanhuitong # 0也是默认值,表示你要操控的 Redis 上的哪个数据库 spring.data.redis.database=0 # 6379也是默认值,表示 Redis 端口 -spring.data.redis.port=6379 +spring.data.redis.port=16379 # 这里填写你的服务器地址 -spring.data.redis.host=127.0.0.1 +spring.data.redis.host=139.199.77.221 spring.data.redis.password=fanhuitong # 可省略 spring.data.redis.lettuce.pool.min-idle=5 @@ -33,3 +32,23 @@ jwt.secret='wPQ1qRFo4YbuA849tmwKnDpQ8891vJBo' # 可选,根据你的需要设置过期时间 jwt.access-expire=86400000 jwt.refresh-expire=2592000000 + +jwt.tablet.secret='Your256BitSecretKeyMustBeAtLeast32BytesLong!' + +# 指定日志文件名(项目根目录生成) +logging.file.name=app.log +# 或指定日志目录(目录下生成 spring.log) +logging.file.path=/var/log/myapp + +# 设置日志级别 +logging.level.root=INFO//////////////////////////////////////////////////////////////////////////// + +logging.level.com.example=DEBUG + +# 自定义日志格式 +logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n +logging.pattern.file=%d{yyyy-MM-dd} [%thread] %-5level %logger - %msg%n + +# 日志文件切割(默认10MB分割,保留7天) +logging.logback.rollingpolicy.max-file-size=10MB +logging.logback.rollingpolicy.max-history=30 \ No newline at end of file diff --git a/src/main/resources/application-test.properties b/src/main/resources/application-test.properties index 4a374d0..b2aeafe 100644 --- a/src/main/resources/application-test.properties +++ b/src/main/resources/application-test.properties @@ -3,8 +3,7 @@ server.port=8088 ## mysql 数据连接信息 spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver -#spring.datasource.url=jdbc:mysql://139.199.77.221:3306/spring_boot?useUnicode=true&characterEncoding=utf8&useSSL=false -spring.datasource.url=jdbc:mysql://127.0.0.1:3305/video_tablet_db?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true +spring.datasource.url=jdbc:mysql://139.199.77.221:13306/video_tablet_db?useUnicode=true&characterEncoding=utf8&useSSL=false&allowPublicKeyRetrieval=true spring.datasource.username=tt spring.datasource.password=fanhuitong @@ -12,9 +11,9 @@ spring.datasource.password=fanhuitong # 0也是默认值,表示你要操控的 Redis 上的哪个数据库 spring.data.redis.database=0 # 6379也是默认值,表示 Redis 端口 -spring.data.redis.port=6379 +spring.data.redis.port=16379 # 这里填写你的服务器地址 -spring.data.redis.host=127.0.0.1 +spring.data.redis.host=139.199.77.221 spring.data.redis.password=fanhuitong # 可省略 spring.data.redis.lettuce.pool.min-idle=5 @@ -33,3 +32,23 @@ jwt.secret='wPQ1qRFo4YbuA849tmwKnDpQ8891vJBo' # 可选,根据你的需要设置过期时间 jwt.access-expire=86400000 jwt.refresh-expire=2592000000 + +jwt.tablet.secret='Your256BitSecretKeyMustBeAtLeast32BytesLong!' + +# 指定日志文件名(项目根目录生成) +logging.file.name=app.log +# 或指定日志目录(目录下生成 spring.log) +logging.file.path=/var/log/myapp + +# 设置日志级别 +logging.level.root=INFO//////////////////////////////////////////////////////////////////////////// + +logging.level.com.example=DEBUG + +# 自定义日志格式 +logging.pattern.console=%d{yyyy-MM-dd HH:mm:ss} [%thread] %-5level %logger{36} - %msg%n +logging.pattern.file=%d{yyyy-MM-dd} [%thread] %-5level %logger - %msg%n + +# 日志文件切割(默认10MB分割,保留7天) +logging.logback.rollingpolicy.max-file-size=10MB +logging.logback.rollingpolicy.max-history=30 \ No newline at end of file diff --git a/src/main/resources/application.properties b/src/main/resources/application.properties index ccb2bcd..270d01e 100644 --- a/src/main/resources/application.properties +++ b/src/main/resources/application.properties @@ -1,3 +1,5 @@ # application.properties # 默认激活生产环境 -spring.profiles.active=debug +spring.profiles.active=test +#spring.profiles.active=debug +#spring.profiles.active=prod \ No newline at end of file diff --git a/src/test/java/com/onekeycall/videotablet/VideoTabletApplicationTests.java b/src/test/java/com/onekeycall/videotablet/VideoTabletApplicationTests.java index 030ce26..ad1dc6e 100644 --- a/src/test/java/com/onekeycall/videotablet/VideoTabletApplicationTests.java +++ b/src/test/java/com/onekeycall/videotablet/VideoTabletApplicationTests.java @@ -2,18 +2,20 @@ package com.onekeycall.videotablet; import com.github.houbb.sensitive.word.core.SensitiveWordHelper; import org.junit.jupiter.api.Test; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; import org.springframework.boot.test.context.SpringBootTest; @SpringBootTest class VideoTabletApplicationTests { - + private final Logger logger = LoggerFactory.getLogger(VideoTabletApplicationTests.class); @Test void contextLoads() { String text = "测试敏感词赌博"; boolean hasSensitive = SensitiveWordHelper.contains(text); // true String safeText = SensitiveWordHelper.replace(text); // "测试敏感词***" - System.out.println(hasSensitive); - System.out.println(safeText); + logger.debug("hasSensitive:{}",hasSensitive); + logger.debug("safeText:{}",safeText); } }