Files
ai-gateway-go/internal/identity/social_http.go
T
LLMGuardX Dev e31cc54b8e 0.11.2: 旗舰版第三轮完善(通用聊天/企微钉钉飞书扫码登录/个人安全策略)
- 门户通用聊天:选择已批准模型直接对话,审批通过后自动开通用户级运行时
  API Key(加密落库,限额取批准值),聊天经受管网关统一认证/限流/配额/审计;
  会话哈希链完整性 + busy 租约防并发,失败不落库。
- 扫码登录:identity_providers 扩展 wecom/dingtalk/feishu,管理端配置
  (AppID/AppSecret/AgentID/回调/自动开户/默认部门),登录页自动展示;
  one-time state 防 CSRF,provider_uid 全局唯一防多账号绑定,平台端点
  固定公网 URL 复用 public-only 拨号。
- 个人安全策略:账号安全页(登录设备管理/吊销非当前会话/登录提醒开关/
  扫码绑定解绑),登录成功发布 security.login_detected 事件按偏好落站内信
  (新增 security 类别),会话索引只存令牌摘要并惰性清理。
- 迁移 000038-000041;修复 social update 参数越界/凭据回读/路由挂载缺失;
  全量测试 25 包通过,前端 admin/portal 构建通过,端到端验证完成。
2026-08-13 12:53:38 +08:00

123 lines
4.1 KiB
Go

package identity
import (
"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 {
h.socialRedirect(w, r, provider, "sso_error", err.Error())
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)
}