// jwt.go agent 登录令牌的 JWT 校验与签发(HS256)。 // 正式部署由签发方(如 cloud)生成 token 下发给各隧道;节点只做校验。 // SignToken 供 cmd/node -print-token(运维手工签发)与测试使用。 package node import ( "errors" "fmt" "time" "github.com/golang-jwt/jwt/v5" ) // verifyToken 校验 agent 登录 JWT: // - HS256/384/512 签名(显式白名单,拒绝 none 与其它算法) // - 必须带 exp 且未过期 // - 若带 agentId 或 sub claim,则必须与登录 AgentId 一致(防跨 agent 冒用) func verifyToken(token, secret, agentId string) error { if secret == "" { return errors.New("节点未配置JWT密钥") } parsed, err := jwt.Parse(token, func(t *jwt.Token) (any, error) { if _, ok := t.Method.(*jwt.SigningMethodHMAC); !ok { return nil, fmt.Errorf("签名算法不允许:%v", t.Header["alg"]) } return []byte(secret), nil }, jwt.WithValidMethods([]string{ jwt.SigningMethodHS256.Alg(), jwt.SigningMethodHS384.Alg(), jwt.SigningMethodHS512.Alg(), }), jwt.WithExpirationRequired()) if err != nil { return err } claims, ok := parsed.Claims.(jwt.MapClaims) if !ok { return errors.New("claims类型错误") } for _, k := range []string{"agentId", "sub"} { if v, ok := claims[k].(string); ok && v != "" && v != agentId { return fmt.Errorf("token %s 与登录agentId不一致", k) } } return nil } // SignToken 用节点密钥签发一张 agent 登录 JWT。 // ttl<=0 会得到立即过期的 token(仅测试构造过期样例时有意义)。 func SignToken(secret, agentId string, ttl time.Duration) (string, error) { if secret == "" { return "", errors.New("未配置JWT密钥") } now := time.Now() claims := jwt.MapClaims{ "agentId": agentId, "iat": now.Unix(), "exp": now.Add(ttl).Unix(), } return jwt.NewWithClaims(jwt.SigningMethodHS256, claims).SignedString([]byte(secret)) }