Files
jiang13-forum/modules/auth/auth.go
freefire fde5f628ec feat: opaque session、安装/发帖 SSR 与最小 Admin 后台
浏览器登录改为 DB sessions(可吊销);敏感词与 OIDC PEM 入 settings;
落地安装向导、注册发帖与 /admin 仪表盘/板块/审核/设置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-08-29 05:44:16 +08:00

179 lines
4.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
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/RequireAuthrole 来自 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()
}
}