diff --git a/admin/server/api/v1/gaia/forward_proxy.go b/admin/server/api/v1/gaia/forward_proxy.go index c362d3086..e972047ab 100644 --- a/admin/server/api/v1/gaia/forward_proxy.go +++ b/admin/server/api/v1/gaia/forward_proxy.go @@ -23,6 +23,13 @@ type ForwardProxyApi struct{} // @Param path path string true "上游路径" // @Router /gaia/forward/proxy/{path} [get,post,put,patch,delete] func (f *ForwardProxyApi) ForwardProxy(c *gin.Context) { + // 打印请求 Header,便于排查转发问题 + global.GVA_LOG.Info("ForwardProxy 请求头", + zap.Any("headers", c.Request.Header), + zap.String("method", c.Request.Method), + zap.String("path", c.Request.URL.Path), + ) + // 1. 读取转发配置 integrate := systemIntegratedService.GetIntegratedConfig(gaiaModel.SystemIntegrationDingTalk) configMap, err := systemIntegratedService.ParseDingTalkConfig(integrate.Config) @@ -33,12 +40,24 @@ func (f *ForwardProxyApi) ForwardProxy(c *gin.Context) { // 2. 获取并校验 forwarding token(存在有效 Token 即视为开启转发能力) dingId := c.GetHeader("X-Ding-Id") + apiKey := c.GetHeader("X-Api-Key") bearer := c.GetHeader("Authorization") token := c.GetHeader("X-Forward-Token") - if len(bearer) > 7 && len(dingId) == 0 { - // 从 Token 中验签并提取 ding_id - dingId, err = systemIntegratedService.ParseForwardToken(bearer[7:], configMap.ForwardConfig.Tokens) - if err != nil { + + if (len(bearer) > gaiaModel.BearerLength || len(apiKey) > gaiaModel.BearerLength) && len(dingId) == 0 { + if len(bearer) > gaiaModel.BearerLength { + if bearer[:gaiaModel.BearerLength] == "Bearer " { + bearer = bearer[gaiaModel.BearerLength:] + } + } else if len(apiKey) > gaiaModel.BearerLength { + if apiKey[:gaiaModel.BearerLength] == "Bearer " { + bearer = apiKey[gaiaModel.BearerLength:] + } else { + bearer = apiKey + } + } + + if dingId, err = systemIntegratedService.ParseForwardToken(bearer, configMap.ForwardConfig.Tokens); err != nil { c.JSON(http.StatusUnauthorized, gin.H{"error": gin.H{"message": "Token 验证失败: " + err.Error()}}) return } diff --git a/admin/server/api/v1/gaia/system.go b/admin/server/api/v1/gaia/system.go index 4957bba45..26c6b8701 100644 --- a/admin/server/api/v1/gaia/system.go +++ b/admin/server/api/v1/gaia/system.go @@ -2,10 +2,13 @@ package gaia import ( "context" + "crypto/rand" "crypto/sha256" + "encoding/base64" "encoding/json" "fmt" "net/url" + "strconv" "strings" "github.com/flipped-aurora/gin-vue-admin/server/global" @@ -14,6 +17,7 @@ import ( "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/request" gaiaResp "github.com/flipped-aurora/gin-vue-admin/server/model/gaia/response" "github.com/flipped-aurora/gin-vue-admin/server/model/system" + serviceGaia "github.com/flipped-aurora/gin-vue-admin/server/service/gaia" "github.com/flipped-aurora/gin-vue-admin/server/utils" "github.com/gin-gonic/gin" "github.com/google/uuid" @@ -154,10 +158,11 @@ func (systemApi *SystemApi) GetForwardTokens(c *gin.Context) { } tokens := make([]gaiaResp.ForwardTokenInfo, 0, len(configMap.ForwardConfig.Tokens)) - for _, token := range configMap.ForwardConfig.Tokens { + for i, token := range configMap.ForwardConfig.Tokens { tokens = append(tokens, gaiaResp.ForwardTokenInfo{ - ID: utils.AddAsteriskToString(token.ID), + ID: utils.AddAsteriskToString(token.TokenSecret), CreatedAt: token.CreatedAt, + Seq: i + 1, }) } @@ -205,15 +210,24 @@ func (systemApi *SystemApi) CreateForwardToken(c *gin.Context) { // 生成唯一 ID 和哈希 tokenID := "tok_" + uuid.New().String() tokenHash := fmt.Sprintf("%x", sha256.Sum256([]byte(req.Token))) + // 生成 HMAC 签名密钥(仅创建时回传一次) + secretBytes := make([]byte, 32) + if _, err := rand.Read(secretBytes); err != nil { + response.FailWithMessage("生成 TokenSecret 失败:"+err.Error(), c) + return + } + tokenSecret := base64.RawURLEncoding.EncodeToString(secretBytes) newToken := request.ForwardToken{ - ID: tokenID, - TokenHash: tokenHash, - CreatedAt: time.Now(), + ID: tokenID, + TokenHash: tokenHash, + CreatedAt: time.Now(), + TokenSecret: tokenSecret, } // 添加到配置 configMap.ForwardConfig.Tokens = append(configMap.ForwardConfig.Tokens, newToken) + seq := len(configMap.ForwardConfig.Tokens) // 1..N configJSON, _ := json.Marshal(configMap) integrate.Config = string(configJSON) @@ -225,9 +239,10 @@ func (systemApi *SystemApi) CreateForwardToken(c *gin.Context) { // 返回明文 token(仅此次展示) response.OkWithData(gin.H{ - "id": tokenID, - "token": req.Token, - "created_at": newToken.CreatedAt, + "seq": seq, + "token": req.Token, + "token_secret": tokenSecret, + "created_at": newToken.CreatedAt, }, c) } @@ -237,14 +252,19 @@ func (systemApi *SystemApi) CreateForwardToken(c *gin.Context) { // @Security ApiKeyAuth // @accept application/json // @Produce application/json -// @Param id path string true "Token ID" +// @Param seq path int true "Token 序列号(从列表获取,1..N)" // @Param password body string true "当前用户密码" // @Success 200 {object} response.Response{msg=string} "删除成功" -// @Router /gaia/system/forward-tokens/:id [delete] +// @Router /gaia/system/forward-tokens/:seq [delete] func (systemApi *SystemApi) DeleteForwardToken(c *gin.Context) { - tokenID := c.Param("id") - if tokenID == "" { - response.FailWithMessage("Token ID 不能为空", c) + seqStr := c.Param("seq") + if seqStr == "" { + response.FailWithMessage("Token 序列号不能为空", c) + return + } + seq, err := strconv.Atoi(seqStr) + if err != nil || seq <= 0 { + response.FailWithMessage("Token 序列号非法", c) return } @@ -256,14 +276,22 @@ func (systemApi *SystemApi) DeleteForwardToken(c *gin.Context) { return } - // 验证当前用户密码 + // 验证当前用户密码(使用 Dify account 密码体系) userID := utils.GetUserUuid(c).String() var user system.SysUser - if err := global.GVA_DB.Select("password").First(&user, userID).Error; err != nil { + if err := global.GVA_DB.Select("email").Where( + "uuid = ?", userID).First(&user).Error; err != nil { response.FailWithMessage("查询用户失败:"+err.Error(), c) return } - if !utils.BcryptCheck(req.Password, user.Password) { + account, err := user.GetAccount() + if err != nil { + response.FailWithMessage("查询账号失败:"+err.Error(), c) + return + } + var pwd serviceGaia.PasswdEncode + if ok, pwdErr := pwd.ComparePassword( + req.Password, account.Password, account.PasswordSalt); pwdErr != nil || !ok { response.FailWithMessage("密码错误", c) return } @@ -272,34 +300,28 @@ func (systemApi *SystemApi) DeleteForwardToken(c *gin.Context) { integrate := systemIntegratedService.GetIntegratedConfig(gaia.SystemIntegrationDingTalk) var configMap request.DingTalkConfigRequest if integrate.Config != "" { - if err := json.Unmarshal([]byte(integrate.Config), &configMap); err != nil { + if err = json.Unmarshal([]byte(integrate.Config), &configMap); err != nil { response.FailWithMessage("解析配置失败:"+err.Error(), c) return } } // 查找并删除 token - found := false - newTokens := make([]request.ForwardToken, 0, len(configMap.ForwardConfig.Tokens)) - for _, token := range configMap.ForwardConfig.Tokens { - if token.ID == tokenID { - found = true - continue - } - newTokens = append(newTokens, token) - } - - if !found { + if seq > len(configMap.ForwardConfig.Tokens) { response.FailWithMessage("Token 不存在", c) return } + idx := seq - 1 + newTokens := make([]request.ForwardToken, 0, len(configMap.ForwardConfig.Tokens)-1) + newTokens = append(newTokens, configMap.ForwardConfig.Tokens[:idx]...) + newTokens = append(newTokens, configMap.ForwardConfig.Tokens[idx+1:]...) // 更新配置 configMap.ForwardConfig.Tokens = newTokens configJSON, _ := json.Marshal(configMap) integrate.Config = string(configJSON) - if err := systemIntegratedService.SetIntegratedConfig(integrate, "", false); err != nil { + if err = systemIntegratedService.SetIntegratedConfig(integrate, "", false); err != nil { response.FailWithMessage("保存失败:"+err.Error(), c) return } diff --git a/admin/server/model/gaia/response/system.go b/admin/server/model/gaia/response/system.go index 83d572de8..cccca1060 100644 --- a/admin/server/model/gaia/response/system.go +++ b/admin/server/model/gaia/response/system.go @@ -2,9 +2,10 @@ package response import "time" -// ForwardTokenInfo 转发 Token 列表项(脱敏后的 Token ID) +// ForwardTokenInfo 转发 Token 列表项(不暴露内部 ID) type ForwardTokenInfo struct { - ID string `json:"id"` + ID string `json:"id"` // token + Seq int `json:"seq"` // 1..N 序列号(用于删除) CreatedAt time.Time `json:"created_at"` } diff --git a/admin/server/model/gaia/system_integration.go b/admin/server/model/gaia/system_integration.go index ff1382ed1..a66724912 100644 --- a/admin/server/model/gaia/system_integration.go +++ b/admin/server/model/gaia/system_integration.go @@ -5,6 +5,7 @@ const SystemIntegrationDingTalk = uint(1) // 钉钉集成 const SystemIntegrationWeiXin = uint(2) // 微信集成 const SystemIntegrationFeiShu = uint(3) // 飞书集成 const SystemIntegrationOAuth2 = uint(4) // OAuth2集成 +const BearerLength = 7 // OAuth2集成 // SystemIntegration 系统集成表 type SystemIntegration struct { diff --git a/admin/server/router/gaia/system.go b/admin/server/router/gaia/system.go index bf3ecd5e2..54a75bc33 100644 --- a/admin/server/router/gaia/system.go +++ b/admin/server/router/gaia/system.go @@ -21,7 +21,7 @@ func (s *SystemRouter) InitSystemRouter(Router *gin.RouterGroup) { // 转发 Token 管理 systemRouter.GET("forward-tokens", systemApi.GetForwardTokens) // 获取转发 Token 列表 systemRouter.POST("forward-tokens", systemApi.CreateForwardToken) // 新增转发 Token - systemRouter.DELETE("forward-tokens/:id", systemApi.DeleteForwardToken) // 删除转发 Token + systemRouter.DELETE("forward-tokens/:seq", systemApi.DeleteForwardToken) // 删除转发 Token(按序列号) } } diff --git a/admin/server/service/gaia/gaia_login.go b/admin/server/service/gaia/gaia_login.go index 7f5e12ef0..d268408f8 100644 --- a/admin/server/service/gaia/gaia_login.go +++ b/admin/server/service/gaia/gaia_login.go @@ -16,7 +16,6 @@ import ( "io" "net/http" "net/url" - "os" "strings" ) @@ -241,6 +240,7 @@ func (e *SystemIntegratedService) DingTalkCodeLogin(req request.GaiaDingTalkLogi } var dingUser map[string]interface{} + fmt.Println("sssssssss", string(userBody)) if err = json.Unmarshal(userBody, &dingUser); err != nil { return nil, fmt.Errorf("解析钉钉用户信息失败") } @@ -256,7 +256,8 @@ func (e *SystemIntegratedService) DingTalkCodeLogin(req request.GaiaDingTalkLogi } } - // 解析邮箱配置 + // 解析用户名配置 + var emailList []string var configMap request.DingTalkConfigRequest var emailConfig request.EmailApiConfig if integrate.Config != "" { @@ -271,13 +272,11 @@ func (e *SystemIntegratedService) DingTalkCodeLogin(req request.GaiaDingTalkLogi } } - // 优先通过邮箱 API 获取邮箱(新格式) + // 优先通过用户名 API 获取用户名(新格式) if emailConfig.Enabled && dingId != "" { - email, apiErr := e.callEmailApi(dingId, emailConfig) - if apiErr == nil && email != "" { - global.GVA_LOG.Info("DingTalkCodeLogin: 通过第三方邮箱 API 获取邮箱", - zap.String("ding_id", dingId), zap.String("email", email)) - sysUser, findErr := e.findUserByEmail(email) + emailList, err = e.callEmailApi(dingId, emailConfig) + if err == nil && len(emailList) > 0 { + sysUser, findErr := e.findUserByEmail(emailList) if findErr != nil { return nil, findErr } @@ -288,7 +287,7 @@ func (e *SystemIntegratedService) DingTalkCodeLogin(req request.GaiaDingTalkLogi return &response.GaiaLoginResult{User: *sysUser, Token: token, RedirectURI: req.RedirectURI, State: req.State}, nil } global.GVA_LOG.Warn("DingTalkCodeLogin: 第三方邮箱 API 获取失败,尝试钉钉直接返回邮箱", - zap.String("ding_id", dingId), zap.Error(apiErr)) + zap.String("ding_id", dingId), zap.Error(err)) } // 回退:直接从钉钉用户信息获取邮箱 @@ -301,7 +300,7 @@ func (e *SystemIntegratedService) DingTalkCodeLogin(req request.GaiaDingTalkLogi return nil, fmt.Errorf("钉钉未返回邮箱") } - sysUser, err := e.findUserByEmail(email) + sysUser, err := e.findUserByEmail([]string{email}) if err != nil { return nil, err } @@ -318,7 +317,8 @@ func getStringFromMap(m map[string]interface{}, keys ...string) string { continue } if v, ok := m[k]; ok && v != nil { - if s, ok := v.(string); ok { + var s string + if s, ok = v.(string); ok { return s } } @@ -384,21 +384,14 @@ func getStringByPathOrKeys(m map[string]interface{}, path string, fallbackKeys . return getStringFromMap(m, fallbackKeys...) } -// findUserByEmail 按邮箱查找已存在的用户(需在 gaia.accounts 中有对应记录方可签发 JWT) -func (e *SystemIntegratedService) findUserByEmail(email string) (*system.SysUser, error) { - var u system.SysUser - var mailList []string - mailList = append(mailList, email) - parts := strings.Split(email, "@") - defaultMail := os.Getenv(gaia.EmailDomainEnv) - if len(defaultMail) > 0 && len(parts) == 2 { - mailList = append(mailList, parts[0]+"@"+defaultMail) - } +// findUserByEmail 按username查找已存在的用户(需在 gaia.accounts 中有对应记录方可签发 JWT) +func (e *SystemIntegratedService) findUserByEmail(mailList []string) (*system.SysUser, error) { // 查询关联邮箱 + var u system.SysUser if err := global.GVA_DB.Where("email IN (?)", mailList).Preload( "Authorities").Preload("Authority").First(&u).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - return nil, fmt.Errorf("邮箱%s尚未开通账号,请联系管理员", email) + return nil, fmt.Errorf("%s尚未开通账号,请联系管理员", mailList[0]) } return nil, err } @@ -410,20 +403,19 @@ func (e *SystemIntegratedService) findUserByEmail(email string) (*system.SysUser } // findUserByEmailOrPhone 按邮箱或用户唯一标识(如手机号)查找用户,优先邮箱 -func (e *SystemIntegratedService) findUserByEmailOrPhone(email, userID string) (*system.SysUser, error) { - if email != "" { - u, err := e.findUserByEmail(email) - if err == nil { +func (e *SystemIntegratedService) findUserByEmailOrPhone(mail, userID string) (u *system.SysUser, err error) { + if mail != "" { + if u, err = e.findUserByEmail([]string{mail}); err == nil { return u, nil } // 仅当“未开通”时再尝试按 userID(phone) 查,其他错误直接返回 - if err != nil && !strings.Contains(err.Error(), "尚未开通") { + if !strings.Contains(err.Error(), "尚未开通") { return nil, err } } if userID != "" { - var u system.SysUser - if err := global.GVA_DB.Where("phone = ?", userID).Preload("Authorities").Preload("Authority").First(&u).Error; err != nil { + if err = global.GVA_DB.Where("phone = ?", userID).Preload( + "Authorities").Preload("Authority").First(&u).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, fmt.Errorf("该用户唯一标识尚未开通后台账号,请联系管理员") } @@ -432,7 +424,7 @@ func (e *SystemIntegratedService) findUserByEmailOrPhone(email, userID string) ( if u.Enable != 1 { return nil, fmt.Errorf("账号已被禁用") } - return &u, nil + return u, nil } return nil, fmt.Errorf("无法从 OAuth2 用户信息中获取邮箱或用户唯一标识") } diff --git a/admin/server/service/gaia/system.go b/admin/server/service/gaia/system.go index ff8a1829f..cc43dbe9c 100644 --- a/admin/server/service/gaia/system.go +++ b/admin/server/service/gaia/system.go @@ -5,7 +5,6 @@ import ( "crypto/hmac" "crypto/sha256" "encoding/base64" - "encoding/hex" "encoding/json" "errors" "fmt" @@ -727,23 +726,32 @@ func buildURL(baseURL string, config request.EmailApiConfig, dingId string) stri } // callEmailApi 调用第三方邮箱 API,使用 ding_id(用户名) 获取邮箱 -func (e *SystemIntegratedService) callEmailApi(dingId string, config request.EmailApiConfig) (string, error) { +func (e *SystemIntegratedService) callEmailApi( + dingId string, config request.EmailApiConfig) (mailList []string, err error) { + // init respBody, _, err := e.doEmailApiRequest(dingId, config) if err != nil { - return "", err + return mailList, err } var respJSON map[string]interface{} if err = json.Unmarshal(respBody, &respJSON); err != nil { - return "", fmt.Errorf("解析响应 JSON 失败:%s", err.Error()) + return mailList, fmt.Errorf("解析响应 JSON 失败:%s", err.Error()) } email := extractJSONPathAdvanced(respJSON, config.ResponseEmailField) if email == "" { - return "", fmt.Errorf("响应中未找到邮箱(路径:%s)", config.ResponseEmailField) + return mailList, fmt.Errorf("响应中未找到邮箱(路径:%s)", config.ResponseEmailField) + } + // + mailList = append(mailList, email) + parts := strings.Split(email, "@") + defaultMail := os.Getenv(gaia.EmailDomainEnv) + if len(defaultMail) > 0 && len(parts) > 1 && len(parts[0]) > 0 { + mailList = append(mailList, parts[0]+"@"+defaultMail) } - return email, nil + return mailList, nil } // doEmailApiRequest 构建并执行邮箱 API 请求,返回响应体字节和状态码 @@ -920,10 +928,8 @@ func (e *SystemIntegratedService) ParseForwardToken( if t.TokenSecret == "" { continue } - secret, err := hex.DecodeString(t.TokenSecret) - if err != nil { - continue - } + // 直接使用原始字节作为 HMAC 密钥,兼容任意字符串格式的密钥 + secret := []byte(t.TokenSecret) // 验证 HMAC 签名 mac := hmac.New(sha256.New, secret) mac.Write([]byte(payloadB64)) @@ -952,13 +958,13 @@ func (e *SystemIntegratedService) ParseForwardToken( // ResolveAccountByDingId 通过钉钉 ID 解析 gaia account_id // 解析顺序:Redis 缓存 → AccountDingTalkExtend 本地表 → 第三方 EmailApi(邮箱 API) -func (e *SystemIntegratedService) ResolveAccountByDingId(dingId string, apiConfig request.EmailApiConfig) (string, error) { - ctx := context.Background() - redisKey := "gaia:forward:ding:" + dingId +func (e *SystemIntegratedService) ResolveAccountByDingId( + dingId string, apiConfig request.EmailApiConfig) (string, error) { // 1. 查 Redis 缓存 + ctx := context.Background() + redisKey := "gaia:forward:ding:" + dingId if cached, err := global.GVA_REDIS.Get(ctx, redisKey).Result(); err == nil && cached != "" { - global.GVA_LOG.Info("ResolveAccountByDingId: Redis 命中", zap.String("ding_id", dingId), zap.String("account_id", cached)) return cached, nil } @@ -966,7 +972,6 @@ func (e *SystemIntegratedService) ResolveAccountByDingId(dingId string, apiConfi var extend gaia.AccountDingTalkExtend if err := global.GVA_DB.Where("ding_talk = ?", dingId).First(&extend).Error; err == nil { accountID := extend.ID.String() - global.GVA_LOG.Info("ResolveAccountByDingId: 本地表命中", zap.String("ding_id", dingId), zap.String("account_id", accountID)) global.GVA_REDIS.Set(ctx, redisKey, accountID, 24*time.Hour) return accountID, nil } @@ -984,7 +989,7 @@ func (e *SystemIntegratedService) ResolveAccountByDingId(dingId string, apiConfi // 4. 按邮箱查 accounts 表(匹配 email 字段) var account gaia.Account if err = global.GVA_DB.Where("email = ?", email).First(&account).Error; err != nil { - return "", fmt.Errorf("邮箱 %s 不存在(来自第三方邮箱 API)", email) + return "", fmt.Errorf("用户 %s 不存在(来自第三方邮箱 API)", email) } accountID := account.ID.String() @@ -997,11 +1002,5 @@ func (e *SystemIntegratedService) ResolveAccountByDingId(dingId string, apiConfi // 6. 写 Redis 缓存 global.GVA_REDIS.Set(ctx, redisKey, accountID, 24*time.Hour) - - global.GVA_LOG.Info("ResolveAccountByDingId: 第三方邮箱 API 解析成功", - zap.String("ding_id", dingId), - zap.String("email", email), - zap.String("account_id", accountID)) - return accountID, nil } diff --git a/admin/web/src/api/gaia/system.js b/admin/web/src/api/gaia/system.js index 33f9bbdcd..05bc3a531 100644 --- a/admin/web/src/api/gaia/system.js +++ b/admin/web/src/api/gaia/system.js @@ -80,10 +80,10 @@ export const createForwardToken = (data) => { // @Tags systrm // @Summary 删除转发 Token // @Security ApiKeyAuth -// @Router /gaia/system/forward-tokens/:id [delete] -export const deleteForwardToken = (id, password) => { +// @Router /gaia/system/forward-tokens/:seq [delete] +export const deleteForwardToken = (seq, password) => { return service({ - url: `/gaia/system/forward-tokens/${id}`, + url: `/gaia/system/forward-tokens/${seq}`, method: 'delete', data: { password }, }) diff --git a/admin/web/src/view/systemIntegrated/dingTalk/index.vue b/admin/web/src/view/systemIntegrated/dingTalk/index.vue index 79f97aac5..3a3adc19c 100644 --- a/admin/web/src/view/systemIntegrated/dingTalk/index.vue +++ b/admin/web/src/view/systemIntegrated/dingTalk/index.vue @@ -96,17 +96,17 @@
- 点击「生成并保存」将随机生成 Token,保存后会自动复制到系统剪贴板,请粘贴到安全位置保管。Token 仅展示一次。
+ 点击「生成并保存」将随机生成 Token,并返回两种凭证:
+
2)Token Secret(用于生成 Authorization: Bearer ... 的签名密钥)
+
两者仅展示一次,请务必复制到安全位置保管。