对接 License

最近更新时间:2025-09-15 10:12:41

我的收藏
对于通过云应用安装的软件,云应用会为软件颁发 License,软件进程内,可以读取和校验 License。

License 有什么作用?

License 是对软件终端用户的一个许可,授权内容包括:
使用期限:如果软件过期,软件进程可以拒绝服务或提供降级服务。
规格:软件可以基于授权的规格运行,如限制并发数、用户数等。

对接云应用 License 有什么优势?

顺畅:下单即自动颁发 License,无需等待。
灵活:支持续期、升降配等灵活需求。
可信:腾讯云官方颁发,基于角色绑定的实例关联 License,难以伪造和复制。

License 交易与发货




服务商需要为应用包对应的商品定义规格,定义的规格会在用户选购时确定下来,作为 License 的规格。
License 按照客户购买的软件规格颁发,颁发的 License 包含下列信息:
关联的软件包
关联的软件实例
授权的规格信息
授权模式(永久/订阅)
授权状态(有效/过期/失效)
颁发时间、激活时间、过期时间、失效时间。

License 校验

服务商的应用镜像,可以通过云 API 校验 License。流程如下。

为了校验软件的 License,核心步骤是:
获取安装参数 cloudapp_cam_role,使用 CAM 角色申请临时密钥,调用云 API。详见文档调用云 API
调用 cloudapp.DescribeLicense 接口,获取当前进程关联的 License 信息。
软件进程按需校验 License 的状态,或者根据规格做功能控制。
License 信息示例如下:
{
"LicenseMode": "Subscription",
"BillingMode": 1,
"CreateSource": "SN1719406931EJJ1E",
"AuthorizedCloudappRoleId": "700001833621",
"AuthorizedCloudappId": "cloudapp-992nqg9u",
"AuthorizedUserUin": "700001833621",
"LifeSpanUnit": "Y",
"LifeSpan": 365,
"SoftwarePackageId": "pkg-1glehom7",
"SoftwarePackageVersion": "0.0.1",
"AuthorizedSpecification": [
{
"ParamKey": "version",
"ParamKeyName": "版本",
"ParamValue": "basic",
"ParamValueName": "基础版"
},
{
"ParamKey": "size",
"ParamKeyName": "规格",
"ParamValue": "100",
"ParamValueName": "100 人规模"
}
],
"ProviderId": 100000071,
"ProviderUin": "700000918156",
"IssueDate": "2024-06-26T21:02:16+08:00",
"ActivationDate": "2024-06-26T21:02:19+08:00",
"ExpirationDate": "2389-06-26T21:02:19+08:00",
"LicenseStatus": "Active",
"LicenseId": "700000918156:pkg-1glehom7:cloudapp-992nqg9u:3988",
"LicenseType": "Standard",
"LicenseLevel": "Master"
}
这里需要关注的字段有:
LicenseStatus:授权状态,如果为 Active 则表示授权有效,为 Deactive 表示授权已失效。服务商应使用 LicenseStatus 字段来判断授权是否有效。
AuthorizedSpecification:授权的规格,软件进程可以根据该规格控制软件行为。
LicenseMode:授权模式,订阅还是永久买断。
校验 License 的时机,可以由软件进程自主决定,常见的方式有:
定时校验,例如每天或者每小时校验一次。
按需校验,例如在需要校验 License 的产品功能调用时校验。
安全校验:
对 License 安全要求较高的,可进行签名验证(具体做法见 License 签名验证方法),以确保返回的 License 信息未被中间人拦截篡改过。

License 开发调试

通过在应用包的 package.yaml中按如下格式添加specs规格配置,即可在安装应用开发版本时选择规格信息及授权模式创建 License。
# <package.yaml>
# 声明应用包规格
specs:
- key: version
name: 版本
enum:
- value: standard
name: 标准版
- value: advanced
name: 高级版
- value: enterprise
name: 企业版
- key: cluster_mode
name: 集群模式
enum:
- value: single
name: 单集群
- value: double
name: 双集群
- value: triple
name: 三集群

License 签名验证方法

获取授权信息的接口 cloudapp.DescribeLicense 按照 JWT 格式返回了 License 信息,签名使用 RS256 算法,使用以下公钥进行验签:
-----BEGIN PUBLIC KEY-----
MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5EQJv0v0f1hrB7NIGwXi
hFmw+ugrJi7gnmOqaiYp7oFrzf4RSJ3DPr4K01F+CrjTdCPghDLq4fsKVxxHAjNj
nbstbVlHZEVOfzQ4umeocJpxWFuyKyGwHv+obnEZ/4689fxVpTbG3IbUTGn1TRJs
9s3xM8nFd6LLAoh1Hhrdf2D4mLRToLvtRVat1l8fH3gsM+RoG4L4h+3hghn4bpyA
na2MBFDzvmBeVGUVzqRjSvUaexd+Bo1wTsllAdqjP6MTlAAWGmIAMStBSRS+YpRQ
xjhE9Rdb9zTE54q3Ui7UJg5BMe+R3kVrBINbnT6Va8/Lzjg4+THdpMTLr6fY6ObF
7r+i/924XgxqQOFvGaFJSyjXTORnK42T5YRr5TSqxr9CzhybPcdRvws2GdAq9f55
8whj1DYcgg0X8kR06Iu+/9Mk/CqssdrZ8LYDwSkDI8S/RwpdNQfifUa8wyY0R2xN
nY+bnkrjvGPz7Rokr0Ki9/orT9i4yQWA1mMCDi2vcP+oXqrEs7XAyH85gDSzuTp+
dXbTYPZpIAK6Kejwssw1IE1lGNP4PNQZk9EXU7+vB1csz4GUao7Mr7F5VbrGKvTs
aGxbIc6b0MDWMEFA7L/CWC9UtReWCk1MYwJzy105bWU/VBpYJPmyZTFRQaY2MEH4
fnsK2+jtZ1IYIQw/YsHU6CcCAwEAAQ==
-----END PUBLIC KEY-----
接口 cloudapp.DescribeLicense 的返回结构如下:
{
"Response": {
"RequestId": "cd15813b-adff-460e-b9fc-64579e96412d",
"Token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjkzMjQ4MTc5ODAsImlhdCI6MTc1NjE3Nzk4MCwiaXNzIjoibGljZW5zZS1zZXJ2aWNlIiwicGF5bG9hZCI6eyJNYWluTGljZW5zZSI6eyJMaWNlbnNlTW9kZSI6IlN1YnNjcmlwdGlvbiIsIkJpbGxpbmdNb2RlIjoxLCJDcmVhdGVTb3VyY2UiOiJTTjE3MTk0MDc1NDc0SEJETSIsIkF1dGhvcml6ZWRDbG91ZGFwcFJvbGVJZCI6IjcwMDAwMTgzMzgwNiIsIkF1dGhvcml6ZWRDbG91ZGFwcElkIjoiY2xvdWRhcHAtc2V3ZWM2cHMiLCJBdXRob3JpemVkVXNlclVpbiI6IjcwMDAwMDkxODE1NiIsIkxpZmVTcGFuVW5pdCI6IlkiLCJMaWZlU3BhbiI6MzY1LCJTb2Z0d2FyZVBhY2thZ2VJZCI6InBrZy0xZ2xlaG9tNyIsIlNvZnR3YXJlUGFja2FnZVZlcnNpb24iOiIwLjAuMSIsIkF1dGhvcml6ZWRTcGVjaWZpY2F0aW9uIjpbeyJQYXJhbUtleSI6InZlcnNpb24iLCJQYXJhbUtleU5hbWUiOiLniYjmnKwiLCJQYXJhbVZhbHVlIjoiYmFzaWMiLCJQYXJhbVZhbHVlTmFtZSI6IuWfuuehgOeJiCJ9LHsiUGFyYW1LZXkiOiJzaXplIiwiUGFyYW1LZXlOYW1lIjoi6KeE5qC8IiwiUGFyYW1WYWx1ZSI6IjEwMCIsIlBhcmFtVmFsdWVOYW1lIjoiMTAw5Lq66KeE5qihIn1dLCJQcm92aWRlcklkIjoxMDAwMDAwNzEsIlByb3ZpZGVyVWluIjoiNzAwMDAwOTE4MTU2IiwiSXNzdWVEYXRlIjoiMjAyNC0wNi0yNlQyMToxMjozMiswODowMCIsIkFjdGl2YXRpb25EYXRlIjoiMjAyNC0wNi0yNlQyMToxMjozNSswODowMCIsIkV4cGlyYXRpb25EYXRlIjoiMjM4OS0wNi0yNlQyMToxMjozNSswODowMCIsIkxpY2Vuc2VTdGF0dXMiOiJBY3RpdmUiLCJMaWNlbnNlSWQiOiI3MDAwMDA5MTgxNTY6cGtnLTFnbGVob203OmNsb3VkYXBwLXNld2VjNnBzOjgwMDciLCJMaWNlbnNlVHlwZSI6IlN0YW5kYXJkIiwiTGljZW5zZUxldmVsIjoiTWFzdGVyIn0sIkFkZGl0aW9uTGljZW5zZXMiOltdLCJUaW1lc3RhbXAiOiIyMDI1LTA4LTI2VDExOjEzOjAwKzA4OjAwIn19.G8Lx49xZBW0Rh3lRA15XzZ-PzLJj0bAxwnklx0pTjrHWxqxQdETAdGfU_QaGI_WZfYh2IVbFcwHnRLiRj6pQb4guCMpCbcsgL28BRS4g1wnaFhjcyEQLLtpDdz4_lPnOR2VHHvnfwhLZtccAgsRpeedPMBK1hwO9D3WKisQg2LcIr0V-QB8gmgIqqyqrLW6z37QpjgB4ZyJ5bIC1J-0-VmghskA04xnQRPdGJtlyBhjzVjeDxBq5JOqm3Am0Nqu1jyTd3MuYgSRwJqkDyjVBOGFGGy6mZCIYnxU_ET6-0ZEendqYwXDkpYG4rZZv5YmRCXiSESYz0zx4czwmFWkw-TjRSvUQBxBfsoDcAgyzpY7zBOTnbrr7DyoMvVnnHo7vb0if8_vkub6o0MuRnvdDYxNJtnTtlIScCadWAIvWUQ1DlUw2kzS-h9Ju2h7JhKw9cUeutu0X_6V4arZu9JlgWT9Ns7BtS9Y5JxgQOd36Aan39Rwohy_BrVwjOkbvDuTFLc_yNUlNdq5T2GNbDjABCmi73CGhCuWyPgtRs4ftpPugDRrTe4E95F224jdhf7I0He-nY4i1MoVjz8Zzm4v0vH67cMfcu0XVhs7ywvmu5tBSwm0uuhAXFFIbSrgEzuadxNhSi6qVCFNLnjiPYplK1M9mxG8Hc-fU-0A0TPepx8Q"
}
}
字段说明如下:
Response.RequestId 为当前请求的唯一 ID。
Response.Token 为 License 信息按 JWT 协议生成的 Token,该 Token 可使用前述公钥进行签名验证。
其中 Token 中的有效数据负载明文结构如下:
payload.MainLicense 为主 License 信息(区别于增购 License )。
payload.AdditionLicenses 为增购 License 信息,授权软件支持增购项的情况下才有该字段,且默认情况下不返回增购 License 信息,如需返回,请参考 cloudapp.DescribeLicense 接口的 Filter 参数约定指定同时返回增购 License 信息。
{
"exp": 9324758169,
"iat": 1756118169,
"iss": "license-service",
"payload": {
"MainLicense": {
"LicenseMode": "Subscription",
"BillingMode": 1,
"CreateSource": "SN1719406931EJJ1E",
"AuthorizedCloudappRoleId": "700001833621",
"AuthorizedCloudappId": "cloudapp-992nqg9u",
"AuthorizedUserUin": "700001833621",
"LifeSpanUnit": "Y",
"LifeSpan": 365,
"SoftwarePackageId": "pkg-1glehom7",
"SoftwarePackageVersion": "0.0.1",
"AuthorizedSpecification": [
{
"ParamKey": "version",
"ParamKeyName": "版本",
"ParamValue": "basic",
"ParamValueName": "基础版"
},
{
"ParamKey": "size",
"ParamKeyName": "规格",
"ParamValue": "100",
"ParamValueName": "100 人规模"
}
],
"ProviderId": 100000071,
"ProviderUin": "700000918156",
"IssueDate": "2024-06-26T21:02:16+08:00",
"ActivationDate": "2024-06-26T21:02:19+08:00",
"ExpirationDate": "2389-06-26T21:02:19+08:00",
"LicenseStatus": "Active",
"LicenseId": "700000918156:pkg-1glehom7:cloudapp-992nqg9u:3988",
"LicenseType": "Standard",
"LicenseLevel": "Master"
},
"AdditionLicenses": [
{
"LicenseMode": "Subscription",
"BillingMode": 1,
"CreateSource": "SN1719406931EJJ1E",
"AuthorizedCloudappRoleId": "700001833621",
"AuthorizedCloudappId": "cloudapp-992nqg9u",
"AuthorizedUserUin": "700001833621",
"LifeSpanUnit": "Y",
"LifeSpan": 365,
"SoftwarePackageId": "pkg-1glehom7",
"SoftwarePackageVersion": "0.0.1",
"AuthorizedSpecification": [
{
"ParamKey": "version",
"ParamKeyName": "版本",
"ParamValue": "basic",
"ParamValueName": "基础版"
},
{
"ParamKey": "size",
"ParamKeyName": "规格",
"ParamValue": "100",
"ParamValueName": "100 人规模"
}
],
"ProviderId": 100000071,
"ProviderUin": "700000918156",
"IssueDate": "2024-06-26T21:02:16+08:00",
"ActivationDate": "2024-06-26T21:02:19+08:00",
"ExpirationDate": "2389-06-26T21:02:19+08:00",
"LicenseStatus": "Active",
"LicenseId": "700000918156:pkg-1glehom7:cloudapp-992nqg9u:3988",
"LicenseType": "Standard",
"LicenseLevel": "Master"
}
],
"Timestamp": "2025-08-25T18:36:09+08:00"
}
}

代码示例

本节代码示例展示了如何在开发联调阶段完成 License JWT 协议的开发自测。
警告:
示例代码中提供了生成密钥对的工具,方便接入时本地生成公私钥进行 JWT 签名和验签测试,正式通过 DescribeLicense 接口联调时,需切换到前文提供的正式公钥。
package main

import (
"context"
"crypto/rsa"
"crypto/x509"
"encoding/json"
"encoding/pem"
"errors"
"fmt"
"github.com/golang-jwt/jwt/v5"
"time"
)

var (
// 此处可替换为本地调试生成的 RSA 签名私钥
privateKeyStr = `-----BEGIN RSA PRIVATE KEY-----
....
-----END RSA PRIVATE KEY-----`
// 此处可替换为本地调试生成的 RSA 签名公钥
publicKeyPEM = `-----BEGIN RSA PUBLIC KEY-----
....
-----END RSA PUBLIC KEY-----`

licenseDetailBuffer = []byte(`{
"MainLicense": {
"LicenseMode": "Subscription",
"BillingMode": 1,
"CreateSource": "SN1719406931EJJ1E",
"AuthorizedCloudappRoleId": "700001833621",
"AuthorizedCloudappId": "cloudapp-992nqg9u",
"AuthorizedUserUin": "700001833621",
"LifeSpanUnit": "Y",
"LifeSpan": 365,
"SoftwarePackageId": "pkg-1glehom7",
"SoftwarePackageVersion": "0.0.1",
"AuthorizedSpecification": [{
"ParamKey": "version",
"ParamKeyName": "版本",
"ParamValue": "basic",
"ParamValueName": "基础版"
},
{
"ParamKey": "size",
"ParamKeyName": "规格",
"ParamValue": "100",
"ParamValueName": "100人规模"
}
],
"ProviderId": 100000071,
"ProviderUin": "700000918156",
"IssueDate": "2024-06-26T21:02:16+08:00",
"ActivationDate": "2024-06-26T21:02:19+08:00",
"ExpirationDate": "2389-06-26T21:02:19+08:00",
"LicenseStatus": "Active",
"LicenseId": "700000918156:pkg-1glehom7:cloudapp-992nqg9u:3988",
"LicenseType": "Standard",
"LicenseLevel": "Master"
},
"AdditionLicenses": [{
"LicenseMode": "Subscription",
"BillingMode": 1,
"CreateSource": "SN1719406931EJJ1E",
"AuthorizedCloudappRoleId": "700001833621",
"AuthorizedCloudappId": "cloudapp-992nqg9u",
"AuthorizedUserUin": "700001833621",
"LifeSpanUnit": "Y",
"LifeSpan": 365,
"SoftwarePackageId": "pkg-1glehom7",
"SoftwarePackageVersion": "0.0.1",
"AuthorizedSpecification": [{
"ParamKey": "version",
"ParamKeyName": "版本",
"ParamValue": "basic",
"ParamValueName": "基础版"
},
{
"ParamKey": "size",
"ParamKeyName": "规格",
"ParamValue": "100",
"ParamValueName": "100人规模"
}
],
"ProviderId": 100000071,
"ProviderUin": "700000918156",
"IssueDate": "2024-06-26T21:02:16+08:00",
"ActivationDate": "2024-06-26T21:02:19+08:00",
"ExpirationDate": "2389-06-26T21:02:19+08:00",
"LicenseStatus": "Active",
"LicenseId": "700000918156:pkg-1glehom7:cloudapp-992nqg9u:3988",
"LicenseType": "Standard",
"LicenseLevel": "Master"
}],
"Timestamp": "2025-08-26T10:25:43+08:00"
}`)
)

func main() {
ctx := context.Background()
l := &LicenseSvrImpl{}

// 许可证 加签过程,生成jwt
licenseDetail := DescribeLicenseDetail{}
err := json.Unmarshal(licenseDetailBuffer, &licenseDetail)
if err != nil {
panic(err)
}
// 云应用服务端根据许可信息进行加签
jwtToken, err := l.GenerateLicenseJWT(ctx, &licenseDetail)
if err != nil {
panic(err)
}
fmt.Println("jwt: ", jwtToken)

//客户端解签验证jwt过程
decodeData, err := l.VerifyLicenseJWT(ctx, jwtToken, publicKeyPEM)
if err != nil {
panic(err)
}
fmt.Println("decodeData ", decodeData)
}

// DescribeLicenseDetail License详情数据结构,用于JWT payload
type DescribeLicenseDetail struct {
MainLicense *LicenseInfo `json:"MainLicense"`
AdditionLicenses []LicenseInfo `json:"AdditionLicenses"`
Timestamp string `json:"Timestamp"` //生效时间 ISO8601
}

// SaleParam 销售规格相关:售卖参数
type SaleParam struct {
ParamKey string `json:"ParamKey"` // 规格key
ParamKeyName string `json:"ParamKeyName"` // 规格名称
ParamValue string `json:"ParamValue"` // 规格值
ParamValueName string `json:"ParamValueName"` // 规格值名称
ParamType string `json:"ParamType"` // 规格类型
}

type LicenseInfo struct {
// 售卖相关
LicenseMode string `json:"LicenseMode,omitempty"` //* 授权模式
BillingMode int32 `json:"BillingMode,omitempty"` // 售卖模式
CreateSource string `json:"CreateSource,omitempty"` // 创建来源标记:订单号
// 授权对象
AuthorizedCloudappRoleId *string `json:"AuthorizedCloudappRoleId,omitempty"` // 授权的软件运行时角色
AuthorizedCloudappId string `json:"AuthorizedCloudappId,omitempty"` // 云应用id
AuthorizedUserUin string `json:"AuthorizedUserUin,omitempty"`
AuthorizedUserSubUin string `json:"AuthorizedUserSubUin,omitempty"`
// 软件信息规格相关
LifeSpanUnit *string `json:"LifeSpanUnit,omitempty"` // capi用到非null字段
LifeSpan *int64 `json:"LifeSpan,omitempty"` // 授权的有效时长,单位s, capi用到非null字段
SoftwarePackageId string `json:"SoftwarePackageId,omitempty"`
SoftwarePackageName string `json:"SoftwarePackageName,omitempty"`
PackageIdentifier string `json:"PackageIdentifier,omitempty"`
SoftwarePackageVersion *string `json:"SoftwarePackageVersion,omitempty"` // capi用到非null字段
AuthorizedSpecification []*SaleParam `json:"AuthorizedSpecification,omitempty"` // json
// ISV
ProviderId uint64 `json:"ProviderId,omitempty"`
ProviderUin string `json:"ProviderUin,omitempty"`
ProviderName string `json:"ProviderName,omitempty"`
// license自身信息
IssueDate string `json:"IssueDate,omitempty"` //* 颁发时间
ActivationDate string `json:"ActivationDate,omitempty"` //* 激活时间
ExpirationDate string `json:"ExpirationDate,omitempty"` //* 过期时间
LicenseStatus string `json:"LicenseStatus,omitempty"` // 当前状态
LicenseId string `json:"LicenseId,omitempty"`
DeactivationDate string `json:"DeactivationDate,omitempty"` //* 失活时间
LicenseType string `json:"LicenseType,omitempty"` //[(validate.rules).string = {in: ["Standard","Development","Acceptance","Trial"]}];/** 授权模式:Standard正式版/Development开发版/Acceptance验收版/Trial体验版 */
// license 级别
LicenseLevel string `json:"LicenseLevel,omitempty"` //* license级别:Master主license/Child子license
}

type LicenseSvrImpl struct {
}

// GenerateLicenseJWT 生成带有DescribeLicenseDetail作为payload的JWT签名
func (l *LicenseSvrImpl) GenerateLicenseJWT(ctx context.Context, licenseDetail *DescribeLicenseDetail) (string, error) {
// privateKeyStr 是-----BEGIN PRIVATE KEY----- 开头的字符串
privateKey, err := parseRSAPrivateKey(privateKeyStr)
if err != nil {
fmt.Printf("failed to parse private key: %v", err)
return "", errors.New("invalid private key format")
}

// 创建JWT token
token := jwt.NewWithClaims(jwt.SigningMethodRS256, jwt.MapClaims{
"payload": licenseDetail,
"iat": time.Now().Unix(),
"exp": time.Now().Add(24 * time.Hour * 24 * 365 * 10).Unix(), // // 10年有效期
"iss": "license-service",
})

// 设置自定义header
token.Header["alg"] = "RS256"
token.Header["typ"] = "JWT"

// 生成JWT字符串
tokenString, err := token.SignedString(privateKey)
if err != nil {
fmt.Printf("failed to sign JWT token: %v", err)
return "", errors.New("failed to generate JWT token")
}

fmt.Printf("successfully generated JWT token")
return tokenString, nil
}

// parseRSAPrivateKey 解析PEM格式的RSA私钥
func parseRSAPrivateKey(privateKeyPEM string) (*rsa.PrivateKey, error) {
// 解码PEM格式
block, _ := pem.Decode([]byte(privateKeyPEM))
if block == nil {
return nil, errors.New("failed to decode PEM block containing private key")
}

// 解析私钥
var privateKey *rsa.PrivateKey
var err error

switch block.Type {
case "RSA PRIVATE KEY":
// PKCS#1格式
privateKey, err = x509.ParsePKCS1PrivateKey(block.Bytes)
case "PRIVATE KEY":
// PKCS#8格式
key, parseErr := x509.ParsePKCS8PrivateKey(block.Bytes)
if parseErr != nil {
return nil, parseErr
}
var ok bool
privateKey, ok = key.(*rsa.PrivateKey)
if !ok {
return nil, errors.New("not an RSA private key")
}
default:
return nil, errors.New("unsupported private key type")
}

if err != nil {
return nil, err
}

return privateKey, nil
}

// VerifyLicenseJWT 验证JWT签名并解析DescribeLicenseDetail
func (l *LicenseSvrImpl) VerifyLicenseJWT(ctx context.Context, tokenString string, publicKeyPEM string) (*DescribeLicenseDetail, error) {
// 解析公钥
publicKey, err := parseRSAPublicKey(publicKeyPEM)
if err != nil {
fmt.Printf("failed to parse public key: %v", err)
return nil, errors.New("invalid public key format")
}

// 解析并验证JWT token
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (interface{}, error) {
// 验证签名算法
if _, ok := token.Method.(*jwt.SigningMethodRSA); !ok {
return nil, errors.New("unexpected signing method")
}
return publicKey, nil
})

if err != nil {
fmt.Printf("failed to parse JWT token: %v", err)
return nil, errors.New("invalid JWT token")
}

if !token.Valid {
fmt.Printf("JWT token is not valid")
return nil, errors.New("invalid JWT token")
}

// 提取claims
claims, ok := token.Claims.(jwt.MapClaims)
if !ok {
fmt.Printf("failed to extract claims from JWT token")
return nil, errors.New("invalid JWT claims")
}

claimsBuffer, _ := json.Marshal(claims)

fmt.Printf("successfully verified JWT token %s", string(claimsBuffer))

// 提取license_detail
licenseDetailData, ok := claims["payload"]
if !ok {
fmt.Printf("license_detail not found in JWT claims")
return nil, errors.New("license_detail not found in JWT")
}

// 转换为DescribeLicenseDetail结构
licenseDetailBytes, err := json.Marshal(licenseDetailData)
if err != nil {
fmt.Printf("failed to marshal license detail: %v", err)
return nil, errors.New("failed to process license detail")
}

var licenseDetail DescribeLicenseDetail
if err := json.Unmarshal(licenseDetailBytes, &licenseDetail); err != nil {
fmt.Printf("failed to unmarshal license detail: %v", err)
return nil, errors.New("failed to parse license detail")
}

fmt.Printf("successfully verified and extracted license detail from JWT")
return &licenseDetail, nil
}

// parseRSAPublicKey 解析PEM格式的RSA公钥
func parseRSAPublicKey(publicKeyPEM string) (*rsa.PublicKey, error) {
// 解码PEM格式
block, _ := pem.Decode([]byte(publicKeyPEM))
if block == nil {
return nil, errors.New("failed to decode PEM block containing public key")
}

// 解析公钥
var publicKey *rsa.PublicKey
var err error

switch block.Type {
case "RSA PUBLIC KEY":
// PKCS#1格式
publicKey, err = x509.ParsePKCS1PublicKey(block.Bytes)
case "PUBLIC KEY":
// PKCS#8格式
key, parseErr := x509.ParsePKIXPublicKey(block.Bytes)
if parseErr != nil {
return nil, parseErr
}
var ok bool
publicKey, ok = key.(*rsa.PublicKey)
if !ok {
return nil, errors.New("not an RSA public key")
}
default:
return nil, errors.New("unsupported public key type")
}

if err != nil {
return nil, err
}

return publicKey, nil
}

//客户端解签验证jwt过程
//decodeData, err := l.VerifyLicenseJWT(ctx, jwtToken, publicKeyPEM)
//其中 jwtToken为云capi返回的Token内容,publicKeyPEM为云应用提供的公钥信息,固定不变。

关于 License 的常见问题

接入 License 是必须的吗?是否可以自己生成和校验 License?

是必须的,对接之后用户的软件计费可以跟授权做好线上联动,例如:
免费试用转付费,只是 License 更新,用户无需更新实例,可以保留数据。
续费场景,会自动刷新 License,无需再找服务商线下更新。
升降配场景,也会自动刷新 License,实例无需更换。
如果软件本身是免费软件,无需校验软件授权,则可以无需关注 License 的接入。

服务商可以感知 License 的颁发吗?

暂时无法感知。

应该在什么时机校验 License?

每个软件的情况不同,一般会分成两类:
定时校验:例如每小时校验一次,或者一天校验一次。
关键接口校验:例如在要检查配额相关规格时校验。请注意做好缓存管理,如果高频发起校验会触发云 API 限频。

如果校验接口失败如何处理?

为了让客户体验无损,建议软件 License 对接做好校验接口的容错处理,做好重试能力。若 License 校验接口出错,建议先沿用之前的校验结果,直到重试成功拉到最新的 License。

用于验证签名的公钥如何保存?

公钥用于验证 License 信息的签名,确保 VerifyLicense 中的 License 信息是由腾讯云发出且未经中间人篡改。供应商在校验 License 签名时,应该确保加载的公钥为 License 签名验证方法 中提供的公钥,否则可能导致 License 信息被中间人攻击和篡改。因此,不应该将公钥暴露在容易被修改的配置文件中或其他类似的独立配置中。如果需离线保存在软件客户端,至少应保障公钥配置在经过混淆/加固的客户端可执行文件中。