feat: 添加产品激活码接口,支持生成包含授权码和配置签名的产品激活码,优化授权码管理流程
This commit is contained in:
1 parent
04af5eca00
commit
e91775ea76
11 files changed
+465
-619
No files matched your search
@@ -107,7 +107,7 @@ backend/
|
||||
│ │ ├── crypto.go # 加密工具
|
||||
│ │ ├── cu_jwt.go # C端用户JWT工具
|
||||
│ │ ├── jwt.go # JWT工具
|
||||
│ │ └── license_codec.go # 授权码编解码
|
||||
│ │ └── authorization_code.go # 授权码生成
|
||||
│ └── logger/ # 日志封装
|
||||
│ └── logger.go
|
||||
├── configs/
|
||||
|
||||
@@ -0,0 +1,191 @@
|
||||
# 授权码生成规范再变更(2026-01-16)
|
||||
## 1. 背景与目标
|
||||
授权码(Authorization Code)当前用于“在线激活”:客户端携带授权码请求服务端,服务端签发许可证文件。
|
||||
|
||||
当客户环境无网络时,在线激活不可用,因此需要一段可由客户端内置 RSA 公钥解析/验签的字符串,携带授权的配置(到期时间、功能配置、使用限制等),客户端在离线场景可据此完成自校验与功能开关。
|
||||
|
||||
本次“再变更”的核心原则:
|
||||
- 不改动现有授权码本体格式与核心激活流程
|
||||
- 仅在“复制/导出”时提供“产品激活码(Product Activation Code)”扩展串
|
||||
|
||||
## 2. 当前代码实现(现状梳理)
|
||||
### 2.1 授权码基础格式(旧规则,核心流程依赖)
|
||||
当前核心流程依赖的授权码格式为 4 段:
|
||||
|
||||
```
|
||||
LIC-{客户ID前4位}-{12位随机}-{4位校验码}
|
||||
```
|
||||
|
||||
示例:`LIC-F44D-GvBzMfEGbxMP-LVAA`
|
||||
|
||||
对应实现:
|
||||
- 生成:`backend/pkg/utils/authorization_code.go` 的 `GenerateLegacyAuthorizationCode()`(由 `backend/internal/service/authorization_code_service.go` 的 `generateAuthorizationCode()` 调用)
|
||||
- 使用:核心流程以数据库匹配为准(`licenseRepo.GetAuthorizationCodeByCode`),不做额外格式校验
|
||||
|
||||
### 2.2 历史“自包含授权码”方案(已移除)
|
||||
历史上曾存在一套“自包含授权码”的生成/解析方案(已删除,不再使用):
|
||||
- 曾实现:`backend/pkg/utils/license_codec.go`(已删除,原 `EncodeLicenseData()/DecodeLicenseData()`)
|
||||
- 格式:`LIC-{2位随机}-{Base64数据}`(3 段示例:`LIC-B5-xxxx...`)
|
||||
- 特征:将到期时间/配置等编码进 `{Base64数据}`,并做签名校验
|
||||
|
||||
该格式与 2.1 的旧规则(4 段)以及现有激活流程不一致,已按本次“再变更”回退/统一到 2.1。
|
||||
|
||||
受影响的核心流程(已改回旧规则生成):
|
||||
- `POST /api/v1/authorization-codes`(管理端创建授权码)
|
||||
- `POST /api/cu/orders`(C 端下单创建授权码)
|
||||
|
||||
### 2.3 授权码下载包(保持不变)
|
||||
现有下载接口会导出一个 zip,包含:
|
||||
- `authorization_code.txt`:仅包含授权码字符串(`authCode.Code`)
|
||||
- `rsa_public_key.pem`:RSA 公钥(从配置 `cfg.License.RSA.PublicKeyPath` 读取)
|
||||
|
||||
对应实现:
|
||||
- 路由:`GET /api/v1/authorization-codes/:id/download`
|
||||
- Service:`backend/internal/service/authorization_code_service.go` 的 `GenerateAuthorizationFile()`
|
||||
- Handler:`backend/internal/api/handlers/authorization_code_handler.go` 的 `DownloadAuthorizationFile()`
|
||||
|
||||
### 2.4 “公钥可解析”的签名封装(可复用)
|
||||
代码中已存在一套「RSA 私钥签名 + Base64 封装」实现,客户端可用公钥验签后得到原始 JSON:
|
||||
- 服务端签名封装:`backend/internal/service/license_service.go` 的 `signLicenseFile()`
|
||||
- 签名算法:`RSA-PSS-SHA256`(`backend/pkg/utils/crypto.go` 的 `RSAPrivateKey.SignData()` / `RSAPublicKey.VerifySignature()`)
|
||||
- 客户端验签示例:`backend/cmd/client-demo/main.go` 的 `decryptLicenseFile()`
|
||||
|
||||
说明:这里的“加密字符串”在现有体系中更贴近“签名封装字符串”(提供防篡改与来源可信验证,不提供保密性)。
|
||||
|
||||
## 3. 本次再变更:产品激活码(Product Activation Code)
|
||||
### 3.1 输出格式
|
||||
在需要离线激活/离线解析配置的场景,对外展示/复制的字符串为“产品激活码”:
|
||||
|
||||
```
|
||||
{授权码}&{配置签名封装串}
|
||||
```
|
||||
|
||||
示例:`LIC-F44D-GvBzMfEGbxMP-LVAA&{payload}`
|
||||
|
||||
其中:
|
||||
- `{授权码}`:保持现有 4 段格式不变(用于在线激活、数据库查找、订单绑定等核心流程)
|
||||
- `{payload}`:可由客户端内置 RSA 公钥验签并解析出 JSON 的字符串
|
||||
|
||||
### 3.2 payload(配置签名封装串)业务 JSON
|
||||
payload 解析后的业务 JSON(即“配置 JSON”)建议至少包含:
|
||||
|
||||
```json
|
||||
{
|
||||
"authorization_code": "LIC-F44D-GvBzMfEGbxMP-LVAA",
|
||||
"start_date": "2026-01-16T00:00:00+08:00",
|
||||
"end_date": "2026-12-31T23:59:59+08:00",
|
||||
"deployment_type": "standalone",
|
||||
"max_activations": 100,
|
||||
"feature_config": {},
|
||||
"usage_limits": {},
|
||||
"custom_parameters": {},
|
||||
"generated_at": "2026-01-16T12:00:00+08:00",
|
||||
"ver": 1
|
||||
}
|
||||
```
|
||||
|
||||
字段来源:
|
||||
- `authorization_code/start_date/end_date/deployment_type/max_activations/feature_config/usage_limits/custom_parameters`:来自 `authorization_codes` 表记录
|
||||
- `generated_at`:服务端生成时间
|
||||
|
||||
### 3.3 payload 生成规则(服务端)
|
||||
复用许可证文件的签名封装方式(`license_service.signLicenseFile()` 的数据结构与算法):
|
||||
|
||||
1. 构造“配置 JSON”(见 3.2),序列化为 `data`(JSON 字符串)
|
||||
2. 使用 RSA 私钥对 `data` 进行 PSS+SHA256 签名得到 `signature`(Base64)
|
||||
3. 组装签名封装对象:
|
||||
|
||||
```json
|
||||
{
|
||||
"data": "{...配置JSON字符串...}",
|
||||
"signature": "{base64_signature}",
|
||||
"algorithm": "RSA-PSS-SHA256"
|
||||
}
|
||||
```
|
||||
|
||||
4. 将封装对象再序列化为 JSON,并 Base64 编码得到 `{payload}`
|
||||
|
||||
说明:
|
||||
- 私钥路径来自 `cfg.License.RSA.PrivateKeyPath`
|
||||
- 公钥路径来自 `cfg.License.RSA.PublicKeyPath`(客户端内置同一对公钥)
|
||||
- Base64 编码沿用 `base64.StdEncoding`(与现有 license file 一致)
|
||||
|
||||
### 3.4 客户端解析规则(产品端)
|
||||
1. 读取输入字符串,按 `&` 分割:
|
||||
- 只有 1 段:视为仅“在线激活授权码”
|
||||
- 至少 2 段:`base_code` 与 `payload`(取第 1 段为授权码本体)
|
||||
2. 对 `payload` Base64 解码得到封装 JSON
|
||||
3. 解析封装 JSON,校验 `algorithm == "RSA-PSS-SHA256"`
|
||||
4. 用内置 RSA 公钥对 `data` 与 `signature` 验签
|
||||
5. 验签通过后解析 `data` 得到配置 JSON,并自行校验:
|
||||
- 到期时间、功能配置、使用限制等
|
||||
|
||||
补充(在线激活):
|
||||
- `/api/v1/activate` 的 `authorization_code` 参数只使用 `{授权码}` 本体,不依赖 `{payload}`
|
||||
- 客户端若传入完整 `product_activation_code`(`{授权码}&{payload}`),服务端会自动截取 `&` 前的 `{授权码}` 完成激活
|
||||
|
||||
## 4. 接口变更清单(需求说明)
|
||||
### 4.1 现有接口需要调整的内容
|
||||
#### 4.1.1 管理端:创建授权码
|
||||
- 接口:`POST /api/v1/authorization-codes`
|
||||
- 变更点:
|
||||
- `code` 生成规则统一为 2.1 的 4 段旧规则:`LIC-{客户ID前4位}-{12位随机}-{4位校验码}`
|
||||
- `feature_config/usage_limits/custom_parameters/start_date/end_date/...` 等仍按现有结构字段写入数据库(供 license file 与 payload 复用)
|
||||
- 返回:响应中的 `code` 仅返回“旧规则授权码本体”(不包含 `&payload`)
|
||||
|
||||
#### 4.1.2 C 端:下单创建授权码
|
||||
- 接口:`POST /api/cu/orders`
|
||||
- 变更点:
|
||||
- 订单关联的授权码字符串(`order.authorization_code`)生成规则统一为 2.1 的旧规则
|
||||
- 同步把订单对应配置写入 `authorization_codes` 表的结构化字段(不再依赖“授权码本体自包含”)
|
||||
- 返回:仍返回 `authorization_code`(旧规则授权码本体,不包含 `&payload`)
|
||||
- 备注:`POST /api/cu/authorization-codes/:codeId/share` 仍可沿用旧规则生成(如后续需要统一,可复用同一生成器)
|
||||
|
||||
#### 4.1.3 客户端:激活
|
||||
- 接口:`POST /api/v1/activate`
|
||||
- 变更点:
|
||||
- 激活不需要配置 payload;仅以 `{授权码}` 做数据库匹配,不做额外格式校验
|
||||
- 兼容传入完整 `product_activation_code`:服务端自动截取 `&` 前的 `{授权码}`
|
||||
|
||||
### 4.2 新增接口(仅客户端使用):获取产品激活码
|
||||
为客户端提供一个“获取产品激活码”的接口,返回 `{授权码}&{payload}`。管理端不提供该接口(当前仅 C 端“复制/导出”场景需要)。
|
||||
|
||||
#### 4.2.1 建议接口形态(C 端)
|
||||
|
||||
```
|
||||
POST /api/cu/authorization-codes/product-activation-code
|
||||
```
|
||||
|
||||
请求:
|
||||
|
||||
```json
|
||||
{
|
||||
"authorization_code": "LIC-F44D-GvBzMfEGbxMP-LVAA"
|
||||
}
|
||||
```
|
||||
|
||||
返回:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": "000000",
|
||||
"message": "success",
|
||||
"data": {
|
||||
"product_activation_code": "LIC-F44D-GvBzMfEGbxMP-LVAA&{payload}"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
约束与校验(建议):
|
||||
- 授权码存在,且归属当前登录客户(CustomerAuth)
|
||||
- 授权码未锁定,且在有效期内
|
||||
|
||||
## 5. 兼容性与验收标准
|
||||
### 5.1 兼容性
|
||||
- 原授权码本体格式不变,在线激活核心流程不受影响
|
||||
- 旧授权码仍可用;仅新增“产品激活码”扩展串用于离线解析
|
||||
|
||||
### 5.2 验收标准
|
||||
- `product_activation_code` 中的 `{payload}` 能被客户端仅凭公钥验签并解析出配置 JSON
|
||||
- 客户端可在离线场景下依据配置 JSON 完成自校验与功能开关
|
||||
- 不影响 `/api/v1/activate`、授权码管理、订单等核心流程
|
||||
@@ -101,3 +101,67 @@ func (h *CuAuthorizationHandler) ShareAuthorizationCode(c *gin.Context) {
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
|
||||
// GetProductActivationCode 获取产品激活码
|
||||
// @Summary 获取产品激活码
|
||||
// @Description 获取产品激活码:{授权码}&{payload}(payload 为 RSA-PSS-SHA256 签名封装串)
|
||||
// @Tags 用户端授权码管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Security BearerAuth
|
||||
// @Param request body models.ProductActivationCodeRequest true "获取产品激活码请求"
|
||||
// @Success 200 {object} models.APIResponse{data=models.ProductActivationCodeResponse} "获取成功"
|
||||
// @Failure 400 {object} models.ErrorResponse "请求参数无效"
|
||||
// @Failure 401 {object} models.ErrorResponse "未认证"
|
||||
// @Failure 403 {object} models.ErrorResponse "授权码已被锁定"
|
||||
// @Failure 404 {object} models.ErrorResponse "授权码不存在或已过期"
|
||||
// @Failure 500 {object} models.ErrorResponse "服务器内部错误"
|
||||
// @Router /api/cu/authorization-codes/product-activation-code [post]
|
||||
func (h *CuAuthorizationHandler) GetProductActivationCode(c *gin.Context) {
|
||||
claims := c.MustGet("cu_user").(*utils.CuClaims)
|
||||
customerID := claims.CustomerID
|
||||
|
||||
var req models.ProductActivationCodeRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900001", lang)
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message + ": " + err.Error(),
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := middleware.WithLanguage(c.Request.Context(), c)
|
||||
data, err := h.authCodeService.GetProductActivationCode(ctx, customerID, &req)
|
||||
if err != nil {
|
||||
// err已经是i18n.I18nError,直接使用
|
||||
i18nErr, ok := err.(*i18n.I18nError)
|
||||
if ok {
|
||||
c.JSON(i18nErr.HttpCode, models.ErrorResponse{
|
||||
Code: i18nErr.Code,
|
||||
Message: i18nErr.Message,
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
// 如果不是i18n错误,使用通用错误
|
||||
lang := middleware.GetLanguage(c)
|
||||
status, errCode, message := i18n.NewI18nErrorResponse("900004", lang, err.Error())
|
||||
c.JSON(status, models.ErrorResponse{
|
||||
Code: errCode,
|
||||
Message: message,
|
||||
Timestamp: getCurrentTimestamp(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
lang := middleware.GetLanguage(c)
|
||||
successMessage := i18n.GetErrorMessage("000000", lang)
|
||||
c.JSON(http.StatusOK, models.APIResponse{
|
||||
Code: "000000",
|
||||
Message: successMessage,
|
||||
Data: data,
|
||||
})
|
||||
}
|
||||
@@ -225,6 +225,7 @@ func SetupRouter() *gin.Engine {
|
||||
|
||||
// 授权码管理
|
||||
cuAuth.POST("/authorization-codes/:codeId/share", cuAuthorizationHandler.ShareAuthorizationCode)
|
||||
cuAuth.POST("/authorization-codes/product-activation-code", cuAuthorizationHandler.GetProductActivationCode)
|
||||
|
||||
// 设备管理
|
||||
cuAuth.GET("/devices", cuDeviceHandler.GetDevices)
|
||||
|
||||
@@ -280,3 +280,13 @@ type AuthorizationCodeShareResponseItem struct {
|
||||
EndDate string `json:"end_date"` // 结束时间
|
||||
MaxActivations int `json:"max_activations"` // 最大激活次数
|
||||
}
|
||||
|
||||
// ProductActivationCodeRequest 获取产品激活码请求结构
|
||||
type ProductActivationCodeRequest struct {
|
||||
AuthorizationCode string `json:"authorization_code" binding:"required"` // 授权码(旧规则格式)
|
||||
}
|
||||
|
||||
// ProductActivationCodeResponse 获取产品激活码响应结构
|
||||
type ProductActivationCodeResponse struct {
|
||||
ProductActivationCode string `json:"product_activation_code"` // 产品激活码:{授权码}&{payload}
|
||||
}
|
||||
@@ -2,7 +2,7 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"crypto/rand"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
@@ -77,35 +77,8 @@ func (s *authorizationCodeService) CreateAuthorizationCode(ctx context.Context,
|
||||
return nil, i18n.NewI18nError("100004", lang) // 缺少认证信息
|
||||
}
|
||||
|
||||
// 生成自包含授权码
|
||||
var featureConfigMap, usageLimitsMap, customParametersMap map[string]interface{}
|
||||
|
||||
if req.FeatureConfig != nil {
|
||||
if fc, ok := req.FeatureConfig.(map[string]interface{}); ok {
|
||||
featureConfigMap = fc
|
||||
}
|
||||
}
|
||||
if req.UsageLimits != nil {
|
||||
if ul, ok := req.UsageLimits.(map[string]interface{}); ok {
|
||||
usageLimitsMap = ul
|
||||
}
|
||||
}
|
||||
if req.CustomParameters != nil {
|
||||
if cp, ok := req.CustomParameters.(map[string]interface{}); ok {
|
||||
customParametersMap = cp
|
||||
}
|
||||
}
|
||||
|
||||
licenseConfig := &utils.LicenseConfig{
|
||||
EndDate: endDate,
|
||||
FeatureConfig: featureConfigMap,
|
||||
UsageLimits: usageLimitsMap,
|
||||
CustomParameters: customParametersMap,
|
||||
}
|
||||
|
||||
// 从配置获取RSA私钥路径
|
||||
privateKeyPath := config.GetConfig().License.RSA.PrivateKeyPath
|
||||
authCode, err := utils.EncodeLicenseData(licenseConfig, privateKeyPath)
|
||||
// 授权码生成规则回退/统一为旧规则(不再自包含配置)
|
||||
authCode, err := s.generateAuthorizationCode(req.CustomerID)
|
||||
if err != nil {
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
@@ -292,6 +265,102 @@ func (s *authorizationCodeService) GenerateAuthorizationFile(ctx context.Context
|
||||
return []byte(authCode.Code), fileName, authCode.Code, nil
|
||||
}
|
||||
|
||||
// GetProductActivationCode 获取产品激活码:{授权码}&{payload}
|
||||
func (s *authorizationCodeService) GetProductActivationCode(ctx context.Context, customerID string, req *models.ProductActivationCodeRequest) (*models.ProductActivationCodeResponse, error) {
|
||||
lang := pkgcontext.GetLanguageFromContext(ctx)
|
||||
|
||||
if customerID == "" || req == nil || strings.TrimSpace(req.AuthorizationCode) == "" {
|
||||
return nil, i18n.NewI18nError("900001", lang)
|
||||
}
|
||||
|
||||
code := strings.TrimSpace(req.AuthorizationCode)
|
||||
if idx := strings.Index(code, "&"); idx > 0 {
|
||||
code = strings.TrimSpace(code[:idx])
|
||||
}
|
||||
if code == "" {
|
||||
return nil, i18n.NewI18nError("900001", lang)
|
||||
}
|
||||
|
||||
authCode, err := s.licenseRepo.GetAuthorizationCodeByCode(ctx, code)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrAuthorizationCodeNotFound) {
|
||||
return nil, i18n.NewI18nError("300001", lang) // 授权码不存在
|
||||
}
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// 校验归属
|
||||
if authCode.CustomerID != customerID {
|
||||
return nil, i18n.NewI18nError("100005", lang) // 权限不足
|
||||
}
|
||||
|
||||
// 校验状态与有效期
|
||||
now := time.Now()
|
||||
if authCode.IsLocked {
|
||||
return nil, i18n.NewI18nError("300003", lang) // 授权码已被锁定
|
||||
}
|
||||
if now.Before(authCode.StartDate) || now.After(authCode.EndDate) {
|
||||
return nil, i18n.NewI18nError("300001", lang) // 授权码未生效或已过期
|
||||
}
|
||||
|
||||
// 构造配置 JSON(客户端离线解析/自校验)
|
||||
payloadData := map[string]interface{}{
|
||||
"ver": 1,
|
||||
"authorization_code": authCode.Code,
|
||||
"start_date": authCode.StartDate,
|
||||
"end_date": authCode.EndDate,
|
||||
"deployment_type": authCode.DeploymentType,
|
||||
"max_activations": authCode.MaxActivations,
|
||||
"generated_at": time.Now().Format(time.RFC3339),
|
||||
}
|
||||
|
||||
if featureConfig := parseJSONField(authCode.FeatureConfig); len(featureConfig) > 0 {
|
||||
payloadData["feature_config"] = featureConfig
|
||||
}
|
||||
if usageLimits := parseJSONField(authCode.UsageLimits); len(usageLimits) > 0 {
|
||||
payloadData["usage_limits"] = usageLimits
|
||||
}
|
||||
if customParameters := parseJSONField(authCode.CustomParameters); len(customParameters) > 0 {
|
||||
payloadData["custom_parameters"] = customParameters
|
||||
}
|
||||
|
||||
payloadDataBytes, err := json.Marshal(payloadData)
|
||||
if err != nil {
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
// RSA 私钥签名封装(与 license_service.signLicenseFile 相同结构)
|
||||
cfg := config.GetConfig()
|
||||
if cfg == nil || cfg.License.RSA.PrivateKeyPath == "" {
|
||||
return nil, i18n.NewI18nError("900004", lang, "RSA private key path not configured")
|
||||
}
|
||||
|
||||
privateKey, err := utils.LoadRSAPrivateKeyFromFile(cfg.License.RSA.PrivateKeyPath)
|
||||
if err != nil {
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
signature, err := privateKey.SignData(payloadDataBytes)
|
||||
if err != nil {
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
signedPayload := map[string]interface{}{
|
||||
"data": string(payloadDataBytes),
|
||||
"signature": signature,
|
||||
"algorithm": "RSA-PSS-SHA256",
|
||||
}
|
||||
|
||||
signedPayloadBytes, err := json.Marshal(signedPayload)
|
||||
if err != nil {
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
|
||||
payload := base64.StdEncoding.EncodeToString(signedPayloadBytes)
|
||||
productActivationCode := fmt.Sprintf("%s&%s", authCode.Code, payload)
|
||||
return &models.ProductActivationCodeResponse{ProductActivationCode: productActivationCode}, nil
|
||||
}
|
||||
|
||||
// fillAuthorizationCodeDisplayFields 填充完整授权码模型的多语言显示字段
|
||||
func (s *authorizationCodeService) fillAuthorizationCodeDisplayFields(authCode *models.AuthorizationCode, lang string) {
|
||||
// 计算状态
|
||||
@@ -638,112 +707,7 @@ func (s *authorizationCodeService) fillAuthCodeDisplayFields(item *models.Author
|
||||
// 格式: LIC-{4位客户代码}-{12位随机}-{4位校验码}
|
||||
// 安全性:12位随机字符(62字符集)提供约 3.23 × 10^21 种可能组合
|
||||
func (s *authorizationCodeService) generateAuthorizationCode(customerID string) (string, error) {
|
||||
// 获取客户编码的前4位作为前缀
|
||||
customerCode := "COMP" // 默认前缀
|
||||
if len(customerID) >= 4 {
|
||||
customerCode = strings.ToUpper(customerID[:4])
|
||||
}
|
||||
|
||||
// 生成12位随机字符串(从8位增加到12位以提升安全性)
|
||||
randomStr, err := s.generateRandomString(12)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
// 生成4位校验码
|
||||
checksum := s.generateChecksum(customerCode + randomStr)
|
||||
|
||||
// 格式: LIC-{customer_code}-{random}-{checksum}
|
||||
return fmt.Sprintf("LIC-%s-%s-%s", customerCode, randomStr, checksum), nil
|
||||
}
|
||||
|
||||
// generateRandomString 生成随机字符串
|
||||
// 使用62字符集(A-Z, a-z, 0-9)提供更高的熵值
|
||||
func (s *authorizationCodeService) generateRandomString(length int) (string, error) {
|
||||
// 扩展字符集到62字符(A-Z, a-z, 0-9)以提升安全性
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
bytes := make([]byte, length)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i, b := range bytes {
|
||||
bytes[i] = chars[b%byte(len(chars))]
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
// generateChecksum 生成校验码
|
||||
// 基于输入字符串的ASCII值计算,用于格式验证
|
||||
func (s *authorizationCodeService) generateChecksum(input string) string {
|
||||
sum := 0
|
||||
for _, char := range input {
|
||||
sum += int(char)
|
||||
}
|
||||
// 使用与随机字符串相同的62字符集以保持一致性
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
checksum := ""
|
||||
for i := 0; i < 4; i++ {
|
||||
checksum += string(chars[sum%len(chars)])
|
||||
sum = sum / len(chars)
|
||||
}
|
||||
return checksum
|
||||
}
|
||||
|
||||
// ValidateAuthorizationCodeFormat 验证授权码格式和校验和
|
||||
// 格式: LIC-{4位客户代码}-{8或12位随机}-{4位校验码}
|
||||
// 兼容旧格式(8位随机,36字符集)和新格式(12位随机,62字符集)
|
||||
func (s *authorizationCodeService) ValidateAuthorizationCodeFormat(code string) bool {
|
||||
// 解析格式
|
||||
parts := strings.Split(code, "-")
|
||||
if len(parts) != 4 || parts[0] != "LIC" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证各部分长度(兼容旧格式8位和新格式12位)
|
||||
customerCodeLen := len(parts[1])
|
||||
randomLen := len(parts[2])
|
||||
checksumLen := len(parts[3])
|
||||
|
||||
// 客户代码必须为4位,校验和必须为4位
|
||||
if customerCodeLen != 4 || checksumLen != 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 随机部分:兼容8位(旧格式)和12位(新格式)
|
||||
if randomLen != 8 && randomLen != 12 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 根据随机部分长度选择字符集验证校验和
|
||||
// 8位:使用36字符集(旧格式)
|
||||
// 12位:使用62字符集(新格式)
|
||||
expectedChecksum := s.generateChecksumWithCharset(parts[1]+parts[2], randomLen == 12)
|
||||
return expectedChecksum == parts[3]
|
||||
}
|
||||
|
||||
// generateChecksumWithCharset 生成校验码(支持指定字符集)
|
||||
// useExtendedCharset: true使用62字符集(新格式),false使用36字符集(旧格式)
|
||||
func (s *authorizationCodeService) generateChecksumWithCharset(input string, useExtendedCharset bool) string {
|
||||
sum := 0
|
||||
for _, char := range input {
|
||||
sum += int(char)
|
||||
}
|
||||
|
||||
var chars string
|
||||
if useExtendedCharset {
|
||||
// 使用62字符集(A-Z, a-z, 0-9)用于新格式
|
||||
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
} else {
|
||||
// 使用36字符集(A-Z, 0-9)用于旧格式
|
||||
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
}
|
||||
|
||||
checksum := ""
|
||||
for i := 0; i < 4; i++ {
|
||||
checksum += string(chars[sum%len(chars)])
|
||||
sum = sum / len(chars)
|
||||
}
|
||||
return checksum
|
||||
return utils.GenerateLegacyAuthorizationCode(customerID)
|
||||
}
|
||||
|
||||
// GetAuthorizationChangeList 查询授权变更历史列表
|
||||
|
||||
@@ -2,8 +2,8 @@ package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"license-manager/internal/config"
|
||||
"license-manager/internal/models"
|
||||
"license-manager/internal/repository"
|
||||
pkgcontext "license-manager/pkg/context"
|
||||
@@ -250,7 +250,7 @@ func (s *cuOrderService) CreateOrder(ctx context.Context, cuUserID, customerID s
|
||||
expiredAt = nil
|
||||
}
|
||||
|
||||
// 生成自包含授权码
|
||||
// 构建授权配置(用于后续生成产品激活码 payload)
|
||||
var featureConfigMap, usageLimitsMap map[string]interface{}
|
||||
|
||||
// 根据套餐类型设置配置信息
|
||||
@@ -260,20 +260,14 @@ func (s *cuOrderService) CreateOrder(ctx context.Context, cuUserID, customerID s
|
||||
}
|
||||
}
|
||||
|
||||
licenseConfig := &utils.LicenseConfig{
|
||||
EndDate: endDate,
|
||||
FeatureConfig: featureConfigMap,
|
||||
UsageLimits: usageLimitsMap,
|
||||
CustomParameters: map[string]interface{}{
|
||||
customParametersMap := map[string]interface{}{
|
||||
"package_id": req.PackageID,
|
||||
"package_name": pkg.Name,
|
||||
"license_count": req.LicenseCount,
|
||||
},
|
||||
}
|
||||
|
||||
// 从配置获取RSA私钥路径
|
||||
privateKeyPath := config.GetConfig().License.RSA.PrivateKeyPath
|
||||
authCode, err := utils.EncodeLicenseData(licenseConfig, privateKeyPath)
|
||||
// 授权码生成规则回退/统一为旧规则(不再自包含配置)
|
||||
authCode, err := utils.GenerateLegacyAuthorizationCode(customerID)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
@@ -282,6 +276,33 @@ func (s *cuOrderService) CreateOrder(ctx context.Context, cuUserID, customerID s
|
||||
// 构建订单描述
|
||||
description := fmt.Sprintf("%s - %d个许可", pkg.Name, req.LicenseCount)
|
||||
|
||||
// 处理JSON字段(用于数据库存储与后续生成产品激活码 payload)
|
||||
var featureConfig, usageLimits, customParameters models.JSON
|
||||
if featureConfigMap != nil {
|
||||
b, err := json.Marshal(featureConfigMap)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
featureConfig = models.JSON(b)
|
||||
}
|
||||
if usageLimitsMap != nil {
|
||||
b, err := json.Marshal(usageLimitsMap)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
usageLimits = models.JSON(b)
|
||||
}
|
||||
if customParametersMap != nil {
|
||||
b, err := json.Marshal(customParametersMap)
|
||||
if err != nil {
|
||||
tx.Rollback()
|
||||
return nil, i18n.NewI18nError("900004", lang, err.Error())
|
||||
}
|
||||
customParameters = models.JSON(b)
|
||||
}
|
||||
|
||||
// 构建授权码实体(使用生成的授权码)
|
||||
authCodeEntity := &models.AuthorizationCode{
|
||||
ID: uuid.New().String(), // 生成新的UUID作为主键
|
||||
@@ -295,7 +316,9 @@ func (s *cuOrderService) CreateOrder(ctx context.Context, cuUserID, customerID s
|
||||
EncryptionType: &[]string{"standard"}[0],
|
||||
MaxActivations: req.LicenseCount,
|
||||
IsLocked: false,
|
||||
// FeatureConfig 和 UsageLimits 已在授权码字符串中,无需重复存储
|
||||
FeatureConfig: featureConfig,
|
||||
UsageLimits: usageLimits,
|
||||
CustomParameters: customParameters,
|
||||
}
|
||||
|
||||
// 创建授权码
|
||||
|
||||
@@ -47,6 +47,9 @@ type AuthorizationCodeService interface {
|
||||
|
||||
// 用户端分享功能
|
||||
ShareAuthorizationCode(ctx context.Context, authCodeID, userID string, req *models.AuthorizationCodeShareRequest) (*models.AuthorizationCodeShareResponse, error)
|
||||
|
||||
// 用户端获取产品激活码
|
||||
GetProductActivationCode(ctx context.Context, customerID string, req *models.ProductActivationCodeRequest) (*models.ProductActivationCodeResponse, error)
|
||||
}
|
||||
|
||||
// LicenseService 许可证服务接口
|
||||
|
||||
@@ -378,64 +378,6 @@ func (s *licenseService) generateLicenseKey() (string, error) {
|
||||
return fmt.Sprintf("LIC-DEVICE-%s", hex), nil
|
||||
}
|
||||
|
||||
// validateAuthorizationCodeFormat 验证授权码格式和校验和
|
||||
// 格式: LIC-{4位客户代码}-{8或12位随机}-{4位校验码}
|
||||
// 兼容旧格式(8位随机,36字符集)和新格式(12位随机,62字符集)
|
||||
func validateAuthorizationCodeFormat(code string) bool {
|
||||
// 解析格式
|
||||
parts := strings.Split(code, "-")
|
||||
if len(parts) != 4 || parts[0] != "LIC" {
|
||||
return false
|
||||
}
|
||||
|
||||
// 验证各部分长度(兼容旧格式8位和新格式12位)
|
||||
customerCodeLen := len(parts[1])
|
||||
randomLen := len(parts[2])
|
||||
checksumLen := len(parts[3])
|
||||
|
||||
// 客户代码必须为4位,校验和必须为4位
|
||||
if customerCodeLen != 4 || checksumLen != 4 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 随机部分:兼容8位(旧格式)和12位(新格式)
|
||||
if randomLen != 8 && randomLen != 12 {
|
||||
return false
|
||||
}
|
||||
|
||||
// 根据随机部分长度选择字符集验证校验和
|
||||
// 8位:使用36字符集(旧格式)
|
||||
// 12位:使用62字符集(新格式)
|
||||
expectedChecksum := generateChecksumForCode(parts[1]+parts[2], randomLen == 12)
|
||||
return expectedChecksum == parts[3]
|
||||
}
|
||||
|
||||
// generateChecksumForCode 生成校验码(用于验证)
|
||||
// 基于输入字符串的ASCII值计算
|
||||
// useExtendedCharset: true使用62字符集(新格式),false使用36字符集(旧格式)
|
||||
func generateChecksumForCode(input string, useExtendedCharset bool) string {
|
||||
sum := 0
|
||||
for _, char := range input {
|
||||
sum += int(char)
|
||||
}
|
||||
|
||||
var chars string
|
||||
if useExtendedCharset {
|
||||
// 使用62字符集(A-Z, a-z, 0-9)用于新格式
|
||||
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
} else {
|
||||
// 使用36字符集(A-Z, 0-9)用于旧格式
|
||||
chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
}
|
||||
|
||||
checksum := ""
|
||||
for i := 0; i < 4; i++ {
|
||||
checksum += string(chars[sum%len(chars)])
|
||||
sum = sum / len(chars)
|
||||
}
|
||||
return checksum
|
||||
}
|
||||
|
||||
// ActivateLicense 激活许可证
|
||||
func (s *licenseService) ActivateLicense(ctx context.Context, req *models.ActivateRequest, clientIP string) (*models.ActivateResponse, error) {
|
||||
lang := pkgcontext.GetLanguageFromContext(ctx)
|
||||
@@ -445,9 +387,15 @@ func (s *licenseService) ActivateLicense(ctx context.Context, req *models.Activa
|
||||
return nil, i18n.NewI18nError("900001", lang)
|
||||
}
|
||||
|
||||
// 验证授权码格式(在查询数据库前先验证格式,避免无效查询)
|
||||
if !validateAuthorizationCodeFormat(req.AuthorizationCode) {
|
||||
return nil, i18n.NewI18nError("300001", lang) // 授权码格式无效
|
||||
// 兼容“产品激活码”形式:{授权码}&{payload}
|
||||
// 激活流程只需要授权码本体,不依赖payload。
|
||||
req.AuthorizationCode = strings.TrimSpace(req.AuthorizationCode)
|
||||
if idx := strings.Index(req.AuthorizationCode, "&"); idx > 0 {
|
||||
req.AuthorizationCode = strings.TrimSpace(req.AuthorizationCode[:idx])
|
||||
}
|
||||
|
||||
if req.AuthorizationCode == "" {
|
||||
return nil, i18n.NewI18nError("900001", lang)
|
||||
}
|
||||
|
||||
// 获取授权码信息
|
||||
|
||||
@@ -0,0 +1,51 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/rand"
|
||||
"fmt"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// GenerateLegacyAuthorizationCode 生成旧规则授权码:
|
||||
// LIC-{客户ID前4位}-{12位随机}-{4位校验码}
|
||||
func GenerateLegacyAuthorizationCode(customerID string) (string, error) {
|
||||
customerCode := "COMP"
|
||||
if len(customerID) >= 4 {
|
||||
customerCode = strings.ToUpper(customerID[:4])
|
||||
}
|
||||
|
||||
randomStr, err := generateSecureRandomString(12)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
checksum := generateChecksum62(customerCode + randomStr)
|
||||
return fmt.Sprintf("LIC-%s-%s-%s", customerCode, randomStr, checksum), nil
|
||||
}
|
||||
|
||||
func generateSecureRandomString(length int) (string, error) {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
bytes := make([]byte, length)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i, b := range bytes {
|
||||
bytes[i] = chars[b%byte(len(chars))]
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
func generateChecksum62(input string) string {
|
||||
sum := 0
|
||||
for _, char := range input {
|
||||
sum += int(char)
|
||||
}
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
checksum := ""
|
||||
for i := 0; i < 4; i++ {
|
||||
checksum += string(chars[sum%len(chars)])
|
||||
sum = sum / len(chars)
|
||||
}
|
||||
return checksum
|
||||
}
|
||||
|
||||
@@ -1,409 +0,0 @@
|
||||
package utils
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
// LicenseConfig 自包含授权码配置结构(极简字段名)
|
||||
type LicenseConfig struct {
|
||||
EndDate time.Time `json:"e"` // 到期时间
|
||||
FeatureConfig map[string]interface{} `json:"f,omitempty"` // 功能配置
|
||||
UsageLimits map[string]interface{} `json:"l,omitempty"` // 使用限制
|
||||
CustomParameters map[string]interface{} `json:"p,omitempty"` // 自定义参数
|
||||
}
|
||||
|
||||
// EncodeLicenseData 将配置数据编码为自包含授权码(极简模式)
|
||||
func EncodeLicenseData(config *LicenseConfig, privateKeyPath string) (string, error) {
|
||||
if config == nil {
|
||||
return "", errors.New("config cannot be nil")
|
||||
}
|
||||
|
||||
// 使用极简紧凑序列化
|
||||
configBytes, err := ultraCompactMarshal(config)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to marshal config: %w", err)
|
||||
}
|
||||
|
||||
// 使用HMAC-SHA256替代RSA签名(短签名模式)
|
||||
// 从私钥文件中提取密钥作为HMAC密钥(简化处理)
|
||||
hmacKey, err := getHMACKeyFromRSAKey(privateKeyPath)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to get HMAC key: %w", err)
|
||||
}
|
||||
|
||||
signature, err := signWithHMAC(configBytes, hmacKey)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to sign data: %w", err)
|
||||
}
|
||||
|
||||
// 极简格式:[版本(4位)][时间戳(32位)][配置数据][HMAC签名(32字节)]
|
||||
version := uint8(3) // 版本3(极简模式)
|
||||
timestamp := uint32(config.EndDate.Unix())
|
||||
|
||||
// 打包头部:版本(4位) + 时间戳(28位) 到5字节
|
||||
header := make([]byte, 5)
|
||||
header[0] = (version << 4) | byte(timestamp>>28)
|
||||
header[1] = byte(timestamp >> 20)
|
||||
header[2] = byte(timestamp >> 12)
|
||||
header[3] = byte(timestamp >> 4)
|
||||
header[4] = byte(timestamp << 4) // 最后4位保留给标志位
|
||||
|
||||
// 设置字段存在标志
|
||||
flagBits := uint8(0)
|
||||
if config.FeatureConfig != nil {
|
||||
flagBits |= 1 << 0
|
||||
}
|
||||
if config.UsageLimits != nil {
|
||||
flagBits |= 1 << 1
|
||||
}
|
||||
if config.CustomParameters != nil {
|
||||
flagBits |= 1 << 2
|
||||
}
|
||||
header[4] |= flagBits
|
||||
|
||||
// 构建数据:[头部(5字节)][配置数据][签名(32字节)]
|
||||
data := append(header, configBytes...)
|
||||
data = append(data, signature...)
|
||||
|
||||
// 使用URL安全的Base64编码
|
||||
encodedData := base64.RawURLEncoding.EncodeToString(data)
|
||||
|
||||
// 生成随机码(2位)
|
||||
randomStr, err := generateRandomString(2)
|
||||
if err != nil {
|
||||
return "", fmt.Errorf("failed to generate random string: %w", err)
|
||||
}
|
||||
|
||||
// 最终格式:LIC-{2位随机}-{数据}
|
||||
return fmt.Sprintf("LIC-%s-%s", randomStr, encodedData), nil
|
||||
}
|
||||
|
||||
// ultraCompactMarshal 极简紧凑序列化(无长度前缀)
|
||||
func ultraCompactMarshal(config *LicenseConfig) ([]byte, error) {
|
||||
var buf []byte
|
||||
|
||||
// 直接序列化存在的字段,不使用长度前缀
|
||||
// 依赖解码时知道字段顺序和格式
|
||||
|
||||
if config.FeatureConfig != nil {
|
||||
mapBytes, err := json.Marshal(config.FeatureConfig)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf = append(buf, mapBytes...)
|
||||
buf = append(buf, 0) // 字段分隔符
|
||||
}
|
||||
|
||||
if config.UsageLimits != nil {
|
||||
mapBytes, err := json.Marshal(config.UsageLimits)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf = append(buf, mapBytes...)
|
||||
buf = append(buf, 0) // 字段分隔符
|
||||
}
|
||||
|
||||
if config.CustomParameters != nil {
|
||||
mapBytes, err := json.Marshal(config.CustomParameters)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
buf = append(buf, mapBytes...)
|
||||
buf = append(buf, 0) // 字段分隔符
|
||||
}
|
||||
|
||||
return buf, nil
|
||||
}
|
||||
|
||||
// getHMACKeyFromRSAKey 从RSA密钥文件提取HMAC密钥
|
||||
func getHMACKeyFromRSAKey(keyPath string) ([]byte, error) {
|
||||
// 首先尝试作为私钥加载
|
||||
if privateKey, err := getLicensePrivateKey(keyPath); err == nil {
|
||||
// 使用私钥的模数作为HMAC密钥
|
||||
key := privateKey.N.Bytes()
|
||||
// 截取前32字节作为HMAC-SHA256密钥
|
||||
if len(key) > 32 {
|
||||
key = key[:32]
|
||||
} else if len(key) < 32 {
|
||||
// 补齐到32字节
|
||||
padding := make([]byte, 32-len(key))
|
||||
key = append(key, padding...)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
// 如果私钥加载失败,尝试作为公钥加载
|
||||
if publicKey, err := getLicensePublicKey(keyPath); err == nil {
|
||||
// 使用公钥的模数作为HMAC密钥
|
||||
key := publicKey.N.Bytes()
|
||||
// 截取前32字节作为HMAC-SHA256密钥
|
||||
if len(key) > 32 {
|
||||
key = key[:32]
|
||||
} else if len(key) < 32 {
|
||||
// 补齐到32字节
|
||||
padding := make([]byte, 32-len(key))
|
||||
key = append(key, padding...)
|
||||
}
|
||||
return key, nil
|
||||
}
|
||||
|
||||
return nil, errors.New("failed to load RSA key for HMAC")
|
||||
}
|
||||
|
||||
// getLicensePublicKey 获取RSA公钥(复用crypto.go的函数)
|
||||
func getLicensePublicKey(publicKeyPath string) (*RSAPublicKey, error) {
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(publicKeyPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("RSA公钥文件不存在: %s", publicKeyPath)
|
||||
}
|
||||
|
||||
// 加载公钥
|
||||
publicKey, err := LoadRSAPublicKeyFromFile(publicKeyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("RSA公钥文件损坏或格式错误: %w", err)
|
||||
}
|
||||
|
||||
return publicKey, nil
|
||||
}
|
||||
|
||||
// signWithHMAC 使用HMAC-SHA256签名
|
||||
func signWithHMAC(data []byte, key []byte) ([]byte, error) {
|
||||
h := hmac.New(sha256.New, key)
|
||||
h.Write(data)
|
||||
return h.Sum(nil), nil
|
||||
}
|
||||
|
||||
// ultraCompactUnmarshal 从极简格式反序列化配置
|
||||
func ultraCompactUnmarshal(data []byte, flags uint8) (*LicenseConfig, error) {
|
||||
config := &LicenseConfig{}
|
||||
offset := 0
|
||||
|
||||
// 解析f字段(如果存在)
|
||||
if flags&1 != 0 {
|
||||
end := findFieldEnd(data, offset)
|
||||
if end == -1 {
|
||||
return nil, errors.New("invalid feature config format")
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(data[offset:end], &m); err != nil {
|
||||
return nil, fmt.Errorf("invalid feature config: %w", err)
|
||||
}
|
||||
config.FeatureConfig = m
|
||||
offset = end + 1
|
||||
}
|
||||
|
||||
// 解析l字段(如果存在)
|
||||
if flags&(1<<1) != 0 {
|
||||
end := findFieldEnd(data, offset)
|
||||
if end == -1 {
|
||||
return nil, errors.New("invalid usage limits format")
|
||||
}
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(data[offset:end], &m); err != nil {
|
||||
return nil, fmt.Errorf("invalid usage limits: %w", err)
|
||||
}
|
||||
config.UsageLimits = m
|
||||
offset = end + 1
|
||||
}
|
||||
|
||||
// 解析p字段(如果存在)
|
||||
if flags&(1<<2) != 0 {
|
||||
if offset >= len(data) {
|
||||
return nil, errors.New("missing custom parameters")
|
||||
}
|
||||
// 最后一个字段到数据结尾
|
||||
var m map[string]interface{}
|
||||
if err := json.Unmarshal(data[offset:], &m); err != nil {
|
||||
return nil, fmt.Errorf("invalid custom parameters: %w", err)
|
||||
}
|
||||
config.CustomParameters = m
|
||||
}
|
||||
|
||||
return config, nil
|
||||
}
|
||||
|
||||
// findFieldEnd 查找字段结束位置(以null字节分隔)
|
||||
func findFieldEnd(data []byte, start int) int {
|
||||
for i := start; i < len(data); i++ {
|
||||
if data[i] == 0 {
|
||||
return i
|
||||
}
|
||||
}
|
||||
return -1
|
||||
}
|
||||
|
||||
// DecodeLicenseData 从授权码解析配置数据
|
||||
func DecodeLicenseData(code string, publicKeyPath string) (*LicenseConfig, error) {
|
||||
if !strings.HasPrefix(code, "LIC-") {
|
||||
return nil, errors.New("invalid license format")
|
||||
}
|
||||
|
||||
// 解析授权码格式:LIC-{2位随机码}-{数据}
|
||||
parts := strings.Split(code, "-")
|
||||
if len(parts) != 3 || parts[0] != "LIC" {
|
||||
return nil, errors.New("invalid license format")
|
||||
}
|
||||
|
||||
randomStr := parts[1]
|
||||
encodedData := parts[2]
|
||||
|
||||
// 验证随机码长度
|
||||
if len(randomStr) != 2 {
|
||||
return nil, errors.New("invalid random string length")
|
||||
}
|
||||
|
||||
// 使用URL安全的Base64解码
|
||||
dataBytes, err := base64.RawURLEncoding.DecodeString(encodedData)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode base64: %w", err)
|
||||
}
|
||||
|
||||
// 解析极简格式:[头部(5字节)][配置数据][签名(32字节)]
|
||||
if len(dataBytes) < 5+32 { // 头部 + 最少签名
|
||||
return nil, errors.New("invalid data format")
|
||||
}
|
||||
|
||||
// 解析头部
|
||||
header := dataBytes[:5]
|
||||
version := header[0] >> 4
|
||||
timestamp := uint32(header[0]&0x0F)<<28 | uint32(header[1])<<20 | uint32(header[2])<<12 | uint32(header[3])<<4 | uint32(header[4]>>4)
|
||||
flags := header[4] & 0x0F
|
||||
|
||||
if version != 3 {
|
||||
return nil, errors.New("unsupported version")
|
||||
}
|
||||
|
||||
config := &LicenseConfig{
|
||||
EndDate: time.Unix(int64(timestamp), 0),
|
||||
}
|
||||
|
||||
// 分离配置数据和签名
|
||||
configDataEnd := len(dataBytes) - 32
|
||||
configBytes := dataBytes[5:configDataEnd]
|
||||
signature := dataBytes[configDataEnd:]
|
||||
|
||||
// 获取HMAC密钥用于验证
|
||||
hmacKey, err := getHMACKeyFromRSAKey(publicKeyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get HMAC key: %w", err)
|
||||
}
|
||||
|
||||
// 验证HMAC签名
|
||||
if !verifyHMAC(configBytes, signature, hmacKey) {
|
||||
return nil, errors.New("signature verification failed")
|
||||
}
|
||||
|
||||
// 从极简格式反序列化配置
|
||||
fullConfig, err := ultraCompactUnmarshal(configBytes, flags)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to unmarshal config: %w", err)
|
||||
}
|
||||
|
||||
// 合并时间戳
|
||||
fullConfig.EndDate = config.EndDate
|
||||
return fullConfig, nil
|
||||
}
|
||||
|
||||
// verifyHMAC 验证HMAC签名
|
||||
func verifyHMAC(data []byte, signature []byte, key []byte) bool {
|
||||
h := hmac.New(sha256.New, key)
|
||||
h.Write(data)
|
||||
expectedMAC := h.Sum(nil)
|
||||
return hmac.Equal(signature, expectedMAC)
|
||||
}
|
||||
|
||||
// SignLicenseData 使用RSA私钥对数据进行签名
|
||||
func SignLicenseData(data []byte, privateKeyPath string) ([]byte, error) {
|
||||
// 获取RSA私钥
|
||||
privateKey, err := getLicensePrivateKey(privateKeyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get private key: %w", err)
|
||||
}
|
||||
|
||||
// 使用现有的RSA签名方法(PSS模式,与许可证保持一致)
|
||||
signature, err := privateKey.SignData(data)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to sign data: %w", err)
|
||||
}
|
||||
|
||||
// 解码Base64签名
|
||||
signatureBytes, err := base64.StdEncoding.DecodeString(signature)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to decode signature: %w", err)
|
||||
}
|
||||
|
||||
return signatureBytes, nil
|
||||
}
|
||||
|
||||
// VerifyLicenseSignature 使用RSA公钥验证签名
|
||||
func VerifyLicenseSignature(data []byte, signature []byte, publicKeyPath string) error {
|
||||
// 获取RSA公钥
|
||||
publicKey, err := getLicensePublicKey(publicKeyPath)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get public key: %w", err)
|
||||
}
|
||||
|
||||
// 编码签名为Base64(与SignData方法匹配)
|
||||
signatureBase64 := base64.StdEncoding.EncodeToString(signature)
|
||||
|
||||
// 使用现有的RSA验证方法
|
||||
err = publicKey.VerifySignature(data, signatureBase64)
|
||||
if err != nil {
|
||||
return fmt.Errorf("signature verification failed: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// getLicensePrivateKey 获取许可证RSA私钥
|
||||
func getLicensePrivateKey(privateKeyPath string) (*RSAPrivateKey, error) {
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(privateKeyPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("RSA私钥文件不存在,请先生成RSA密钥对: %s", privateKeyPath)
|
||||
}
|
||||
|
||||
// 加载私钥
|
||||
privateKey, err := LoadRSAPrivateKeyFromFile(privateKeyPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("RSA私钥文件损坏或格式错误: %w", err)
|
||||
}
|
||||
|
||||
return privateKey, nil
|
||||
}
|
||||
|
||||
// generateRandomString 生成随机字符串
|
||||
func generateRandomString(length int) (string, error) {
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
bytes := make([]byte, length)
|
||||
if _, err := rand.Read(bytes); err != nil {
|
||||
return "", err
|
||||
}
|
||||
for i, b := range bytes {
|
||||
bytes[i] = chars[b%byte(len(chars))]
|
||||
}
|
||||
return string(bytes), nil
|
||||
}
|
||||
|
||||
// generateChecksum 生成校验码
|
||||
func generateChecksum(input string) string {
|
||||
sum := 0
|
||||
for _, char := range input {
|
||||
sum += int(char)
|
||||
}
|
||||
const chars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
|
||||
checksum := ""
|
||||
for i := 0; i < 4; i++ {
|
||||
checksum += string(chars[sum%len(chars)])
|
||||
sum = sum / len(chars)
|
||||
}
|
||||
return checksum
|
||||
}
|
||||
Reference in new issue
Block a user