批量更新
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
authsvc "server/services/auth"
|
||||
@@ -200,20 +201,33 @@ func (c *AuthLoginController) LogoutPage() {
|
||||
if sid != "" {
|
||||
_ = authsvc.RevokeSession(sid, models.RevokeReasonLogout)
|
||||
}
|
||||
// 走统一登出入口:吊销令牌并通知其他应用(单点登出)
|
||||
if access != "" {
|
||||
_ = authsvc.RevokeTokenPair("", access, models.RevokeReasonLogout)
|
||||
_ = authsvc.Logout(access, "")
|
||||
}
|
||||
clearSessionCookieForCtx(c.Ctx)
|
||||
|
||||
back := strings.TrimSpace(c.GetString("post_logout_redirect_uri"))
|
||||
cid := strings.TrimSpace(c.GetString("client_id"))
|
||||
if back != "" && cid != "" {
|
||||
if client, err := findClient(cid); err == nil && allowRedirect(client, back) {
|
||||
if client, err := findClient(cid); err == nil && allowLogoutRedirect(client, back) {
|
||||
c.Redirect(back, 302)
|
||||
return
|
||||
}
|
||||
// 回跳地址未登记时不要直接抛 JSON 给用户,退回登录页
|
||||
target := "/auth/login?error=" + url.QueryEscape("登出回跳地址未登记,已返回登录页")
|
||||
if cid != "" {
|
||||
target += "&client_id=" + url.QueryEscape(cid)
|
||||
}
|
||||
c.Redirect(target, 302)
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "已登出"})
|
||||
// 浏览器直接访问(无回跳参数):跳登录页,避免页面显示裸 JSON
|
||||
target := "/auth/login"
|
||||
if cid != "" {
|
||||
target += "?client_id=" + url.QueryEscape(cid)
|
||||
}
|
||||
c.Redirect(target, 302)
|
||||
}
|
||||
|
||||
// Sessions 在线设备列表
|
||||
|
||||
@@ -10,6 +10,7 @@ import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
@@ -262,10 +263,36 @@ func (c *AuthOidcController) UserInfo() {
|
||||
c.fail(500, "server_error")
|
||||
return
|
||||
}
|
||||
|
||||
// 业务接口与前端统一使用 identity_id 作为用户标识
|
||||
account, name, groupID := "", "", uint64(0)
|
||||
if bind, err := authsvc.GetTenantUser(identity.ID, uint64(claims.TenantId)); err == nil {
|
||||
groupID = bind.GroupID
|
||||
if bind.Account != nil {
|
||||
account = *bind.Account
|
||||
}
|
||||
if bind.Name != nil {
|
||||
name = *bind.Name
|
||||
}
|
||||
}
|
||||
if account == "" {
|
||||
account = profile.Mobile
|
||||
}
|
||||
if name == "" {
|
||||
name = profile.Nickname
|
||||
}
|
||||
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"sub": fmt.Sprintf("%d", identity.ID),
|
||||
"union_id": identity.UnionID,
|
||||
"tid": claims.TenantId,
|
||||
"sub": fmt.Sprintf("%d", identity.ID),
|
||||
"id": identity.ID,
|
||||
"union_id": identity.UnionID,
|
||||
"tid": claims.TenantId,
|
||||
// 诊断用:业务接口按 user_type 判定权限(backend / app),
|
||||
// 出现「无权访问」时可先看这里的值是否正确
|
||||
"user_type": claims.UserType,
|
||||
"group_id": groupID,
|
||||
"account": account,
|
||||
"name": name,
|
||||
"nickname": profile.Nickname,
|
||||
"mobile": profile.Mobile,
|
||||
"email": profile.Email,
|
||||
@@ -397,6 +424,46 @@ func allowRedirect(client *models.AuthClient, uri string) bool {
|
||||
return false
|
||||
}
|
||||
|
||||
// allowLogoutRedirect 登出回跳地址校验。
|
||||
//
|
||||
// 先精确匹配白名单,再按 origin(协议+主机+端口)放宽匹配:
|
||||
// 实际使用中「末尾斜杠」「带 #/login 片段」等差异很常见,
|
||||
// 只做精确匹配会导致明明同域却跳不回去,因此同域即放行。
|
||||
func allowLogoutRedirect(client *models.AuthClient, uri string) bool {
|
||||
raw := ""
|
||||
if client.PostLogoutURIs != nil {
|
||||
raw = *client.PostLogoutURIs
|
||||
}
|
||||
var list []string
|
||||
if raw != "" {
|
||||
_ = json.Unmarshal([]byte(raw), &list)
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return false
|
||||
}
|
||||
target := strings.TrimSpace(uri)
|
||||
for _, item := range list {
|
||||
if strings.TrimSpace(item) == target {
|
||||
return true
|
||||
}
|
||||
}
|
||||
|
||||
targetURL, err := url.Parse(target)
|
||||
if err != nil || targetURL.Scheme == "" || targetURL.Host == "" {
|
||||
return false
|
||||
}
|
||||
for _, item := range list {
|
||||
base, err := url.Parse(strings.TrimSpace(item))
|
||||
if err != nil || base.Scheme == "" || base.Host == "" {
|
||||
continue
|
||||
}
|
||||
if base.Scheme == targetURL.Scheme && base.Host == targetURL.Host {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// issueAuthCode 生成一次性授权码(明文返回,库中只存哈希)
|
||||
func issueAuthCode(clientID string, identityID, tid uint64, redirectURI, challenge, method, scope, nonce string) (string, error) {
|
||||
plain, err := randomString(32)
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"net/url"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"server/models"
|
||||
authsvc "server/services/auth"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// AuthSessionController 会话管理(用户自助:查看在线设备、踢下线)
|
||||
type AuthSessionController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *AuthSessionController) serveJSON(data map[string]interface{}) {
|
||||
c.Data["json"] = data
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// DevicesPage 在线设备页(浏览器访问,依赖认证中心会话 Cookie)
|
||||
// GET /auth/devices
|
||||
func (c *AuthSessionController) DevicesPage() {
|
||||
sid := strings.TrimSpace(c.Ctx.GetCookie(sessionCookieName))
|
||||
if sid == "" {
|
||||
c.Redirect("/auth/login", 302)
|
||||
return
|
||||
}
|
||||
session, err := authsvc.GetSession(sid)
|
||||
if err != nil {
|
||||
c.Redirect("/auth/login", 302)
|
||||
return
|
||||
}
|
||||
|
||||
list, err := authsvc.ListActiveSessions(session.IdentityID)
|
||||
if err != nil {
|
||||
list = nil
|
||||
}
|
||||
|
||||
items := make([]map[string]interface{}, 0, len(list))
|
||||
now := time.Now()
|
||||
for _, s := range list {
|
||||
device := "未知设备"
|
||||
if s.DeviceName != nil && strings.TrimSpace(*s.DeviceName) != "" {
|
||||
device = *s.DeviceName
|
||||
}
|
||||
ip := "-"
|
||||
if s.IP != nil {
|
||||
ip = *s.IP
|
||||
}
|
||||
client := s.ClientID
|
||||
if client == "" {
|
||||
client = "-"
|
||||
}
|
||||
items = append(items, map[string]interface{}{
|
||||
"Sid": s.Sid,
|
||||
"Current": s.Sid == sid,
|
||||
"Device": device,
|
||||
"IP": ip,
|
||||
"Client": client,
|
||||
"LoginAt": s.LoginAt.Format("2006-01-02 15:04"),
|
||||
"LastAt": s.LastAccessAt.Format("2006-01-02 15:04"),
|
||||
"Expired": s.ExpiresAt.Before(now),
|
||||
"Tid": s.Tid,
|
||||
})
|
||||
}
|
||||
|
||||
c.Data["Sessions"] = items
|
||||
c.Data["Success"] = c.GetString("success")
|
||||
c.Data["Error"] = c.GetString("error")
|
||||
c.TplName = "auth/devices.tpl"
|
||||
}
|
||||
|
||||
// KickPage 踢下线指定设备(页面入口,踢完后回到设备页)
|
||||
// GET /auth/devices/kick?sid=
|
||||
func (c *AuthSessionController) KickPage() {
|
||||
target := strings.TrimSpace(c.GetString("sid"))
|
||||
current := strings.TrimSpace(c.Ctx.GetCookie(sessionCookieName))
|
||||
if current == "" {
|
||||
c.Redirect("/auth/login", 302)
|
||||
return
|
||||
}
|
||||
session, err := authsvc.GetSession(current)
|
||||
if err != nil {
|
||||
c.Redirect("/auth/login", 302)
|
||||
return
|
||||
}
|
||||
if target == "" {
|
||||
c.Redirect("/auth/devices?error="+url.QueryEscape("参数错误"), 302)
|
||||
return
|
||||
}
|
||||
|
||||
// 只允许操作自己名下的会话
|
||||
owned := false
|
||||
if list, e := authsvc.ListActiveSessions(session.IdentityID); e == nil {
|
||||
for _, s := range list {
|
||||
if s.Sid == target {
|
||||
owned = true
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
if !owned {
|
||||
c.Redirect("/auth/devices?error="+url.QueryEscape("无权操作该设备"), 302)
|
||||
return
|
||||
}
|
||||
if target == current {
|
||||
c.Redirect("/auth/devices?error="+url.QueryEscape("不能踢掉当前设备,请直接登出"), 302)
|
||||
return
|
||||
}
|
||||
|
||||
if err := authsvc.RevokeSession(target, models.RevokeReasonAdmin); err != nil {
|
||||
c.Redirect("/auth/devices?error="+url.QueryEscape("操作失败"), 302)
|
||||
return
|
||||
}
|
||||
// 同步吊销该会话的刷新令牌,确保对方无法续期
|
||||
_, _ = models.Orm.QueryTable(new(models.AuthRefreshToken)).
|
||||
Filter("sid", target).
|
||||
Update(map[string]interface{}{"revoked": 1})
|
||||
|
||||
c.Redirect("/auth/devices?success="+url.QueryEscape("已将该设备下线"), 302)
|
||||
}
|
||||
@@ -0,0 +1,592 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
authsvc "server/services/auth"
|
||||
"server/services/auth/idp"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
)
|
||||
|
||||
// authBase 认证中心对外基址(用于拼接第三方回调地址)
|
||||
func authBase(c *AuthThirdController) string {
|
||||
if iss := jwtutil.Issuer(); iss != "" {
|
||||
return iss
|
||||
}
|
||||
scheme := "https"
|
||||
if c.Ctx.Request.TLS == nil {
|
||||
scheme = "http"
|
||||
}
|
||||
return scheme + "://" + c.Ctx.Request.Host + "/auth"
|
||||
}
|
||||
|
||||
// claimsFromCtxThird 从 Authorization 头解析登录态
|
||||
func claimsFromCtxThird(ctx *context.Context) *jwtutil.Claims {
|
||||
header := ctx.Request.Header.Get("Authorization")
|
||||
if header == "" {
|
||||
return nil
|
||||
}
|
||||
parts := strings.SplitN(header, " ", 2)
|
||||
if len(parts) != 2 || parts[0] != "Bearer" {
|
||||
return nil
|
||||
}
|
||||
claims, err := jwtutil.ParseToken(strings.TrimSpace(parts[1]))
|
||||
if err != nil {
|
||||
return nil
|
||||
}
|
||||
return claims
|
||||
}
|
||||
|
||||
// 第三方登录未绑定时的错误
|
||||
var ErrThirdNotBound = errors.New("该第三方账号尚未绑定,请先用账号密码登录后绑定")
|
||||
|
||||
// AuthThirdController 第三方登录(微信/钉钉/飞书/QQ/GitHub/Google)
|
||||
type AuthThirdController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *AuthThirdController) serveJSON(data map[string]interface{}) {
|
||||
c.Data["json"] = data
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// thirdState 第三方授权流程中通过 state 透传的上下文(OIDC 原始参数)
|
||||
type thirdState struct {
|
||||
ClientID string `json:"client_id"`
|
||||
RedirectURI string `json:"redirect_uri"`
|
||||
CodeChallenge string `json:"code_challenge"`
|
||||
Method string `json:"code_challenge_method"`
|
||||
Scope string `json:"scope"`
|
||||
Nonce string `json:"nonce"`
|
||||
OIDCState string `json:"oidc_state"`
|
||||
Tid uint64 `json:"tid"`
|
||||
// Action:login=第三方登录(默认);bind=已登录用户绑定第三方账号
|
||||
Action string `json:"action"`
|
||||
}
|
||||
|
||||
// List 已开通的第三方登录方式(供登录页展示按钮)
|
||||
// GET /auth/third/list?tid=
|
||||
func (c *AuthThirdController) List() {
|
||||
tid, _ := c.GetInt64("tid", 0)
|
||||
list := make([]map[string]interface{}, 0)
|
||||
for _, p := range idp.Supported() {
|
||||
_, configured := idp.LoadConfig(p, uint64(tid))
|
||||
list = append(list, map[string]interface{}{
|
||||
"provider": p,
|
||||
"name": providerName(p),
|
||||
"enabled": configured,
|
||||
})
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "data": list})
|
||||
}
|
||||
|
||||
// Authorize 跳转到第三方授权页
|
||||
// GET /auth/third/:provider/authorize?client_id=&redirect_uri=&code_challenge=&state=&tid=
|
||||
func (c *AuthThirdController) Authorize() {
|
||||
provider := strings.TrimSpace(c.Ctx.Input.Param(":provider"))
|
||||
if !idp.IsSupported(provider) {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
_, _ = c.Ctx.ResponseWriter.Write([]byte("不支持的登录方式: " + provider))
|
||||
return
|
||||
}
|
||||
tid, _ := c.GetInt64("tid", 0)
|
||||
|
||||
cfg, configured := idp.LoadConfig(provider, uint64(tid))
|
||||
if !configured || cfg.AppID == "" || cfg.AppSecret == "" {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
_, _ = c.Ctx.ResponseWriter.Write([]byte("该登录方式尚未开通,请联系管理员配置"))
|
||||
return
|
||||
}
|
||||
|
||||
st := thirdState{
|
||||
ClientID: c.GetString("client_id"),
|
||||
RedirectURI: c.GetString("redirect_uri"),
|
||||
CodeChallenge: c.GetString("code_challenge"),
|
||||
Method: c.GetString("code_challenge_method"),
|
||||
Scope: c.GetString("scope"),
|
||||
Nonce: c.GetString("nonce"),
|
||||
OIDCState: c.GetString("state"),
|
||||
Tid: uint64(tid),
|
||||
Action: strings.TrimSpace(c.GetString("action")),
|
||||
}
|
||||
|
||||
// 登录页场景:参数封装在 redirect(base64 编码的 authorize URL)里,
|
||||
// 这里解出来,使第三方登录完成后能回到原来的 OIDC 授权流程。
|
||||
if enc := c.GetString("redirect"); enc != "" && st.ClientID == "" {
|
||||
if raw, err := base64.RawURLEncoding.DecodeString(enc); err == nil {
|
||||
if u, err := url.Parse(string(raw)); err == nil {
|
||||
q := u.Query()
|
||||
st.ClientID = q.Get("client_id")
|
||||
st.RedirectURI = q.Get("redirect_uri")
|
||||
st.CodeChallenge = q.Get("code_challenge")
|
||||
st.Method = q.Get("code_challenge_method")
|
||||
st.Scope = q.Get("scope")
|
||||
st.Nonce = q.Get("nonce")
|
||||
st.OIDCState = q.Get("state")
|
||||
}
|
||||
}
|
||||
}
|
||||
// 第三方登录仅面向租户域应用:平台端(platform.yunzer.cn)不走认证中心,
|
||||
// 这里再校验一次 realm,避免平台域应用被误接入。
|
||||
if st.ClientID != "" {
|
||||
if client, err := findClient(st.ClientID); err == nil && client.Realm != models.AuthRealmTenant {
|
||||
c.Ctx.Output.SetStatus(400)
|
||||
_, _ = c.Ctx.ResponseWriter.Write([]byte("该应用不支持第三方登录"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
raw, err := json.Marshal(st)
|
||||
if err != nil {
|
||||
c.Ctx.Output.SetStatus(500)
|
||||
return
|
||||
}
|
||||
state := base64.RawURLEncoding.EncodeToString(raw)
|
||||
|
||||
// 注意:authBase 已包含 /auth 前缀(issuer = https://api.yunzer.cn/auth),
|
||||
// 这里不能再拼 /auth,否则回调地址会变成 /auth/auth/third/...
|
||||
callbackURI := authBase(c) + "/third/" + provider + "/callback"
|
||||
c.Redirect(idp.BuildAuthURL(cfg, callbackURI, state), 302)
|
||||
}
|
||||
|
||||
// Callback 第三方回调:换取身份 → 建会话 → 回到 OIDC authorize 继续发码
|
||||
// GET /auth/third/:provider/callback?code=&state=
|
||||
func (c *AuthThirdController) Callback() {
|
||||
provider := strings.TrimSpace(c.Ctx.Input.Param(":provider"))
|
||||
code := c.GetString("code")
|
||||
stateParam := c.GetString("state")
|
||||
|
||||
fail := func(msg string) {
|
||||
c.Redirect("/auth/login?error="+url.QueryEscape(msg), 302)
|
||||
}
|
||||
|
||||
if code == "" {
|
||||
fail("第三方登录失败:缺少授权码")
|
||||
return
|
||||
}
|
||||
|
||||
var st thirdState
|
||||
if raw, err := base64.RawURLEncoding.DecodeString(stateParam); err == nil {
|
||||
_ = json.Unmarshal(raw, &st)
|
||||
}
|
||||
|
||||
cfg, configured := idp.LoadConfig(provider, st.Tid)
|
||||
if !configured {
|
||||
fail("该登录方式尚未开通")
|
||||
return
|
||||
}
|
||||
info, err := idp.Exchange(cfg, code)
|
||||
if err != nil {
|
||||
fail(err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 绑定场景:把第三方账号挂到当前登录身份上
|
||||
if st.Action == "bind" {
|
||||
finishBind(c, provider, info)
|
||||
return
|
||||
}
|
||||
|
||||
// 查找绑定关系:open_id 优先,其次 union_id
|
||||
bind, err := findThirdBinding(provider, info.OpenID, info.UnionID)
|
||||
if err != nil {
|
||||
fail(ErrThirdNotBound.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 校验身份状态
|
||||
var identity models.AuthIdentity
|
||||
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
|
||||
Filter("id", bind.IdentityID).One(&identity); err != nil {
|
||||
fail("账号不存在")
|
||||
return
|
||||
}
|
||||
if identity.Status == models.AuthIdentityStatusDisabled {
|
||||
fail("账号已禁用")
|
||||
return
|
||||
}
|
||||
|
||||
// 建会话(tid=0 表示待选企业,authorize 会引导用户选择)
|
||||
sess, err := authsvc.CreateSession(authsvc.SessionInfo{
|
||||
IdentityID: identity.ID,
|
||||
Tid: authsvc.PendingTenantID,
|
||||
ClientID: st.ClientID,
|
||||
IP: c.Ctx.Input.IP(),
|
||||
UserAgent: c.Ctx.Request.UserAgent(),
|
||||
LoginType: authsvc.LoginTypeThird,
|
||||
Amr: provider,
|
||||
})
|
||||
if err != nil {
|
||||
fail("创建会话失败: " + err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 更新绑定信息(昵称/头像可能变化)
|
||||
update := map[string]interface{}{}
|
||||
if info.Nickname != "" {
|
||||
update["nickname"] = info.Nickname
|
||||
}
|
||||
if info.Avatar != "" {
|
||||
update["avatar"] = info.Avatar
|
||||
}
|
||||
if info.Raw != "" {
|
||||
update["raw"] = info.Raw
|
||||
}
|
||||
if len(update) > 0 {
|
||||
_, _ = models.Orm.QueryTable(new(models.AuthIdentityThird)).
|
||||
Filter("id", bind.ID).Update(update)
|
||||
}
|
||||
|
||||
setSessionCookieForCtx(c.Ctx, sess.Sid)
|
||||
|
||||
// 回到 OIDC 授权流程继续签发授权码
|
||||
if st.ClientID != "" && st.RedirectURI != "" {
|
||||
v := url.Values{}
|
||||
v.Set("client_id", st.ClientID)
|
||||
v.Set("redirect_uri", st.RedirectURI)
|
||||
v.Set("response_type", ResponseTypeCode)
|
||||
v.Set("code_challenge", st.CodeChallenge)
|
||||
v.Set("code_challenge_method", defaultStr(st.Method, "S256"))
|
||||
if st.Scope != "" {
|
||||
v.Set("scope", st.Scope)
|
||||
}
|
||||
if st.Nonce != "" {
|
||||
v.Set("nonce", st.Nonce)
|
||||
}
|
||||
if st.OIDCState != "" {
|
||||
v.Set("state", st.OIDCState)
|
||||
}
|
||||
c.Redirect("/auth/authorize?"+v.Encode(), 302)
|
||||
return
|
||||
}
|
||||
c.Redirect("/auth/login", 302)
|
||||
}
|
||||
|
||||
// Bind 已登录用户绑定第三方账号
|
||||
// POST /auth/third/bind { provider, code, tid }
|
||||
func (c *AuthThirdController) Bind() {
|
||||
claims := claimsFromCtxThird(c.Ctx)
|
||||
if claims == nil || claims.UserID <= 0 {
|
||||
c.Ctx.Output.SetStatus(401)
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "未登录"})
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
Provider string `json:"provider"`
|
||||
Code string `json:"code"`
|
||||
Tid uint64 `json:"tid"`
|
||||
}
|
||||
_ = json.Unmarshal(c.Ctx.Input.RequestBody, &req)
|
||||
req.Provider = strings.TrimSpace(req.Provider)
|
||||
if !idp.IsSupported(req.Provider) || req.Code == "" {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
|
||||
cfg, configured := idp.LoadConfig(req.Provider, req.Tid)
|
||||
if !configured {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "该登录方式尚未开通"})
|
||||
return
|
||||
}
|
||||
info, err := idp.Exchange(cfg, req.Code)
|
||||
if err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 该第三方账号若已绑定到别的身份,拒绝重复绑定
|
||||
if exist := models.Orm.QueryTable(new(models.AuthIdentityThird)).
|
||||
Filter("provider", req.Provider).
|
||||
Filter("open_id", info.OpenID).Exist(); exist {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "该第三方账号已被其他账号绑定"})
|
||||
return
|
||||
}
|
||||
|
||||
item := &models.AuthIdentityThird{
|
||||
IdentityID: uint64(claims.UserID),
|
||||
Provider: req.Provider,
|
||||
OpenID: info.OpenID,
|
||||
Nickname: strPtr(info.Nickname),
|
||||
Avatar: strPtr(info.Avatar),
|
||||
Raw: strPtr(info.Raw),
|
||||
}
|
||||
if info.UnionID != "" {
|
||||
item.UnionID = &info.UnionID
|
||||
}
|
||||
if _, err := models.Orm.Insert(item); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 500, "msg": "绑定失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "绑定成功"})
|
||||
}
|
||||
|
||||
// BoundList 当前登录身份已绑定的第三方账号
|
||||
// GET /auth/third/bound
|
||||
func (c *AuthThirdController) BoundList() {
|
||||
claims := claimsFromCtxThird(c.Ctx)
|
||||
if claims == nil || claims.UserID <= 0 {
|
||||
c.Ctx.Output.SetStatus(401)
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "未登录"})
|
||||
return
|
||||
}
|
||||
var rows []models.AuthIdentityThird
|
||||
if _, err := models.Orm.QueryTable(new(models.AuthIdentityThird)).
|
||||
Filter("identity_id", uint64(claims.UserID)).All(&rows); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 500, "msg": "查询失败"})
|
||||
return
|
||||
}
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, map[string]interface{}{
|
||||
"id": r.ID,
|
||||
"provider": r.Provider,
|
||||
"name": providerName(r.Provider),
|
||||
"nickname": deref(r.Nickname),
|
||||
"bind_time": r.BindTime,
|
||||
})
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "data": list})
|
||||
}
|
||||
|
||||
// Unbind 解绑第三方账号
|
||||
// POST /auth/third/unbind { provider }
|
||||
func (c *AuthThirdController) Unbind() {
|
||||
claims := claimsFromCtxThird(c.Ctx)
|
||||
if claims == nil || claims.UserID <= 0 {
|
||||
c.Ctx.Output.SetStatus(401)
|
||||
c.serveJSON(map[string]interface{}{"code": 401, "msg": "未登录"})
|
||||
return
|
||||
}
|
||||
var req struct {
|
||||
Provider string `json:"provider"`
|
||||
}
|
||||
_ = json.Unmarshal(c.Ctx.Input.RequestBody, &req)
|
||||
if strings.TrimSpace(req.Provider) == "" {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "参数错误"})
|
||||
return
|
||||
}
|
||||
// 至少保留一种登录方式:若未设置密码则不允许解绑最后一个第三方
|
||||
var identity models.AuthIdentity
|
||||
// 未设置密码的用户,解绑最后一个第三方账号后将无法登录
|
||||
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
|
||||
Filter("id", uint64(claims.UserID)).One(&identity); err == nil {
|
||||
hasPassword := identity.PasswordHash != nil && *identity.PasswordHash != ""
|
||||
if !hasPassword {
|
||||
cnt, _ := models.Orm.QueryTable(new(models.AuthIdentityThird)).
|
||||
Filter("identity_id", identity.ID).Count()
|
||||
if cnt <= 1 {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "未设置密码,不能解绑最后一个第三方账号"})
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := models.Orm.QueryTable(new(models.AuthIdentityThird)).
|
||||
Filter("identity_id", uint64(claims.UserID)).
|
||||
Filter("provider", strings.TrimSpace(req.Provider)).
|
||||
Delete(); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 500, "msg": "解绑失败"})
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "已解绑"})
|
||||
}
|
||||
|
||||
// BindPage 第三方账号绑定管理页(浏览器访问,依赖认证中心会话 Cookie)
|
||||
// GET /auth/bind
|
||||
func (c *AuthThirdController) BindPage() {
|
||||
sid := strings.TrimSpace(c.Ctx.GetCookie(sessionCookieName))
|
||||
if sid == "" {
|
||||
c.Redirect("/auth/login", 302)
|
||||
return
|
||||
}
|
||||
session, err := authsvc.GetSession(sid)
|
||||
if err != nil {
|
||||
c.Redirect("/auth/login", 302)
|
||||
return
|
||||
}
|
||||
|
||||
// 已绑定列表
|
||||
var bound []models.AuthIdentityThird
|
||||
_, _ = models.Orm.QueryTable(new(models.AuthIdentityThird)).
|
||||
Filter("identity_id", session.IdentityID).All(&bound)
|
||||
boundMap := map[string]models.AuthIdentityThird{}
|
||||
for _, b := range bound {
|
||||
boundMap[b.Provider] = b
|
||||
}
|
||||
|
||||
// 全部平台(只展示已开通的)
|
||||
items := make([]map[string]interface{}, 0)
|
||||
for _, p := range idp.Supported() {
|
||||
_, configured := idp.LoadConfig(p, session.Tid)
|
||||
if !configured {
|
||||
continue
|
||||
}
|
||||
item := map[string]interface{}{
|
||||
"provider": p,
|
||||
"name": providerName(p),
|
||||
"bound": false,
|
||||
"nickname": "",
|
||||
}
|
||||
if b, ok := boundMap[p]; ok {
|
||||
item["bound"] = true
|
||||
item["nickname"] = deref(b.Nickname)
|
||||
}
|
||||
items = append(items, item)
|
||||
}
|
||||
|
||||
c.Data["Providers"] = items
|
||||
c.Data["Success"] = c.GetString("success")
|
||||
c.Data["Error"] = c.GetString("error")
|
||||
c.TplName = "auth/bind.tpl"
|
||||
}
|
||||
|
||||
// UnbindPage 解绑(页面入口,用会话 Cookie 鉴权,解绑后回到绑定页)
|
||||
// GET /auth/third/:provider/unbind
|
||||
func (c *AuthThirdController) UnbindPage() {
|
||||
provider := strings.TrimSpace(c.Ctx.Input.Param(":provider"))
|
||||
sid := strings.TrimSpace(c.Ctx.GetCookie(sessionCookieName))
|
||||
if sid == "" {
|
||||
c.Redirect("/auth/login", 302)
|
||||
return
|
||||
}
|
||||
session, err := authsvc.GetSession(sid)
|
||||
if err != nil {
|
||||
c.Redirect("/auth/login", 302)
|
||||
return
|
||||
}
|
||||
|
||||
var identity models.AuthIdentity
|
||||
hasPassword := true
|
||||
if err := models.Orm.QueryTable(new(models.AuthIdentity)).
|
||||
Filter("id", session.IdentityID).One(&identity); err == nil {
|
||||
hasPassword = identity.PasswordHash != nil && *identity.PasswordHash != ""
|
||||
}
|
||||
if !hasPassword {
|
||||
cnt, _ := models.Orm.QueryTable(new(models.AuthIdentityThird)).
|
||||
Filter("identity_id", session.IdentityID).Count()
|
||||
if cnt <= 1 {
|
||||
c.Redirect("/auth/bind?error="+url.QueryEscape("未设置密码,不能解绑最后一个第三方账号"), 302)
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
if _, err := models.Orm.QueryTable(new(models.AuthIdentityThird)).
|
||||
Filter("identity_id", session.IdentityID).
|
||||
Filter("provider", provider).Delete(); err != nil {
|
||||
c.Redirect("/auth/bind?error="+url.QueryEscape("解绑失败"), 302)
|
||||
return
|
||||
}
|
||||
c.Redirect("/auth/bind?success="+url.QueryEscape("已解绑"), 302)
|
||||
}
|
||||
|
||||
// finishBind 绑定场景的回调处理:把第三方账号挂到当前登录身份
|
||||
func finishBind(c *AuthThirdController, provider string, info *idp.UserInfo) {
|
||||
sid := strings.TrimSpace(c.Ctx.GetCookie(sessionCookieName))
|
||||
if sid == "" {
|
||||
c.Redirect("/auth/login", 302)
|
||||
return
|
||||
}
|
||||
session, err := authsvc.GetSession(sid)
|
||||
if err != nil {
|
||||
c.Redirect("/auth/login", 302)
|
||||
return
|
||||
}
|
||||
|
||||
// 该第三方账号已绑定到别的身份 → 拒绝
|
||||
if exist := models.Orm.QueryTable(new(models.AuthIdentityThird)).
|
||||
Filter("provider", provider).
|
||||
Filter("open_id", info.OpenID).Exist(); exist {
|
||||
c.Redirect("/auth/bind?error="+url.QueryEscape("该"+providerName(provider)+"账号已被其他账号绑定"), 302)
|
||||
return
|
||||
}
|
||||
// 同一身份重复绑定同一平台 → 先清掉旧的
|
||||
_, _ = models.Orm.QueryTable(new(models.AuthIdentityThird)).
|
||||
Filter("identity_id", session.IdentityID).
|
||||
Filter("provider", provider).Delete()
|
||||
|
||||
item := &models.AuthIdentityThird{
|
||||
IdentityID: session.IdentityID,
|
||||
Provider: provider,
|
||||
OpenID: info.OpenID,
|
||||
Nickname: strPtr(info.Nickname),
|
||||
Avatar: strPtr(info.Avatar),
|
||||
Raw: strPtr(info.Raw),
|
||||
}
|
||||
if info.UnionID != "" {
|
||||
item.UnionID = &info.UnionID
|
||||
}
|
||||
if _, err := models.Orm.Insert(item); err != nil {
|
||||
c.Redirect("/auth/bind?error="+url.QueryEscape("绑定失败"), 302)
|
||||
return
|
||||
}
|
||||
c.Redirect("/auth/bind?success="+url.QueryEscape("绑定成功"), 302)
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 工具
|
||||
|
||||
// findThirdBinding 按 open_id 查找,其次按 union_id 查找
|
||||
func findThirdBinding(provider, openID, unionID string) (*models.AuthIdentityThird, error) {
|
||||
var bind models.AuthIdentityThird
|
||||
qs := models.Orm.QueryTable(new(models.AuthIdentityThird)).Filter("provider", provider)
|
||||
|
||||
if openID != "" {
|
||||
if err := qs.Filter("open_id", openID).One(&bind); err == nil {
|
||||
return &bind, nil
|
||||
}
|
||||
}
|
||||
if unionID != "" {
|
||||
if err := qs.Filter("union_id", unionID).One(&bind); err == nil {
|
||||
return &bind, nil
|
||||
}
|
||||
}
|
||||
return nil, fmt.Errorf("not bound")
|
||||
}
|
||||
|
||||
func providerName(p string) string {
|
||||
switch p {
|
||||
case models.IdPWechat:
|
||||
return "微信"
|
||||
case models.IdPDingTalk:
|
||||
return "钉钉"
|
||||
case models.IdPFeishu:
|
||||
return "飞书"
|
||||
case models.IdPQQ:
|
||||
return "QQ"
|
||||
case models.IdPGitHub:
|
||||
return "GitHub"
|
||||
case models.IdPGoogle:
|
||||
return "Google"
|
||||
}
|
||||
return p
|
||||
}
|
||||
|
||||
func defaultStr(v, def string) string {
|
||||
if strings.TrimSpace(v) == "" {
|
||||
return def
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
||||
func strPtr(s string) *string {
|
||||
if s == "" {
|
||||
return nil
|
||||
}
|
||||
return &s
|
||||
}
|
||||
|
||||
func deref(p *string) string {
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
return *p
|
||||
}
|
||||
@@ -0,0 +1,391 @@
|
||||
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
|
||||
}
|
||||
@@ -0,0 +1,240 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
authsvc "server/services/auth"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// PlatformAuthConfigController 统一认证 —— 租户登录策略配置(平台端管理)
|
||||
//
|
||||
// 可配置:登录验证码方式、密码强度、会话时长、同账号最大在线设备数(1号1机)
|
||||
// 以及超限策略(踢掉旧会话 / 拒绝新登录)等。
|
||||
type PlatformAuthConfigController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *PlatformAuthConfigController) serveJSON(data map[string]interface{}) {
|
||||
c.Data["json"] = data
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Prepare 统一鉴权:仅平台管理员可配置
|
||||
func (c *PlatformAuthConfigController) 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
|
||||
}
|
||||
}
|
||||
|
||||
// Detail 查询租户登录策略;未配置时返回系统默认值
|
||||
// GET /platform/authConfig/detail?tid=
|
||||
func (c *PlatformAuthConfigController) Detail() {
|
||||
tid, _ := c.GetInt64("tid", 0)
|
||||
if tid == 0 {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "tid 不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
var cfg models.AuthTenantAuthConfig
|
||||
err := models.Orm.QueryTable(new(models.AuthTenantAuthConfig)).
|
||||
Filter("tid", tid).One(&cfg)
|
||||
|
||||
if err != nil {
|
||||
// 未配置:返回默认值
|
||||
policy := authsvc.GetTenantSessionPolicy(uint64(tid))
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"tid": tid,
|
||||
"verify_type": "captcha",
|
||||
"open_verify": 1,
|
||||
"pwd_min_len": 8,
|
||||
"pwd_complexity": 0,
|
||||
"session_ttl": policy.SessionTTL,
|
||||
"max_session": policy.MaxSession,
|
||||
"kick_strategy": policy.KickStrategy,
|
||||
"mfa_required": 0,
|
||||
"ip_whitelist": "",
|
||||
"allow_third": "",
|
||||
"configured": false,
|
||||
},
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ipList := ""
|
||||
if cfg.IPWhitelist != nil {
|
||||
ipList = *cfg.IPWhitelist
|
||||
}
|
||||
allowThird := ""
|
||||
if cfg.AllowThird != nil {
|
||||
allowThird = *cfg.AllowThird
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"msg": "success",
|
||||
"data": map[string]interface{}{
|
||||
"tid": cfg.Tid,
|
||||
"verify_type": cfg.VerifyType,
|
||||
"open_verify": cfg.OpenVerify,
|
||||
"pwd_min_len": cfg.PwdMinLen,
|
||||
"pwd_complexity": cfg.PwdComplexity,
|
||||
"session_ttl": cfg.SessionTTL,
|
||||
"max_session": cfg.MaxSession,
|
||||
"kick_strategy": cfg.KickStrategy,
|
||||
"mfa_required": cfg.MfaRequired,
|
||||
"ip_whitelist": ipList,
|
||||
"allow_third": allowThird,
|
||||
"configured": true,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// Save 保存租户登录策略(不存在则创建)
|
||||
// POST /platform/authConfig/save
|
||||
func (c *PlatformAuthConfigController) Save() {
|
||||
var p struct {
|
||||
Tid uint64 `json:"tid"`
|
||||
VerifyType string `json:"verify_type"`
|
||||
OpenVerify *int8 `json:"open_verify"`
|
||||
PwdMinLen int `json:"pwd_min_len"`
|
||||
PwdComplexity int8 `json:"pwd_complexity"`
|
||||
SessionTTL int `json:"session_ttl"`
|
||||
MaxSession int `json:"max_session"`
|
||||
KickStrategy int8 `json:"kick_strategy"`
|
||||
MfaRequired int8 `json:"mfa_required"`
|
||||
IPWhitelist string `json:"ip_whitelist"`
|
||||
AllowThird string `json:"allow_third"`
|
||||
}
|
||||
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
|
||||
}
|
||||
}
|
||||
if p.Tid == 0 {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "tid 不能为空"})
|
||||
return
|
||||
}
|
||||
|
||||
// 校验 IP 白名单为合法 JSON 数组
|
||||
ip := strings.TrimSpace(p.IPWhitelist)
|
||||
if ip != "" {
|
||||
var arr []string
|
||||
if err := json.Unmarshal([]byte(ip), &arr); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "IP白名单必须是 JSON 数组,如 [\"1.2.3.4\"]"})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
cfg := models.AuthTenantAuthConfig{
|
||||
Tid: p.Tid,
|
||||
VerifyType: defaultStrAuth2(p.VerifyType, "captcha"),
|
||||
OpenVerify: 1,
|
||||
PwdMinLen: p.PwdMinLen,
|
||||
PwdComplexity: p.PwdComplexity,
|
||||
SessionTTL: p.SessionTTL,
|
||||
MaxSession: p.MaxSession,
|
||||
KickStrategy: p.KickStrategy,
|
||||
MfaRequired: p.MfaRequired,
|
||||
}
|
||||
if p.OpenVerify != nil {
|
||||
cfg.OpenVerify = *p.OpenVerify
|
||||
}
|
||||
if cfg.PwdMinLen <= 0 {
|
||||
cfg.PwdMinLen = 8
|
||||
}
|
||||
if cfg.SessionTTL <= 0 {
|
||||
cfg.SessionTTL = authsvc.DefaultSessionTTL
|
||||
}
|
||||
if cfg.MaxSession <= 0 {
|
||||
cfg.MaxSession = authsvc.DefaultMaxSession
|
||||
}
|
||||
if cfg.KickStrategy != models.KickStrategyReject {
|
||||
cfg.KickStrategy = models.KickStrategyKickOld
|
||||
}
|
||||
if ip != "" {
|
||||
cfg.IPWhitelist = &ip
|
||||
}
|
||||
if v := strings.TrimSpace(p.AllowThird); v != "" {
|
||||
cfg.AllowThird = &v
|
||||
}
|
||||
|
||||
exist := models.Orm.QueryTable(new(models.AuthTenantAuthConfig)).Filter("tid", p.Tid).Exist()
|
||||
if exist {
|
||||
fields := []string{"verify_type", "open_verify", "pwd_min_len", "pwd_complexity",
|
||||
"session_ttl", "max_session", "kick_strategy", "mfa_required"}
|
||||
if cfg.IPWhitelist != nil {
|
||||
fields = append(fields, "ip_whitelist")
|
||||
}
|
||||
if cfg.AllowThird != nil {
|
||||
fields = append(fields, "allow_third")
|
||||
}
|
||||
if _, err := models.Orm.Update(&cfg, fields...); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 500, "msg": "保存失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
} else {
|
||||
if _, err := models.Orm.Insert(&cfg); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 500, "msg": "保存失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "success"})
|
||||
}
|
||||
|
||||
// Reset 恢复默认值(删除自定义配置)
|
||||
// DELETE /platform/authConfig/reset/:tid
|
||||
func (c *PlatformAuthConfigController) Reset() {
|
||||
tid, err := strconv.ParseUint(c.Ctx.Input.Param(":tid"), 10, 64)
|
||||
if err != nil || tid == 0 {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "无效 tid"})
|
||||
return
|
||||
}
|
||||
if _, err := models.Orm.QueryTable(new(models.AuthTenantAuthConfig)).
|
||||
Filter("tid", tid).Delete(); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 500, "msg": "重置失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "已恢复默认"})
|
||||
}
|
||||
|
||||
func defaultStrAuth2(v, def string) string {
|
||||
if strings.TrimSpace(v) == "" {
|
||||
return def
|
||||
}
|
||||
return strings.TrimSpace(v)
|
||||
}
|
||||
@@ -0,0 +1,257 @@
|
||||
package controllers
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"server/models"
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
)
|
||||
|
||||
// PlatformAuthIdpController 统一认证 —— 第三方登录配置(平台端管理)
|
||||
//
|
||||
// 配置说明:
|
||||
// - tid = 0 :平台全局配置,所有租户共用
|
||||
// - tid > 0 :租户自带身份源,仅该租户可见(第10条需求:企业自带钉钉/飞书等)
|
||||
//
|
||||
// 配置对象只有「租户用户登录」,平台端自身不使用第三方登录。
|
||||
type PlatformAuthIdpController struct {
|
||||
beego.Controller
|
||||
}
|
||||
|
||||
func (c *PlatformAuthIdpController) serveJSON(data map[string]interface{}) {
|
||||
c.Data["json"] = data
|
||||
_ = c.ServeJSON()
|
||||
}
|
||||
|
||||
// Prepare 统一鉴权:仅平台管理员可维护第三方登录配置
|
||||
func (c *PlatformAuthIdpController) 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 authIdpPayload struct {
|
||||
Tid uint64 `json:"tid"`
|
||||
Provider string `json:"provider"`
|
||||
Name string `json:"name"`
|
||||
AppID string `json:"app_id"`
|
||||
AppSecret string `json:"app_secret"`
|
||||
ProxyURL string `json:"proxy_url"`
|
||||
Scopes string `json:"scopes"`
|
||||
AuthURL string `json:"auth_url"`
|
||||
TokenURL string `json:"token_url"`
|
||||
UserinfoURL string `json:"userinfo_url"`
|
||||
Status *int8 `json:"status"`
|
||||
}
|
||||
|
||||
func (c *PlatformAuthIdpController) parsePayload() (authIdpPayload, bool) {
|
||||
var p authIdpPayload
|
||||
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 配置列表
|
||||
// GET /platform/authIdp/list?tid=0
|
||||
func (c *PlatformAuthIdpController) List() {
|
||||
tid, _ := c.GetInt64("tid", 0)
|
||||
var rows []models.AuthTenantIdp
|
||||
if _, err := models.Orm.QueryTable(new(models.AuthTenantIdp)).
|
||||
Filter("tid", tid).OrderBy("id").All(&rows); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 500, "msg": "查询失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
|
||||
// 脱敏:不下发 AppSecret
|
||||
list := make([]map[string]interface{}, 0, len(rows))
|
||||
for _, r := range rows {
|
||||
list = append(list, map[string]interface{}{
|
||||
"id": r.ID,
|
||||
"tid": r.Tid,
|
||||
"provider": r.Provider,
|
||||
"name": derefAuthIdp(r.Name),
|
||||
"app_id": derefAuthIdp(r.AppID),
|
||||
"proxy_url": derefAuthIdp(r.ProxyURL),
|
||||
"scopes": derefAuthIdp(r.Scopes),
|
||||
"auth_url": derefAuthIdp(r.AuthURL),
|
||||
"token_url": derefAuthIdp(r.TokenURL),
|
||||
"userinfo_url": derefAuthIdp(r.UserinfoURL),
|
||||
"status": r.Status,
|
||||
"has_secret": r.AppSecret != nil && *r.AppSecret != "",
|
||||
})
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200, "msg": "success",
|
||||
"data": map[string]interface{}{"list": list, "total": len(list)},
|
||||
})
|
||||
}
|
||||
|
||||
// Create 新增配置
|
||||
// POST /platform/authIdp/create
|
||||
func (c *PlatformAuthIdpController) Create() {
|
||||
p, ok := c.parsePayload()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
p.Provider = strings.TrimSpace(p.Provider)
|
||||
if p.Provider == "" {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "provider 不能为空"})
|
||||
return
|
||||
}
|
||||
if exist := models.Orm.QueryTable(new(models.AuthTenantIdp)).
|
||||
Filter("tid", p.Tid).Filter("provider", p.Provider).Exist(); exist {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "该登录方式已配置,请直接编辑"})
|
||||
return
|
||||
}
|
||||
|
||||
item := &models.AuthTenantIdp{
|
||||
Tid: p.Tid,
|
||||
Provider: p.Provider,
|
||||
Status: 1,
|
||||
}
|
||||
fillAuthIdp(item, &p)
|
||||
if p.Status != nil {
|
||||
item.Status = *p.Status
|
||||
}
|
||||
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": "success", "data": map[string]interface{}{"id": id}})
|
||||
}
|
||||
|
||||
// Edit 编辑配置(app_secret 留空表示不修改)
|
||||
// POST /platform/authIdp/edit/:id
|
||||
func (c *PlatformAuthIdpController) Edit() {
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "无效ID"})
|
||||
return
|
||||
}
|
||||
p, ok := c.parsePayload()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
var row models.AuthTenantIdp
|
||||
if err := models.Orm.QueryTable(new(models.AuthTenantIdp)).Filter("id", id).One(&row); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 404, "msg": "记录不存在"})
|
||||
return
|
||||
}
|
||||
fillAuthIdp(&row, &p)
|
||||
if p.Status != nil {
|
||||
row.Status = *p.Status
|
||||
}
|
||||
if _, err := models.Orm.Update(&row); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 500, "msg": "更新失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "success"})
|
||||
}
|
||||
|
||||
// Delete 删除配置
|
||||
// DELETE /platform/authIdp/delete/:id
|
||||
func (c *PlatformAuthIdpController) Delete() {
|
||||
id, err := strconv.ParseUint(c.Ctx.Input.Param(":id"), 10, 64)
|
||||
if err != nil || id == 0 {
|
||||
c.serveJSON(map[string]interface{}{"code": 400, "msg": "无效ID"})
|
||||
return
|
||||
}
|
||||
if _, err := models.Orm.QueryTable(new(models.AuthTenantIdp)).Filter("id", id).Delete(); err != nil {
|
||||
c.serveJSON(map[string]interface{}{"code": 500, "msg": "删除失败: " + err.Error()})
|
||||
return
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{"code": 200, "msg": "success"})
|
||||
}
|
||||
|
||||
// CallbackHint 返回各平台需要填写的回调地址(方便复制)
|
||||
// GET /platform/authIdp/callbackHint?provider=wechat
|
||||
func (c *PlatformAuthIdpController) CallbackHint() {
|
||||
provider := strings.TrimSpace(c.GetString("provider"))
|
||||
base := jwtutil.Issuer()
|
||||
if base == "" {
|
||||
scheme := "https"
|
||||
if c.Ctx.Request.TLS == nil {
|
||||
scheme = "http"
|
||||
}
|
||||
base = fmt.Sprintf("%s://%s/auth", scheme, c.Ctx.Request.Host)
|
||||
}
|
||||
c.serveJSON(map[string]interface{}{
|
||||
"code": 200,
|
||||
"data": map[string]interface{}{
|
||||
"callback": fmt.Sprintf("%s/third/%s/callback", base, provider),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
// ---------------------------------------------------------------- 工具
|
||||
|
||||
// fillAuthIdp 把请求参数写入模型;留空字段表示不修改
|
||||
func fillAuthIdp(item *models.AuthTenantIdp, p *authIdpPayload) {
|
||||
if v := strings.TrimSpace(p.Name); v != "" {
|
||||
item.Name = &v
|
||||
}
|
||||
if v := strings.TrimSpace(p.AppID); v != "" {
|
||||
item.AppID = &v
|
||||
}
|
||||
if v := strings.TrimSpace(p.AppSecret); v != "" {
|
||||
item.AppSecret = &v
|
||||
}
|
||||
if v := strings.TrimSpace(p.ProxyURL); v != "" {
|
||||
item.ProxyURL = &v
|
||||
}
|
||||
if v := strings.TrimSpace(p.Scopes); v != "" {
|
||||
item.Scopes = &v
|
||||
}
|
||||
if v := strings.TrimSpace(p.AuthURL); v != "" {
|
||||
item.AuthURL = &v
|
||||
}
|
||||
if v := strings.TrimSpace(p.TokenURL); v != "" {
|
||||
item.TokenURL = &v
|
||||
}
|
||||
if v := strings.TrimSpace(p.UserinfoURL); v != "" {
|
||||
item.UserinfoURL = &v
|
||||
}
|
||||
}
|
||||
|
||||
func derefAuthIdp(p *string) string {
|
||||
if p == nil {
|
||||
return ""
|
||||
}
|
||||
return *p
|
||||
}
|
||||
Reference in New Issue
Block a user