Files
2026-01-16 15:49:34 +08:00

125 lines
3.6 KiB
Go

package middleware
import (
"time"
"github.com/goravel/framework/contracts/http"
"github.com/goravel/framework/facades"
"github.com/goravel/framework/support/str"
"goravel/app/http/trans"
"goravel/app/models"
"goravel/app/services"
"goravel/app/utils/logger"
)
// min 返回两个整数中的较小值
func min(a, b int) int {
if a < b {
return a
}
return b
}
func Jwt() http.Middleware {
return func(ctx http.Context) {
// 如果路径是api/admin前缀,使用admin guard
path := ctx.Request().Path()
pathStr := str.Of(path)
if pathStr.IsEmpty() || (!pathStr.StartsWith("/api/admin") && !pathStr.StartsWith("/admin")) {
ctx.Request().Next()
return
}
token := ctx.Request().Header("Authorization", "")
// 如果 Header 中没有 token,尝试从 URL 参数中获取(用于 SSE 等不支持自定义 headers 的场景)
if str.Of(token).IsEmpty() {
token = ctx.Request().Query("_token", "")
}
if str.Of(token).IsEmpty() {
_ = ctx.Response().Json(http.StatusUnauthorized, http.Json{
"code": http.StatusUnauthorized,
"message": trans.Get(ctx, "not_logged_in"),
}).Abort()
return
}
// 移除Bearer前缀(如果有)
token = str.Of(token).ChopStart("Bearer ").Trim().String()
if token == "" {
_ = ctx.Response().Json(http.StatusUnauthorized, http.Json{
"code": http.StatusUnauthorized,
"message": trans.Get(ctx, "not_logged_in"),
}).Abort()
return
}
// 从数据库查找token
tokenService := services.NewTokenServiceImpl()
accessToken, err := tokenService.FindToken(token)
if err != nil {
// token查找失败或已过期
logger.ErrorfHTTP(ctx, "JWT middleware: FindToken error: %v, token prefix: %s", err, token[:min(20, len(token))])
_ = ctx.Response().Json(http.StatusUnauthorized, http.Json{
"code": http.StatusUnauthorized,
"message": trans.Get(ctx, "invalid_token"),
}).Abort()
return
}
if accessToken == nil {
logger.ErrorfHTTP(ctx, "JWT middleware: accessToken is nil, token prefix: %s", token[:min(20, len(token))])
_ = ctx.Response().Json(http.StatusUnauthorized, http.Json{
"code": http.StatusUnauthorized,
"message": trans.Get(ctx, "invalid_token"),
}).Abort()
return
}
// 检查token类型
if accessToken.TokenableType != "admin" {
_ = ctx.Response().Json(http.StatusUnauthorized, http.Json{
"code": http.StatusUnauthorized,
"message": trans.Get(ctx, "invalid_token"),
}).Abort()
return
}
// 查询用户信息
var admin models.Admin
if err := facades.Orm().Query().Where("id", accessToken.TokenableID).First(&admin); err != nil {
_ = ctx.Response().Json(http.StatusUnauthorized, http.Json{
"code": http.StatusUnauthorized,
"message": trans.Get(ctx, "user_not_found"),
}).Abort()
return
}
// 更新最后使用时间
_ = tokenService.UpdateLastUsedAt(token)
// 滑动过期:如果token有过期时间,每次请求时自动延长过期时间
if accessToken.ExpiresAt != nil {
ttl := facades.Config().GetInt("jwt.ttl", 60) // 默认60分钟
if ttl > 0 {
newExpiresAt := time.Now().Add(time.Duration(ttl) * time.Minute)
// 更新token的过期时间
_, _ = facades.Orm().Query().
Model(&models.PersonalAccessToken{}).
Where("id", accessToken.ID).
Update("expires_at", newExpiresAt)
}
}
// 将用户信息存储到context中,供后续中间件使用
ctx.WithValue("admin", admin)
ctx.WithValue("token", accessToken)
// facades.Log().Debugf("JWT middleware: admin set in context, ID: %d, Username: %s", admin.ID, admin.Username)
ctx.Request().Next()
}
}