58535fda7b
安全: - 渠道 webhook 入站强制令牌鉴权(恒定时间比较+统一文案),企微签名官方算法; - 报表/概览/systemInfo 端点按 usage:read/audit:read/system:manage 授权; - sso_error 固定错误码;个人渠道令牌仅请求头;工具出站 Dialer.Control 消除 DNS rebinding TOCTOU;新增 channel:read/manage 权限;限流倍数上限 10。 并发/一致性: - 任务上报单条条件 UPDATE 防重放双提交;认领回收过期 claimed 任务; - 审批改先开通后落记录(幂等,无嵌套事务);聊天消息单事务落库; - 会话列表校验 AuthVersion;吊销先 Del 后 SRem;删工具保护调用历史; - rejected 冷却 24h;限流被拒补偿;maintenance 清理限流窗口。 前端/菜单: - 修复 gatewayChildren late-append 导致 reports/tenants/channels 菜单不可见; - 聊天改名 PUT 对齐;渠道编辑清空凭据防串写+启用开关; - 聊天响应防串扰;报表本地时区日期。
135 lines
4.4 KiB
Go
135 lines
4.4 KiB
Go
package identity
|
|
|
|
import (
|
|
"errors"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
|
|
"aigateway.local/core/internal/platform/apiresponse"
|
|
)
|
|
|
|
// registerSocial 注册扫码登录的公开与已认证端点。
|
|
// 公开入口复用 SSO 的 start/callback 路径(按 provider kind 分发);
|
|
// 绑定/解绑/绑定列表挂在 portal 账号安全页面。
|
|
func (h *HTTPHandler) registerSocial() {
|
|
h.mux.HandleFunc("POST /api/v1/portal/social/{kind}/bind/start", h.bindStart)
|
|
h.mux.HandleFunc("DELETE /api/v1/portal/social/{kind}/bind", h.unbind)
|
|
h.mux.HandleFunc("GET /api/v1/portal/social/bindings", h.bindings)
|
|
}
|
|
|
|
// startSocial 处理扫码登录的 start 分发(由 startSSO 按 kind 调用)。
|
|
func (h *HTTPHandler) startSocial(w http.ResponseWriter, r *http.Request) {
|
|
provider, err := h.service.repository.GetSocialProviderByCode(r.Context(), r.PathValue("provider_code"))
|
|
if err != nil || !provider.Enabled {
|
|
http.NotFound(w, r)
|
|
return
|
|
}
|
|
redirectURL, err := h.service.SocialLoginURL(r.Context(), provider.Kind, "login", "")
|
|
if err != nil {
|
|
apiresponse.Error(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
http.Redirect(w, r, redirectURL, http.StatusFound)
|
|
}
|
|
|
|
// callbackSocial 处理扫码登录回调:成功后 302 回门户 return_url 并携带
|
|
// sso_code(登录)或 bind_result(绑定),失败携带 sso_error。
|
|
func (h *HTTPHandler) callbackSocial(w http.ResponseWriter, r *http.Request) {
|
|
provider, err := h.service.repository.GetSocialProviderByCode(r.Context(), r.PathValue("provider_code"))
|
|
if err != nil {
|
|
apiresponse.Error(w, http.StatusBadRequest, "登录方式不存在")
|
|
return
|
|
}
|
|
if r.URL.Query().Get("error") != "" {
|
|
h.socialRedirect(w, r, provider, "sso_error", "企业登录已取消或拒绝")
|
|
return
|
|
}
|
|
state := strings.TrimSpace(r.URL.Query().Get("state"))
|
|
code := strings.TrimSpace(r.URL.Query().Get("code"))
|
|
if state == "" || code == "" {
|
|
h.socialRedirect(w, r, provider, "sso_error", "登录回调参数无效")
|
|
return
|
|
}
|
|
result, err := h.service.CompleteSocialLogin(r.Context(), provider.Kind, state, code, SessionMeta{IP: h.service.ClientIP(r), UserAgent: r.UserAgent()})
|
|
if err != nil {
|
|
// 只回传固定错误码,内部细节写服务端日志,避免内部信息进浏览器
|
|
// 地址栏/历史/Referer。
|
|
code := "login_failed"
|
|
switch {
|
|
case errors.Is(err, ErrSocialUnbound):
|
|
code = "unbound"
|
|
case errors.Is(err, ErrAccountDisabled):
|
|
code = "disabled"
|
|
case errors.Is(err, ErrInvalidSession):
|
|
code = "expired"
|
|
}
|
|
h.socialRedirect(w, r, provider, "sso_error", code)
|
|
return
|
|
}
|
|
switch result.Purpose {
|
|
case "bind":
|
|
if result.BindConflict {
|
|
h.socialRedirect(w, r, provider, "bind_result", "conflict")
|
|
return
|
|
}
|
|
h.socialRedirect(w, r, provider, "bind_result", "ok")
|
|
case "login":
|
|
h.socialRedirect(w, r, provider, "sso_code", result.SSOCode)
|
|
}
|
|
}
|
|
|
|
// socialRedirect 302 到门户 return_url 并携带结果参数。
|
|
func (h *HTTPHandler) socialRedirect(w http.ResponseWriter, r *http.Request, provider SocialProvider, key, value string) {
|
|
target, err := url.Parse(provider.PortalReturnURL)
|
|
if err != nil {
|
|
apiresponse.Error(w, http.StatusBadGateway, "门户返回地址无效")
|
|
return
|
|
}
|
|
query := target.Query()
|
|
query.Set(key, value)
|
|
target.RawQuery = query.Encode()
|
|
http.Redirect(w, r, target.String(), http.StatusFound)
|
|
}
|
|
|
|
// bindStart 已认证用户发起扫码绑定:返回跳转企业身份源的 URL。
|
|
func (h *HTTPHandler) bindStart(w http.ResponseWriter, r *http.Request) {
|
|
account, ok := h.requireAccount(w, r, KindPortal)
|
|
if !ok {
|
|
return
|
|
}
|
|
redirectURL, err := h.service.SocialLoginURL(r.Context(), r.PathValue("kind"), "bind", account.ID)
|
|
if err != nil {
|
|
apiresponse.Error(w, http.StatusBadRequest, err.Error())
|
|
return
|
|
}
|
|
apiresponse.OK(w, map[string]string{"redirect_url": redirectURL})
|
|
}
|
|
|
|
// unbind 解除扫码绑定(仅本人)。
|
|
func (h *HTTPHandler) unbind(w http.ResponseWriter, r *http.Request) {
|
|
account, ok := h.requireAccount(w, r, KindPortal)
|
|
if !ok {
|
|
return
|
|
}
|
|
if err := h.service.UnbindProvider(r.Context(), account.ID, r.PathValue("kind")); err != nil {
|
|
h.writeIdentityError(w, err)
|
|
return
|
|
}
|
|
apiresponse.OK(w, map[string]bool{"unbound": true})
|
|
}
|
|
|
|
// bindings 返回账号的扫码绑定列表。
|
|
func (h *HTTPHandler) bindings(w http.ResponseWriter, r *http.Request) {
|
|
account, ok := h.requireAccount(w, r, KindPortal)
|
|
if !ok {
|
|
return
|
|
}
|
|
items, err := h.service.ProviderBindings(r.Context(), account.ID)
|
|
if err != nil {
|
|
h.writeIdentityError(w, err)
|
|
return
|
|
}
|
|
apiresponse.OK(w, items)
|
|
}
|