浏览器登录改为 DB sessions(可吊销);敏感词与 OIDC PEM 入 settings; 落地安装向导、注册发帖与 /admin 仪表盘/板块/审核/设置。 Co-authored-by: Cursor <cursoragent@cursor.com>
179 lines
4.0 KiB
Go
179 lines
4.0 KiB
Go
package auth
|
||
|
||
import (
|
||
"fmt"
|
||
"net/http"
|
||
"net/url"
|
||
"strings"
|
||
|
||
"git.iioio.com/freefire/jiang13-forum/models"
|
||
"git.iioio.com/freefire/jiang13-forum/services"
|
||
"github.com/gin-gonic/gin"
|
||
)
|
||
|
||
const (
|
||
CtxUserID = "user_id"
|
||
CtxUsername = "username"
|
||
CtxRole = "role"
|
||
CtxSessionID = "session_id"
|
||
CookieName = "jiang13_session"
|
||
)
|
||
|
||
type AuthMiddleware struct {
|
||
auth *services.AuthService
|
||
}
|
||
|
||
func NewAuthMiddleware(auth *services.AuthService) *AuthMiddleware {
|
||
return &AuthMiddleware{auth: auth}
|
||
}
|
||
|
||
// OptionalAuth 可选鉴权:有会话则加载用户;禁言/无效则清 Cookie
|
||
func (m *AuthMiddleware) OptionalAuth() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
sid := extractSessionID(c)
|
||
if sid == "" {
|
||
c.Next()
|
||
return
|
||
}
|
||
user, sess, err := services.ResolveSession(sid)
|
||
if err != nil || user == nil {
|
||
ClearAuthCookie(c)
|
||
c.Next()
|
||
return
|
||
}
|
||
if user.Banned {
|
||
services.RevokeUserSessions(user.ID)
|
||
ClearAuthCookie(c)
|
||
c.Next()
|
||
return
|
||
}
|
||
c.Set(CtxUserID, user.ID)
|
||
c.Set(CtxUsername, user.Username)
|
||
c.Set(CtxRole, user.Role)
|
||
c.Set(CtxSessionID, sess.ID)
|
||
m.auth.TouchLastAccess(user.ID)
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
// RequireAuth 必须登录且未禁言
|
||
func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
sid := extractSessionID(c)
|
||
if sid == "" {
|
||
respondAuthRequired(c)
|
||
c.Abort()
|
||
return
|
||
}
|
||
user, sess, err := services.ResolveSession(sid)
|
||
if err != nil || user == nil {
|
||
respondAuthExpired(c)
|
||
c.Abort()
|
||
return
|
||
}
|
||
if user.Banned {
|
||
services.RevokeUserSessions(user.ID)
|
||
ClearAuthCookie(c)
|
||
respondBanned(c)
|
||
c.Abort()
|
||
return
|
||
}
|
||
c.Set(CtxUserID, user.ID)
|
||
c.Set(CtxUsername, user.Username)
|
||
c.Set(CtxRole, user.Role)
|
||
c.Set(CtxSessionID, sess.ID)
|
||
m.auth.TouchLastAccess(user.ID)
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
// RequireAdmin 必须管理员(依赖上游已跑 OptionalAuth/RequireAuth,role 来自 DB)
|
||
func (m *AuthMiddleware) RequireAdmin() gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
uid, ok := c.Get(CtxUserID)
|
||
if !ok || uid == nil {
|
||
respondAuthRequired(c)
|
||
c.Abort()
|
||
return
|
||
}
|
||
role, exists := c.Get(CtxRole)
|
||
if !exists || role != models.RoleAdmin {
|
||
if isAPI(c) {
|
||
c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
|
||
} else {
|
||
c.Redirect(http.StatusFound, "/")
|
||
}
|
||
c.Abort()
|
||
return
|
||
}
|
||
c.Next()
|
||
}
|
||
}
|
||
|
||
func extractSessionID(c *gin.Context) string {
|
||
if sid, err := c.Cookie(CookieName); err == nil && sid != "" {
|
||
return sid
|
||
}
|
||
// 兼容清理旧 Cookie 名(一次性)
|
||
if old, err := c.Cookie("jiang13_token"); err == nil && old != "" {
|
||
ClearNamedCookie(c, "jiang13_token")
|
||
}
|
||
return ""
|
||
}
|
||
|
||
func isAPI(c *gin.Context) bool {
|
||
return strings.HasPrefix(c.Request.URL.Path, "/api/")
|
||
}
|
||
|
||
func adminLoginPath(c *gin.Context) string {
|
||
if strings.HasPrefix(c.Request.URL.Path, "/admin") {
|
||
return "/login?redirect=" + url.QueryEscape("/admin/dashboard")
|
||
}
|
||
return "/login"
|
||
}
|
||
|
||
func respondAuthRequired(c *gin.Context) {
|
||
if isAPI(c) {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
|
||
return
|
||
}
|
||
redir := c.Request.URL.RequestURI()
|
||
if strings.HasPrefix(c.Request.URL.Path, "/admin") {
|
||
redir = "/admin/dashboard"
|
||
}
|
||
c.Redirect(http.StatusFound, "/login?redirect="+url.QueryEscape(redir))
|
||
}
|
||
|
||
func respondAuthExpired(c *gin.Context) {
|
||
ClearAuthCookie(c)
|
||
if isAPI(c) {
|
||
c.JSON(http.StatusUnauthorized, gin.H{"error": "登录已过期"})
|
||
return
|
||
}
|
||
c.Redirect(http.StatusFound, adminLoginPath(c))
|
||
}
|
||
|
||
func respondBanned(c *gin.Context) {
|
||
if isAPI(c) {
|
||
c.JSON(http.StatusForbidden, gin.H{"error": "账号已被禁言"})
|
||
return
|
||
}
|
||
c.Redirect(http.StatusFound, "/login?banned=1")
|
||
}
|
||
|
||
// RateLimitMiddleware 限流中间件
|
||
func RateLimitMiddleware(limiter *services.RateLimiter, action string) gin.HandlerFunc {
|
||
return func(c *gin.Context) {
|
||
key := c.ClientIP()
|
||
if uid, ok := c.Get(CtxUserID); ok {
|
||
key = fmt.Sprintf("%d", uid.(uint))
|
||
}
|
||
if !limiter.Allow(action, key) {
|
||
c.JSON(http.StatusTooManyRequests, gin.H{"error": "操作过于频繁,请稍后再试"})
|
||
c.Abort()
|
||
return
|
||
}
|
||
c.Next()
|
||
}
|
||
}
|