云应用 对接 License

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

相关产品
云应用 产品页,含版本规格、价格与购买入口。

一、License 有什么作用?

License 是对软件终端用户的一个许可,授权内容包括:

·使用期限:如果软件过期,软件进程可以拒绝服务或提供降级服务。

·规格:软件可以基于授权的规格运行,如限制并发数、用户数等。

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

·顺畅:下单即自动颁发 License,无需等待。

·灵活:支持续期、升降配等灵活需求。

·可信:腾讯云颁发,基于角色绑定的实例关联 License,难以伪造和复制。

三、License 交易与发货

服务商需要为应用包对应的商品定义规格,定义的规格会在用户选购时确定下来,作为 License 的规格。

License 按照客户购买的软件规格颁发,颁发的 License 包含下列信息:

·关联的软件包

·关联的软件实例

·授权的规格信息

·授权模式:永久 (Permanent)/订阅 (Subscription)/验收 (Accept)

·授权状态:已颁发待激活 (Issued)/有效 (Active)/隔离 (Isolated)/失效 (Destroyed)

·其他信息,如颁发时间、激活时间。

四、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"}

这里介绍一些主要字段:(关于 License 每个字段的详细信息可参见 License 结构体 的说明)

·LicenseStatus:授权状态,目前可能的值如下表,服务商应使用 LicenseStatus 字段来判断授权是否有效。

枚举值 说明
Issued 已颁发,等待激活。一般来说,如果软件已经在运行,不会出现该状态。
Active 授权在有效期内,这是软件运行期间最常见的状态。
Isolated 授权已隔离。有截止日期的授权,当用户授权到期时,先进入此状态,用户可以去续费,超过7天不续费则授权进入 Destroyed 状态。
Destroyed 授权已失效/销毁。用户如果退货软件,则授权会自动失效。历史上一些比较旧的 License 该状态为 Expired,如果收到状态为 Expired 等同 Destroyed。

·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—–MIICIjANBgkqhkiG9w0BAQEFAAOCAg8AMIICCgKCAgEA5EQJv0v0f1hrB7NIGwXihFmw+ugrJi7gnmOqaiYp7oFrzf4RSJ3DPr4K01F+CrjTdCPghDLq4fsKVxxHAjNjnbstbVlHZEVOfzQ4umeocJpxWFuyKyGwHv+obnEZ/4689fxVpTbG3IbUTGn1TRJs9s3xM8nFd6LLAoh1Hhrdf2D4mLRToLvtRVat1l8fH3gsM+RoG4L4h+3hghn4bpyAna2MBFDzvmBeVGUVzqRjSvUaexd+Bo1wTsllAdqjP6MTlAAWGmIAMStBSRS+YpRQxjhE9Rdb9zTE54q3Ui7UJg5BMe+R3kVrBINbnT6Va8/Lzjg4+THdpMTLr6fY6ObF7r+i/924XgxqQOFvGaFJSyjXTORnK42T5YRr5TSqxr9CzhybPcdRvws2GdAq9f558whj1DYcgg0X8kR06Iu+/9Mk/CqssdrZ8LYDwSkDI8S/RwpdNQfifUa8wyY0R2xNnY+bnkrjvGPz7Rokr0Ki9/orT9i4yQWA1mMCDi2vcP+oXqrEs7XAyH85gDSzuTp+dXbTYPZpIAK6Kejwssw1IE1lGNP4PNQZk9EXU7+vB1csz4GUao7Mr7F5VbrGKvTsaGxbIc6b0MDWMEFA7L/CWC9UtReWCk1MYwJzy105bWU/VBpYJPmyZTFRQaY2MEH4fnsK2+jtZ1IYIQw/YsHU6CcCAwEAAQ==—–END PUBLIC KEY—–

接口 cloudapp.DescribeLicense 的返回结构如下:

{ "Response": { "RequestId": "cd15813b-adff-460e-b9fc-64579e96412d", "Token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJleHAiOjkzMzU2MTc2OTAsImlhdCI6MTc2Njk3NzY5MCwiaXNzIjoibGljZW5zZS1zZXJ2aWNlIiwicGF5bG9hZCI6eyJNYWluTGljZW5zZSI6eyJMaWNlbnNlTW9kZSI6IlN1YnNjcmlwdGlvbiIsIkJpbGxpbmdNb2RlIjoxLCJDcmVhdGVTb3VyY2UiOiJTTjE3MTk0MTc0NjhHR1JLTSIsIkF1dGhvcml6ZWRDbG91ZGFwcFJvbGVJZCI6IjEwMDAwMDAzMjAzNiIsIkF1dGhvcml6ZWRDbG91ZGFwcElkIjoiY2xvdWRhcHAtYmdpOHhzZnMiLCJBdXRob3JpemVkVXNlclVpbiI6IjcwMDAwMTEzNzA5MSIsIkxpZmVTcGFuVW5pdCI6IlkiLCJMaWZlU3BhbiI6MSwiU29mdHdhcmVQYWNrYWdlSWQiOiJwa2ctMWdsZWhvbTciLCJTb2Z0d2FyZVBhY2thZ2VWZXJzaW9uIjoiMC4wLjEiLCJBdXRob3JpemVkU3BlY2lmaWNhdGlvbiI6W3siUGFyYW1LZXkiOiJ2ZXJzaW9uIiwiUGFyYW1LZXlOYW1lIjoi54mI5pysIiwiUGFyYW1WYWx1ZSI6ImJhc2ljIiwiUGFyYW1WYWx1ZU5hbWUiOiLln7rnoYDniYgifSx7IlBhcmFtS2V5Ijoic2l6ZSIsIlBhcmFtS2V5TmFtZSI6IuinhOagvCIsIlBhcmFtVmFsdWUiOiIxMDAiLCJQYXJhbVZhbHVlTmFtZSI6IjEwMOS6uuinhOaooSJ9XSwiUHJvdmlkZXJJZCI6MTAwMDAwMDcxLCJQcm92aWRlclVpbiI6IjcwMDAwMDkxODE1NiIsIklzc3VlRGF0ZSI6IjIwMjQtMDYtMjZUMjM6NTc6NTMrMDg6MDAiLCJMaWNlbnNlU3RhdHVzIjoiSXNzdWVkIiwiTGljZW5zZUlkIjoiNzAwMDAxMTM3MDkxOnBrZy0xZ2xlaG9tNzoxOjUwOTgiLCJMaWNlbnNlVHlwZSI6IlN0YW5kYXJkIiwiTGljZW5zZUxldmVsIjoiTWFzdGVyIn0sIkFkZGl0aW9uTGljZW5zZXMiOlt7IkxpY2Vuc2VNb2RlIjoiU3Vic2NyaXB0aW9uIiwiQmlsbGluZ01vZGUiOjEsIkNyZWF0ZVNvdXJjZSI6IlNOMTcxOTQwOTA0MU5XMVM4IiwiQXV0aG9yaXplZENsb3VkYXBwUm9sZUlkIjoiMTAwMDAwMDMyMDM2IiwiQXV0aG9yaXplZENsb3VkYXBwSWQiOiJjbG91ZGFwcC1iZ2k4eHNmcyIsIkF1dGhvcml6ZWRVc2VyVWluIjoiNzAwMDAwOTE4MTU2IiwiTGlmZVNwYW5Vbml0IjoiWSIsIkxpZmVTcGFuIjozNjUsIlNvZnR3YXJlUGFja2FnZUlkIjoicGtnLTFnbGVob203IiwiU29mdHdhcmVQYWNrYWdlVmVyc2lvbiI6IjAuMC4xIiwiQXV0aG9yaXplZFNwZWNpZmljYXRpb24iOlt7IlBhcmFtS2V5IjoidmVyc2lvbiIsIlBhcmFtS2V5TmFtZSI6IueJiOacrCIsIlBhcmFtVmFsdWUiOiJiYXNpYyIsIlBhcmFtVmFsdWVOYW1lIjoi5Z-656GA54mIIn0seyJQYXJhbUtleSI6InNpemUiLCJQYXJhbUtleU5hbWUiOiLop4TmoLwiLCJQYXJhbVZhbHVlIjoiMTAwIiwiUGFyYW1WYWx1ZU5hbWUiOiIxMDDkurrop4TmqKEifV0sIlByb3ZpZGVySWQiOjEwMDAwMDA3MSwiUHJvdmlkZXJVaW4iOiI3MDAwMDA5MTgxNTYiLCJJc3N1ZURhdGUiOiIyMDI0LTA2LTI2VDIxOjM3OjI5KzA4OjAwIiwiQWN0aXZhdGlvbkRhdGUiOiIyMDI0LTA2LTI2VDIxOjM3OjMxKzA4OjAwIiwiRXhwaXJhdGlvbkRhdGUiOiIyMzg5LTA2LTI2VDIxOjM3OjMxKzA4OjAwIiwiTGljZW5zZVN0YXR1cyI6IkFjdGl2ZSIsIkxpY2Vuc2VJZCI6IjcwMDAwMDkxODE1Njpwa2ctMWdsZWhvbTc6Y2xvdWRhcHAtYmdpOHhzZnM6NTYzMyIsIkxpY2Vuc2VUeXBlIjoiU3RhbmRhcmQiLCJMaWNlbnNlTGV2ZWwiOiJDaGlsZCJ9XSwiVGltZXN0YW1wIjoiMjAyNS0xMi0yOVQxMTowODoxMCswODowMCJ9fQ.iTJFsWI9qRsn1O5QLHRyk855TCFWPKjqapMETWiCofoxaxTvggR7paB_tSftBtz8mjCZx9PnzKCX6uqbnPccw1ah5ACDP1bmffd5eLuKIsBtsqwN5mueTR0BgJRfnckphFxp3RqdhteLRZbAq6pLHDHD9kqWButPLIbDuSpGV0t5xy70WMbGs76yaPXLAhmuetUtePS-iIlP4lxMqRu9TyWZQZrxRQ-j12lMYluQ-tOGFDaG3-eR7HNabVLNaxR_7H9Rz4LufybX96l_3wFIr3iAwpXeOAog9QLz6niawAhNW9Hddiagg4IrIxfBV3mD1ECQGgAsbKB3NZMRgB1SZM_Il_3sVVUbN53WW4PM0jfrv8b41hfpUFQlCdeE-brpfseBBBUYJv0kF2SR7eL85Ajef1KaN24X9z-XV1jAjt0rSJGh8Lvkf1KA6odNLnObje_JfTuQ6Nu-hmpXZmFu90ZvjX2L_wCEuMDOeGVLahhbL02_H0tJ2jX3eFiM_urFoknFJBT_dUHuxlVS1J3ecqrDbOaK4pTNDfA1r-WPDCbPVGLeTMgmLySqRrOF54PDq2qU16WKmqRrvV6sEEliB0s2aeI7wsR-lACmGMPVrAUJ7HGoncZDJ7gxT-ZqYsl8JO8lMdPNdzK1eWfnqVp5T5Du0yxUpkBGOQ-LLK1_r2k" }}

字段说明如下:

·Response.RequestId 为当前请求的唯一 ID。

·Response.Token 为 License 信息按 JWT 协议生成的 Token,该 Token 可使用前述公钥进行签名验证。

其中 Token 中的有效数据负载明文结构如下:

·payload.MainLicense 为主 License 信息(区别于增购 License )。

·payload.AdditionLicenses 为增购 License 信息,授权软件支持增购项的情况下才有该字段,且默认情况下不返回增购 License 信息,如需返回,请参考 cloudapp.DescribeLicense 接口的 Filter 参数约定指定同时返回增购 License 信息。

{ "exp": 9335617690, "iat": 1766977690, "iss": "license-service", "payload": { "AdditionLicenses": [{ "ActivationDate": "2024-06-26T21:37:31+08:00", "AuthorizedCloudappId": "cloudapp-bgi8xsfs", "AuthorizedCloudappRoleId": "100000032036", "AuthorizedSpecification": [{ "ParamKey": "version", "ParamKeyName": "版本", "ParamValue": "basic", "ParamValueName": "基础版" }, { "ParamKey": "size", "ParamKeyName": "规格", "ParamValue": "100", "ParamValueName": "100人规模" }], "AuthorizedUserUin": "700000918156", "BillingMode": 1, "CreateSource": "SN1719409041NW1S8", "ExpirationDate": "2389-06-26T21:37:31+08:00", "IssueDate": "2024-06-26T21:37:29+08:00", "LicenseId": "700000918156:pkg-1glehom7:cloudapp-bgi8xsfs:5633", "LicenseLevel": "Child", "LicenseMode": "Subscription", "LicenseStatus": "Active", "LicenseType": "Standard", "LifeSpan": 365, "LifeSpanUnit": "Y", "ProviderId": 100000071, "ProviderUin": "700000918156", "SoftwarePackageId": "pkg-1glehom7", "SoftwarePackageVersion": "0.0.1" }], "MainLicense": { "AuthorizedCloudappId": "cloudapp-bgi8xsfs", "AuthorizedCloudappRoleId": "100000032036", "AuthorizedSpecification": [{ "ParamKey": "version", "ParamKeyName": "版本", "ParamValue": "basic", "ParamValueName": "基础版" }, { "ParamKey": "size", "ParamKeyName": "规格", "ParamValue": "100", "ParamValueName": "100人规模" }], "AuthorizedUserUin": "700001137091", "BillingMode": 1, "CreateSource": "SN1719417468GGRKM", "IssueDate": "2024-06-26T23:57:53+08:00", "LicenseId": "700001137091:pkg-1glehom7:1:5098", "LicenseLevel": "Master", "LicenseMode": "Subscription", "LicenseStatus": "Issued", "LicenseType": "Standard", "LifeSpan": 1, "LifeSpanUnit": "Y", "ProviderId": 100000071, "ProviderUin": "700000918156", "SoftwarePackageId": "pkg-1glehom7", "SoftwarePackageVersion": "0.0.1" }, "Timestamp": "2025-12-29T11:08:10+08:00" }}

七、代码示例

本节代码示例展示了如何在开发联调阶段完成 License JWT 协议的开发自测。

注意:警告:

注意:示例代码中提供了生成密钥对的工具,方便接入时本地生成公私钥进行 JWT 签名和验签测试,正式通过 DescribeLicense 接口联调时,需切换到前文提供的正式公钥。

package mainimport ( "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 payloadtype 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签名并解析DescribeLicenseDetailfunc (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?

每个软件的情况不同,一般会分成两类:

·定时校验:例如每小时校验一次,或者一天校验一次。

·关键接口校验:例如在要检查配额相关规格时校验。请注意做好缓存管理,如果高频发起校验会触发云 API 限频。

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

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

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

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

赞 (0)
上一篇 4天前
下一篇 4天前