package handler import ( "errors" "net/http" "strconv" "strings" "time" "bj_power_wms/internal/svc" "github.com/golang-jwt/jwt/v4" ) // JWTClaims 自定义声明 type JWTClaims struct { UserID int `json:"userId"` Username string `json:"username"` RealName string `json:"realName"` Role string `json:"role"` jwt.RegisteredClaims } // sessionExpire 会话闲置窗口(秒),默认 1 小时 func sessionExpire(ctx *svc.ServiceContext) int64 { if ctx.Config.Auth.AccessExpire > 0 { return ctx.Config.Auth.AccessExpire } return 3600 } // SignToken 签发 JWT func SignToken(ctx *svc.ServiceContext, userID int, username, realName, role string) (string, int64, error) { expire := sessionExpire(ctx) exp := time.Now().Add(time.Duration(expire) * time.Second) claims := &JWTClaims{ UserID: userID, Username: username, RealName: realName, Role: role, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(exp), IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: "bj_power_wms", }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signed, err := token.SignedString([]byte(ctx.Config.Auth.AccessSecret)) return signed, exp.Unix(), err } // ParseToken 校验并解析 JWT func ParseToken(ctx *svc.ServiceContext, tokenStr string) (*JWTClaims, error) { token, err := jwt.ParseWithClaims(tokenStr, &JWTClaims{}, func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New("非法签名算法") } return []byte(ctx.Config.Auth.AccessSecret), nil }) if err != nil { return nil, err } claims, okk := token.Claims.(*JWTClaims) if !okk || !token.Valid { return nil, errors.New("token 无效") } return claims, nil } // renewIfNearExpiry 滑动续签:每个携带有效 token 的请求都重签, // 会话有效期 = 距最后一次请求 sessionExpire 秒(默认 1 小时),闲置超时即 401。 func renewIfNearExpiry(ctx *svc.ServiceContext, w http.ResponseWriter, claims *JWTClaims) { if time.Until(claims.ExpiresAt.Time) > 0 { if newToken, _, err := SignToken(ctx, claims.UserID, claims.Username, claims.RealName, claims.Role); err == nil { w.Header().Set("X-Renewed-Token", newToken) } } } // authInterceptor 登录鉴权中间件(白名单之外的 /api/* 需要 JWT) // go-zero rest.Middleware 签名:func(next http.HandlerFunc) http.HandlerFunc func authInterceptor(ctx *svc.ServiceContext) func(next http.HandlerFunc) http.HandlerFunc { return func(next http.HandlerFunc) http.HandlerFunc { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { path := r.URL.Path // 白名单:健康检查 / 登录注册 / 免登录大屏 / 内部API(自带写死token校验) if path == "/api/health" || strings.HasPrefix(path, "/api/auth/") || strings.HasPrefix(path, "/api/display/") || strings.HasPrefix(path, "/api/internal/") { next.ServeHTTP(w, r) return } authHeader := r.Header.Get("Authorization") if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { failCode(w, http.StatusUnauthorized, 401, "未登录或 token 缺失") return } tokenStr := strings.TrimPrefix(authHeader, "Bearer ") claims, err := ParseToken(ctx, tokenStr) if err != nil { failCode(w, http.StatusUnauthorized, 401, "登录已失效,请重新登录") return } renewIfNearExpiry(ctx, w, claims) r.Header.Set("X-Username", claims.Username) r.Header.Set("X-Role", claims.Role) r.Header.Set("X-UserID", strconv.Itoa(claims.UserID)) next.ServeHTTP(w, r) }) } } // internalTokenInterceptor 项目间 API 写死 token 校验 func internalTokenInterceptor(ctx *svc.ServiceContext) func(next http.Handler) http.Handler { return func(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { expect := ctx.Config.Internal.Token got := r.Header.Get("X-API-TOKEN") if expect != "" && got != expect { failCode(w, http.StatusUnauthorized, 401, "内部接口 token 错误") return } next.ServeHTTP(w, r) }) } } // wrapInternal 组合内部 token 校验与 handler func wrapInternal(ctx *svc.ServiceContext) func(h http.HandlerFunc) http.HandlerFunc { return func(h http.HandlerFunc) http.HandlerFunc { return internalTokenInterceptor(ctx)(h).ServeHTTP } }