# 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 可执行文件 ```