做统一认证登录
This commit is contained in:
+198
-75
@@ -1,75 +1,198 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
"github.com/beego/beego/v2/server/web"
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
)
|
||||
|
||||
// JWTAuthMiddleware JWT认证中间件
|
||||
func JWTAuthMiddleware() web.FilterFunc {
|
||||
return func(ctx *context.Context) {
|
||||
// 跳过登录相关的路由
|
||||
if strings.HasPrefix(ctx.Request.RequestURI, "/api/login") ||
|
||||
strings.HasPrefix(ctx.Request.RequestURI, "/api/reset-password") {
|
||||
return
|
||||
}
|
||||
|
||||
// 从请求头中获取Authorization
|
||||
authHeader := ctx.Request.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
ctx.Output.SetStatus(401)
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "未提供认证信息",
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
|
||||
// 按空格分割
|
||||
authParts := strings.SplitN(authHeader, " ", 2)
|
||||
if !(len(authParts) == 2 && authParts[0] == "Bearer") {
|
||||
ctx.Output.SetStatus(401)
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "认证信息格式错误",
|
||||
}, false, false)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析token
|
||||
claims, err := jwtutil.ParseToken(authParts[1])
|
||||
if err != nil {
|
||||
// 处理各种错误情况
|
||||
ctx.Output.SetStatus(401)
|
||||
switch err.Error() {
|
||||
case "token is expired":
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "token已过期",
|
||||
}, false, false)
|
||||
default:
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": "无效的token",
|
||||
}, false, false)
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存储在上下文
|
||||
ctx.Input.SetData("userId", claims.UserID)
|
||||
ctx.Input.SetData("username", claims.Username)
|
||||
ctx.Input.SetData("tenantId", claims.TenantId)
|
||||
|
||||
// 从token中获取用户类型(如果token中没有,则默认为"user")
|
||||
userType := claims.UserType
|
||||
if userType == "" {
|
||||
userType = "user"
|
||||
}
|
||||
ctx.Input.SetData("userType", userType)
|
||||
}
|
||||
}
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"server/pkg/jwtutil"
|
||||
|
||||
beego "github.com/beego/beego/v2/server/web"
|
||||
"github.com/beego/beego/v2/server/web/context"
|
||||
)
|
||||
|
||||
// 鉴权开关(app.conf: auth_enforce)
|
||||
// - off :完全不启用(等同改造前的行为)
|
||||
// - warn :观察模式,只记录「无 token / token 无效」的接口,不拦截(默认,零风险上线)
|
||||
// - on :真正拦截并返回 401
|
||||
//
|
||||
// 上线建议:先 warn 跑一段时间,用日志核对白名单是否遗漏,确认无误后再切 on。
|
||||
const (
|
||||
AuthModeOff = "off"
|
||||
AuthModeWarn = "warn"
|
||||
AuthModeOn = "on"
|
||||
)
|
||||
|
||||
// protectedPrefixes 需要鉴权的业务前缀。
|
||||
// 仅保护业务 API:官网(/ /site/ /index/…)、开放 API(/api/…)、
|
||||
// 静态资源与 ACME 验证路径均不在其中,避免误伤公开接口。
|
||||
var protectedPrefixes = []string{"/backend/", "/platform/", "/app/"}
|
||||
|
||||
// authWhitelist 受保护前缀内无需鉴权的路径(前缀匹配)
|
||||
var authWhitelist = []string{
|
||||
// 统一认证中心(P1 上线)
|
||||
"/auth/",
|
||||
|
||||
// backend 登录/注册/找回密码
|
||||
"/backend/login",
|
||||
"/backend/sendLoginCode",
|
||||
"/backend/register",
|
||||
"/backend/sendRegisterCode",
|
||||
"/backend/resetPassword",
|
||||
"/backend/sendResetCode",
|
||||
"/backend/verifyAccount",
|
||||
"/backend/logout",
|
||||
|
||||
// platform 登录/找回密码
|
||||
"/platform/login",
|
||||
"/platform/sendLoginCode",
|
||||
"/platform/resetPassword",
|
||||
"/platform/logout",
|
||||
// 平台开放接口(客户端更新通知,无需登录)
|
||||
"/platform/api/",
|
||||
|
||||
// app 登录/注册/找回密码
|
||||
"/app/login",
|
||||
"/app/sendLoginCode",
|
||||
"/app/register",
|
||||
"/app/sendRegisterCode",
|
||||
"/app/resetPassword",
|
||||
"/app/sendResetCode",
|
||||
"/app/verifyAccount",
|
||||
"/app/logout",
|
||||
}
|
||||
|
||||
var (
|
||||
modeOnce sync.Once
|
||||
modeVal = AuthModeWarn
|
||||
warnSeen sync.Map // 观察模式下按 path 去重,避免日志刷屏
|
||||
)
|
||||
|
||||
func authMode() string {
|
||||
modeOnce.Do(func() {
|
||||
if v, err := beego.AppConfig.String("auth_enforce"); err == nil {
|
||||
switch strings.ToLower(strings.TrimSpace(v)) {
|
||||
case AuthModeOff, AuthModeWarn, AuthModeOn:
|
||||
modeVal = strings.ToLower(strings.TrimSpace(v))
|
||||
}
|
||||
}
|
||||
// 额外白名单:auth_whitelist = /backend/xxx,/app/yyy
|
||||
if extra, err := beego.AppConfig.String("auth_whitelist"); err == nil && strings.TrimSpace(extra) != "" {
|
||||
for _, p := range strings.Split(extra, ",") {
|
||||
if p = strings.TrimSpace(p); p != "" {
|
||||
authWhitelist = append(authWhitelist, p)
|
||||
}
|
||||
}
|
||||
}
|
||||
log.Printf("[auth] JWT 全局鉴权模式=%s,受保护前缀=%v", modeVal, protectedPrefixes)
|
||||
})
|
||||
return modeVal
|
||||
}
|
||||
|
||||
// cleanPath 去掉 query / fragment 后返回纯路径
|
||||
func cleanPath(uri string) string {
|
||||
if i := strings.IndexAny(uri, "?#"); i >= 0 {
|
||||
return uri[:i]
|
||||
}
|
||||
return uri
|
||||
}
|
||||
|
||||
func isProtected(path string) bool {
|
||||
for _, p := range protectedPrefixes {
|
||||
if strings.HasPrefix(path, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
func isWhitelisted(path string) bool {
|
||||
for _, p := range authWhitelist {
|
||||
if strings.HasPrefix(path, p) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// warnLog 观察模式下记录一次「本会被拦截」的请求(按路径去重)
|
||||
func warnLog(path, reason string) {
|
||||
if _, loaded := warnSeen.LoadOrStore(path, struct{}{}); loaded {
|
||||
return
|
||||
}
|
||||
log.Printf("[auth][warn] 该接口在 enforce=on 时会被拦截: %s (%s)", path, reason)
|
||||
}
|
||||
|
||||
// JWTAuthMiddleware JWT认证中间件
|
||||
func JWTAuthMiddleware() beego.FilterFunc {
|
||||
return func(ctx *context.Context) {
|
||||
mode := authMode()
|
||||
if mode == AuthModeOff {
|
||||
return
|
||||
}
|
||||
// 预检请求直接放行
|
||||
if ctx.Input.Method() == http.MethodOptions {
|
||||
return
|
||||
}
|
||||
|
||||
path := cleanPath(ctx.Request.RequestURI)
|
||||
if !isProtected(path) || isWhitelisted(path) {
|
||||
return
|
||||
}
|
||||
|
||||
// 失败处理:观察模式只记日志,强制模式才真正拦截
|
||||
reject := func(status int, message string) {
|
||||
if mode == AuthModeWarn {
|
||||
warnLog(path, message)
|
||||
return
|
||||
}
|
||||
ctx.Output.SetStatus(status)
|
||||
ctx.Output.JSON(map[string]interface{}{
|
||||
"success": false,
|
||||
"message": message,
|
||||
}, false, false)
|
||||
}
|
||||
|
||||
// 从请求头中获取Authorization
|
||||
authHeader := ctx.Request.Header.Get("Authorization")
|
||||
if authHeader == "" {
|
||||
reject(401, "未提供认证信息")
|
||||
return
|
||||
}
|
||||
|
||||
// 按空格分割
|
||||
authParts := strings.SplitN(authHeader, " ", 2)
|
||||
if !(len(authParts) == 2 && authParts[0] == "Bearer") {
|
||||
reject(401, "认证信息格式错误")
|
||||
return
|
||||
}
|
||||
|
||||
// 解析token
|
||||
claims, err := jwtutil.ParseToken(authParts[1])
|
||||
if err != nil {
|
||||
message := "无效的token"
|
||||
if strings.Contains(err.Error(), "expired") {
|
||||
message = "token已过期"
|
||||
}
|
||||
reject(401, message)
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存储在上下文
|
||||
ctx.Input.SetData("userId", claims.UserID)
|
||||
ctx.Input.SetData("username", claims.Username)
|
||||
ctx.Input.SetData("tenantId", claims.TenantId)
|
||||
|
||||
// 从token中获取用户类型(如果token中没有,则默认为"user")
|
||||
userType := claims.UserType
|
||||
if userType == "" {
|
||||
userType = "user"
|
||||
}
|
||||
ctx.Input.SetData("userType", userType)
|
||||
|
||||
// 认证中心扩展字段(旧 token 为空值,不影响现有逻辑)
|
||||
ctx.Input.SetData("clientId", claims.ClientID)
|
||||
ctx.Input.SetData("sid", claims.Sid)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user