package app import ( "context" "crypto/sha256" "database/sql" "encoding/base64" "encoding/json" "errors" "fmt" "io" "net/http" "net/url" "strconv" "strings" "time" ) const ( adminOAuthStateTTL = 10 * time.Minute adminOAuthCodeTTL = 5 * time.Minute adminOAuthBodyMax = 1 << 20 ) var adminOAuthProviderNames = map[string]string{ "wechat": "微信", "qq": "QQ", "github": "GitHub", "google": "Google", } var adminOAuthAllowedHosts = map[string]map[string]bool{ "wechat": {"open.weixin.qq.com": true, "api.weixin.qq.com": true}, "qq": {"graph.qq.com": true}, "github": {"github.com": true, "api.github.com": true}, "google": {"accounts.google.com": true, "oauth2.googleapis.com": true, "openidconnect.googleapis.com": true}, } type adminOAuthProvider struct { Code string Name string ClientID string ClientSecret string AuthorizationURL string TokenURL string OpenIDURL string UserInfoURL string Scope string RedirectURI string } type adminOAuthIdentity struct { Subject string Email string DisplayName string AvatarURL string } type adminOAuthLoginCode struct { Provider string Subject string Email string DisplayName string AvatarURL string AdminUserID sql.NullInt64 } func oauthHash(value string) []byte { hash := sha256.Sum256([]byte(value)) return hash[:] } func pkceChallenge(verifier string) string { hash := sha256.Sum256([]byte(verifier)) return base64.RawURLEncoding.EncodeToString(hash[:]) } func (a *App) adminOAuthProvider(ctx context.Context, code string) (adminOAuthProvider, error) { name, ok := adminOAuthProviderNames[code] if !ok { return adminOAuthProvider{}, errors.New("不支持的第三方登录渠道") } prefix := "oauth." + code + "." provider := adminOAuthProvider{ Code: code, Name: name, ClientID: strings.TrimSpace(a.configPlain(ctx, prefix+"client_id", "")), ClientSecret: strings.TrimSpace(a.configPlain(ctx, prefix+"client_secret", "")), AuthorizationURL: strings.TrimSpace(a.configPlain(ctx, prefix+"authorization_url", "")), TokenURL: strings.TrimSpace(a.configPlain(ctx, prefix+"token_url", "")), OpenIDURL: strings.TrimSpace(a.configPlain(ctx, prefix+"openid_url", "")), UserInfoURL: strings.TrimSpace(a.configPlain(ctx, prefix+"userinfo_url", "")), Scope: strings.TrimSpace(a.configPlain(ctx, prefix+"scope", "")), RedirectURI: strings.TrimSpace(a.configPlain(ctx, prefix+"redirect_uri", "")), } if provider.ClientID == "" || provider.ClientSecret == "" || provider.AuthorizationURL == "" || provider.TokenURL == "" || provider.UserInfoURL == "" || provider.Scope == "" || provider.RedirectURI == "" { return adminOAuthProvider{}, fmt.Errorf("%s登录配置不完整", name) } if code == "qq" && provider.OpenIDURL == "" { return adminOAuthProvider{}, errors.New("QQ 登录 OpenID 地址未配置") } for label, raw := range map[string]string{ "授权地址": provider.AuthorizationURL, "令牌地址": provider.TokenURL, "用户信息地址": provider.UserInfoURL, } { if err := validateAdminOAuthEndpoint(code, raw); err != nil { return adminOAuthProvider{}, fmt.Errorf("%s%s无效:%w", name, label, err) } } if provider.OpenIDURL != "" { if err := validateAdminOAuthEndpoint(code, provider.OpenIDURL); err != nil { return adminOAuthProvider{}, fmt.Errorf("%s OpenID 地址无效:%w", name, err) } } if err := a.validateAdminOAuthRedirectURL(provider.RedirectURI); err != nil { return adminOAuthProvider{}, fmt.Errorf("%s回调地址无效:%w", name, err) } return provider, nil } func validateAdminOAuthEndpoint(provider, raw string) error { parsed, err := url.Parse(raw) if err != nil || parsed.Scheme != "https" || parsed.Hostname() == "" || parsed.User != nil { return errors.New("必须是合法的 HTTPS 地址") } if !adminOAuthAllowedHosts[provider][strings.ToLower(parsed.Hostname())] { return errors.New("域名不在该渠道的官方白名单内") } if port := parsed.Port(); port != "" && port != "443" { return errors.New("仅允许使用标准 HTTPS 端口") } return nil } func (a *App) validateAdminOAuthRedirectURL(raw string) error { parsed, err := url.Parse(raw) if err != nil || parsed.Hostname() == "" || parsed.User != nil || (parsed.Scheme != "https" && parsed.Scheme != "http") { return errors.New("必须是合法的 HTTP(S) 地址") } if parsed.Scheme == "http" { host := strings.ToLower(parsed.Hostname()) if a.config.Environment == "production" || (host != "localhost" && host != "127.0.0.1" && host != "::1") { return errors.New("仅本地开发允许 HTTP,生产环境必须使用 HTTPS") } } return nil } func (a *App) adminOAuthFrontendURL(ctx context.Context) (string, error) { raw := strings.TrimSpace(a.configPlain(ctx, "oauth.admin.frontend_callback_url", "")) if raw == "" { return "", errors.New("管理端登录结果页未配置") } if err := a.validateAdminOAuthRedirectURL(raw); err != nil { return "", fmt.Errorf("管理端登录结果页无效:%w", err) } return raw, nil } func (a *App) enabledAdminOAuthProviders(ctx context.Context) ([]adminOAuthProvider, error) { providers := make([]adminOAuthProvider, 0, len(adminOAuthProviderNames)) for _, code := range []string{"wechat", "qq", "github", "google"} { if !a.configBool(ctx, "oauth."+code+".enabled", false) { continue } provider, err := a.adminOAuthProvider(ctx, code) if err != nil { return nil, err } providers = append(providers, provider) } return providers, nil } func (a *App) oauthConfigurationReady(ctx context.Context) bool { adminProviders, err := a.enabledAdminOAuthProviders(ctx) if err != nil { return false } userProviders, err := a.enabledUserOAuthProviders(ctx) if err != nil { return false } if len(adminProviders) > 0 { if _, err := a.adminOAuthFrontendURL(ctx); err != nil { return false } } if len(userProviders) > 0 { if _, err := a.userOAuthFrontendURL(ctx); err != nil { return false } } _, err = a.enabledAppOAuthProviders(ctx) return err == nil } func (a *App) validateAdminOAuthConfigValues(ctx context.Context, values map[string]string, clearSecrets map[string]bool) error { value := func(key string) string { if clearSecrets[key] { return "" } if candidate, exists := values[key]; exists { if candidate != "" || !strings.HasSuffix(key, "client_secret") { return strings.TrimSpace(candidate) } } return strings.TrimSpace(a.configPlain(ctx, key, "")) } for _, client := range []struct{ prefix, callback, name string }{ {"oauth.", "oauth.admin.frontend_callback_url", "管理端"}, {"oauth.user.", "oauth.user.frontend_callback_url", "H5"}, } { for _, code := range []string{"wechat", "qq", "github", "google"} { if value(client.prefix+code+".enabled") == "true" { if err := a.validateAdminOAuthRedirectURL(value(client.callback)); err != nil { return fmt.Errorf("%s 登录结果页无效:%w", client.name, err) } break } } } for _, code := range []string{"wechat", "qq", "github", "google"} { adminEnabled := strings.ToLower(value("oauth."+code+".enabled")) == "true" userEnabled := strings.ToLower(value("oauth.user."+code+".enabled")) == "true" appEnabled := value("oauth.app."+code+".enabled") == "true" if appEnabled { if code == "github" { if value("oauth.app.frontend_callback_url") != appOAuthCallbackURL { return errors.New("App 回调地址必须为 " + appOAuthCallbackURL) } } else if err := validateNativeOAuthConfig(code, value); err != nil { return err } } if !adminEnabled && !userEnabled && !(appEnabled && code == "github") { continue } prefix := "oauth." + code + "." required := []string{"client_id", "client_secret", "authorization_url", "token_url", "userinfo_url", "scope", "redirect_uri"} if code == "qq" { required = append(required, "openid_url") } for _, suffix := range required { if value(prefix+suffix) == "" { return fmt.Errorf("%s登录的%s不能为空", adminOAuthProviderNames[code], suffix) } } for label, raw := range map[string]string{ "授权地址": value(prefix + "authorization_url"), "令牌地址": value(prefix + "token_url"), "用户信息地址": value(prefix + "userinfo_url"), } { if err := validateAdminOAuthEndpoint(code, raw); err != nil { return fmt.Errorf("%s%s无效:%w", adminOAuthProviderNames[code], label, err) } } if code == "qq" { if err := validateAdminOAuthEndpoint(code, value(prefix+"openid_url")); err != nil { return fmt.Errorf("QQ OpenID 地址无效:%w", err) } } if err := a.validateAdminOAuthRedirectURL(value(prefix + "redirect_uri")); err != nil { return fmt.Errorf("%s回调地址无效:%w", adminOAuthProviderNames[code], err) } } return nil } func (a *App) adminOAuthProviders(w http.ResponseWriter, r *http.Request) { items := make([]map[string]string, 0, len(adminOAuthProviderNames)) for _, code := range []string{"wechat", "qq", "github", "google"} { if !a.configBool(r.Context(), "oauth."+code+".enabled", false) { continue } provider, err := a.adminOAuthProvider(r.Context(), code) if err != nil { // A broken channel must not hide other correctly configured channels. continue } items = append(items, map[string]string{"code": provider.Code, "name": provider.Name}) } reply(w, map[string]any{"items": items}) } func (a *App) adminOAuthStart(w http.ResponseWriter, r *http.Request) { var req struct { Provider string `json:"provider"` } if decode(r, &req) != nil { fail(w, http.StatusBadRequest, 20001, "请选择第三方登录渠道") return } req.Provider = strings.ToLower(strings.TrimSpace(req.Provider)) if !a.rateLimit(w, r, "admin_oauth_start", clientIP(r), 30, 10*time.Minute) { return } if !a.configBool(r.Context(), "oauth."+req.Provider+".enabled", false) { fail(w, http.StatusBadRequest, 20001, "该登录方式未启用") return } provider, err := a.adminOAuthProvider(r.Context(), req.Provider) if err != nil { fail(w, http.StatusBadRequest, 20001, "该登录方式配置不完整") return } state := randomToken() verifier := "" if provider.Code == "github" || provider.Code == "google" { verifier = randomToken() + randomToken() } _, err = a.db.ExecContext(r.Context(), `INSERT INTO admin_oauth_states(state_hash,provider,code_verifier,expires_at) VALUES(?,?,?,?)`, oauthHash(state), provider.Code, verifier, time.Now().Add(adminOAuthStateTTL)) if err != nil { fail(w, http.StatusInternalServerError, 50001, "创建第三方登录请求失败") return } a.cleanupAdminOAuthRecords(r.Context()) authorizationURL, _ := url.Parse(provider.AuthorizationURL) query := authorizationURL.Query() if provider.Code == "wechat" { query.Set("appid", provider.ClientID) } else { query.Set("client_id", provider.ClientID) } query.Set("redirect_uri", provider.RedirectURI) query.Set("response_type", "code") query.Set("scope", provider.Scope) query.Set("state", state) if verifier != "" { query.Set("code_challenge", pkceChallenge(verifier)) query.Set("code_challenge_method", "S256") } authorizationURL.RawQuery = query.Encode() if provider.Code == "wechat" { authorizationURL.Fragment = "wechat_redirect" } reply(w, map[string]string{"authorizationUrl": authorizationURL.String(), "provider": provider.Code}) } func (a *App) adminOAuthCallback(w http.ResponseWriter, r *http.Request) { frontendURL, frontendErr := a.adminOAuthFrontendURL(r.Context()) if frontendErr != nil { fail(w, http.StatusServiceUnavailable, 50001, "第三方登录回调未配置") return } state := strings.TrimSpace(r.URL.Query().Get("state")) if state == "" { a.redirectAdminOAuthResult(w, r, frontendURL, "", "登录状态无效或已过期") return } var providerCode, verifier string err := a.db.QueryRowContext(r.Context(), `SELECT provider,code_verifier FROM admin_oauth_states WHERE state_hash=? AND used_at IS NULL AND expires_at>NOW(3)`, oauthHash(state)).Scan(&providerCode, &verifier) if err != nil { a.redirectAdminOAuthResult(w, r, frontendURL, "", "登录状态无效或已过期") return } result, err := a.db.ExecContext(r.Context(), `UPDATE admin_oauth_states SET used_at=NOW(3) WHERE state_hash=? AND used_at IS NULL AND expires_at>NOW(3)`, oauthHash(state)) if err != nil { a.redirectAdminOAuthResult(w, r, frontendURL, "", "第三方登录处理失败") return } affected, _ := result.RowsAffected() if affected != 1 { a.redirectAdminOAuthResult(w, r, frontendURL, "", "登录状态已被使用") return } if providerError := strings.TrimSpace(r.URL.Query().Get("error")); providerError != "" { a.redirectAdminOAuthResult(w, r, frontendURL, "", "第三方授权已取消或失败") return } code := strings.TrimSpace(r.URL.Query().Get("code")) if code == "" { a.redirectAdminOAuthResult(w, r, frontendURL, "", "第三方平台未返回授权码") return } if !a.configBool(r.Context(), "oauth."+providerCode+".enabled", false) { a.redirectAdminOAuthResult(w, r, frontendURL, "", "该登录方式已停用") return } provider, err := a.adminOAuthProvider(r.Context(), providerCode) if err != nil { a.redirectAdminOAuthResult(w, r, frontendURL, "", "该登录方式配置不可用") return } identity, err := a.fetchAdminOAuthIdentity(r.Context(), provider, code, verifier) if err != nil { a.redirectAdminOAuthResult(w, r, frontendURL, "", "获取第三方账号信息失败") return } var adminUserID sql.NullInt64 _ = a.db.QueryRowContext(r.Context(), `SELECT admin_user_id FROM admin_oauth_identities WHERE provider=? AND subject=?`, provider.Code, identity.Subject).Scan(&adminUserID) loginCode := randomToken() _, err = a.db.ExecContext(r.Context(), `INSERT INTO admin_oauth_login_codes(code_hash,provider,subject,email,display_name,avatar_url,admin_user_id,expires_at) VALUES(?,?,?,?,?,?,?,?)`, oauthHash(loginCode), provider.Code, identity.Subject, identity.Email, identity.DisplayName, identity.AvatarURL, adminUserID, time.Now().Add(adminOAuthCodeTTL)) if err != nil { a.redirectAdminOAuthResult(w, r, frontendURL, "", "创建登录凭证失败") return } a.redirectAdminOAuthResult(w, r, frontendURL, loginCode, "") } func (a *App) redirectAdminOAuthResult(w http.ResponseWriter, r *http.Request, frontendURL, code, message string) { target, err := url.Parse(frontendURL) if err != nil { fail(w, http.StatusInternalServerError, 50001, "登录结果页地址无效") return } query := target.Query() if code != "" { query.Set("oauthCode", code) } else { query.Set("oauthError", message) } target.RawQuery = query.Encode() http.Redirect(w, r, target.String(), http.StatusFound) } func (a *App) adminOAuthExchange(w http.ResponseWriter, r *http.Request) { var req struct { Code string `json:"code"` } if decode(r, &req) != nil || strings.TrimSpace(req.Code) == "" { fail(w, http.StatusBadRequest, 20001, "第三方登录凭证无效") return } if !a.rateLimit(w, r, "admin_oauth_exchange", clientIP(r), 20, 10*time.Minute) { return } loginCode, err := a.readAdminOAuthLoginCode(r.Context(), strings.TrimSpace(req.Code), false) if err != nil { fail(w, http.StatusBadRequest, 20001, "第三方登录凭证无效或已过期") return } if !loginCode.AdminUserID.Valid { reply(w, map[string]any{ "requiresLink": true, "provider": loginCode.Provider, "providerName": adminOAuthProviderNames[loginCode.Provider], "displayName": loginCode.DisplayName, "email": loginCode.Email, "avatarUrl": loginCode.AvatarURL, }) return } adminID, realName, err := a.consumeAdminOAuthCode(r.Context(), strings.TrimSpace(req.Code), loginCode.AdminUserID.Int64) if err != nil { fail(w, http.StatusUnauthorized, 10001, err.Error()) return } payload, err := a.newAdminSession(r.Context(), w, r, adminID, realName) if err != nil { fail(w, http.StatusInternalServerError, 50001, "创建管理会话失败") return } payload["requiresLink"] = false reply(w, payload) } func (a *App) adminOAuthLink(w http.ResponseWriter, r *http.Request) { var req struct { Code string `json:"code"` Username string `json:"username"` Password string `json:"password"` } if decode(r, &req) != nil || strings.TrimSpace(req.Code) == "" || strings.TrimSpace(req.Username) == "" || req.Password == "" { fail(w, http.StatusBadRequest, 20001, "请输入管理员账号和密码完成绑定") return } username := strings.TrimSpace(req.Username) if !a.rateLimit(w, r, "admin_oauth_link_ip", clientIP(r), 10, 15*time.Minute) || !a.rateLimit(w, r, "admin_oauth_link_user", strings.ToLower(username), 10, 15*time.Minute) { return } loginCode, err := a.readAdminOAuthLoginCode(r.Context(), strings.TrimSpace(req.Code), true) if err != nil { fail(w, http.StatusBadRequest, 20001, "第三方登录凭证无效或已过期") return } var adminID int64 var passwordHash, realName string var status int err = a.db.QueryRowContext(r.Context(), `SELECT id,password_hash,real_name,status FROM admin_users WHERE username=?`, username).Scan(&adminID, &passwordHash, &realName, &status) if err != nil || !checkPassword(passwordHash, req.Password) { // 这里返回 400,避免前端全局 401 拦截器丢弃尚可重试的一次性绑定码。 fail(w, http.StatusBadRequest, 10001, "管理员账号或密码错误") return } if status != 1 { fail(w, http.StatusForbidden, 10006, "管理员账号已停用") return } tx, err := a.db.BeginTx(r.Context(), &sql.TxOptions{Isolation: sql.LevelReadCommitted}) if err != nil { fail(w, http.StatusInternalServerError, 50001, "绑定第三方账号失败") return } defer func() { _ = tx.Rollback() }() result, err := tx.ExecContext(r.Context(), `UPDATE admin_oauth_login_codes SET used_at=NOW(3),admin_user_id=? WHERE code_hash=? AND used_at IS NULL AND expires_at>NOW(3)`, adminID, oauthHash(strings.TrimSpace(req.Code))) if err != nil { fail(w, http.StatusInternalServerError, 50001, "绑定第三方账号失败") return } affected, _ := result.RowsAffected() if affected != 1 { fail(w, http.StatusBadRequest, 20001, "第三方登录凭证已被使用") return } _, err = tx.ExecContext(r.Context(), `INSERT INTO admin_oauth_identities(provider,subject,admin_user_id,email,display_name,avatar_url,last_login_at) VALUES(?,?,?,?,?,?,NOW(3))`, loginCode.Provider, loginCode.Subject, adminID, loginCode.Email, loginCode.DisplayName, loginCode.AvatarURL) if err != nil { fail(w, http.StatusConflict, 20001, "该第三方账号或管理员账号已绑定此渠道") return } if _, err = tx.ExecContext(r.Context(), `UPDATE admin_users SET last_login_at=NOW(3) WHERE id=?`, adminID); err != nil { fail(w, http.StatusInternalServerError, 50001, "绑定第三方账号失败") return } if err = tx.Commit(); err != nil { fail(w, http.StatusInternalServerError, 50001, "绑定第三方账号失败") return } auditPayload, _ := json.Marshal(map[string]any{"provider": loginCode.Provider, "externalSubjectHash": fmt.Sprintf("%x", sha256.Sum256([]byte(loginCode.Subject)))}) _, _ = a.db.ExecContext(r.Context(), `INSERT INTO admin_audit_logs(admin_user_id,action,target_type,target_id,request_data,ip) VALUES(?,?,?,?,?,?)`, adminID, "bind_oauth_identity", "admin_user", adminID, auditPayload, clientIP(r)) payload, err := a.newAdminSession(r.Context(), w, r, adminID, realName) if err != nil { fail(w, http.StatusInternalServerError, 50001, "创建管理会话失败") return } payload["requiresLink"] = false reply(w, payload) } func (a *App) readAdminOAuthLoginCode(ctx context.Context, code string, requireUnlinked bool) (adminOAuthLoginCode, error) { var result adminOAuthLoginCode query := `SELECT provider,subject,email,display_name,avatar_url,admin_user_id FROM admin_oauth_login_codes WHERE code_hash=? AND used_at IS NULL AND expires_at>NOW(3)` if requireUnlinked { query += ` AND admin_user_id IS NULL` } err := a.db.QueryRowContext(ctx, query, oauthHash(code)).Scan(&result.Provider, &result.Subject, &result.Email, &result.DisplayName, &result.AvatarURL, &result.AdminUserID) return result, err } func (a *App) consumeAdminOAuthCode(ctx context.Context, code string, adminID int64) (int64, string, error) { var realName string var status int if err := a.db.QueryRowContext(ctx, `SELECT real_name,status FROM admin_users WHERE id=?`, adminID).Scan(&realName, &status); err != nil || status != 1 { return 0, "", errors.New("管理员账号不存在或已停用") } result, err := a.db.ExecContext(ctx, `UPDATE admin_oauth_login_codes SET used_at=NOW(3) WHERE code_hash=? AND admin_user_id=? AND used_at IS NULL AND expires_at>NOW(3)`, oauthHash(code), adminID) if err != nil { return 0, "", errors.New("第三方登录处理失败") } affected, _ := result.RowsAffected() if affected != 1 { return 0, "", errors.New("第三方登录凭证无效或已使用") } _, _ = a.db.ExecContext(ctx, `UPDATE admin_users SET last_login_at=NOW(3) WHERE id=?`, adminID) _, _ = a.db.ExecContext(ctx, `UPDATE admin_oauth_identities SET last_login_at=NOW(3) WHERE admin_user_id=?`, adminID) return adminID, realName, nil } func (a *App) cleanupAdminOAuthRecords(ctx context.Context) { _, _ = a.db.ExecContext(ctx, `DELETE FROM admin_oauth_states WHERE expires_at= 300 { return "", "", errors.New("第三方令牌服务请求失败") } var payload struct { AccessToken string `json:"access_token"` Error string `json:"error"` ErrorDescription string `json:"error_description"` } if json.Unmarshal(body, &payload) != nil || payload.AccessToken == "" { parsed, parseErr := url.ParseQuery(string(body)) if parseErr != nil { return "", "", errors.New("第三方令牌响应无效") } payload.AccessToken = parsed.Get("access_token") payload.Error = parsed.Get("error") } if payload.Error != "" || payload.AccessToken == "" { return "", "", errors.New("第三方平台拒绝了令牌请求") } return payload.AccessToken, "", nil } func (a *App) oauthBearerJSON(ctx context.Context, endpoint, accessToken string, out any) error { request, _ := http.NewRequestWithContext(ctx, http.MethodGet, endpoint, nil) request.Header.Set("Authorization", "Bearer "+accessToken) request.Header.Set("Accept", "application/json") request.Header.Set("User-Agent", "XingYu-Admin-OAuth/1.0") return a.oauthDoJSON(request, out) } func (a *App) oauthGetJSON(ctx context.Context, endpoint string, values url.Values, out any) error { parsed, err := url.Parse(endpoint) if err != nil { return err } query := parsed.Query() for key, items := range values { for _, item := range items { query.Add(key, item) } } parsed.RawQuery = query.Encode() request, _ := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil) request.Header.Set("Accept", "application/json") request.Header.Set("User-Agent", "XingYu-Admin-OAuth/1.0") return a.oauthDoJSON(request, out) } func (a *App) oauthDoJSON(request *http.Request, out any) error { response, err := a.oauthHTTPClient().Do(request) if err != nil { return err } defer response.Body.Close() if response.StatusCode < 200 || response.StatusCode >= 300 { return fmt.Errorf("第三方平台返回 HTTP %d", response.StatusCode) } decoder := json.NewDecoder(io.LimitReader(response.Body, adminOAuthBodyMax)) return decoder.Decode(out) } func (a *App) fetchQQOpenID(ctx context.Context, endpoint, accessToken string) (string, error) { parsed, _ := url.Parse(endpoint) query := parsed.Query() query.Set("access_token", accessToken) query.Set("fmt", "json") parsed.RawQuery = query.Encode() request, _ := http.NewRequestWithContext(ctx, http.MethodGet, parsed.String(), nil) request.Header.Set("Accept", "application/json") response, err := a.oauthHTTPClient().Do(request) if err != nil { return "", err } defer response.Body.Close() body, err := io.ReadAll(io.LimitReader(response.Body, adminOAuthBodyMax)) if err != nil || response.StatusCode < 200 || response.StatusCode >= 300 { return "", errors.New("QQ OpenID 请求失败") } text := strings.TrimSpace(string(body)) if strings.HasPrefix(text, "callback") { start, end := strings.Index(text, "("), strings.LastIndex(text, ")") if start >= 0 && end > start { text = text[start+1 : end] } } var payload struct { OpenID string `json:"openid"` Error int `json:"error"` } if json.Unmarshal([]byte(text), &payload) != nil || payload.Error != 0 || payload.OpenID == "" { return "", errors.New("QQ OpenID 响应无效") } return payload.OpenID, nil }