diff --git a/main.go b/main.go index aefd1e1..3648733 100644 --- a/main.go +++ b/main.go @@ -2,7 +2,6 @@ package main import ( "encoding/json" - "errors" "fmt" "github.com/gin-gonic/gin" "github.com/golang-jwt/jwt" @@ -62,8 +61,7 @@ func init() { os.MkdirAll(proto.CID_BASE_DIR+"workspace", os.ModePerm) //读取配置文件 //文件地址/home/videoplayer/vp.conf - //configPath := "/home/videoplayer/vp.conf" - configPath := "/etc/vp-app/vp.conf" + configPath := "/home/videoplayer/vp.conf" //读取配置文件 err := proto.ReadConfig(configPath) if err != nil { @@ -106,17 +104,17 @@ func JWTAuthMiddleware() gin.HandlerFunc { if tokenString == "" { tokenString = c.Query("token") } - for k, _ := range proto.Url_map { - if strings.Contains(c.Request.URL.Path, k) { - log.Println("need not check token:", c.Request.URL.Path) - c.Next() - return - } - } - //if proto.Url_map[c.Request.URL.Path] == true { //查看是否在不需要token的url中 - // c.Next() - // return + //如果请求为login或register,则不需要验证token + //for k, _ := range proto.Url_map { + // if strings.Contains(c.Request.URL.Path, k) { + // c.Next() + // return + // } //} + if proto.Url_map[c.Request.URL.Path] == true { //查看是否在不需要token的url中 + c.Next() + return + } if tokenString == "" { c.AbortWithStatusJSON(http.StatusOK, gin.H{"message": "unauthorized", "error": "token is empty", "code": proto.TokenIsNull}) return @@ -147,33 +145,21 @@ func JWTAuthMiddleware() gin.HandlerFunc { return } + proto.SigningKeyRWLock.RLock() //加读锁 // 使用加密secret 解析 JWT 令牌 - //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 - } + token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) { return proto.SigningKey, nil }) - // 错误处理 - if err != nil { - var ve *jwt.ValidationError - if errors.As(err, &ve) { - switch { - case ve.Errors&jwt.ValidationErrorMalformed != 0: - c.AbortWithStatusJSON(http.StatusOK, gin.H{"error": "Malformed token:" + err.Error(), "code": proto.TokenInvalid}) - case ve.Errors&jwt.ValidationErrorExpired != 0: - 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 - } + proto.SigningKeyRWLock.RUnlock() + + // 验证令牌 + if err != nil || !token.Valid { + c.AbortWithStatusJSON(http.StatusOK, gin.H{ + "message": "NOT_LOGIN", + "error": "Invalid token", + "code": proto.TokenExpired, + }) + return } // 将用户信息添加到上下文中 @@ -187,9 +173,9 @@ func JWTAuthMiddleware() gin.HandlerFunc { c.AbortWithStatusJSON(http.StatusOK, gin.H{"message": "unauthorized", "error": "no function permission", "code": proto.NoPermission}) return } + // 继续处理请求 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() service.ShellWillRunFromServer() + service.SyncTokenSecretFromUserCenter() } diff --git a/proto/conf.go b/proto/conf.go index 7389795..258698b 100644 --- a/proto/conf.go +++ b/proto/conf.go @@ -6,6 +6,7 @@ import ( "gorm.io/gorm" "log" "os" + "sync" ) 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 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 ConfigRWLock = &sync.RWMutex{} +var SigningKeyRWLock = &sync.RWMutex{} + +var SyncSecretReqLog int64 + const ( MYSQL_USER = "video_t2" MYSQL_DB = "video_t2" @@ -86,6 +94,30 @@ type ConfigStruct struct { 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 { //查看配置文件是否存在,不存在则创建 diff --git a/proto/user_req.go b/proto/user_req.go index 84af5b1..08d70bd 100644 --- a/proto/user_req.go +++ b/proto/user_req.go @@ -117,3 +117,29 @@ type ResponseOAuth struct { Email string `json:"email" form:"email"` 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"` +} diff --git a/service/toolService.go b/service/toolService.go index 5c9e9b9..fc82c1f 100644 --- a/service/toolService.go +++ b/service/toolService.go @@ -1,9 +1,12 @@ package service import ( + "encoding/json" "errors" "fmt" + "log" "regexp" + "strconv" "time" "videoplayer/proto" "videoplayer/worker" @@ -184,3 +187,133 @@ func DelMonitorDeviceListWithStatus(userId int, deviceReq []proto.GetMonitorDevi } 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 +} diff --git a/worker/tool.go b/worker/tool.go index e9a0b55..3301ea0 100644 --- a/worker/tool.go +++ b/worker/tool.go @@ -1,13 +1,93 @@ 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 { str := "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZ" bytes := []byte(str) var result []byte 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) } + +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)) // 将二进制数据转换为十六进制字符串 +}