c22669c31d
- 新增迁移 000031-000034(登录日志/角色/模型配额/记忆) - 新增包: license/memory/modelquota/assistant,扫描引擎 - 全部功能后端+前端+端到端验证通过(25 包单测)
259 lines
6.7 KiB
Go
259 lines
6.7 KiB
Go
// Package license 实现平台 License 授权校验与账号数管控。
|
|
//
|
|
// License 是一个 JSON 文件(路径由 LICENSE_FILE 指定),内容为声明字段 +
|
|
// HMAC-SHA256 签名(密钥由 CREDENTIAL_MASTER_KEY 派生)。未配置 LICENSE_FILE
|
|
// 时按社区版 Free(3 账号)处理;管理员可通过管理端上传 License 热更新。
|
|
package license
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/json"
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"strings"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// Edition 是版本枚举;功能矩阵以 features 列表为准,edition 只做展示与
|
|
// 默认能力集合。
|
|
const (
|
|
EditionFree = "free"
|
|
EditionPro = "pro"
|
|
EditionUltra = "ultra"
|
|
DefaultAccountLimit = 3 // 未配置 License 时按社区版 Free
|
|
)
|
|
|
|
var ErrInvalidLicense = errors.New("license 文件无效或签名不匹配")
|
|
var ErrLicenseExpired = errors.New("license 已过期")
|
|
|
|
// Claims 是 License 的声明部分(不含签名)。
|
|
type Claims struct {
|
|
Edition string `json:"edition"`
|
|
IssuedTo string `json:"issued_to"`
|
|
MaxAccounts int `json:"max_accounts"` // 0 = 不限
|
|
Features []string `json:"features"` // 额外授权特性名(预留)
|
|
NotBefore string `json:"not_before"` // RFC3339
|
|
NotAfter string `json:"not_after"` // RFC3339,空 = 永久
|
|
}
|
|
|
|
// License 是完整的 License 文件内容。
|
|
type License struct {
|
|
Claims
|
|
Signature string `json:"signature"` // base64(HMAC-SHA256(claimsCanonicalJSON, key))
|
|
}
|
|
|
|
// Manager 持有当前 License 状态,支持热更新。
|
|
type Manager struct {
|
|
mu sync.RWMutex
|
|
filePath string
|
|
master []byte
|
|
current License
|
|
}
|
|
|
|
// NewManager 创建 License 管理器并从 filePath 加载(路径为空表示 Free 版)。
|
|
func NewManager(filePath, masterKey string) (*Manager, error) {
|
|
m := &Manager{filePath: filePath, master: deriveKey(masterKey)}
|
|
if strings.TrimSpace(filePath) == "" {
|
|
return m, nil // Free 版,无需文件
|
|
}
|
|
if err := m.Reload(); err != nil {
|
|
return nil, err
|
|
}
|
|
return m, nil
|
|
}
|
|
|
|
// deriveKey 从 master key 派生 License 签名密钥(独立用途域,不与凭据加密混用)。
|
|
func deriveKey(masterKey string) []byte {
|
|
sum := sha256.Sum256([]byte("aigateway-license-v1\x00" + masterKey))
|
|
return sum[:]
|
|
}
|
|
|
|
// Reload 重新读取并校验 License 文件。
|
|
func (m *Manager) Reload() error {
|
|
if m == nil {
|
|
return nil
|
|
}
|
|
raw, err := os.ReadFile(m.filePath)
|
|
if err != nil {
|
|
if errors.Is(err, os.ErrNotExist) {
|
|
return ErrInvalidLicense
|
|
}
|
|
return err
|
|
}
|
|
lic, err := Parse(raw, m.master)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
m.mu.Lock()
|
|
m.current = lic
|
|
m.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// Parse 解析并校验 License 内容(签名 + 有效期)。
|
|
func Parse(raw []byte, key []byte) (License, error) {
|
|
var lic License
|
|
if err := json.Unmarshal(raw, &lic); err != nil {
|
|
return License{}, ErrInvalidLicense
|
|
}
|
|
claims, err := json.Marshal(lic.Claims)
|
|
if err != nil {
|
|
return License{}, ErrInvalidLicense
|
|
}
|
|
mac := hmac.New(sha256.New, key)
|
|
_, _ = mac.Write(claims)
|
|
expected := base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|
if !hmac.Equal([]byte(expected), []byte(strings.TrimSpace(lic.Signature))) {
|
|
return License{}, ErrInvalidLicense
|
|
}
|
|
now := time.Now()
|
|
if lic.NotBefore != "" {
|
|
if start, err := time.Parse(time.RFC3339, lic.NotBefore); err == nil && now.Before(start) {
|
|
return License{}, ErrInvalidLicense
|
|
}
|
|
}
|
|
if lic.NotAfter != "" {
|
|
if end, err := time.Parse(time.RFC3339, lic.NotAfter); err == nil && now.After(end) {
|
|
return License{}, ErrLicenseExpired
|
|
}
|
|
}
|
|
edition := strings.ToLower(strings.TrimSpace(lic.Edition))
|
|
switch edition {
|
|
case EditionFree, EditionPro, EditionUltra, "":
|
|
default:
|
|
return License{}, ErrInvalidLicense
|
|
}
|
|
if lic.Edition == "" {
|
|
lic.Edition = EditionFree
|
|
}
|
|
return lic, nil
|
|
}
|
|
|
|
// Sign 生成 License(供本地签发工具/测试使用)。
|
|
func Sign(claims Claims, masterKey string) (License, error) {
|
|
claims.Edition = strings.ToLower(strings.TrimSpace(claims.Edition))
|
|
claims.Features = normalize(claims.Features)
|
|
raw, err := json.Marshal(claims)
|
|
if err != nil {
|
|
return License{}, err
|
|
}
|
|
mac := hmac.New(sha256.New, deriveKey(masterKey))
|
|
_, _ = mac.Write(raw)
|
|
return License{Claims: claims, Signature: base64.StdEncoding.EncodeToString(mac.Sum(nil))}, nil
|
|
}
|
|
|
|
func normalize(values []string) []string {
|
|
seen := map[string]bool{}
|
|
out := make([]string, 0, len(values))
|
|
for _, v := range values {
|
|
v = strings.ToLower(strings.TrimSpace(v))
|
|
if v != "" && !seen[v] {
|
|
seen[v] = true
|
|
out = append(out, v)
|
|
}
|
|
}
|
|
return out
|
|
}
|
|
|
|
// Current 返回当前 License 快照。
|
|
func (m *Manager) Current() License {
|
|
if m == nil {
|
|
return License{Claims: Claims{Edition: EditionFree}}
|
|
}
|
|
m.mu.RLock()
|
|
defer m.mu.RUnlock()
|
|
return m.current
|
|
}
|
|
|
|
// AccountLimit 返回账号数上限;0 表示不限。
|
|
func (m *Manager) AccountLimit() int {
|
|
lic := m.Current()
|
|
// 未配置 License 时 Edition 为空,同样按社区版 Free(3 账号)处理。
|
|
if lic.Edition == "" || lic.Edition == EditionFree {
|
|
if lic.MaxAccounts > 0 {
|
|
return lic.MaxAccounts
|
|
}
|
|
return DefaultAccountLimit
|
|
}
|
|
return lic.MaxAccounts // pro/ultra 由 License 指定;0=不限
|
|
}
|
|
|
|
// EditionName 返回可读版本名。
|
|
func (m *Manager) EditionName() string {
|
|
switch m.Current().Edition {
|
|
case EditionPro:
|
|
return "专业版 Pro"
|
|
case EditionUltra:
|
|
return "旗舰版 Ultra"
|
|
default:
|
|
return "社区版 Free"
|
|
}
|
|
}
|
|
|
|
// FilePath 返回 License 文件路径。
|
|
func (m *Manager) FilePath() string {
|
|
if m == nil {
|
|
return ""
|
|
}
|
|
return m.filePath
|
|
}
|
|
|
|
// Load 原子替换 License 文件并热更新(管理端上传)。
|
|
func (m *Manager) Load(raw []byte) error {
|
|
if m == nil || m.filePath == "" {
|
|
return errors.New("未配置 LICENSE_FILE,无法保存 License")
|
|
}
|
|
lic, err := Parse(raw, m.master)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
tmp := m.filePath + ".tmp"
|
|
if err := os.WriteFile(tmp, raw, 0o600); err != nil {
|
|
return err
|
|
}
|
|
if err := os.Rename(tmp, m.filePath); err != nil {
|
|
return err
|
|
}
|
|
m.mu.Lock()
|
|
m.current = lic
|
|
m.mu.Unlock()
|
|
return nil
|
|
}
|
|
|
|
// Summary 返回给管理端展示的信息。
|
|
func (m *Manager) Summary() map[string]any {
|
|
lic := m.Current()
|
|
expires := lic.NotAfter
|
|
if expires == "" {
|
|
expires = "永久"
|
|
}
|
|
return map[string]any{
|
|
"edition": lic.Edition,
|
|
"edition_name": m.EditionName(),
|
|
"issued_to": lic.IssuedTo,
|
|
"max_accounts": m.AccountLimit(),
|
|
"features": lic.Features,
|
|
"expires": expires,
|
|
"file": m.FilePath(),
|
|
"licensed": m.FilePath() != "",
|
|
}
|
|
}
|
|
|
|
// 便捷格式化错误信息。
|
|
func FormatError(err error) string {
|
|
switch {
|
|
case errors.Is(err, ErrInvalidLicense):
|
|
return "License 无效或签名不匹配"
|
|
case errors.Is(err, ErrLicenseExpired):
|
|
return "License 已过期"
|
|
case err == nil:
|
|
return ""
|
|
default:
|
|
return fmt.Sprintf("License 加载失败: %v", err)
|
|
}
|
|
}
|