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
This commit is contained in:
@@ -0,0 +1,136 @@
|
||||
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)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user