同步密钥

This commit is contained in:
junleea 2025-07-31 21:10:46 +08:00
commit 7033b0ce5f
5 changed files with 298 additions and 40 deletions

57
main.go
View File

@ -2,7 +2,6 @@ package main
import ( import (
"encoding/json" "encoding/json"
"errors"
"fmt" "fmt"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt" "github.com/golang-jwt/jwt"
@ -62,8 +61,7 @@ func init() {
os.MkdirAll(proto.CID_BASE_DIR+"workspace", os.ModePerm) os.MkdirAll(proto.CID_BASE_DIR+"workspace", os.ModePerm)
//读取配置文件 //读取配置文件
//文件地址/home/videoplayer/vp.conf //文件地址/home/videoplayer/vp.conf
//configPath := "/home/videoplayer/vp.conf" configPath := "/home/videoplayer/vp.conf"
configPath := "/etc/vp-app/vp.conf"
//读取配置文件 //读取配置文件
err := proto.ReadConfig(configPath) err := proto.ReadConfig(configPath)
if err != nil { if err != nil {
@ -106,17 +104,17 @@ func JWTAuthMiddleware() gin.HandlerFunc {
if tokenString == "" { if tokenString == "" {
tokenString = c.Query("token") tokenString = c.Query("token")
} }
for k, _ := range proto.Url_map { //如果请求为login或register则不需要验证token
if strings.Contains(c.Request.URL.Path, k) { //for k, _ := range proto.Url_map {
log.Println("need not check token:", c.Request.URL.Path) // if strings.Contains(c.Request.URL.Path, k) {
c.Next()
return
}
}
//if proto.Url_map[c.Request.URL.Path] == true { //查看是否在不需要token的url中
// c.Next() // c.Next()
// return // return
// } // }
//}
if proto.Url_map[c.Request.URL.Path] == true { //查看是否在不需要token的url中
c.Next()
return
}
if tokenString == "" { if tokenString == "" {
c.AbortWithStatusJSON(http.StatusOK, gin.H{"message": "unauthorized", "error": "token is empty", "code": proto.TokenIsNull}) c.AbortWithStatusJSON(http.StatusOK, gin.H{"message": "unauthorized", "error": "token is empty", "code": proto.TokenIsNull})
return return
@ -147,34 +145,22 @@ func JWTAuthMiddleware() gin.HandlerFunc {
return return
} }
proto.SigningKeyRWLock.RLock() //加读锁
// 使用加密secret 解析 JWT 令牌 // 使用加密secret 解析 JWT 令牌
//token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// return proto.SigningKey, nil
//})
token, err := jwt.Parse(tokenString, func(t *jwt.Token) (interface{}, error) {
// 验证签名算法
if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, jwt.ErrSignatureInvalid
}
return proto.SigningKey, nil return proto.SigningKey, nil
}) })
// 错误处理 proto.SigningKeyRWLock.RUnlock()
if err != nil {
var ve *jwt.ValidationError // 验证令牌
if errors.As(err, &ve) { if err != nil || !token.Valid {
switch { c.AbortWithStatusJSON(http.StatusOK, gin.H{
case ve.Errors&jwt.ValidationErrorMalformed != 0: "message": "NOT_LOGIN",
c.AbortWithStatusJSON(http.StatusOK, gin.H{"error": "Malformed token:" + err.Error(), "code": proto.TokenInvalid}) "error": "Invalid token",
case ve.Errors&jwt.ValidationErrorExpired != 0: "code": proto.TokenExpired,
c.AbortWithStatusJSON(http.StatusOK, gin.H{"error": "Token expired:" + err.Error(), "code": proto.TokenExpired}) })
case ve.Errors&jwt.ValidationErrorNotValidYet != 0:
c.AbortWithStatusJSON(http.StatusOK, gin.H{"error": "Token not active yet:" + err.Error(), "code": proto.TokenInvalid})
default:
c.AbortWithStatusJSON(http.StatusOK, gin.H{"error": "Invalid token:" + err.Error(), "code": proto.TokenInvalid})
}
return return
} }
}
// 将用户信息添加到上下文中 // 将用户信息添加到上下文中
id := token.Claims.(jwt.MapClaims)["id"] id := token.Claims.(jwt.MapClaims)["id"]
@ -187,9 +173,9 @@ func JWTAuthMiddleware() gin.HandlerFunc {
c.AbortWithStatusJSON(http.StatusOK, gin.H{"message": "unauthorized", "error": "no function permission", "code": proto.NoPermission}) c.AbortWithStatusJSON(http.StatusOK, gin.H{"message": "unauthorized", "error": "no function permission", "code": proto.NoPermission})
return return
} }
// 继续处理请求 // 继续处理请求
c.Next() c.Next()
//log.Println("JWT token is valid, user ID:", token.Claims.(jwt.MapClaims)["id"], " path:", c.Request.URL.Path)
} }
} }
@ -203,6 +189,7 @@ func myTask() {
//其它定时任务-通用 //其它定时任务-通用
RunGeneralCron() RunGeneralCron()
service.ShellWillRunFromServer() service.ShellWillRunFromServer()
service.SyncTokenSecretFromUserCenter()
} }

View File

@ -6,6 +6,7 @@ import (
"gorm.io/gorm" "gorm.io/gorm"
"log" "log"
"os" "os"
"sync"
) )
var Config ConfigStruct var Config ConfigStruct
@ -13,6 +14,13 @@ var SigningKey = []byte{}
var Url_map = map[string]bool{"/login": true, "/register": true, "/uuid": true, "/gqr": true, "/cid/callback": true, "/tool/monitor": true, "/user/sync": true, "/tool/file/": true, "/user/reset": true} // 不需要token验证的url var Url_map = map[string]bool{"/login": true, "/register": true, "/uuid": true, "/gqr": true, "/cid/callback": true, "/tool/monitor": true, "/user/sync": true, "/tool/file/": true, "/user/reset": true} // 不需要token验证的url
var Per_menu_map = map[string]int{"/video/": 1, "/device/": 2, "/cid/": 3} var Per_menu_map = map[string]int{"/video/": 1, "/device/": 2, "/cid/": 3}
var File_Type = map[string]int{"im": 1, "avatar": 2, "file": 3, "config": 4} // 文件类型 var File_Type = map[string]int{"im": 1, "avatar": 2, "file": 3, "config": 4} // 文件类型
// 配置读写锁
var ConfigRWLock = &sync.RWMutex{}
var SigningKeyRWLock = &sync.RWMutex{}
var SyncSecretReqLog int64
const ( const (
MYSQL_USER = "video_t2" MYSQL_USER = "video_t2"
MYSQL_DB = "video_t2" MYSQL_DB = "video_t2"
@ -86,6 +94,30 @@ type ConfigStruct struct {
MONITOR_SERVER_TOKEN string `json:"monitor_server_token"` // 监控服务器token,用于状态监控及邮件通知 MONITOR_SERVER_TOKEN string `json:"monitor_server_token"` // 监控服务器token,用于状态监控及邮件通知
} }
func WriteConfigToFile() {
//系统是linux、macos还是windows
var configPath string
if os.Getenv("OS") == "Windows_NT" {
configPath = "D:/Code/videoplayer/vp.conf"
} else if os.Getenv("OS") == "linux" {
//文件地址/home/saw-ai/saw-ai.conf
configPath = "/home/videoplayer/vp.conf"
} else {
configPath = "/home/videoplayer/vp.conf"
}
configData, err := json.MarshalIndent(Config, "", " ")
if err != nil {
log.Println("WriteConfigToFile json marshal error:", err)
return
}
err = os.WriteFile(configPath, configData, 0644)
if err != nil {
log.Println("WriteConfigToFile write file error:", err)
return
}
log.Println("WriteConfigToFile write config to file success")
}
// 读取配置文件 // 读取配置文件
func ReadConfig(path string) error { func ReadConfig(path string) error {
//查看配置文件是否存在,不存在则创建 //查看配置文件是否存在,不存在则创建

View File

@ -117,3 +117,29 @@ type ResponseOAuth struct {
Email string `json:"email" form:"email"` Email string `json:"email" form:"email"`
Token string `json:"token" form:"token"` Token string `json:"token" form:"token"`
} }
type SecretSyncSettings struct {
Prev string `json:"prev"` // 前一个secret
Curr string `json:"curr"` // 当前的secret
Next string `json:"next"` // 下一个secret
CurrExpectedExpiration int64 `json:"curr_expected_expiration"` // 当前密钥的预期过期时间戳
PrevEndTimestamp int64 `json:"prev_end_timestamp"` // 前一个secret的结束时间戳
CurrStartTimestamp int64 `json:"curr_start_timestamp"` // 当前secret的开始时间戳
NextStartTimestamp int64 `json:"next_start_timestamp"` // 下一个secret的开始时间戳
}
type SyncSystemConfigRequest struct {
//时间戳
Timestamp int64 `json:"timestamp" form:"timestamp"` // 时间戳
//设备标识
DeviceApp string `json:"device_app" form:"device_app"` // 设备标识
//加密信息
Sign string `json:"sign" form:"sign"` // 加密信息,app的secret加密后的值
SecretKeyMd5 string `json:"secret_key_md5" form:"secret_key_md5"` // 密钥的MD5值,用于验证
}
type RequestSyncSecretResp struct {
Code int `json:"code"`
Message string `json:"message"`
Data string `json:"data"`
}

View File

@ -1,9 +1,12 @@
package service package service
import ( import (
"encoding/json"
"errors" "errors"
"fmt" "fmt"
"log"
"regexp" "regexp"
"strconv"
"time" "time"
"videoplayer/proto" "videoplayer/proto"
"videoplayer/worker" "videoplayer/worker"
@ -184,3 +187,133 @@ func DelMonitorDeviceListWithStatus(userId int, deviceReq []proto.GetMonitorDevi
} }
return delDevices, err return delDevices, err
} }
// 更新token密钥
func SyncTokenSecretFromUserCenter() {
secretSettings, err := GetTokenSecretFromUserCenter()
//写入redis
secretSettingsBytes, err2 := json.Marshal(secretSettings)
if err2 != nil {
log.Println("SyncTokenSecretFromUserCenter json marshal error:", err2)
return
}
if proto.SyncSecretReqLog%100 == 0 {
log.Println("SyncTokenSecretFromUserCenter req data:", string(secretSettingsBytes))
}
worker.SetRedis("secret_sync_settings", string(secretSettingsBytes)) //将密钥信息存入redis
if err != nil {
log.Println("SyncTokenSecretFromUserCenter error:", err)
return
}
if secretSettings.Curr != "" && secretSettings.Curr != proto.TOKEN_SECRET && secretSettings.Next == "" { //如果当前密钥不为空且不等于配置文件中的密钥,并且下一个密钥为空,则需要更新配置文件中的密钥
log.Printf("SyncTokenSecretFromUserCenter current secret is not equal to config secret, current: %s, config: %s\n", secretSettings.Curr, proto.TOKEN_SECRET)
//如果当前密钥与配置文件中的密钥不一致,则需要更新配置文件中的密钥
proto.SigningKeyRWLock.Lock()
proto.SigningKey = []byte(secretSettings.Curr)
proto.Config.TOKEN_SECRET = secretSettings.Curr
proto.SigningKeyRWLock.Unlock()
//配置写回文件
go proto.WriteConfigToFile()
log.Println("SyncTokenSecretFromUserCenter current secret updated successfully")
}
if secretSettings.Next == "" {
log.Println("SyncTokenSecretFromUserCenter secret is empty")
return
} else if proto.SyncSecretReqLog%100 == 0 {
go SetNextSecretToCurrent(*secretSettings) //异步设置下一个密钥为当前密钥
}
proto.SyncSecretReqLog++ //记录同步密钥请求次数
}
func SetNextSecretToCurrent(secret_copy proto.SecretSyncSettings) {
var secret_sync_settings proto.SecretSyncSettings
redisKey := "secret_sync_settings"
settingsStr := worker.GetRedis(redisKey)
err := json.Unmarshal([]byte(settingsStr), &secret_sync_settings)
if err != nil {
log.Println("Error decoding secret sync settings:", err)
} else {
//如果当前密钥的下一个密钥与传入的密钥不一致,则不进行设置
if secret_copy.Next != secret_sync_settings.Next {
return
}
//获取需要等待时间
waitTime := secret_sync_settings.NextStartTimestamp - worker.GetCurrentTimestamp()
if waitTime > 0 {
log.Printf("Waiting for %d seconds before setting the next secret as current secret\n", waitTime)
time.Sleep(time.Duration(waitTime) * time.Second) //等待时间
} else {
log.Println("No need to wait, setting the next secret as current secret immediately")
}
//设置下一个密钥为当前密钥
secret_sync_settings.Prev = secret_sync_settings.Curr
secret_sync_settings.PrevEndTimestamp = worker.GetCurrentTimestamp()
secret_sync_settings.Curr = secret_sync_settings.Next
secret_sync_settings.Next = ""
secret_sync_settings.CurrStartTimestamp = secret_sync_settings.PrevEndTimestamp
//设置当前程序的密钥
//获取写锁
proto.SigningKeyRWLock.Lock()
defer proto.SigningKeyRWLock.Unlock()
proto.SigningKey = []byte(secret_sync_settings.Curr)
proto.Config.TOKEN_SECRET = secret_sync_settings.Curr
//配置写回文件
go proto.WriteConfigToFile()
}
settinsStr, err2 := json.Marshal(secret_sync_settings)
if err2 != nil {
log.Println("Error encoding set secret sync settings:", err2)
return
}
worker.SetRedis(redisKey, string(settinsStr)) //将当前的密钥信息存入redis
}
// 获取token密钥请求
func GetTokenSecretFromUserCenter() (*proto.SecretSyncSettings, error) {
url := "https://uc.ljsea.top/tool/sync_system_config"
var req proto.SyncSystemConfigRequest
proto.SigningKeyRWLock.Lock()
defer proto.SigningKeyRWLock.Unlock()
req.SecretKeyMd5 = worker.GenerateMD5(string(proto.SigningKey))
req.DeviceApp = "StuAcaWorksAI"
req.Timestamp = worker.GetCurrentTimestamp()
req.Sign = worker.GenerateMD5(req.SecretKeyMd5 + req.DeviceApp + strconv.FormatInt(req.Timestamp, 10))
reqBytes, err2 := json.Marshal(req)
if err2 != nil {
log.Println("GetTokenSecretFromUserCenter json marshal error:", err2)
return nil, err2
}
err, resp := worker.DoPostRequestJSON(url, reqBytes, nil)
if err != nil {
log.Println("GetTokenSecretFromUserCenter post request error:", err)
return nil, err
}
var respObject proto.RequestSyncSecretResp
err = json.Unmarshal(resp, &respObject)
if err != nil {
log.Println("GetTokenSecretFromUserCenter json unmarshal error:", err)
return nil, err
}
//对称加密密钥。通过密钥加 secret_key 取md5
secretKeyMd5 := worker.GenerateMD5(proto.TOKEN_SECRET + "_sync_secret")
//解密返回数据
dataContent, err2 := worker.AESDecrypt(respObject.Data, []byte(secretKeyMd5))
if err2 != nil {
log.Println("GetTokenSecretFromUserCenter aes decrypt error:", err2)
return nil, err2
}
var secretResp proto.SecretSyncSettings
err = json.Unmarshal(dataContent, &secretResp)
if err != nil {
log.Println("GetTokenSecretFromUserCenter json unmarshal error:", err)
return nil, err
}
return &secretResp, nil
}

View File

@ -1,13 +1,93 @@
package worker package worker
import "math/rand" import (
"crypto/aes"
"crypto/cipher"
"crypto/md5"
"crypto/rand"
"encoding/base64"
"encoding/hex"
"fmt"
"io"
mrand "math/rand"
"time"
)
func GetRandomString(l int) string { func GetRandomString(l int) string {
str := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" str := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ"
bytes := []byte(str) bytes := []byte(str)
var result []byte var result []byte
for i := 0; i < l; i++ { for i := 0; i < l; i++ {
result = append(result, bytes[rand.Intn(len(bytes))]) result = append(result, bytes[mrand.Intn(len(bytes))])
} }
return string(result) return string(result)
} }
func GetCurrentTimestamp() int64 {
// 获取当前时间戳
return time.Now().Unix()
}
// AESEncrypt 函数使用AES-GCM算法对明文进行加密
func AESEncrypt(plaintext []byte, key []byte) (string, error) {
// 创建AES加密块
block, err := aes.NewCipher(key)
if err != nil {
return "", err
}
// 创建GCM模式的加密器
gcm, err := cipher.NewGCM(block)
if err != nil {
return "", err
}
// 生成随机的nonce
nonce := make([]byte, gcm.NonceSize())
if _, err = io.ReadFull(rand.Reader, nonce); err != nil {
return "", err
}
// 执行加密操作
ciphertext := gcm.Seal(nonce, nonce, plaintext, nil)
// 将加密结果转换为Base64编码字符串
return base64.StdEncoding.EncodeToString(ciphertext), nil
}
// AESDecrypt 函数使用AES-GCM算法对密文进行解密
func AESDecrypt(ciphertext string, key []byte) ([]byte, error) {
// 将Base64编码的密文转换为字节切片
data, err := base64.StdEncoding.DecodeString(ciphertext)
if err != nil {
return nil, err
}
// 创建AES加密块
block, err := aes.NewCipher(key)
if err != nil {
return nil, err
}
// 创建GCM模式的解密器
gcm, err := cipher.NewGCM(block)
if err != nil {
return nil, err
}
// 提取nonce和真正的密文
nonceSize := gcm.NonceSize()
if len(data) < nonceSize {
return nil, fmt.Errorf("密文长度过短")
}
nonce, ciphertextBytes := data[:nonceSize], data[nonceSize:]
// 执行解密操作
return gcm.Open(nil, nonce, ciphertextBytes, nil)
}
func GenerateMD5(secretKey string) string {
hasher := md5.New()
hasher.Write([]byte(secretKey))
return hex.EncodeToString(hasher.Sum(nil)) // 将二进制数据转换为十六进制字符串
}