Files
jiang13-forum/middleware/auth.go
freefire 822eef96be 新增 OIDC/SSO、邮件验证码与 Gitea 项目同步,并强化 Feed 与管理后台。
作为 OIDC Provider 对接 Gitea;注册支持邮件验证码/验证码;侧栏同步公开仓库;Feed 分页、文章大纲、标签云与站点品牌设置。

Co-authored-by: Cursor <cursoragent@cursor.com>
2026-07-31 16:58:22 +08:00

163 lines
3.8 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 middleware
import (
"fmt"
"net/http"
"strings"
"github.com/gin-gonic/gin"
"git.iioio.com/freefire/jiang13-forum/model"
"git.iioio.com/freefire/jiang13-forum/service"
)
const (
CtxUserID = "user_id"
CtxUsername = "username"
CtxRole = "role"
CookieName = "jiang13_token"
)
type AuthMiddleware struct {
auth *service.AuthService
}
func NewAuthMiddleware(auth *service.AuthService) *AuthMiddleware {
return &AuthMiddleware{auth: auth}
}
// OptionalAuth 可选鉴权:有 token 则解析,无 token 不拦截。
// 用户已删除或不存在时清除失效 cookie避免前端误显示为已登录。
func (m *AuthMiddleware) OptionalAuth() gin.HandlerFunc {
return func(c *gin.Context) {
token := extractToken(c)
if token != "" {
if claims, err := m.auth.ParseToken(token); err == nil {
var user model.User
if err := model.DB.Select("id", "username", "role").First(&user, claims.UserID).Error; err != nil {
c.SetCookie(CookieName, "", -1, "/", "", false, true)
} else {
c.Set(CtxUserID, user.ID)
c.Set(CtxUsername, user.Username)
c.Set(CtxRole, user.Role)
}
}
}
c.Next()
}
}
// RequireAuth 必须登录
func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
return func(c *gin.Context) {
token := extractToken(c)
if token == "" {
respondAuthRequired(c)
c.Abort()
return
}
claims, err := m.auth.ParseToken(token)
if err != nil {
respondAuthExpired(c)
c.Abort()
return
}
// 检查禁言
var user model.User
if err := model.DB.First(&user, claims.UserID).Error; err != nil || user.Banned {
respondBanned(c)
c.Abort()
return
}
c.Set(CtxUserID, claims.UserID)
c.Set(CtxUsername, claims.Username)
c.Set(CtxRole, claims.Role)
c.Next()
}
}
// RequireAdmin 必须管理员
func (m *AuthMiddleware) RequireAdmin() gin.HandlerFunc {
return func(c *gin.Context) {
role, exists := c.Get(CtxRole)
if !exists || role != model.RoleAdmin {
if isAPI(c) {
c.JSON(http.StatusForbidden, gin.H{"error": "需要管理员权限"})
} else {
c.Redirect(http.StatusFound, "/admin/login")
}
c.Abort()
return
}
c.Next()
}
}
func extractToken(c *gin.Context) string {
if auth := c.GetHeader("Authorization"); auth != "" {
if strings.HasPrefix(auth, "Bearer ") {
return strings.TrimPrefix(auth, "Bearer ")
}
}
if token, err := c.Cookie(CookieName); err == nil {
return token
}
return c.Query("token")
}
func isAPI(c *gin.Context) bool {
p := c.Request.URL.Path
return strings.HasPrefix(p, "/api/") || strings.HasPrefix(p, "/admin/api/")
}
func adminLoginPath(c *gin.Context) string {
if strings.HasPrefix(c.Request.URL.Path, "/admin") {
return "/admin/login"
}
return "/login"
}
func respondAuthRequired(c *gin.Context) {
if isAPI(c) {
c.JSON(http.StatusUnauthorized, gin.H{"error": "请先登录"})
return
}
c.Redirect(http.StatusFound, adminLoginPath(c))
}
func respondAuthExpired(c *gin.Context) {
c.SetCookie(CookieName, "", -1, "/", "", false, true)
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
}
if strings.HasPrefix(c.Request.URL.Path, "/admin") {
c.Redirect(http.StatusFound, "/admin/login?banned=1")
return
}
c.Redirect(http.StatusFound, "/login?banned=1")
}
// RateLimitMiddleware 限流中间件
func RateLimitMiddleware(limiter *service.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()
}
}