更新
This commit is contained in:
@@ -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
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user