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
+57 -32
View File
@@ -174,16 +174,25 @@ func (a *App) enabledAdminOAuthProviders(ctx context.Context) ([]adminOAuthProvi
}
func (a *App) oauthConfigurationReady(ctx context.Context) bool {
if _, err := a.adminOAuthFrontendURL(ctx); err != nil {
adminProviders, err := a.enabledAdminOAuthProviders(ctx)
if err != nil {
return false
}
if _, err := a.userOAuthFrontendURL(ctx); err != nil {
userProviders, err := a.enabledUserOAuthProviders(ctx)
if err != nil {
return false
}
if _, err := a.enabledAdminOAuthProviders(ctx); err != nil {
return false
if len(adminProviders) > 0 {
if _, err := a.adminOAuthFrontendURL(ctx); err != nil {
return false
}
}
_, err := a.enabledUserOAuthProviders(ctx)
if len(userProviders) > 0 {
if _, err := a.userOAuthFrontendURL(ctx); err != nil {
return false
}
}
_, err = a.enabledAppOAuthProviders(ctx)
return err == nil
}
@@ -199,24 +208,33 @@ func (a *App) validateAdminOAuthConfigValues(ctx context.Context, values map[str
}
return strings.TrimSpace(a.configPlain(ctx, key, ""))
}
frontendURL := value("oauth.admin.frontend_callback_url")
if frontendURL == "" {
return errors.New("管理端登录结果页不能为空")
}
if err := a.validateAdminOAuthRedirectURL(frontendURL); err != nil {
return fmt.Errorf("管理端登录结果页无效:%w", err)
}
userFrontendURL := value("oauth.user.frontend_callback_url")
if userFrontendURL == "" {
return errors.New("客户端 H5 登录结果页不能为空")
}
if err := a.validateAdminOAuthRedirectURL(userFrontendURL); err != nil {
return fmt.Errorf("客户端 H5 登录结果页无效:%w", err)
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"
if !adminEnabled && !userEnabled {
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 + "."
@@ -425,12 +443,18 @@ func (a *App) adminOAuthExchange(w http.ResponseWriter, r *http.Request) {
})
return
}
token, err := a.consumeAdminOAuthCodeAndIssueToken(r.Context(), strings.TrimSpace(req.Code), loginCode.AdminUserID.Int64)
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
}
reply(w, map[string]any{"accessToken": token, "requiresLink": false})
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) {
@@ -496,8 +520,13 @@ func (a *App) adminOAuthLink(w http.ResponseWriter, r *http.Request) {
}
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))
token, _ := a.token(adminID, "admin", realName, 8*time.Hour)
reply(w, map[string]any{"accessToken": token, "requiresLink": false})
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) {
@@ -510,27 +539,23 @@ func (a *App) readAdminOAuthLoginCode(ctx context.Context, code string, requireU
return result, err
}
func (a *App) consumeAdminOAuthCodeAndIssueToken(ctx context.Context, code string, adminID int64) (string, error) {
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 "", errors.New("管理员账号不存在或已停用")
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 "", errors.New("第三方登录处理失败")
return 0, "", errors.New("第三方登录处理失败")
}
affected, _ := result.RowsAffected()
if affected != 1 {
return "", errors.New("第三方登录凭证无效或已使用")
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)
token, err := a.token(adminID, "admin", realName, 8*time.Hour)
if err != nil {
return "", errors.New("创建登录令牌失败")
}
return token, nil
return adminID, realName, nil
}
func (a *App) cleanupAdminOAuthRecords(ctx context.Context) {