Files
bj_power/bj_power_workstation/internal/auth/jwt.go
T
SunYF a2afd2f6dc feat: 五项目业务实现并对接完成
- MES(B): 工单/BOM/备料/工序字典/PLC下发/拧紧/扫码报工/半成品/AGV/追溯逻辑,WMS与海康RCS客户端,看板Redis缓存API(概览/设备/进度/报警/趋势)+SSE
- WMS(C): JWT滑动续签、Excel导入、盘点、内部API、种子数据、独立Postgres配置
- WMS客户端(E): Go网关8891反向代理+内嵌Vue3十页
- 工位终端(D): SQLite本地缓存+模拟拧紧源+内嵌Vue3页面
- Dashboard(A): 看板数据改接MES内部缓存API,SSE实时刷新+vite代理
- 清理各项目球形磨遗留代码,新增部署手册.md
2026-08-27 19:50:57 +08:00

137 lines
4.0 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 auth
import (
"encoding/json"
"errors"
"net/http"
"strings"
"time"
"github.com/golang-jwt/jwt/v4"
)
// Claims 自定义 JWT 声明(结构仿 bj_power_wms/internal/handler/jwt.go
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, okk := token.Claims.(*Claims)
if !okk || !token.Valid {
return nil, errors.New("token 无效")
}
return claims, nil
}
// renewIfNearExpiry 滑动续签:剩余有效期不足一半时重签新 token,
// 写入响应头 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} 封装保持一致的 401 响应。
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/<name> 工艺 PDF 免鉴权直链(供 iframe/window.open 引用)
//
// 非 /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" ||
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)
})
}
}