Files
ai-gateway-go/internal/platform/license/license.go
T
superidou c22669c31d 0.11.0: 旗舰版功能补齐(License/登录记录/会话管理/角色管理/门户定时任务/模型配额/输出脱敏/供应链扫描/记忆管理/AI助手/真实概览)
- 新增迁移 000031-000034(登录日志/角色/模型配额/记忆)
- 新增包: license/memory/modelquota/assistant,扫描引擎
- 全部功能后端+前端+端到端验证通过(25 包单测)
2026-08-13 11:37:18 +08:00

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)
}
}