登录和刷新token优化
This commit is contained in:
@@ -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;
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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<String, Object> 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");
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -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;
|
||||
|
||||
|
||||
}
|
||||
@@ -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<String, Object> 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<String, Object> tokenMap = new HashMap<>();
|
||||
@@ -158,9 +168,11 @@ public class LoginController {
|
||||
return Result.error().message("verify key is expired");
|
||||
}
|
||||
}
|
||||
|
||||
// @PostMapping("/device_login")
|
||||
// public Result loginByDeviceSn(){
|
||||
//
|
||||
// }
|
||||
|
||||
|
||||
}
|
||||
@@ -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<String, Object> map = new HashMap<>();
|
||||
map.put("sn", sn);
|
||||
|
||||
return Result.ok().data(map);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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<String, Object> 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));
|
||||
|
||||
@@ -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<String, Object> 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<String, Object> 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<String, Object> 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);
|
||||
}
|
||||
}
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
@@ -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<? extends GrantedAuthority> 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);
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
@@ -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());
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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<SendSmsResponse> 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;
|
||||
});*/
|
||||
|
||||
|
||||
329
src/main/java/com/onekeycall/videotablet/utils/CXAESUtil.java
Normal file
329
src/main/java/com/onekeycall/videotablet/utils/CXAESUtil.java
Normal file
@@ -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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -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<PushResponse> 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());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -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
|
||||
@@ -1,3 +1,5 @@
|
||||
# application.properties
|
||||
# 默认激活生产环境
|
||||
spring.profiles.active=debug
|
||||
spring.profiles.active=test
|
||||
#spring.profiles.active=debug
|
||||
#spring.profiles.active=prod
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user