package auth import ( "encoding/json" "errors" "net/http" "strings" "time" "github.com/golang-jwt/jwt/v4" ) // Claims 自定义 JWT 声明 type Claims struct { UserID int64 `json:"userId"` Username string `json:"username"` RealName string `json:"realName"` Role string `json:"role"` jwt.RegisteredClaims } // TokenInfo 签发 token 所需的用户信息 type TokenInfo struct { UserID int64 Username string RealName string Role string } const issuer = "bj_power_workstation" func defaultExpire(expire int64) int64 { if expire <= 0 { return 1800 } return expire } // SignToken 签发 JWT,返回 (token, 过期秒级时间戳, err) func SignToken(secret string, expire int64, info TokenInfo) (string, int64, error) { exp := time.Now().Add(time.Duration(defaultExpire(expire)) * time.Second) claims := &Claims{ UserID: info.UserID, Username: info.Username, RealName: info.RealName, Role: info.Role, RegisteredClaims: jwt.RegisteredClaims{ ExpiresAt: jwt.NewNumericDate(exp), IssuedAt: jwt.NewNumericDate(time.Now()), Issuer: issuer, }, } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signed, err := token.SignedString([]byte(secret)) return signed, exp.Unix(), err } // ParseToken 校验并解析 JWT func ParseToken(secret, tokenStr string) (*Claims, error) { token, err := jwt.ParseWithClaims(tokenStr, &Claims{}, func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, errors.New("非法签名算法") } return []byte(secret), nil }) if err != nil { return nil, err } claims, ok := token.Claims.(*Claims) if !ok || !token.Valid { return nil, errors.New("token 无效") } return claims, nil } // renewIfNearExpiry 滑动续签:剩余有效期不足一半时重签,写入响应头 X-Renewed-Token func renewIfNearExpiry(secret string, expire int64, w http.ResponseWriter, claims *Claims) { remaining := time.Until(claims.ExpiresAt.Time) if remaining > 0 && remaining < time.Duration(defaultExpire(expire))*time.Second/2 { if newToken, _, err := SignToken(secret, expire, TokenInfo{ UserID: claims.UserID, Username: claims.Username, RealName: claims.RealName, Role: claims.Role, }); err == nil { w.Header().Set("X-Renewed-Token", newToken) } } } // unauthorized 与业务侧 {code,message,data} 封装保持一致 func unauthorized(w http.ResponseWriter, msg string) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(http.StatusUnauthorized) _ = json.NewEncoder(w).Encode(map[string]any{"code": 401, "message": msg}) } // Interceptor 登录鉴权中间件。 // 白名单:/api/health、/api/auth/login、/pdfopen/*。 // 非 /api/ 且非 /pdfopen/ 的路径为前端静态资源,直接放行。 func Interceptor(secret string, accessExpire int64) 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 notAPI := !strings.HasPrefix(path, "/api/") && !strings.HasPrefix(path, "/pdfopen/") whitelisted := path == "/api/health" || path == "/api/auth/login" || path == "/api/config" || strings.HasPrefix(path, "/pdfopen/") if notAPI || whitelisted { next.ServeHTTP(w, r) return } authHeader := r.Header.Get("Authorization") if authHeader == "" || !strings.HasPrefix(authHeader, "Bearer ") { unauthorized(w, "未登录或 token 缺失") return } tokenStr := strings.TrimPrefix(authHeader, "Bearer ") claims, err := ParseToken(secret, tokenStr) if err != nil { unauthorized(w, "登录已失效") return } renewIfNearExpiry(secret, accessExpire, w, claims) r.Header.Set("X-Username", claims.Username) next.ServeHTTP(w, r) }) } }