package controllers import ( "crypto/rand" "crypto/sha256" "encoding/hex" "encoding/json" "fmt" "io" "strings" "server/models" "server/pkg/jwtutil" beego "github.com/beego/beego/v2/server/web" ) // PlatformAuthClientController 统一认证中心 —— 接入应用(OIDC Client)管理。 // // 以后每开发一个新软件,只需在这里注册一条即可接入统一认证, // 无需改代码、无需手写 SQL。 type PlatformAuthClientController struct { beego.Controller } func (c *PlatformAuthClientController) serveJSON(data map[string]interface{}) { c.Data["json"] = data _ = c.ServeJSON() } // Prepare 统一鉴权:接入应用属于平台级敏感配置,仅平台管理员可操作。 // 不依赖全局中间件(其处于 warn 观察模式时不拦截),这里主动校验。 func (c *PlatformAuthClientController) Prepare() { authHeader := c.Ctx.Request.Header.Get("Authorization") if authHeader == "" { c.Ctx.Output.SetStatus(401) c.serveJSON(map[string]interface{}{"code": 401, "msg": "未登录"}) c.StopRun() return } parts := strings.SplitN(authHeader, " ", 2) if len(parts) != 2 || parts[0] != "Bearer" { c.Ctx.Output.SetStatus(401) c.serveJSON(map[string]interface{}{"code": 401, "msg": "认证信息格式错误"}) c.StopRun() return } claims, err := jwtutil.ParseToken(parts[1]) if err != nil { c.Ctx.Output.SetStatus(401) c.serveJSON(map[string]interface{}{"code": 401, "msg": "登录已失效,请重新登录"}) c.StopRun() return } // 平台端应用注册只允许平台管理员操作;租户管理员无权访问 if claims.UserType != "platform" { c.Ctx.Output.SetStatus(403) c.serveJSON(map[string]interface{}{"code": 403, "msg": "无权访问"}) c.StopRun() return } } type authClientPayload struct { ClientID string `json:"client_id"` AppCode string `json:"app_code"` Name string `json:"name"` AppType int8 `json:"app_type"` RedirectURIs *string `json:"redirect_uris"` PostLogoutURIs *string `json:"post_logout_uris"` BackchannelLogoutURI *string `json:"backchannel_logout_uri"` GrantTypes string `json:"grant_types"` Scope *string `json:"scope"` AccessTTL int `json:"access_ttl"` RefreshTTL int `json:"refresh_ttl"` Realm string `json:"realm"` Status *int8 `json:"status"` } func (c *PlatformAuthClientController) parsePayload() (authClientPayload, bool) { var p authClientPayload raw, _ := io.ReadAll(c.Ctx.Request.Body) if len(raw) > 0 { if err := json.Unmarshal(raw, &p); err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"}) return p, false } } return p, true } // List 接入应用列表(不返回 client_secret) // GET /platform/authClient/list func (c *PlatformAuthClientController) List() { var rows []models.AuthClient if _, err := models.Orm.QueryTable(new(models.AuthClient)). OrderBy("-id").All(&rows); err != nil { c.serveJSON(map[string]interface{}{"code": 500, "msg": "查询失败: " + err.Error()}) return } // 脱敏:只告知是否配置了密钥,不下发明文或哈希 list := make([]map[string]interface{}, 0, len(rows)) for _, r := range rows { list = append(list, map[string]interface{}{ "id": r.ID, "client_id": r.ClientID, "app_code": r.AppCode, "name": r.Name, "app_type": r.AppType, "redirect_uris": derefStrAuth(r.RedirectURIs), "post_logout_uris": derefStrAuth(r.PostLogoutURIs), "backchannel_logout_uri": derefStrAuth(r.BackchannelLogoutURI), "grant_types": r.GrantTypes, "scope": derefStrAuth(r.Scope), "access_ttl": r.AccessTTL, "refresh_ttl": r.RefreshTTL, "realm": r.Realm, "status": r.Status, "has_secret": r.ClientSecret != nil, "create_time": r.CreateTime, }) } c.serveJSON(map[string]interface{}{ "code": 200, "msg": "success", "data": map[string]interface{}{"list": list, "total": len(list)}, }) } // Detail 应用详情 // GET /platform/authClient/detail/:id func (c *PlatformAuthClientController) Detail() { id, err := c.parseID() if err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": "无效ID"}) return } var row models.AuthClient if err := models.Orm.QueryTable(new(models.AuthClient)).Filter("id", id).One(&row); err != nil { c.serveJSON(map[string]interface{}{"code": 404, "msg": "记录不存在"}) return } row.ClientSecret = nil c.serveJSON(map[string]interface{}{"code": 200, "msg": "success", "data": row}) } // Create 新增应用 // POST /platform/authClient/create func (c *PlatformAuthClientController) Create() { p, ok := c.parsePayload() if !ok { return } p.ClientID = strings.TrimSpace(p.ClientID) p.AppCode = strings.TrimSpace(p.AppCode) p.Name = strings.TrimSpace(p.Name) if p.ClientID == "" || p.AppCode == "" || p.Name == "" { c.serveJSON(map[string]interface{}{"code": 400, "msg": "client_id、应用编码、名称均不能为空"}) return } if err := validateJSONStringArray(p.RedirectURIs); err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": "redirect_uris 必须是 JSON 数组: " + err.Error()}) return } if err := validateJSONStringArray(p.PostLogoutURIs); err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": "post_logout_uris 必须是 JSON 数组: " + err.Error()}) return } if exist := existsAuthClient("client_id", p.ClientID); exist { c.serveJSON(map[string]interface{}{"code": 400, "msg": "client_id 已存在"}) return } if exist := existsAuthClient("app_code", p.AppCode); exist { c.serveJSON(map[string]interface{}{"code": 400, "msg": "应用编码已存在"}) return } item := &models.AuthClient{ ClientID: p.ClientID, AppCode: p.AppCode, Name: p.Name, AppType: p.AppType, RedirectURIs: p.RedirectURIs, PostLogoutURIs: p.PostLogoutURIs, BackchannelLogoutURI: p.BackchannelLogoutURI, GrantTypes: defaultStrAuth(p.GrantTypes, "authorization_code,refresh_token"), Scope: p.Scope, AccessTTL: defaultIntAuth(p.AccessTTL, 1800), RefreshTTL: defaultIntAuth(p.RefreshTTL, 2592000), Realm: defaultStrAuth(p.Realm, models.AuthRealmTenant), Status: 1, } if p.Status != nil { item.Status = *p.Status } // 机密客户端(有后端的 Web 应用)才下发密钥;SPA/APP 走 PKCE,不持有密钥 secretPlain := "" if p.AppType == models.AuthAppTypeWeb { plain, hashed, err := generateClientSecret() if err != nil { c.serveJSON(map[string]interface{}{"code": 500, "msg": "生成密钥失败"}) return } secretPlain = plain item.ClientSecret = &hashed } id, err := models.Orm.Insert(item) if err != nil { c.serveJSON(map[string]interface{}{"code": 500, "msg": "创建失败: " + err.Error()}) return } c.serveJSON(map[string]interface{}{ "code": 200, "msg": "创建成功" + map[bool]string{true: "(请妥善保存密钥,仅此一次显示)", false: ""}[secretPlain != ""], "data": map[string]interface{}{"id": id, "client_secret": secretPlain}, }) } // Edit 编辑应用 // POST /platform/authClient/edit/:id func (c *PlatformAuthClientController) Edit() { id, err := c.parseID() if err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": "无效ID"}) return } p, ok := c.parsePayload() if !ok { return } if err := validateJSONStringArray(p.RedirectURIs); err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": "redirect_uris 必须是 JSON 数组: " + err.Error()}) return } if err := validateJSONStringArray(p.PostLogoutURIs); err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": "post_logout_uris 必须是 JSON 数组: " + err.Error()}) return } update := map[string]interface{}{} if v := strings.TrimSpace(p.Name); v != "" { update["name"] = v } if p.AppType > 0 { update["app_type"] = p.AppType } if p.RedirectURIs != nil { update["redirect_uris"] = *p.RedirectURIs } if p.PostLogoutURIs != nil { update["post_logout_uris"] = *p.PostLogoutURIs } if p.BackchannelLogoutURI != nil { update["backchannel_logout_uri"] = *p.BackchannelLogoutURI } if v := strings.TrimSpace(p.GrantTypes); v != "" { update["grant_types"] = v } if p.Scope != nil { update["scope"] = *p.Scope } if p.AccessTTL > 0 { update["access_ttl"] = p.AccessTTL } if p.RefreshTTL > 0 { update["refresh_ttl"] = p.RefreshTTL } if v := strings.TrimSpace(p.Realm); v != "" { update["realm"] = v } if p.Status != nil { update["status"] = *p.Status } if len(update) == 0 { c.serveJSON(map[string]interface{}{"code": 400, "msg": "无更新字段"}) return } if _, err := models.Orm.QueryTable(new(models.AuthClient)).Filter("id", id).Update(update); err != nil { c.serveJSON(map[string]interface{}{"code": 500, "msg": "更新失败: " + err.Error()}) return } c.serveJSON(map[string]interface{}{"code": 200, "msg": "success"}) } // ResetSecret 重置密钥(仅机密客户端),返回新明文,仅此一次显示 // POST /platform/authClient/resetSecret/:id func (c *PlatformAuthClientController) ResetSecret() { id, err := c.parseID() if err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": "无效ID"}) return } var row models.AuthClient if err := models.Orm.QueryTable(new(models.AuthClient)).Filter("id", id).One(&row); err != nil { c.serveJSON(map[string]interface{}{"code": 404, "msg": "记录不存在"}) return } if row.AppType != models.AuthAppTypeWeb { c.serveJSON(map[string]interface{}{"code": 400, "msg": "仅「Web 后端」类型的应用需要密钥,SPA/APP 使用 PKCE"}) return } plain, hashed, err := generateClientSecret() if err != nil { c.serveJSON(map[string]interface{}{"code": 500, "msg": "生成密钥失败"}) return } if _, err := models.Orm.QueryTable(new(models.AuthClient)). Filter("id", id). Update(map[string]interface{}{"client_secret": hashed}); err != nil { c.serveJSON(map[string]interface{}{"code": 500, "msg": "重置失败: " + err.Error()}) return } c.serveJSON(map[string]interface{}{ "code": 200, "msg": "重置成功,请妥善保存新密钥", "data": map[string]interface{}{"client_secret": plain}, }) } // Delete 停用应用(逻辑删除:status=0,避免误删导致线上应用无法登录) // DELETE /platform/authClient/delete/:id func (c *PlatformAuthClientController) Delete() { id, err := c.parseID() if err != nil { c.serveJSON(map[string]interface{}{"code": 400, "msg": "无效ID"}) return } if _, err := models.Orm.QueryTable(new(models.AuthClient)). Filter("id", id). Update(map[string]interface{}{"status": 0}); err != nil { c.serveJSON(map[string]interface{}{"code": 500, "msg": "停用失败: " + err.Error()}) return } c.serveJSON(map[string]interface{}{"code": 200, "msg": "已停用"}) } // ---------------------------------------------------------------- 工具 func (c *PlatformAuthClientController) parseID() (uint64, error) { var id uint64 _, err := fmt.Sscanf(c.Ctx.Input.Param(":id"), "%d", &id) if err != nil || id == 0 { return 0, fmt.Errorf("invalid id") } return id, nil } func existsAuthClient(field, value string) bool { return models.Orm.QueryTable(new(models.AuthClient)).Filter(field, value).Exist() } // generateClientSecret 生成密钥:明文只在创建/重置时返回一次,库里存 sha256 哈希 func generateClientSecret() (plain, hashed string, err error) { buf := make([]byte, 24) if _, err = rand.Read(buf); err != nil { return "", "", err } plain = "yzs_" + hex.EncodeToString(buf) sum := sha256.Sum256([]byte(plain)) return plain, hex.EncodeToString(sum[:]), nil } // validateJSONStringArray 校验字段为合法的字符串数组(为空时跳过) func validateJSONStringArray(p *string) error { if p == nil || strings.TrimSpace(*p) == "" { return nil } var arr []string return json.Unmarshal([]byte(*p), &arr) } func derefStrAuth(p *string) string { if p == nil { return "" } return *p } func defaultStrAuth(v, def string) string { if strings.TrimSpace(v) == "" { return def } return strings.TrimSpace(v) } func defaultIntAuth(v, def int) int { if v <= 0 { return def } return v }