This commit is contained in:
Your Name
2026-09-03 08:38:17 +08:00
parent 6cd4f1b1db
commit 842990b0e7
1853 changed files with 278406 additions and 361 deletions
+86 -29
View File
@@ -11,12 +11,15 @@ import (
)
type userOAuthLoginCode struct {
Provider string
Subject string
Email string
DisplayName string
AvatarURL string
UserID sql.NullInt64
Provider string
Platform string
IdentityScope string
AppProofHash []byte
Subject string
Email string
DisplayName string
AvatarURL string
UserID sql.NullInt64
}
func (a *App) userOAuthFrontendURL(ctx context.Context) (string, error) {
@@ -69,33 +72,55 @@ func (a *App) oauthCallback(w http.ResponseWriter, r *http.Request) {
}
func (a *App) userOAuthProviders(w http.ResponseWriter, r *http.Request) {
platform, err := oauthClientPlatform(r.URL.Query().Get("platform"))
if err != nil {
fail(w, 400, 20001, err.Error())
return
}
items := make([]map[string]string, 0, len(adminOAuthProviderNames))
for _, code := range []string{"wechat", "qq", "github", "google"} {
if !a.configBool(r.Context(), "oauth.user."+code+".enabled", false) {
if !a.configBool(r.Context(), userOAuthEnabledKey(platform, code), false) {
continue
}
provider, err := a.adminOAuthProvider(r.Context(), code)
var provider adminOAuthProvider
var err error
if platform == "app" {
provider, err = a.appOAuthProvider(r.Context(), code)
} else {
provider, err = a.adminOAuthProvider(r.Context(), code)
}
if err != nil {
continue
}
items = append(items, map[string]string{"code": provider.Code, "name": provider.Name})
}
w.Header().Set("Cache-Control", "no-store")
reply(w, map[string]any{"items": items})
}
func (a *App) userOAuthStart(w http.ResponseWriter, r *http.Request) {
var req struct {
Provider string `json:"provider"`
Platform string `json:"platform"`
}
if decode(r, &req) != nil {
fail(w, http.StatusBadRequest, 20001, "请选择第三方登录渠道")
return
}
req.Provider = strings.ToLower(strings.TrimSpace(req.Provider))
platform, err := oauthClientPlatform(req.Platform)
if err != nil {
fail(w, 400, 20001, err.Error())
return
}
if platform == "app" && req.Provider != "github" {
fail(w, 400, 20001, "该 App 渠道需使用原生 SDK 授权")
return
}
if !a.rateLimit(w, r, "user_oauth_start", clientIP(r), 30, 10*time.Minute) {
return
}
if !a.configBool(r.Context(), "oauth.user."+req.Provider+".enabled", false) {
if !a.configBool(r.Context(), userOAuthEnabledKey(platform, req.Provider), false) {
fail(w, http.StatusBadRequest, 20001, "该客户端登录方式未启用")
return
}
@@ -104,7 +129,12 @@ func (a *App) userOAuthStart(w http.ResponseWriter, r *http.Request) {
fail(w, http.StatusBadRequest, 20001, "该登录方式配置不完整")
return
}
if _, err = a.userOAuthFrontendURL(r.Context()); err != nil {
if platform == "app" {
_, err = a.appOAuthFrontendURL(r.Context())
} else {
_, err = a.userOAuthFrontendURL(r.Context())
}
if err != nil {
fail(w, http.StatusBadRequest, 20001, "客户端登录结果页配置不完整")
return
}
@@ -113,7 +143,13 @@ func (a *App) userOAuthStart(w http.ResponseWriter, r *http.Request) {
if provider.Code == "github" || provider.Code == "google" {
verifier = randomToken() + randomToken()
}
_, err = a.db.ExecContext(r.Context(), `INSERT INTO user_oauth_states(state_hash,provider,code_verifier,expires_at) VALUES(?,?,?,?)`, oauthHash(state), provider.Code, verifier, time.Now().Add(adminOAuthStateTTL))
appProof := ""
var proofHash []byte
if platform == "app" {
appProof = randomToken()
proofHash = oauthHash(appProof)
}
_, err = a.db.ExecContext(r.Context(), `INSERT INTO user_oauth_states(state_hash,provider,code_verifier,expires_at,client_platform,app_proof_hash) VALUES(?,?,?,?,?,?)`, oauthHash(state), provider.Code, verifier, time.Now().Add(adminOAuthStateTTL), platform, proofHash)
if err != nil {
fail(w, http.StatusInternalServerError, 50001, "创建第三方登录请求失败")
return
@@ -139,24 +175,38 @@ func (a *App) userOAuthStart(w http.ResponseWriter, r *http.Request) {
if provider.Code == "wechat" {
authorizationURL.Fragment = "wechat_redirect"
}
reply(w, map[string]string{"authorizationUrl": authorizationURL.String(), "provider": provider.Code})
w.Header().Set("Cache-Control", "no-store")
response := map[string]string{"authorizationUrl": authorizationURL.String(), "provider": provider.Code}
if platform == "app" {
response["appProof"] = appProof
response["requestId"] = state
response["callbackUrl"] = appOAuthCallbackURL
}
reply(w, response)
}
func (a *App) userOAuthCallback(w http.ResponseWriter, r *http.Request) {
frontendURL, err := a.userOAuthFrontendURL(r.Context())
if err != nil {
fail(w, http.StatusServiceUnavailable, 50001, "客户端第三方登录回调未配置")
return
}
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Referrer-Policy", "no-referrer")
state := strings.TrimSpace(r.URL.Query().Get("state"))
if state == "" {
a.redirectUserOAuthResult(w, r, frontendURL, "", "登录状态无效或已过期")
var providerCode, verifier, platform string
var proofHash []byte
err := a.db.QueryRowContext(r.Context(), `SELECT provider,code_verifier,client_platform,app_proof_hash FROM user_oauth_states WHERE state_hash=? AND used_at IS NULL AND expires_at>NOW(3)`, oauthHash(state)).Scan(&providerCode, &verifier, &platform, &proofHash)
if err != nil {
fail(w, 400, 20001, "登录状态无效或已过期,请返回客户端重试")
return
}
var providerCode, verifier string
err = a.db.QueryRowContext(r.Context(), `SELECT provider,code_verifier FROM user_oauth_states WHERE state_hash=? AND used_at IS NULL AND expires_at>NOW(3)`, oauthHash(state)).Scan(&providerCode, &verifier)
var frontendURL string
if platform == "app" {
frontendURL, err = a.appOAuthFrontendURL(r.Context())
if err == nil {
frontendURL += "?requestId=" + url.QueryEscape(state)
}
} else {
frontendURL, err = a.userOAuthFrontendURL(r.Context())
}
if err != nil {
a.redirectUserOAuthResult(w, r, frontendURL, "", "登录状态无效或已过期")
fail(w, 503, 50001, "客户端第三方登录回调未配置")
return
}
result, err := a.db.ExecContext(r.Context(), `UPDATE user_oauth_states SET used_at=NOW(3) WHERE state_hash=? AND used_at IS NULL AND expires_at>NOW(3)`, oauthHash(state))
@@ -178,7 +228,7 @@ func (a *App) userOAuthCallback(w http.ResponseWriter, r *http.Request) {
a.redirectUserOAuthResult(w, r, frontendURL, "", "第三方平台未返回授权码")
return
}
if !a.configBool(r.Context(), "oauth.user."+providerCode+".enabled", false) {
if !a.configBool(r.Context(), userOAuthEnabledKey(platform, providerCode), false) {
a.redirectUserOAuthResult(w, r, frontendURL, "", "该客户端登录方式已停用")
return
}
@@ -192,10 +242,7 @@ func (a *App) userOAuthCallback(w http.ResponseWriter, r *http.Request) {
a.redirectUserOAuthResult(w, r, frontendURL, "", "获取第三方账号信息失败")
return
}
var userID sql.NullInt64
_ = a.db.QueryRowContext(r.Context(), `SELECT user_id FROM user_oauth_identities WHERE provider=? AND subject=?`, provider.Code, identity.Subject).Scan(&userID)
loginCode := randomToken()
_, err = a.db.ExecContext(r.Context(), `INSERT INTO user_oauth_login_codes(code_hash,provider,subject,email,display_name,avatar_url,user_id,expires_at) VALUES(?,?,?,?,?,?,?,?)`, oauthHash(loginCode), provider.Code, identity.Subject, identity.Email, identity.DisplayName, identity.AvatarURL, userID, time.Now().Add(adminOAuthCodeTTL))
loginCode, err := a.issueUserOAuthCode(r.Context(), provider.Code, platform, "", proofHash, identity)
if err != nil {
a.redirectUserOAuthResult(w, r, frontendURL, "", "创建登录凭证失败")
return
@@ -223,6 +270,7 @@ func (a *App) userOAuthExchange(w http.ResponseWriter, r *http.Request) {
var req struct {
Code string `json:"code"`
DeviceID string `json:"deviceId"`
AppProof string `json:"appProof"`
}
if decode(r, &req) != nil || strings.TrimSpace(req.Code) == "" {
fail(w, http.StatusBadRequest, 20001, "第三方登录凭证无效")
@@ -236,6 +284,10 @@ func (a *App) userOAuthExchange(w http.ResponseWriter, r *http.Request) {
fail(w, http.StatusBadRequest, 20001, "第三方登录凭证无效或已过期")
return
}
if err = a.validateUserOAuthCode(r.Context(), loginCode, req.AppProof); err != nil {
fail(w, 400, 20001, err.Error())
return
}
if !loginCode.UserID.Valid {
reply(w, map[string]any{
"requiresLink": true,
@@ -260,6 +312,7 @@ func (a *App) userOAuthLink(w http.ResponseWriter, r *http.Request) {
Phone string `json:"phone"`
SMSCode string `json:"smsCode"`
DeviceID string `json:"deviceId"`
AppProof string `json:"appProof"`
}
if decode(r, &req) != nil || strings.TrimSpace(req.Code) == "" || !validPhone(req.Phone) || len(req.SMSCode) != 6 {
fail(w, http.StatusBadRequest, 20001, "请输入已注册手机号和正确的短信验证码")
@@ -274,6 +327,10 @@ func (a *App) userOAuthLink(w http.ResponseWriter, r *http.Request) {
fail(w, http.StatusBadRequest, 20001, "第三方登录凭证无效、已绑定或已过期")
return
}
if err = a.validateUserOAuthCode(r.Context(), loginCode, req.AppProof); err != nil {
fail(w, 400, 20001, err.Error())
return
}
var userID int64
var nickname string
var status int
@@ -302,7 +359,7 @@ func (a *App) userOAuthLink(w http.ResponseWriter, r *http.Request) {
fail(w, http.StatusBadRequest, 20001, "第三方登录凭证已被使用")
return
}
_, err = tx.ExecContext(r.Context(), `INSERT INTO user_oauth_identities(provider,subject,user_id,email,display_name,avatar_url,last_login_at) VALUES(?,?,?,?,?,?,NOW(3))`, loginCode.Provider, loginCode.Subject, userID, loginCode.Email, loginCode.DisplayName, loginCode.AvatarURL)
_, err = tx.ExecContext(r.Context(), `INSERT INTO user_oauth_identities(provider,subject,user_id,email,display_name,avatar_url,identity_scope,last_login_at) VALUES(?,?,?,?,?,?,?,NOW(3))`, loginCode.Provider, loginCode.Subject, userID, loginCode.Email, loginCode.DisplayName, loginCode.AvatarURL, loginCode.IdentityScope)
if err != nil {
fail(w, http.StatusConflict, 20001, "该第三方账号或手机号已绑定此渠道")
return
@@ -316,7 +373,7 @@ func (a *App) userOAuthLink(w http.ResponseWriter, r *http.Request) {
func (a *App) readUserOAuthLoginCode(ctx context.Context, code string) (userOAuthLoginCode, error) {
var result userOAuthLoginCode
err := a.db.QueryRowContext(ctx, `SELECT provider,subject,email,display_name,avatar_url,user_id FROM user_oauth_login_codes WHERE code_hash=? AND used_at IS NULL AND expires_at>NOW(3)`, oauthHash(code)).Scan(&result.Provider, &result.Subject, &result.Email, &result.DisplayName, &result.AvatarURL, &result.UserID)
err := a.db.QueryRowContext(ctx, `SELECT provider,subject,email,display_name,avatar_url,user_id,client_platform,identity_scope,app_proof_hash FROM user_oauth_login_codes WHERE code_hash=? AND used_at IS NULL AND expires_at>NOW(3)`, oauthHash(code)).Scan(&result.Provider, &result.Subject, &result.Email, &result.DisplayName, &result.AvatarURL, &result.UserID, &result.Platform, &result.IdentityScope, &result.AppProofHash)
return result, err
}