341 lines
11 KiB
Markdown
341 lines
11 KiB
Markdown
|
|
# Ops Licence 许可证管理系统
|
|||
|
|
|
|||
|
|
Ops Licence 是供内部人员使用的离线许可证签发管理系统。系统通过“平台名称 + Workspace”维护授权对象,支持许可证签发、签发记录查询、续期和 `licence.key` 下载,并提供可复制到其他 Go 项目的本地验证 SDK。
|
|||
|
|
|
|||
|
|
系统不依赖目标平台调用在线接口。许可证由内部人员在管理页面生成后,手动交付给使用方;使用方通过 Go SDK 在本地完成真实性、授权对象、有效期和配额验证。
|
|||
|
|
|
|||
|
|
## 主要功能
|
|||
|
|
|
|||
|
|
- 授权对象:以“平台名称 + Workspace”作为唯一授权标识,区分大小写。
|
|||
|
|
- 直接签发:设置生效日期、到期日期和全部配额,生成并下载许可证文件。
|
|||
|
|
- 续期:从原签发记录发起,生成一份新的许可证文件,原记录和原文件保持不变。
|
|||
|
|
- 签发记录:查看签发类型、授权对象、有效期、配额、签发时间和前一份许可证。
|
|||
|
|
- 再次下载:从历史签发记录重新下载当时生成的许可证文件。
|
|||
|
|
- 本地验证:Go SDK 不访问许可证管理系统,也不绑定具体机器。
|
|||
|
|
- 简约 Web 管理界面:前端已经嵌入 Go 可执行文件,无需单独部署静态站点。
|
|||
|
|
|
|||
|
|
当前系统不包含登录、会话、在线激活、许可证吊销和机器绑定功能。
|
|||
|
|
|
|||
|
|
## 业务规则
|
|||
|
|
|
|||
|
|
### 授权对象
|
|||
|
|
|
|||
|
|
平台名称与 Workspace 共同组成一个授权对象,两者都不能为空。同一组合不能重复,并且按原始大小写精确匹配。
|
|||
|
|
|
|||
|
|
- 没有签发记录时:允许修改、删除。
|
|||
|
|
- 存在任意签发记录后:禁止修改、删除。
|
|||
|
|
|
|||
|
|
### 日期
|
|||
|
|
|
|||
|
|
生效日期和到期日期均采用 `YYYY-MM-DD`,按照 `Asia/Shanghai` 自然日判断,首尾日期都包含在有效期内。到期日期不能早于生效日期。
|
|||
|
|
|
|||
|
|
### 配额
|
|||
|
|
|
|||
|
|
每次直接签发或续期都会保存当次全部配额的完整快照。这里的“全部配额快照”是指:签发记录会固定保存下面 11 个配额在当次签发时的数值,以后不会跟随其他记录变化。续期页面会预填上一份许可证的配额,但提交后会生成新的独立记录。
|
|||
|
|
|
|||
|
|
所有配额都是非负整数,`0` 表示该项未授权:
|
|||
|
|
|
|||
|
|
| 字段 | 含义 |
|
|||
|
|
| --- | --- |
|
|||
|
|
| `max_database` | 数据库数量上限 |
|
|||
|
|
| `max_middleware` | 中间件数量上限 |
|
|||
|
|
| `max_network_device` | 网络设备数量上限 |
|
|||
|
|
| `max_security` | 安全设备数量上限 |
|
|||
|
|
| `max_storage` | 存储设备数量上限 |
|
|||
|
|
| `max_pc` | PC 数量上限 |
|
|||
|
|
| `max_server` | 服务器数量上限 |
|
|||
|
|
| `max_user` | 用户数量上限 |
|
|||
|
|
| `max_role` | 角色数量上限 |
|
|||
|
|
| `max_permission` | 权限数量上限 |
|
|||
|
|
| `max_menu` | 菜单数量上限 |
|
|||
|
|
|
|||
|
|
## 快速启动
|
|||
|
|
|
|||
|
|
### 1. 准备 PostgreSQL
|
|||
|
|
|
|||
|
|
数据库名称必须精确为 `ops_licence`。服务首次启动时会自动执行内嵌迁移,创建本项目自己的数据表:
|
|||
|
|
|
|||
|
|
- `schema_migrations`
|
|||
|
|
- `authorization_subjects`
|
|||
|
|
- `signing_keys`
|
|||
|
|
- `licence_issuances`
|
|||
|
|
|
|||
|
|
不需要手工建表,也不会使用其他项目的业务表。
|
|||
|
|
|
|||
|
|
### 2. 配置 YAML
|
|||
|
|
|
|||
|
|
本地配置文件为 `D:\work\license\etc\license.yaml`。配置只包含服务监听地址、数据库 DSN 和当前签发密钥文件:
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
server:
|
|||
|
|
listen_addr: "127.0.0.1:8080"
|
|||
|
|
|
|||
|
|
database:
|
|||
|
|
dsn: "postgresql://username:password@127.0.0.1:5432/ops_licence?sslmode=disable"
|
|||
|
|
|
|||
|
|
signing:
|
|||
|
|
private_key_file: "D:\\work\\license\\secrets\\signing-private.pem"
|
|||
|
|
certificate_file: "D:\\work\\license\\secrets\\signing-certificate.json"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
注意:
|
|||
|
|
|
|||
|
|
- `listen_addr` 必须使用回环 IP,例如 `127.0.0.1:8080`。
|
|||
|
|
- DSN 必须显式包含用户名、密码、单一主机、端口、数据库名和 `sslmode`。
|
|||
|
|
- 支持的 `sslmode` 为 `disable`、`require`、`verify-ca`、`verify-full`。
|
|||
|
|
- 数据库用户名或密码包含特殊字符时,需要使用 URL 百分号编码。
|
|||
|
|
- DSN 不要添加 `TimeZone` 等额外查询参数。
|
|||
|
|
- 服务只读取 YAML,不需要配置数据库环境变量;运行用户已有的 `PG*` 数据库连接环境变量需要清除。
|
|||
|
|
- 签发私钥和签发密钥凭证必须使用绝对路径,并且必须相互匹配。
|
|||
|
|
|
|||
|
|
### 3. 启动服务
|
|||
|
|
|
|||
|
|
在项目目录执行:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
.\bin\ops-licence.exe serve --config D:\work\license\etc\license.yaml
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
启动成功后访问:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
http://127.0.0.1:8080
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 页面操作
|
|||
|
|
|
|||
|
|
### 新建授权对象
|
|||
|
|
|
|||
|
|
1. 打开“授权对象”。
|
|||
|
|
2. 点击“新建授权对象”。
|
|||
|
|
3. 填写平台名称和 Workspace 后保存。
|
|||
|
|
|
|||
|
|
如果填写错误,只要该对象还没有签发记录,就可以从详情页修改或删除。
|
|||
|
|
|
|||
|
|
### 直接签发
|
|||
|
|
|
|||
|
|
1. 打开授权对象详情。
|
|||
|
|
2. 点击“直接签发”。
|
|||
|
|
3. 选择生效日期、到期日期并填写全部配额。
|
|||
|
|
4. 点击“签发并下载”。
|
|||
|
|
5. 将下载得到的 `{许可证ID}.licence.key` 文件手动交付给使用方。
|
|||
|
|
|
|||
|
|
签发成功后记录不可修改、不可删除。
|
|||
|
|
|
|||
|
|
### 续期
|
|||
|
|
|
|||
|
|
1. 打开“签发记录”。
|
|||
|
|
2. 进入需要续期的记录详情。
|
|||
|
|
3. 点击“续期”。
|
|||
|
|
4. 确认或调整日期和配额。
|
|||
|
|
5. 提交后下载新生成的 `{许可证ID}.licence.key`。
|
|||
|
|
|
|||
|
|
续期不会覆盖旧记录,也不会让旧文件失效;新记录会保存其来源许可证 ID。
|
|||
|
|
|
|||
|
|
## 密钥管理
|
|||
|
|
|
|||
|
|
许可证采用两级 Ed25519 信任链:
|
|||
|
|
|
|||
|
|
1. 根私钥签发“签发密钥凭证”。
|
|||
|
|
2. 签发私钥签发具体的许可证。
|
|||
|
|
3. SDK 内置根公钥,先验证签发密钥凭证,再验证许可证签名。
|
|||
|
|
|
|||
|
|
根私钥不需要部署到许可证管理服务器。服务器运行时只需要当前签发私钥和对应的签发密钥凭证。
|
|||
|
|
|
|||
|
|
### 更换签发密钥
|
|||
|
|
|
|||
|
|
使用现有根私钥签发一组新的签发密钥和凭证:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
.\bin\ops-licence.exe keys issue `
|
|||
|
|
--root-private D:\keys\root-private.pem `
|
|||
|
|
--private-out D:\keys\signing-private-2026.pem `
|
|||
|
|
--certificate-out D:\keys\signing-certificate-2026.json
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
输出文件不能预先存在。生成后修改 YAML 中的两个 `signing` 文件路径并重启服务。
|
|||
|
|
|
|||
|
|
更换签发密钥不需要重新生成或复制 SDK,因为新签发密钥仍由同一个根私钥签发。只有主动更换根信任时,才需要更新 SDK 内置根公钥并重新复制 SDK。
|
|||
|
|
|
|||
|
|
### 重新建立根信任
|
|||
|
|
|
|||
|
|
当前项目已经包含根公钥。下面的命令只用于明确决定更换整套根信任时,不属于日常签发密钥轮换:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
go run .\cmd\root-keygen `
|
|||
|
|
--private-out D:\keys\new-root-private.pem `
|
|||
|
|
--sdk-public-out D:\keys\root_public_key.go
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
生成后需要用新的 `root_public_key.go` 替换 SDK 中的根公钥文件,再用新根私钥签发新的签发密钥。更换根信任后必须重新分发 SDK。
|
|||
|
|
|
|||
|
|
## Linux 部署
|
|||
|
|
|
|||
|
|
项目提供了 Linux systemd 和 Nginx 配置模板:
|
|||
|
|
|
|||
|
|
- `deploy/ops-licence.service`
|
|||
|
|
- `deploy/nginx.conf`
|
|||
|
|
|
|||
|
|
### 文件位置
|
|||
|
|
|
|||
|
|
建议按模板放置:
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
/usr/local/bin/ops-licence
|
|||
|
|
/etc/ops-licence/ops-licence.yaml
|
|||
|
|
/etc/ops-licence/keys/signing-private.pem
|
|||
|
|
/etc/ops-licence/keys/signing-certificate.json
|
|||
|
|
/var/lib/ops-licence
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Linux YAML 示例:
|
|||
|
|
|
|||
|
|
```yaml
|
|||
|
|
server:
|
|||
|
|
listen_addr: "127.0.0.1:8080"
|
|||
|
|
|
|||
|
|
database:
|
|||
|
|
dsn: "postgresql://username:password@database-host:5432/ops_licence?sslmode=disable"
|
|||
|
|
|
|||
|
|
signing:
|
|||
|
|
private_key_file: "/etc/ops-licence/keys/signing-private.pem"
|
|||
|
|
certificate_file: "/etc/ops-licence/keys/signing-certificate.json"
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### systemd
|
|||
|
|
|
|||
|
|
创建与模板一致的 `ops-licence` 系统用户和目录后,安装并启动服务:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
sudo install -m 0755 dist/ops-licence /usr/local/bin/ops-licence
|
|||
|
|
sudo install -m 0644 deploy/ops-licence.service /etc/systemd/system/ops-licence.service
|
|||
|
|
sudo systemctl daemon-reload
|
|||
|
|
sudo systemctl enable --now ops-licence
|
|||
|
|
sudo systemctl status ops-licence
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
查看运行日志:
|
|||
|
|
|
|||
|
|
```bash
|
|||
|
|
sudo journalctl -u ops-licence -f
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
### Nginx
|
|||
|
|
|
|||
|
|
`deploy/nginx.conf` 将 HTTPS 请求反向代理到 `127.0.0.1:8080`。部署时将其中证书路径替换为实际路径,并把配置放入 Nginx 的站点配置目录后重新加载 Nginx。
|
|||
|
|
|
|||
|
|
Go 可执行文件已经包含前端资源,Nginx 只需要代理所有路径,不需要另外复制 `web/dist`。
|
|||
|
|
|
|||
|
|
## Go SDK 接入
|
|||
|
|
|
|||
|
|
将整个 `sdk/licence` 目录复制到目标 Go 项目中,保持目录内所有文件完整。目标项目不需要连接许可证管理系统。
|
|||
|
|
|
|||
|
|
使用时传入许可证文件路径,以及目标程序实际使用的平台名称和 Workspace:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
result, err := licence.VerifyFile(
|
|||
|
|
"/etc/example/licence.key",
|
|||
|
|
licence.Subject{
|
|||
|
|
PlatformName: "平台名称",
|
|||
|
|
Workspace: "workspace",
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
if err != nil {
|
|||
|
|
return err
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
databaseLimit := result.Quotas.MaxDatabase
|
|||
|
|
expiresOn := result.ExpiresOn
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
`VerifyFile` 会一次完成:
|
|||
|
|
|
|||
|
|
- 文件格式和大小检查。
|
|||
|
|
- 根公钥到签发密钥的凭证验证。
|
|||
|
|
- 许可证签名验证。
|
|||
|
|
- 平台名称与 Workspace 精确匹配。
|
|||
|
|
- 按 `Asia/Shanghai` 日期检查是否生效、是否到期。
|
|||
|
|
- 所有配额非负检查。
|
|||
|
|
|
|||
|
|
可以通过 `errors.Is` 区分主要验证结果:
|
|||
|
|
|
|||
|
|
```go
|
|||
|
|
switch {
|
|||
|
|
case errors.Is(err, licence.ErrNotYetValid):
|
|||
|
|
// 许可证尚未生效
|
|||
|
|
case errors.Is(err, licence.ErrExpired):
|
|||
|
|
// 许可证已经到期
|
|||
|
|
case errors.Is(err, licence.ErrSubjectMismatch):
|
|||
|
|
// 平台名称或 Workspace 不匹配
|
|||
|
|
case errors.Is(err, licence.ErrInvalidSignature):
|
|||
|
|
// 许可证签名无效
|
|||
|
|
case err != nil:
|
|||
|
|
// 文件格式、签发密钥凭证或配额无效
|
|||
|
|
}
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
复制后,SDK 的 Go 导入路径以目标项目的模块路径为准。
|
|||
|
|
|
|||
|
|
## HTTP 接口
|
|||
|
|
|
|||
|
|
管理页面使用以下接口,接口不要求登录或会话:
|
|||
|
|
|
|||
|
|
| 方法 | 路径 | 用途 |
|
|||
|
|
| --- | --- | --- |
|
|||
|
|
| `GET` | `/api/v1/dashboard` | 获取首页统计 |
|
|||
|
|
| `GET` | `/api/v1/subjects` | 查询授权对象 |
|
|||
|
|
| `POST` | `/api/v1/subjects` | 新建授权对象 |
|
|||
|
|
| `GET` | `/api/v1/subjects/:id` | 获取授权对象详情 |
|
|||
|
|
| `PUT` | `/api/v1/subjects/:id` | 修改未签发的授权对象 |
|
|||
|
|
| `DELETE` | `/api/v1/subjects/:id` | 删除未签发的授权对象 |
|
|||
|
|
| `POST` | `/api/v1/subjects/:id/licences` | 直接签发并下载许可证 |
|
|||
|
|
| `GET` | `/api/v1/licences` | 查询签发记录 |
|
|||
|
|
| `GET` | `/api/v1/licences/:id` | 获取签发记录详情 |
|
|||
|
|
| `GET` | `/api/v1/licences/:id/download` | 再次下载许可证 |
|
|||
|
|
| `POST` | `/api/v1/licences/:id/renew` | 续期并下载新许可证 |
|
|||
|
|
|
|||
|
|
## 构建
|
|||
|
|
|
|||
|
|
构建环境:Go `1.25.1`、Node.js、pnpm。
|
|||
|
|
|
|||
|
|
前端构建结果会直接写入 `internal/webui/dist`:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
Set-Location D:\work\license\web
|
|||
|
|
pnpm.cmd install
|
|||
|
|
pnpm.cmd build
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
前端有变化时,先构建前端,再构建 Go 程序。
|
|||
|
|
|
|||
|
|
Windows:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
Set-Location D:\work\license
|
|||
|
|
go build -buildvcs=false -o bin\ops-licence.exe .\cmd\ops-licence
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
Linux AMD64:
|
|||
|
|
|
|||
|
|
```powershell
|
|||
|
|
Set-Location D:\work\license
|
|||
|
|
$env:GOOS = "linux"
|
|||
|
|
$env:GOARCH = "amd64"
|
|||
|
|
go build -buildvcs=false -o dist\ops-licence .\cmd\ops-licence
|
|||
|
|
```
|
|||
|
|
|
|||
|
|
## 项目目录
|
|||
|
|
|
|||
|
|
```text
|
|||
|
|
cmd/ops-licence 服务与签发密钥命令
|
|||
|
|
cmd/root-keygen 根密钥生成工具
|
|||
|
|
deploy Linux systemd 与 Nginx 模板
|
|||
|
|
docs 设计、实施与使用文档
|
|||
|
|
etc 本地 YAML 配置
|
|||
|
|
internal 服务端实现、数据库迁移和内嵌前端
|
|||
|
|
sdk/licence 可复制的 Go 本地验证 SDK
|
|||
|
|
web Vue 管理界面源码
|
|||
|
|
bin Windows 可执行文件
|
|||
|
|
dist Linux 可执行文件
|
|||
|
|
```
|