diff --git a/.github/workflows/sdk-release-checksums.yml b/.github/workflows/sdk-release-checksums.yml new file mode 100644 index 0000000..d1e8a44 --- /dev/null +++ b/.github/workflows/sdk-release-checksums.yml @@ -0,0 +1,39 @@ +# 手动触发:打包 SDK + Native(.so) 并生成 SHA256SUMS,供 GitHub Release 上传 +name: sdk-release-checksums + +on: + workflow_dispatch: + +jobs: + checksums: + runs-on: ubuntu-latest + steps: + - uses: actions/checkout@v4 + - uses: actions/setup-java@v4 + with: + distribution: temurin + java-version: "17" + cache: maven + - name: Install native build deps + run: | + sudo apt-get update + sudo apt-get install -y build-essential cmake + - name: Build native (.so) + run: | + cmake -S native -B native/build -DCMAKE_BUILD_TYPE=Release -DCRAFTLABS_BUILD_JNI=ON + cmake --build native/build --parallel + - name: Maven package (SDK jars) + run: mvn -f java/pom.xml -B -DskipTests package + - name: Generate SHA256SUMS + run: | + chmod +x scripts/sdk-release-checksums.sh + ./scripts/sdk-release-checksums.sh --no-mvn --output dist/sdk-release --native-path "${{ github.workspace }}/native/build" + - name: Upload artifacts + uses: actions/upload-artifact@v4 + with: + name: sdk-release-${{ github.ref_name }} + path: | + dist/sdk-release/ + java/craftlabs-auth-core/target/*.jar + java/craftlabs-auth-bitanswer/target/*.jar + java/craftlabs-auth-selfhosted/target/*.jar diff --git a/docs/auth-config.md b/docs/auth-config.md new file mode 100644 index 0000000..20eb473 --- /dev/null +++ b/docs/auth-config.md @@ -0,0 +1,35 @@ +# `initialize(config_json)` 配置说明 + +`AuthProvider.initialize(String)` 与 C API `auth_initialize(const char* config_json)` 共用同一 JSON 语义,版本由 **`schemaVersion: 1`** 锁定。 + +## 规范与示例 + +| 资源 | 路径 | +|------|------| +| JSON Schema | `schemas/craftlabs-auth-config.schema.json` | +| 码头 / 集团拓扑示例 | `examples/config/wharf.bitanswer.json` | +| 学校 / 边设备示例 | `examples/config/school.bitanswer.json` | +| 流动人口 / 项目示例 | `examples/config/floating.bitanswer.json` | +| 学校 / 自研 HTTP 示例 | `examples/config/school.selfhosted.json` | + +## Java 解析与校验 + +```java +import cn.craftlabs.auth.config.AuthConfig; +import cn.craftlabs.auth.config.AuthConfigException; +import cn.craftlabs.auth.config.AuthConfigs; + +AuthConfig cfg = AuthConfigs.parse(jsonString); +// 再交给实现(与校验使用同一字符串即可) +provider.initialize(jsonString); +``` + +`AuthConfig` 提供 `bitanswerFeatureId(String logicalKey)` 等辅助方法,供后续在 `hasFeature` 与比特 `QueryFeature` 之间做映射。 + +## 场景字段摘要 + +- **wharf**:`wharf.topology`(`cloud` / `group` / `local_float`)等与集中授权拓扑相关的**提示**;实际连网方式仍以 `bitanswer.url` 为准。 +- **school**:`school.edgeDeviceId` 等用于运营对账;是否写入比特自定义字段由实现阶段决定。 +- **floating**:**必填** `floating.projectId`;表达「按项目」授权锚点,与商务合同、控制台业务模板需一致。 + +后续优化(错误码、OIDC、集团占点策略等)在保持 `schemaVersion` 或递增版本的前提下扩展字段即可。 diff --git a/docs/c.md b/docs/c.md index 7d0874e..e8b734f 100644 --- a/docs/c.md +++ b/docs/c.md @@ -1,12 +1,11 @@ # 比特授权云 · C 语言接口定义(离线摘录) -> 本文档由官网页面离线摘录并整理排版,便于本地检索。官方地址: +> 本文档由官网页面离线摘录并整理排版,便于本地检索。官方地址:[https://doc.bitanswer.cn/docs/client-api/c-interface-definitions-v2/](https://doc.bitanswer.cn/docs/client-api/c-interface-definitions-v2/) --- ## 认证 - ### Bit_Login / Bit_LoginEx ```c @@ -26,6 +25,7 @@ BIT_STATUS Bit_LoginEx( BIT_HANDLE *pHandle, LOGIN_MODE mode); ``` + 授权登录。初始化运行环境,获取操作句柄。必须在除升级函数之外的其它操作前执行。根据登录模式的不同可能需要连接授权服务器。 Bit_Login 等价于 Bit_LoginEx(…, featureId=0,szReserved=NULL, …),当需要登录包含指定特征项的授权时才需要调用Bit_LoginEx。 @@ -50,6 +50,7 @@ Bit_Login 等价于 Bit_LoginEx(…, featureId=0,szReserved=NULL, …),当 lic:///tmp,lic:///data,bit://127.0.0.1:8273 /tmp,/data,127.0.0.1:8273 // 等价第1条 ``` + > 注意:该接口支持通过[配置文件](https://doc.bitanswer.cn/docs/client-api/examples-of-using-the-sdk/#%E9%85%8D%E7%BD%AE%E6%96%87%E4%BB%B6)和[环境变量](https://doc.bitanswer.cn/docs/client-api/examples-of-using-the-sdk/#%E7%8E%AF%E5%A2%83%E5%8F%98%E9%87%8F)来设置该值,如果同时设置优先级为:环境变量 > API传入 > 配置文件。 - **szSN** - [IN] 授权码(SN)字符串。如果为空(空串或NULL)则尝试寻找所有当前本机可用的SN。 @@ -57,10 +58,10 @@ lic:///tmp,lic:///data,bit://127.0.0.1:8273 | **类型** | **格式** | **说明** | | -------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------ | -| **授权码** | 比特授权云平台产生的SN 示例:`JNLTGZSE********` | 检查指定SN的License | +| **授权码** | 比特授权云平台产生的SN 示例:`JNLTGZSE******`** | 检查指定SN的License | | **BIT-ID(BIT-ID硬件,类似于加密的USB设备)** | 格式:#`<序号>` 或 #bitid:`<序号>` 说明:`<序号>` 为 BIT-ID 的索引号,从 0 开始递增 示例:#0、#1、#`bitid:0、#bitid:1` | 使用BIT-ID授权 | | **激活口令(帐号授权密码或SN激活口令)** | <`xxx`> 通过“<>”包裹起来,中间部分为密码 示例:<1234>, <1233>65447> | 激活口令,是指授权码激活时,需要输入的一种口令,激活后不再需要,目前仅支持浮动授权 使用帐号授权时保存的是帐号授权的密码(已经不推荐),其它授权代表激活口令 | -| **既输入SN又输入激活口令** | `xxx`,授权码后边紧跟“<>” 示例:`JNLTGZSE********`<123> | | +| **既输入SN又输入激活口令** | `xxx`,授权码后边紧跟“<>” 示例:`JNLTGZSE******`**<123> | | - **featureId** - [IN] 登录授权所需要包含的特征项ID。 @@ -73,6 +74,7 @@ lic:///tmp,lic:///data,bit://127.0.0.1:8273 ``` + - **pApplicationData** - [IN] 产品识别码,在Bitanswer SDK头文件里。 - **pHandle** - [OUT] 通过Login函数返回的上下文句柄。 - **mode** - [IN] 登录模式,按位操作。 @@ -154,6 +156,7 @@ BIT_STATUS Bit_LoginByToken( BIT_UCHAR *pApplicationData, BIT_HANDLE *pHandle) ``` + 帐号授权的登录接口。仅支持两种认证方式,这里是使用比特授权云平台自己产生的access_token进行认证。 ### 参数 @@ -182,6 +185,7 @@ BIT_STATUS Bit_LoginByTokenEx( BIT_UCHAR *pApplicationData, BIT_HANDLE *pHandle) ``` + 帐号授权的登录接口。仅支持两种认证方式,这里是使用OIDC协议产生的id_token认证。 ### 参数 @@ -213,6 +217,7 @@ BIT_STATUS Bit_LoginByPassword( BIT_UCHAR *pApplicationData, BIT_HANDLE *pHandle); ``` + 通过用户名和密码登录帐号授权。 ### 参数 @@ -231,6 +236,7 @@ BIT_STATUS Bit_LoginByPassword( BIT_STATUS Bit_Logout ( BIT_HANDLE handle) ``` + 此函数用于释放上下文句柄,退出登录状态,与Login相关接口一一对应。 ### 参数 @@ -247,6 +253,7 @@ BIT_STATUS Bit_Revoke ( BIT_CHAR *pRevocationInfo, BIT_UINT32 *pRevocationInfoSize) ``` + 从客户端迁出已激活的浮动授权码。授权码迁出后,可以用于其它的客户端。根据输入参数的不同,本函数可用于在线或离线迁出。 ### 参数 @@ -264,6 +271,7 @@ BIT_STATUS Bit_RemoveSn ( BIT_PCSTR szSN, BIT_UCHAR *pApplicationData) ``` + 删除指定授权码在本机的授权数据。 ### 参数 @@ -278,6 +286,7 @@ BIT_STATUS Bit_Heartbeat( BIT_HANDLE handle, BIT_UINT32 *pReconnectsNum) ``` + 手动心跳,可以无限次调用,10s只会触发一次。 注:当自动心跳停止后,调用该接口可以尝试恢复自动心跳。 @@ -298,6 +307,7 @@ BIT_STATUS Bit_SessionControl( BIT_CHAR *pValue, BIT_UINT32 *pValueLen) ``` + 控制或设置session的信息。具体使用场景可参考《浏览器并发控制》文档。 ### 参数 @@ -325,6 +335,7 @@ BIT_STATUS Bit_SetSessionState ( BIT_UINT32 state, BIT_VOID *pReserved) ``` + 设置客户端的状态为空闲状态或繁忙状态或激活状态。 ### 参数 @@ -358,7 +369,6 @@ if (用户正在操作) { ## 激活升级 - ### Bit_UpdateOnline ```c @@ -367,6 +377,7 @@ BIT_STATUS Bit_UpdateOnline ( BIT_PCSTR szSN, BIT_UCHAR *pApplicationData) ``` + 此函数用于与授权服务器在线连接,自动完成本地授权的升级操作。本函数需要进行网络连接。 ### 参数 @@ -395,6 +406,7 @@ BIT_STATUS Bit_GetRequestInfo ( BIT_CHAR *pRequestInfo, BIT_UINT32 *pRequestInfoSize) ``` + 获取当前运行环境的升级请求码,用于发起本地授权激活及升级请求。如果第一次调用返回错误码260,说明传入的pRequestInfoSize太小,可传入返回的pRequestInfoSize再重新调用一次。 ### 参数 @@ -442,6 +454,7 @@ BIT_STATUS Bit_ApplyUpdateInfo( BIT_CHAR *pReceipt, BIT_UINT32 *pReceiptSize) ``` + 应用升级码完成本地授权激活或升级。本函数必须在获取请求码的同一环境下执行。 ### 参数 @@ -475,6 +488,7 @@ BIT_STATUS Bit_ApplyUpdateInfoEx( BIT_CHAR *pReceipt, BIT_UINT32 *pReceiptSize); ``` + 应用升级码完成远程集团授权激活或升级。一般不建议使用。 ### 参数 @@ -495,6 +509,7 @@ BIT_STATUS Bit_GetUpdateInfo ( BIT_CHAR *pUpdateInfo, BIT_UINT32 *pUpdateInfoSize) ``` + 使用请求码与授权服务器进行连接,获取升级码。本函数需要进行网络连接。 ### 参数 @@ -508,7 +523,6 @@ BIT_STATUS Bit_GetUpdateInfo ( ## 特征项操作 - ### Bit_BatchBegin ```c @@ -516,6 +530,7 @@ BIT_STATUS Bit_BatchBegin( BIT_HANDLE handle, BIT_UINT32 mode) ``` + 开启批量Query模式,调用此API后调用的所有Query相关API,全部由Bit_BatchEnd接口批量提交连接集团服务。 ### 参数 @@ -538,6 +553,7 @@ BIT_STATUS Bit_BatchEnd( BIT_UINT32 *pResultList, BIT_UINT32 *pResultListSize) ``` + 批量提交Query请求。 ### 参数 @@ -577,6 +593,7 @@ BIT_STATUS Bit_QueryFeature ( BIT_UINT32 featureId, BIT_UINT32 *pCapacity) ``` + 开发商可以对软件的某个功能进行单独授权,当程序运行该模块时,可以通过该接口检查是否可用。 对于单机授权:只检查特征项是否可用。 @@ -610,6 +627,7 @@ BIT_STATUS Bit_ReleaseFeature ( BIT_UINT32 featureId, BIT_UINT32 *pCapacity) ``` + 释放所占用的用户数,该函数和Bit_QueryFeature一一对应。 ### 参数 @@ -643,6 +661,7 @@ BIT_STATUS Bit_QueryFeatureEx ( BIT_UINT32 *pCapacity, BIT_PCSTR xmlScope) ``` + 集团授权专用,该接口支持占用指定用户数,支持通过特征项版本进行检查。 ### 参数 @@ -702,6 +721,7 @@ BIT_STATUS Bit_ReleaseFeatureEx ( BIT_UINT32 *pCapacity, BIT_PCSTR xmlScope) ``` + 集团授权专用,释放指定的用户数。该接口与Bit_QueryFeatureEx对应。 ### 参数 @@ -740,6 +760,7 @@ BIT_STATUS Bit_QueryFeatureEx2 ( BIT_PCSTR xmlScope, BIT_TICKET *pTicket) ``` + 可以使用“特征项名称 + 特征项版本”进行用户数占用,支持队列模式。 对于单机授权:只检查特征项是否可用。 @@ -796,6 +817,7 @@ BIT_STATUS Bit_ReleaseFeatureEx2 ( BIT_TICKET ticket, BIT_UINT32 consumed) ``` + 释放ticket所占用的用户数。该函数和Bit_QueryFeatureEx2一一对应。 ### 参数 @@ -811,6 +833,7 @@ if (status == BIT_SUCCESS) { // 释放ticket占用的用户数 } ``` + **特征项Query相关接口比较** @@ -830,6 +853,7 @@ BIT_STATUS Bit_GetFeatureInfo2 ( BIT_PCSTR xmlScope, BIT_INT32 *pExpired) ``` + 检查特征项是否存在,不会占用授权码或特征项的用户数,获取特征项的剩余有效期。 ### 参数 @@ -845,6 +869,7 @@ BIT_STATUS Bit_GetFeatureInfo2 ( ``` + - **pExpired** - [OUT] 返回该特征项的剩余有效期,正数代表剩余有效期,负数代表过期天数,36500代表永久有效。 ### Bit_GetFeatureInfoEx2 @@ -857,6 +882,7 @@ BIT_STATUS Bit_GetFeatureInfoEx2( BIT_CHAR *pFeatureInfo, BIT_UINT32 *pFeatureInfoSize); ``` + 获取指定feature的信息,以XML格式返回。 ### 参数 @@ -872,6 +898,7 @@ BIT_STATUS Bit_GetFeatureInfoEx2( ``` + - **pFeatureInfo** - [OUT] feature信息存储区地址。 - **pFeatureInfoSize** - [IN/OUT] 会话存储区大小。 @@ -884,6 +911,7 @@ BIT_STATUS Bit_GetTicketInfo( BIT_CHAR *pXmlInfo, BIT_UINT32 *pSize) ``` + 获取ticket信息。 ### 参数 @@ -946,6 +974,7 @@ BIT_STATUS Bit_ConvertFeature( BIT_UINT32 para4, BIT_UINT32 *pResult); ``` + 使用“算法”类型的特征项对输入参数进行变换操作,得到唯一对应的4字节结果。 ### 参数 @@ -981,6 +1010,7 @@ BIT_STATUS Bit_EncryptFeature( BIT_VOID *pCipherBuffer, BIT_UINT32 dataBufferSize); ``` + 使用“密钥”类型的特征项对输入的明文进行加密,返回密文结果。 ### 参数 @@ -1013,6 +1043,7 @@ BIT_STATUS Bit_DecryptFeature( BIT_VOID *pPlainBuffer, BIT_UINT32 dataBufferSize); ``` + 使用“密钥”类型的特征项对输入的密文进行解密,返回明文结果。 ### 参数 @@ -1043,6 +1074,7 @@ BIT_STATUS Bit_ReadFeature( BIT_UINT32 featureId, BIT_UINT32 *pFeatureValue); ``` + 读取特征项的数据内容,可用于“只读”和“读写”特征类型。 ### 参数 @@ -1059,6 +1091,7 @@ BIT_STATUS Bit_WriteFeature( BIT_UINT32 featureId, BIT_UINT32 featureValue); ``` + 更新“读写”类型的特征项的数据内容。 ### 参数 @@ -1069,7 +1102,6 @@ BIT_STATUS Bit_WriteFeature( ## 配置项操作 - ### Bit_GetDataItem ```c @@ -1079,6 +1111,7 @@ BIT_STATUS Bit_GetDataItem ( BIT_VOID *pDataItemValue, BIT_UINT32 *pDataItemValueSize) ``` + 读取指定的配置项数据。 ### 参数 @@ -1097,6 +1130,7 @@ BIT_STATUS Bit_SetDataItem ( BIT_VOID *pDataItemValue, BIT_UINT32 dataItemValueSize) ``` + 创建或更新配置项。如果相同名称的配置项存在,则会更新其中的数据;否则将添加新的授权码配置项。 ### 参数 @@ -1113,6 +1147,7 @@ BIT_STATUS Bit_GetDataItemNum ( BIT_HANDLE handle, BIT_UINT32 *pNum) ``` + 此函数用于获取可访问配置项的数量,一般用于配置项的枚举操作。 ### 参数 @@ -1129,6 +1164,7 @@ BIT_STATUS Bit_GetDataItemName ( BIT_CHAR *pDataItemName, BIT_UINT32 *pDataItemNameSize) ``` + 根据配置项索引获取其名称,一般用于配置项的枚举操作。 ### 参数 @@ -1145,6 +1181,7 @@ BIT_STATUS Bit_RemoveDataItem ( BIT_HANDLE handle, BIT_PCSTR szDataItemName) ``` + 删除指定的配置项。该操作无法删除通过控制台设置的产品配置项或模版配置项。 ### 参数 @@ -1154,7 +1191,6 @@ BIT_STATUS Bit_RemoveDataItem ( ## 信息获取 - ### Bit_GetSessionInfo ```c @@ -1164,6 +1200,7 @@ BIT_STATUS Bit_GetSessionInfo ( BIT_CHAR *pSessionInfo, BIT_UINT32 *pSessionInfoSize) ``` + 获取当前会话信息,以字符串形式返回。根据获取的内容不同,返回结果可能是XML格式或非XML格式。返回数据中的日期项已根据客户端的本地时区进行调整。如果Login时未指定SN,返回串为当前系统所有可用SN的综合结果。 当函数返回BIT_ERR_BUFFER_SMALL时,pSessionInfoSize保存的是实际数据的大小。 @@ -1242,6 +1279,7 @@ BIT_STATUS Bit_GetInfo ( BIT_CHAR *pInfo, BIT_UINT32 *pInfoSize) ``` + 获取本地授权信息。 如果要获取集团授权的信息,则szSN输入“`@bit://ip:port`”,会获取集团授权信息(仅支持type=BIT_INFO_SN)。 @@ -1281,6 +1319,7 @@ BIT_STATUS Bit_GetServerInfo( BIT_CHAR *pServerInfo, BIT_UINT32 *pServerInfoSize); ``` + 获取集团服务的License信息,数据以XML格式返回。调用此函数前客户端不需要执行登录操作。 ### 参数 @@ -1321,6 +1360,7 @@ BIT_STATUS Bit_GetServerInfo( …… ``` + 1. 指定分组(group)查询 仅支持BIT_SERVER_INFO_SN_USERS、BIT_SERVER_INFO_FEATURE_USERS、BIT_SERVER_INFO_FEATURE_USERS_EX三种类型,通过name限定待查询的分组。 @@ -1332,6 +1372,7 @@ BIT_STATUS Bit_GetServerInfo( …… ``` + > 说明:通过Bit_SetCustomInfo的CUSTOM_GROUP_NAME(0xA)设置分组。 ### 返回XML数据说明 @@ -1360,6 +1401,7 @@ XML返回示例: BIT_STATUS Bit_GetVersion ( BIT_UINT32 *pVersion) ``` + 获取客户端安全库版本号。 ### 参数 @@ -1373,6 +1415,7 @@ BIT_STATUS Bit_GetNextHandle( BIT_HANDLE handle, BIT_HANDLE *pNextHandle) ``` + 用来遍历当前进程内的handle。 ### 参数 @@ -1398,6 +1441,7 @@ BIT_STATUS Bit_TestBitService ( BIT_UINT32 featureId, BIT_UCHAR *pApplicationData) ``` + 测试集团授权的特征项是否可用,不会占用授权码或特征项的用户数。 ### 参数 @@ -1421,6 +1465,7 @@ BIT_STATUS Bit_GetProductPath( BIT_CHAR *pPath, BIT_UINT32 lenPath); ``` + 获取授权存储目录。 ### 参数 @@ -1434,6 +1479,7 @@ BIT_STATUS Bit_GetProductPath( ```c BIT_STATUS Bit_GetLastError(); ``` + 获取上一个API调用返回的错误码。 ### 参数 @@ -1442,7 +1488,6 @@ BIT_STATUS Bit_GetLastError(); ## 属性设置 - ### Bit_SetAttr ```c @@ -1451,6 +1496,7 @@ BIT_STATUS Bit_SetAttr ( BIT_UINT32 type, BIT_VOID *pValue) ``` + 设置全局配置或当前会话的配置。 ### 参数 @@ -1516,6 +1562,7 @@ BIT_STATUS Bit_SetProxy ( BIT_PCSTR szUserID, BIT_PCSTR szPassword) ``` + 设置代理服务的地址和端口。 ### 参数 @@ -1540,6 +1587,7 @@ BIT_STATUS Bit_SetCustomInfo ( BIT_VOID *pInfoData, BIT_UINT32 infoDataSize) ``` + 设置客户端运行自定义信息,需要在程序的最开始调用。 ### 参数 @@ -1589,6 +1637,7 @@ if (status == BIT_SUCCESS) { BIT_STATUS Bit_SetRootPath ( BIT_PCSTR szPath) ``` + 设置授权文件的存储路径。 ### 参数 @@ -1604,6 +1653,7 @@ BIT_STATUS Bit_SetLocalServer ( BIT_UINT32 nPort, BIT_UINT32 nTimeoutSecondes) ``` + 设置集团服务的地址和端口。 ### 参数 @@ -1615,7 +1665,6 @@ BIT_STATUS Bit_SetLocalServer ( ## 借出操作 - ### Bit_GetBorrowRequest ```c @@ -1626,6 +1675,7 @@ BIT_STATUS Bit_GetBorrowRequest( BIT_CHAR *pRequestInfo, BIT_UINT32 *pRequestInfoSize) ``` + 获取借出请求串,用来离线借出。 ### 参数 @@ -1647,6 +1697,7 @@ BIT_STATUS Bit_GetBorrowFeatureRequest( BIT_CHAR *pRequestInfo, BIT_UINT32 *pRequestInfoSize) ``` + 获取借出请求串,该接口支持指定特征项借出。 ### 参数 @@ -1684,6 +1735,7 @@ BIT_STATUS Bit_CheckOutSnEx( BIT_UCHAR *pApplicationData, BIT_UINT32 nDurationDays); ``` + 从授权服务器在线借出一个完整的授权码,以允许客户端服务器单独使用。被借出的授权码必须具有可借出属性,并在客户端成功借出后减少一个可用用户数。被借出的用户数在到期后将自动返还给服务器。 ### 参数 @@ -1721,6 +1773,7 @@ BIT_STATUS Bit_CheckOut( BIT_UCHAR *pApplicationData, BIT_UINT32 nDurationDays) ``` + 从授权服务器在线借出授权码或者特征项。 ### 参数 @@ -1740,6 +1793,7 @@ type:借出类型。目前仅支持ws,表示从外网借出(针对云授 sn:指定借出的SN --> ``` + - **pFeatureList** xml结构,指定借出的特征项列表,如果不传则借出整个SN。 ```xml @@ -1749,6 +1803,7 @@ sn:指定借出的SN ``` + 当feature指定了借出时间,则会覆盖nDurationDays该时间,没有指定,则借出nDurationDays天。 借出feature时,如果指定了版本,则直接大于等于借出版本的特征项,只借一个,如果没有指定版本,则该特征项的所有版本都借出一个。 @@ -1792,6 +1847,7 @@ BIT_STATUS Bit_CheckOutFeatures( BIT_UINT32 nFeatureInList, BIT_UINT32 nDurationDays); ``` + 从集团授权服务器借出一组特征项,这些特征项必须包含在同一个授权码中。被借出的集团授权码必须具有可借出属性,并在客户端成功借出后减少一个可用用户数。被借出的用户数在到期后将自动返还给集团服务器。 ### 参数 @@ -1831,6 +1887,7 @@ BIT_STATUS Bit_CheckIn( BIT_UINT32 featureId, BIT_UCHAR *pApplicationData) ``` + 提前返还从集团授权服务器借出的授权。要提前返还授权,该授权码必须具有允许提前返还属性。 ### 参数 @@ -1848,6 +1905,7 @@ BIT_STATUS Bit_CheckInEx( BIT_PCSTR szScope, BIT_UCHAR *pApplicationData) ``` + 提前返还从授权服务器借出的授权。要提前返还授权,该授权码必须具有允许提前返还属性。 ### 参数 @@ -1861,6 +1919,7 @@ BIT_STATUS Bit_CheckInEx( ws ``` + 用于指定借出的参数,格式:wsxxxx,type设定为ws时表示从云授权服务器借出和归还。 - **pApplicationData** - [IN] 产品识别码。记录在接口定义文件中,与产品一一对应。 @@ -1872,9 +1931,11 @@ BIT_STATUS Bit_ApplyBorrowInfo( BIT_UCHAR *pApplicationData, BIT_PCSTR pBorrowInfo) ``` + 客户端应用离线借出串,Bit_ApplyUpdateInfo可以兼容该接口。 ### 参数 - **pApplicationData** - [IN] 产品识别码。记录在接口定义文件中,与产品一一对应。 - **pBorrowInfo** - [IN] 借出请求串。 + diff --git a/docs/java.md b/docs/java.md index dc14a45..91a45af 100644 --- a/docs/java.md +++ b/docs/java.md @@ -1,12 +1,11 @@ # 比特授权云 · Java 语言接口(离线摘录) -> 本文档由官网页面离线摘录并整理排版,便于本地检索。官方地址: +> 本文档由官网页面离线摘录并整理排版,便于本地检索。官方地址:[https://doc.bitanswer.cn/docs/client-api/java-interface-v2/](https://doc.bitanswer.cn/docs/client-api/java-interface-v2/) --- ## 构造方法详细信息 - ### BitAnswer ```java @@ -16,7 +15,6 @@ public BitAnswer(String url, String sn, LoginMode mode) ## 认证 - ### login / loginEx ```java @@ -30,6 +28,7 @@ void loginEx (String url, String xmlScope, LoginMode mode) ``` + 授权登录。初始化运行环境,获取操作句柄。必须在除升级函数之外的其它操作前执行。根据登录模式的不同可能需要连接授权服务器。 Login 等价于 LoginEx(featureId=0);当需要登录包含指定特征项的授权时才需要调用LoginEx。 @@ -75,6 +74,7 @@ Login 等价于 LoginEx(featureId=0);当需要登录包含指定特征项 void loginByToken (String url, String token) ``` + 帐号授权的登录接口。仅支持两种认证方式,这里是使用比特授权云平台自己产生的access_token进行认证。 ### 参数 @@ -95,6 +95,7 @@ void loginByTokenEx (String url, String idpGuid, String grantType) ``` + 帐号授权的登录接口。仅支持两种认证方式,这里是使用OIDC协议产生的id_token认证。 ### 参数 @@ -118,6 +119,7 @@ void loginByPassword (String url, String account, String password) ``` + 通过用户名和密码登录帐号授权。 ### 参数 @@ -137,6 +139,7 @@ void loginByPassword (String url, ```java void logout() ``` + 此函数用于释放上下文句柄,退出登录状态,与Login相关接口一一对应。 ### 参数 @@ -152,6 +155,7 @@ void logout() ```java String revoke (String sn) ``` + 从客户端迁出已激活的浮动授权码。授权码迁出后,可以用于其它的客户端。根据输入参数的不同,本函数可用于在线或离线迁出。 ### 参数 @@ -168,6 +172,7 @@ String revoke (String sn) void revokeOnline (String url, String sn) ``` + 从客户端迁出已激活的浮动授权码。授权码迁出后,可以用于其它的客户端。本函数需要进行网络连接。 ### 参数 @@ -184,6 +189,7 @@ void revokeOnline (String url, ```java void removeSn (String sn) ``` + 删除指定授权码在本机的授权数据。 ### 参数 @@ -199,6 +205,7 @@ void removeSn (String sn) ```java int heartbeat() ``` + 手动心跳,可以无限次调用,10s只会触发一次。 注:当自动心跳停止后,调用该接口可以尝试恢复自动心跳。 @@ -218,6 +225,7 @@ String sessionControl (String url, String sessionId, SessionCtlType type) ``` + 控制或设置session的信息。具体使用场景可参考《浏览器并发控制》文档。 ### 参数 @@ -243,6 +251,7 @@ String sessionControl (String url, void setSessionState (int state, byte[] pReserved) ``` + 设置客户端的状态为空闲状态或繁忙状态或激活状态。 ### 参数 @@ -264,13 +273,13 @@ void setSessionState (int state, ## 激活升级 - ### updateOnline ```java void updateOnline (String url, String sn) ``` + 此函数用于与授权服务器在线连接,自动完成本地授权的升级操作。本函数需要进行网络连接。 ### 参数 @@ -288,6 +297,7 @@ void updateOnline (String url, String getRequestInfo (String sn, BindingType type) ``` + 获取当前运行环境的升级请求码,用于发起本地授权激活及升级请求。 ### 参数 @@ -310,6 +320,7 @@ String getRequestInfo (String sn, ```java String applyUpdateInfo (String updateInfo) ``` + 应用升级码完成本地授权激活或升级。本函数必须在获取请求码的同一环境下执行。 ### 参数 @@ -327,6 +338,7 @@ String getUpdateInfo (String url, String sn, String requestInfo) ``` + 使用请求码与授权服务器进行连接,获取升级码。本函数需要进行网络连接。 ### 参数 @@ -341,12 +353,12 @@ String getUpdateInfo (String url, ## 特征项操作 - ### batchBegin ```java void batchBegin (BatchMode mode) ``` + 开启批量Query模式,调用此API后调用的所有Query相关API,全部由Bit_batchEnd接口批量提交连接集团服务。 ### 参数 @@ -365,6 +377,7 @@ void batchBegin (BatchMode mode) ```java int[] batchEnd() ``` + 批量提交Query请求。 ### 参数 @@ -380,6 +393,7 @@ int[] batchEnd() ```java int queryFeature (int featureId) ``` + 开发商可以对软件的某个功能进行单独授权,当程序运行该模块时,可以通过该接口检查是否可用。 ### 参数 @@ -395,6 +409,7 @@ int queryFeature (int featureId) ```java int releaseFeature (int featureId) ``` + 释放所占用的用户数,该函数和Bit_QueryFeature一一对应。 ### 参数 @@ -413,6 +428,7 @@ int queryFeatureEx (int featureId, int required, String scope) ``` + 集团授权专用,该接口支持占用指定用户数,支持通过特征项版本进行检查。 ### 参数 @@ -439,6 +455,7 @@ int releaseFeatureEx (int featureId, int consumed, String scope) ``` + 集团授权专用,释放指定的用户数。该接口与Bit_QueryFeatureEx对应。 ### 参数 @@ -459,6 +476,7 @@ long queryFeatureEx2 (String featureName, int required, String scope) ``` + 可以使用“特征项名称 + 特征项版本”进行用户数占用,支持队列模式。 ### 参数 @@ -486,6 +504,7 @@ long queryFeatureEx2 (String featureName, void releaseFeatureEx2 (byte[] ticket, int consumed) ``` + 释放ticket所占用的用户数。该函数和Bit_QueryFeatureEx2一一对应。 ### 参数 @@ -503,6 +522,7 @@ void releaseFeatureEx2 (byte[] ticket, int getFeatureInfo2 (String featureName, String scope) ``` + 检查特征项是否存在,不会占用授权码或特征项的用户数,获取特征项的剩余有效期。 ### 参数 @@ -520,6 +540,7 @@ int getFeatureInfo2 (String featureName, String getFeatureInfoEx2 (String featureName, String scope) ``` + 获取指定feature的信息,以XML格式返回。 ### 参数 @@ -544,6 +565,7 @@ String getFeatureInfoEx2 (String featureName, String getTicketInfo (byte[] ticket, int type) ``` + 获取ticket信息。 ### 参数 @@ -568,6 +590,7 @@ int convertFeature (int featureId, int para3, int para4) ``` + 使用“算法”类型的特征项对输入参数进行变换操作,得到唯一对应的4字节结果。 ### 参数 @@ -588,6 +611,7 @@ int convertFeature (int featureId, byte[] encryptFeature (int featureId, byte[] pPlainBuffer) ``` + 使用“密钥”类型的特征项对输入的明文进行加密,返回密文结果。 ### 参数 @@ -605,6 +629,7 @@ byte[] encryptFeature (int featureId, byte[] decryptFeature (int featureId, byte[] pCipherBuffer) ``` + 使用“密钥”类型的特征项对输入的密文进行解密,返回明文结果。 ### 参数 @@ -621,6 +646,7 @@ byte[] decryptFeature (int featureId, ```java int readFeature (int featureId) ``` + 读取特征项的数据内容,可用于“只读”和“读写”特征类型。 ### 参数 @@ -637,6 +663,7 @@ int readFeature (int featureId) void writeFeature (int featureId, int featureValue) ``` + 更新“读写”类型的特征项的数据内容。 ### 参数 @@ -650,12 +677,12 @@ void writeFeature (int featureId, ## 配置项操作 - ### getDataItem ```java byte[] getDataItem (String dataItemName) ``` + 读取指定的配置项数据。 ### 参数 @@ -672,6 +699,7 @@ byte[] getDataItem (String dataItemName) void setDataItem (String dataItemName, byte[] dataItemValue) ``` + 创建或更新配置项。如果相同名称的配置项存在,则会更新其中的数据;否则将添加新的授权码配置项。 ### 参数 @@ -688,6 +716,7 @@ void setDataItem (String dataItemName, ```java int getDataItemNum() ``` + 此函数用于获取可访问配置项的数量,一般用于配置项的枚举操作。 ### 参数 @@ -703,6 +732,7 @@ int getDataItemNum() ```java String getDataItemName (int index) ``` + 根据配置项索引获取其名称,一般用于配置项的枚举操作。 ### 参数 @@ -718,6 +748,7 @@ String getDataItemName (int index) ```java void removeDataItem (String dataItemName) ``` + 删除指定的配置项。该操作无法删除通过控制台设置的产品配置项或模版配置项。 ### 参数 @@ -730,12 +761,12 @@ void removeDataItem (String dataItemName) ## 信息获取 - ### getSessionInfo ```java String getSessionInfo (SessionType type) ``` + 获取当前会话信息,以字符串形式返回。根据获取的内容不同,返回结果可能是XML格式或非XML格式。返回数据中的日期项已根据客户端的本地时区进行调整。如果Login时未指定SN,返回串为当前系统所有可用SN的综合结果。 ### 参数 @@ -771,6 +802,7 @@ String getSessionInfo (SessionType type) String getInfo (String sn, InfoType type) ``` + 获取本地授权信息。 ### 参数 @@ -800,6 +832,7 @@ String getServerInfo (String url, String scope, ServerInfoType type) ``` + 获取集团服务的license信息,数据以XML格式返回。调用此函数前客户端不需要执行登录操作。 ### 参数 @@ -824,6 +857,7 @@ String getServerInfo (String url, ```java int getVersion() ``` + 获取客户端安全库版本号。 ### 参数 @@ -841,6 +875,7 @@ void testBitService (String url, String sn, int featureId) ``` + 测试集团授权的特征项是否可用,不会占用授权码或特征项的用户数。 ### 参数 @@ -858,6 +893,7 @@ void testBitService (String url, ```java String getProductPath() ``` + 获取授权存储目录。 ### 参数 @@ -873,6 +909,7 @@ String getProductPath() ```java int getLastError() ``` + 获取上一个API调用返回的错误码。 ### 参数 @@ -889,13 +926,13 @@ int getLastError() ## 属性设置 - ### setAttr ```java void setAttr (int type, byte[] pValue) ``` + 设置全局配置或当前会话的配置。 ### 参数 @@ -924,6 +961,7 @@ void setProxy (String hostName, String userId, String password) ``` + 设置代理服务的地址和端口。 ### 参数 @@ -943,6 +981,7 @@ void setProxy (String hostName, void setCustomInfo (int infoId, String infoData) ``` + 设置客户端运行自定义信息,需要在程序的最开始调用。infoId定义详见CustomInfo类型。 ### 参数 @@ -958,6 +997,7 @@ void setCustomInfo (int infoId, void setCustomInfo (int infoId, int infoData) ``` + 设置客户端运行自定义信息,需要在程序的最开始调用。infoId定义详见CustomInfo类型。 ### 参数 @@ -980,6 +1020,7 @@ void setCustomInfo (int infoId, ```java void setRootPath (String rootPath) ``` + 设置授权文件的存储路径。 ### 参数 @@ -997,6 +1038,7 @@ void setLocalServer (String hostName, int port, int timeout) ``` + 设置集团服务的地址和端口。 ### 参数 @@ -1011,13 +1053,13 @@ void setLocalServer (String hostName, ## 借出操作 - ### getBorrowRequest ```java String getBorrowRequest (String sn, int durationDay) ``` + 获取借出请求串,用来离线借出。 ### 参数 @@ -1035,6 +1077,7 @@ String getBorrowRequest (String sn, String getBorrowFeatureRequest (int durationDay, String borrowScope) ``` + 获取借出请求串,该接口支持指定特征项借出。 ### 参数 @@ -1056,6 +1099,7 @@ String getBorrowFeatureRequest (int durationDay, void checkOutSn (String url, int featureId) ``` + 从授权服务器在线借出一个完整的授权码,以允许客户端服务器单独使用。被借出的授权码必须具有可借出属性,并在客户端成功借出后减少一个可用用户数。被借出的用户数在到期后将自动返还给服务器。 ### 参数 @@ -1075,6 +1119,7 @@ void checkOutSnEx (String url, String xmlScope, int durationDays) ``` + 从授权服务器在线借出一个完整的授权码,以允许客户端服务器单独使用。被借出的授权码必须具有可借出属性,并在客户端成功借出后减少一个可用用户数。被借出的用户数在到期后将自动返还给服务器。 ### 参数 @@ -1096,6 +1141,7 @@ void checkOut (String url, String featureList, int durationDays) ``` + 从授权服务器在线借出授权码或者特征项。 ### 参数 @@ -1113,6 +1159,7 @@ void checkOut (String url, type:借出类型。目前仅支持ws,表示从外网借出(针对云授权和集团授权) sn:指定借出的SN ``` + - **featureList** - [IN] 特征项列表,指定借出的特征项列表,如果不传则借出整个SN。示例: ```xml @@ -1125,6 +1172,7 @@ sn:指定借出的SN 当feature指定了借出时间,则会覆盖nDurationDays该时间,没有指定,则借出nDurationDays天。 借出feature时,如果指定了版本,则直接大于等于借出版本的特征项,只借一个,如果没有指定版本,则该特征项的所有版本都借出一个。 ``` + - **durationDays** - [IN] 借出有效期,单位为天,需小于等于授权码允许的最大借出天数。 ### 返回 @@ -1138,6 +1186,7 @@ void checkOutFeatures (String url, int[] featureList, int durationDays) ``` + 从集团授权服务器借出一组特征项,这些特征项必须包含在同一个授权码中。被借出的集团授权码必须具有可借出属性,并在客户端成功借出后减少一个可用用户数。被借出的用户数在到期后将自动返还给集团服务器。 ### 参数 @@ -1156,6 +1205,7 @@ void checkOutFeatures (String url, void checkIn (String url, int featureId) ``` + 提前返还从集团授权服务器借出的授权。要提前返还授权,该授权码必须具有允许提前返还属性。 ### 参数 @@ -1174,6 +1224,7 @@ void checkInEx (String url, int featureId, String xmlScope) ``` + 提前返还从授权服务器借出的授权。要提前返还授权,该授权码必须具有允许提前返还属性。 ### 参数 @@ -1191,6 +1242,7 @@ void checkInEx (String url, ```java void ApplyBorrowInfo (string borrowInfo) ``` + 客户端应用离线借出串,ApplyUpdateInfo可以兼容该接口。 ### 参数 @@ -1199,4 +1251,4 @@ void ApplyBorrowInfo (string borrowInfo) ### 返回 -无 +无 \ No newline at end of file diff --git a/examples/config/floating.bitanswer.json b/examples/config/floating.bitanswer.json new file mode 100644 index 0000000..ebcefc3 --- /dev/null +++ b/examples/config/floating.bitanswer.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "provider": "bitanswer", + "scenario": "floating", + "bitanswer": { + "url": "https://cloud.bitanswer.example/e3", + "loginMode": "REMOTE" + }, + "features": { + "face": { "bitanswerFeatureId": 301 } + }, + "floating": { + "projectId": "migrant-flow-prj-2026-001", + "projectName": "某市流动人口人像核验", + "contractRef": "PO-2026-8848" + } +} diff --git a/examples/config/school.bitanswer.json b/examples/config/school.bitanswer.json new file mode 100644 index 0000000..647e33a --- /dev/null +++ b/examples/config/school.bitanswer.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "provider": "bitanswer", + "scenario": "school", + "bitanswer": { + "url": "https://cloud.bitanswer.example/e3", + "loginMode": "AUTO", + "sn": "" + }, + "features": { + "face": { "bitanswerFeatureId": 201 }, + "expression": { "bitanswerFeatureId": 202 } + }, + "school": { + "edgeDeviceId": "classroom-gate-01", + "tenantId": "school-district-demo" + } +} diff --git a/examples/config/school.selfhosted.json b/examples/config/school.selfhosted.json new file mode 100644 index 0000000..f27cc74 --- /dev/null +++ b/examples/config/school.selfhosted.json @@ -0,0 +1,16 @@ +{ + "schemaVersion": 1, + "provider": "selfhosted", + "scenario": "school", + "selfhosted": { + "baseUrl": "https://license.internal.example/api/v1", + "tenantKey": "district-west" + }, + "features": { + "face": {}, + "expression": {} + }, + "school": { + "edgeDeviceId": "gate-02" + } +} diff --git a/examples/config/wharf.bitanswer.json b/examples/config/wharf.bitanswer.json new file mode 100644 index 0000000..8d3bada --- /dev/null +++ b/examples/config/wharf.bitanswer.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 1, + "provider": "bitanswer", + "scenario": "wharf", + "bitanswer": { + "url": "bit://license.example.com:8273", + "loginMode": "AUTO", + "rootPath": "/var/lib/craftlabs/bitanswer" + }, + "features": { + "container_number_detect": { "bitanswerFeatureId": 101 }, + "container_number_recognize": { "bitanswerFeatureId": 102 }, + "iso_detect": { "bitanswerFeatureId": 103 }, + "iso_recognize": { "bitanswerFeatureId": 104 } + }, + "wharf": { + "topology": "group", + "groupServiceUrl": "bit://license.example.com:8273", + "notes": "边设备集中连集团服务;特征项与控制台产品一致后替换 id。" + } +} diff --git a/examples/java/ExampleApp.java b/examples/java/ExampleApp.java index cf867ea..84935c8 100644 --- a/examples/java/ExampleApp.java +++ b/examples/java/ExampleApp.java @@ -1,9 +1,14 @@ import cn.craftlabs.auth.AuthProvider; import cn.craftlabs.auth.AuthResult; import cn.craftlabs.auth.bitanswer.BitAnswerProvider; +import cn.craftlabs.auth.config.AuthConfig; +import cn.craftlabs.auth.config.AuthConfigException; +import cn.craftlabs.auth.config.AuthConfigs; /** - * 演示:通过 {@link BitAnswerProvider} 完成初始化与许可校验。 + * 演示:校验 {@code config_json} 后初始化并完成许可校验。 + * + *

编译时需将 {@code craftlabs-auth-core} 与 Jackson 置于 classpath(与 Maven 模块依赖一致)。 * *

版权所有 © 广州创飞人工智能技术有限公司 * @@ -11,8 +16,30 @@ import cn.craftlabs.auth.bitanswer.BitAnswerProvider; */ public class ExampleApp { public static void main(String[] args) { + String json = + """ + { + "schemaVersion": 1, + "provider": "bitanswer", + "scenario": "school", + "bitanswer": { "url": "https://example.invalid/bitanswer", "loginMode": "AUTO" }, + "features": { + "face": { "bitanswerFeatureId": 201 }, + "expression": { "bitanswerFeatureId": 202 } + }, + "school": { "edgeDeviceId": "demo-edge-01", "tenantId": "demo-tenant" } + } + """; + try { + AuthConfig cfg = AuthConfigs.parse(json); + System.out.println("scenario=" + cfg.scenario() + ", face id=" + cfg.bitanswerFeatureId("face")); + } catch (AuthConfigException e) { + System.err.println("config invalid: " + e.getMessage()); + return; + } + try (AuthProvider p = new BitAnswerProvider()) { - AuthResult r = p.initialize("{}"); + AuthResult r = p.initialize(json); System.out.println("init: " + r.isSuccess() + " " + r.getMessage()); r = p.checkLicense(); System.out.println("check: " + r.isSuccess() + " " + r.getMessage()); diff --git a/java/RELEASING.md b/java/RELEASING.md new file mode 100644 index 0000000..1fda366 --- /dev/null +++ b/java/RELEASING.md @@ -0,0 +1,93 @@ +# CraftLabs 授权 SDK — 发布与完整性 + +对齐架构文档 [SYSTEM_ARCHITECTURE §9.8](../docs/engineering/SYSTEM_ARCHITECTURE.md):官方渠道、**SHA-256 清单**、**GPG 签名(建议)**、`java` 与 `native` **同 Git tag**。 + +## 1. 发布前检查 + +- [ ] `mvn -f java/pom.xml verify` 通过(JDK 17+)。 +- [ ] `native` 已在各目标平台完成构建,且与本次 **同一 tag** 一并交付。 +- [ ] `CHANGELOG`(或发布说明)写明 **比特 SDK / 运行时** 兼容版本。 + +## 2. 生成 SHA256SUMS(必做) + +在仓库根目录执行(会先 `mvn -DskipTests package`): + +```bash +chmod +x scripts/sdk-release-checksums.sh +./scripts/sdk-release-checksums.sh --output dist/sdk-release +``` + +已构建过时跳过 Maven: + +```bash +./scripts/sdk-release-checksums.sh --no-mvn --output dist/sdk-release +``` + +把 **本机构建出的 Native** 一并写入清单(路径相对于仓库根,便于客户校验): + +```bash +./scripts/sdk-release-checksums.sh --output dist/sdk-release --native-path "$(pwd)/native/build" +``` + +含完整测试的构建后再生成清单: + +```bash +./scripts/sdk-release-checksums.sh --verify --output dist/sdk-release +``` + +输出: + +- `dist/sdk-release/SHA256SUMS` — 每行:`哈希 相对路径` +- `dist/sdk-release/RELEASE-MANIFEST.txt` — 提交 SHA、UTC 时间 + +**客户校验**(在克隆/解压后的仓库根或同目录结构下): + +```bash +sha256sum -c dist/sdk-release/SHA256SUMS +``` + +macOS: + +```bash +shasum -a 256 -c dist/sdk-release/SHA256SUMS +``` + +对 `SHA256SUMS` 本身做 **分离签名**(本机已配置 GPG): + +```bash +SIGN=1 ./scripts/sdk-release-checksums.sh --no-mvn --output dist/sdk-release +``` + +生成 `SHA256SUMS.asc`;客户使用公布的公钥:`gpg --verify SHA256SUMS.asc SHA256SUMS`。 + +## 3. Maven JAR 的 GPG 签名(强烈建议) + +父 POM 已配置 `maven-gpg-plugin`,**默认跳过**(`gpg.skip=true`),不影响日常 `verify`。 + +发布前在已导入私钥的机器上: + +```bash +gpg --version # 确认可用 +mvn -f java/pom.xml -Prelease-sign verify +``` + +各 **可发布模块**(`craftlabs-auth-core`、`craftlabs-auth-bitanswer`、`craftlabs-auth-selfhosted`)的 `target/*.jar` 旁会出现 **`.asc`**。`craftlabs-auth-tests` 模块固定 **不签名**。 + +若无私钥或未就绪,可只发 **SHA256SUMS**;待密钥就绪后再打开 `-Prelease-sign`。 + +### CI / 无人值守(可选) + +在构建机配置 `MAVEN_GPG_PASSPHRASE` 等环境变量,或使用 `gpg-agent`;勿将私钥提交仓库。GitHub Actions 可用 `GPG_PRIVATE_KEY` secret + [crazy-max/ghaction-import-gpg](https://github.com/crazy-max/ghaction-import-gpg) 导入后再执行 `mvn -Prelease-sign verify`。 + +## 4. GitHub Release 建议资产 + +每个 **tag** 上传: + +1. 三个 **release JAR**(及对应 **.asc**,若已签名) +2. 各平台 **Native** 压缩包 +3. **`SHA256SUMS`** 与 **`SHA256SUMS.asc`** +4. 固定页面公布 **GPG 公钥指纹** 与下载说明 + +--- + +版权所有 © 广州创飞人工智能技术有限公司(以项目实际声明为准)。 diff --git a/java/craftlabs-auth-core/pom.xml b/java/craftlabs-auth-core/pom.xml index bfb4ed8..37b0cd2 100644 --- a/java/craftlabs-auth-core/pom.xml +++ b/java/craftlabs-auth-core/pom.xml @@ -13,4 +13,21 @@ craftlabs-auth-core CraftLabs Auth — core API jar + + + + com.fasterxml.jackson.core + jackson-databind + + + org.junit.jupiter + junit-jupiter + test + + + com.networknt + json-schema-validator + test + + diff --git a/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/AuthProvider.java b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/AuthProvider.java index ead73af..9898b74 100644 --- a/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/AuthProvider.java +++ b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/AuthProvider.java @@ -3,6 +3,9 @@ package cn.craftlabs.auth; /** * 授权能力的统一契约:初始化、激活、校验许可、查询特性与释放等生命周期方法。 * + *

{@link #initialize(String)} 的 JSON 建议使用 {@link cn.craftlabs.auth.config.AuthConfigs#parse(String)} + * 先校验后再传入,格式见仓库 {@code schemas/craftlabs-auth-config.schema.json} 与 {@code examples/config/}。 + * *

实现类负责加载对应 native 或远端适配器;调用方应在不再使用时调用 {@link #close()} 释放底层资源。 * *

版权所有 © 广州创飞人工智能技术有限公司 diff --git a/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/AuthConfig.java b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/AuthConfig.java new file mode 100644 index 0000000..013d87f --- /dev/null +++ b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/AuthConfig.java @@ -0,0 +1,47 @@ +package cn.craftlabs.auth.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; +import java.util.Collections; +import java.util.LinkedHashMap; +import java.util.Map; +import java.util.Objects; + +/** + * {@link cn.craftlabs.auth.AuthProvider#initialize(String)} 所用 JSON 的配置模型,与 {@code + * schemas/craftlabs-auth-config.schema.json} 对齐。 + * + *

版权所有 © 广州创飞人工智能技术有限公司 + * + * @author huangping@craftlabs.cn + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record AuthConfig( + @JsonProperty("schemaVersion") int schemaVersion, + @JsonProperty("provider") String provider, + @JsonProperty("scenario") String scenario, + @JsonProperty("bitanswer") BitanswerConfigSection bitanswer, + @JsonProperty("selfhosted") SelfhostedConfigSection selfhosted, + @JsonProperty("features") Map features, + @JsonProperty("wharf") WharfScenarioSection wharf, + @JsonProperty("school") SchoolScenarioSection school, + @JsonProperty("floating") FloatingScenarioSection floating) { + + public AuthConfig { + features = features == null ? Map.of() : Collections.unmodifiableMap(new LinkedHashMap<>(features)); + provider = Objects.requireNonNullElse(provider, ""); + scenario = Objects.requireNonNullElse(scenario, ""); + } + + /** 逻辑特性键对应的比特特征 id;未配置或非数值映射时返回 {@code null}。 */ + public Integer bitanswerFeatureId(String logicalFeatureKey) { + FeatureMapping m = features.get(logicalFeatureKey); + return m != null ? m.bitanswerFeatureId() : null; + } + + /** 逻辑特性键对应的比特特征名称;未配置时返回 {@code null}。 */ + public String bitanswerFeatureName(String logicalFeatureKey) { + FeatureMapping m = features.get(logicalFeatureKey); + return m != null ? m.bitanswerFeatureName() : null; + } +} diff --git a/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/AuthConfigException.java b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/AuthConfigException.java new file mode 100644 index 0000000..27e5ff6 --- /dev/null +++ b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/AuthConfigException.java @@ -0,0 +1,19 @@ +package cn.craftlabs.auth.config; + +/** + * {@link AuthConfigs#parse(String)} 或校验失败时抛出。 + * + *

版权所有 © 广州创飞人工智能技术有限公司 + * + * @author huangping@craftlabs.cn + */ +public final class AuthConfigException extends Exception { + + public AuthConfigException(String message) { + super(message); + } + + public AuthConfigException(String message, Throwable cause) { + super(message, cause); + } +} diff --git a/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/AuthConfigs.java b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/AuthConfigs.java new file mode 100644 index 0000000..e0653aa --- /dev/null +++ b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/AuthConfigs.java @@ -0,0 +1,111 @@ +package cn.craftlabs.auth.config; + +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import java.util.ArrayList; +import java.util.List; +import java.util.Locale; + +/** + * 解析并校验 {@link cn.craftlabs.auth.AuthProvider#initialize(String)} 的 JSON 配置。 + * + *

版权所有 © 广州创飞人工智能技术有限公司 + * + * @author huangping@craftlabs.cn + */ +public final class AuthConfigs { + + /** 与 {@code schemas/craftlabs-auth-config.schema.json} 中 {@code schemaVersion} 一致。 */ + public static final int SCHEMA_VERSION = 1; + + private static final ObjectMapper MAPPER = + new ObjectMapper().configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + + private AuthConfigs() {} + + /** 解析 JSON 并执行 {@link #validate(AuthConfig)}。 */ + public static AuthConfig parse(String json) throws AuthConfigException { + if (json == null || json.isBlank()) { + throw new AuthConfigException("config JSON is null or blank"); + } + try { + AuthConfig cfg = MAPPER.readValue(json, AuthConfig.class); + validate(cfg); + return cfg; + } catch (JsonProcessingException e) { + String msg = e.getOriginalMessage() != null ? e.getOriginalMessage() : e.getMessage(); + throw new AuthConfigException("Invalid config JSON: " + msg, e); + } + } + + /** 将配置写回 JSON(便于日志脱敏后落盘或调试)。 */ + public static String toJson(AuthConfig config) throws AuthConfigException { + try { + return MAPPER.writerWithDefaultPrettyPrinter().writeValueAsString(config); + } catch (JsonProcessingException e) { + throw new AuthConfigException("Failed to serialize config", e); + } + } + + /** + * 校验必填组合;通过后可安全交给 native / 供应商实现。 + * + * @throws AuthConfigException 校验失败,消息为多条原因拼接 + */ + public static void validate(AuthConfig c) throws AuthConfigException { + List err = new ArrayList<>(); + + if (c.schemaVersion() != SCHEMA_VERSION) { + err.add("schemaVersion must be " + SCHEMA_VERSION); + } + + String provider = norm(c.provider()); + if (provider.isEmpty()) { + err.add("provider is required"); + } else if (!provider.equals("bitanswer") && !provider.equals("selfhosted")) { + err.add("provider must be bitanswer or selfhosted"); + } + + String scenario = norm(c.scenario()); + if (scenario.isEmpty()) { + err.add("scenario is required"); + } else if (!scenario.equals("wharf") && !scenario.equals("school") && !scenario.equals("floating")) { + err.add("scenario must be wharf, school, or floating"); + } + + if (provider.equals("bitanswer")) { + if (c.bitanswer() == null) { + err.add("bitanswer section is required when provider=bitanswer"); + } else if (isBlank(c.bitanswer().url())) { + err.add("bitanswer.url is required and must be non-blank"); + } + } + + if (provider.equals("selfhosted")) { + if (c.selfhosted() == null) { + err.add("selfhosted section is required when provider=selfhosted"); + } else if (isBlank(c.selfhosted().baseUrl())) { + err.add("selfhosted.baseUrl is required and must be non-blank"); + } + } + + if (scenario.equals("floating")) { + if (c.floating() == null || isBlank(c.floating().projectId())) { + err.add("floating.projectId is required when scenario=floating"); + } + } + + if (!err.isEmpty()) { + throw new AuthConfigException(String.join("; ", err)); + } + } + + private static String norm(String s) { + return s == null ? "" : s.trim().toLowerCase(Locale.ROOT); + } + + private static boolean isBlank(String s) { + return s == null || s.trim().isEmpty(); + } +} diff --git a/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/BitanswerConfigSection.java b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/BitanswerConfigSection.java new file mode 100644 index 0000000..2995d28 --- /dev/null +++ b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/BitanswerConfigSection.java @@ -0,0 +1,17 @@ +package cn.craftlabs.auth.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * {@code config_json} 中与比特安索客户端相关的字段子集。 + * + * @author huangping@craftlabs.cn + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record BitanswerConfigSection( + @JsonProperty("url") String url, + @JsonProperty("loginMode") String loginMode, + @JsonProperty("rootPath") String rootPath, + @JsonProperty("sn") String sn, + @JsonProperty("applicationData") String applicationData) {} diff --git a/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/FeatureMapping.java b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/FeatureMapping.java new file mode 100644 index 0000000..893d0ab --- /dev/null +++ b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/FeatureMapping.java @@ -0,0 +1,14 @@ +package cn.craftlabs.auth.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * 逻辑特性键(如 {@code face})到比特特征项的映射;至少填 id 或 name 之一即可在实现层选用。 + * + * @author huangping@craftlabs.cn + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record FeatureMapping( + @JsonProperty("bitanswerFeatureId") Integer bitanswerFeatureId, + @JsonProperty("bitanswerFeatureName") String bitanswerFeatureName) {} diff --git a/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/FloatingScenarioSection.java b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/FloatingScenarioSection.java new file mode 100644 index 0000000..a06f2a8 --- /dev/null +++ b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/FloatingScenarioSection.java @@ -0,0 +1,11 @@ +package cn.craftlabs.auth.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** 流动人口按项目授权场景补充字段。 */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record FloatingScenarioSection( + @JsonProperty("projectId") String projectId, + @JsonProperty("projectName") String projectName, + @JsonProperty("contractRef") String contractRef) {} diff --git a/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/SchoolScenarioSection.java b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/SchoolScenarioSection.java new file mode 100644 index 0000000..db5616b --- /dev/null +++ b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/SchoolScenarioSection.java @@ -0,0 +1,10 @@ +package cn.craftlabs.auth.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** 学校按边设备运营场景补充字段。 */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record SchoolScenarioSection( + @JsonProperty("edgeDeviceId") String edgeDeviceId, + @JsonProperty("tenantId") String tenantId) {} diff --git a/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/SelfhostedConfigSection.java b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/SelfhostedConfigSection.java new file mode 100644 index 0000000..52b1ee4 --- /dev/null +++ b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/SelfhostedConfigSection.java @@ -0,0 +1,14 @@ +package cn.craftlabs.auth.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** + * {@code provider=selfhosted} 时的 HTTP 后端参数。 + * + * @author huangping@craftlabs.cn + */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record SelfhostedConfigSection( + @JsonProperty("baseUrl") String baseUrl, + @JsonProperty("tenantKey") String tenantKey) {} diff --git a/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/WharfScenarioSection.java b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/WharfScenarioSection.java new file mode 100644 index 0000000..7a0a454 --- /dev/null +++ b/java/craftlabs-auth-core/src/main/java/cn/craftlabs/auth/config/WharfScenarioSection.java @@ -0,0 +1,11 @@ +package cn.craftlabs.auth.config; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonProperty; + +/** 码头 / 集中授权场景补充字段。 */ +@JsonInclude(JsonInclude.Include.NON_NULL) +public record WharfScenarioSection( + @JsonProperty("topology") String topology, + @JsonProperty("groupServiceUrl") String groupServiceUrl, + @JsonProperty("notes") String notes) {} diff --git a/java/craftlabs-auth-core/src/test/java/cn/craftlabs/auth/config/AuthConfigsTest.java b/java/craftlabs-auth-core/src/test/java/cn/craftlabs/auth/config/AuthConfigsTest.java new file mode 100644 index 0000000..c25d020 --- /dev/null +++ b/java/craftlabs-auth-core/src/test/java/cn/craftlabs/auth/config/AuthConfigsTest.java @@ -0,0 +1,78 @@ +package cn.craftlabs.auth.config; + +import static org.junit.jupiter.api.Assertions.*; + +import java.io.InputStream; +import java.nio.charset.StandardCharsets; +import org.junit.jupiter.api.Test; + +class AuthConfigsTest { + + @Test + void parseWharfExampleFromClasspath() throws Exception { + String json = readClasspath("examples/config/wharf.bitanswer.json"); + AuthConfig c = AuthConfigs.parse(json); + assertEquals(1, c.schemaVersion()); + assertEquals("bitanswer", c.provider()); + assertEquals("wharf", c.scenario()); + assertNotNull(c.bitanswer()); + assertTrue(c.bitanswer().url().startsWith("bit://")); + assertEquals(101, c.bitanswerFeatureId("container_number_detect")); + assertEquals("group", c.wharf().topology()); + } + + @Test + void parseSchoolExample() throws Exception { + String json = readClasspath("examples/config/school.bitanswer.json"); + AuthConfig c = AuthConfigs.parse(json); + assertEquals("school", c.scenario()); + assertEquals(201, c.bitanswerFeatureId("face")); + assertEquals("classroom-gate-01", c.school().edgeDeviceId()); + } + + @Test + void parseFloatingExample_requiresProjectId() throws Exception { + String json = readClasspath("examples/config/floating.bitanswer.json"); + AuthConfig c = AuthConfigs.parse(json); + assertEquals("floating", c.scenario()); + assertEquals("migrant-flow-prj-2026-001", c.floating().projectId()); + } + + @Test + void validateFails_whenFloatingWithoutProject() { + String json = + """ + {"schemaVersion":1,"provider":"bitanswer","scenario":"floating",\ + "bitanswer":{"url":"http://x"},"floating":{}}\ + """; + AuthConfigException ex = assertThrows(AuthConfigException.class, () -> AuthConfigs.parse(json)); + assertTrue(ex.getMessage().contains("projectId")); + } + + @Test + void validateFails_whenBitanswerWithoutUrl() { + String json = + """ + {"schemaVersion":1,"provider":"bitanswer","scenario":"school",\ + "bitanswer":{"url":""}}\ + """; + assertThrows(AuthConfigException.class, () -> AuthConfigs.parse(json)); + } + + @Test + void roundTrip_toJson() throws Exception { + String json = readClasspath("examples/config/school.bitanswer.json"); + AuthConfig c = AuthConfigs.parse(json); + String out = AuthConfigs.toJson(c); + AuthConfig again = AuthConfigs.parse(out); + assertEquals(c.scenario(), again.scenario()); + assertEquals(c.bitanswerFeatureId("expression"), again.bitanswerFeatureId("expression")); + } + + private static String readClasspath(String path) throws Exception { + try (InputStream in = AuthConfigsTest.class.getClassLoader().getResourceAsStream(path)) { + assertNotNull(in, "missing classpath resource: " + path); + return new String(in.readAllBytes(), StandardCharsets.UTF_8); + } + } +} diff --git a/java/craftlabs-auth-core/src/test/java/cn/craftlabs/auth/config/ExamplesConfigSchemaValidationTest.java b/java/craftlabs-auth-core/src/test/java/cn/craftlabs/auth/config/ExamplesConfigSchemaValidationTest.java new file mode 100644 index 0000000..42b7d8a --- /dev/null +++ b/java/craftlabs-auth-core/src/test/java/cn/craftlabs/auth/config/ExamplesConfigSchemaValidationTest.java @@ -0,0 +1,69 @@ +package cn.craftlabs.auth.config; + +import com.fasterxml.jackson.databind.JsonNode; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.networknt.schema.JsonSchema; +import com.networknt.schema.JsonSchemaFactory; +import com.networknt.schema.SpecVersion; +import com.networknt.schema.ValidationMessage; +import org.junit.jupiter.api.Test; + +import java.io.IOException; +import java.nio.file.Files; +import java.nio.file.Path; +import java.util.Set; +import java.util.stream.Stream; + +import static org.junit.jupiter.api.Assertions.assertTrue; + +/** + * BP-10 / I5:{@code examples/config/*.json} 与仓库根 {@code schemas/craftlabs-auth-config.schema.json} 一致。 + */ +class ExamplesConfigSchemaValidationTest { + + private static final ObjectMapper MAPPER = new ObjectMapper(); + + @Test + void allExampleConfigsValidateAgainstSchema() throws Exception { + Path repoRoot = findRepoRoot(); + Path schemaPath = repoRoot.resolve("schemas/craftlabs-auth-config.schema.json"); + Path examplesDir = repoRoot.resolve("examples/config"); + assertTrue(Files.isRegularFile(schemaPath), "schema missing: " + schemaPath); + assertTrue(Files.isDirectory(examplesDir), "examples dir missing: " + examplesDir); + + JsonNode schemaNode = MAPPER.readTree(schemaPath.toFile()); + JsonSchemaFactory factory = JsonSchemaFactory.getInstance(SpecVersion.VersionFlag.V202012); + JsonSchema schema = factory.getSchema(schemaNode); + + try (Stream stream = Files.list(examplesDir)) { + stream.filter(p -> p.toString().endsWith(".json")).forEach(jsonFile -> { + try { + JsonNode instance = MAPPER.readTree(jsonFile.toFile()); + Set errors = schema.validate(instance); + assertTrue( + errors.isEmpty(), + () -> jsonFile.getFileName() + ": " + errors); + } catch (IOException e) { + throw new AssertionError(jsonFile.toString(), e); + } + }); + } + } + + /** + * 自 {@code user.dir} 向上查找含 {@code schemas/craftlabs-auth-config.schema.json} 的目录(兼容模块目录或仓库根执行)。 + */ + static Path findRepoRoot() { + Path p = Path.of(System.getProperty("user.dir", ".")).toAbsolutePath().normalize(); + for (int i = 0; i < 8 && p != null; i++) { + Path candidate = p.resolve("schemas/craftlabs-auth-config.schema.json"); + if (Files.isRegularFile(candidate)) { + return p; + } + p = p.getParent(); + } + throw new IllegalStateException( + "Could not find repo root (schemas/craftlabs-auth-config.schema.json) from user.dir=" + + System.getProperty("user.dir")); + } +} diff --git a/java/craftlabs-auth-core/src/test/resources/examples/config/floating.bitanswer.json b/java/craftlabs-auth-core/src/test/resources/examples/config/floating.bitanswer.json new file mode 100644 index 0000000..ebcefc3 --- /dev/null +++ b/java/craftlabs-auth-core/src/test/resources/examples/config/floating.bitanswer.json @@ -0,0 +1,17 @@ +{ + "schemaVersion": 1, + "provider": "bitanswer", + "scenario": "floating", + "bitanswer": { + "url": "https://cloud.bitanswer.example/e3", + "loginMode": "REMOTE" + }, + "features": { + "face": { "bitanswerFeatureId": 301 } + }, + "floating": { + "projectId": "migrant-flow-prj-2026-001", + "projectName": "某市流动人口人像核验", + "contractRef": "PO-2026-8848" + } +} diff --git a/java/craftlabs-auth-core/src/test/resources/examples/config/school.bitanswer.json b/java/craftlabs-auth-core/src/test/resources/examples/config/school.bitanswer.json new file mode 100644 index 0000000..647e33a --- /dev/null +++ b/java/craftlabs-auth-core/src/test/resources/examples/config/school.bitanswer.json @@ -0,0 +1,18 @@ +{ + "schemaVersion": 1, + "provider": "bitanswer", + "scenario": "school", + "bitanswer": { + "url": "https://cloud.bitanswer.example/e3", + "loginMode": "AUTO", + "sn": "" + }, + "features": { + "face": { "bitanswerFeatureId": 201 }, + "expression": { "bitanswerFeatureId": 202 } + }, + "school": { + "edgeDeviceId": "classroom-gate-01", + "tenantId": "school-district-demo" + } +} diff --git a/java/craftlabs-auth-core/src/test/resources/examples/config/wharf.bitanswer.json b/java/craftlabs-auth-core/src/test/resources/examples/config/wharf.bitanswer.json new file mode 100644 index 0000000..8d3bada --- /dev/null +++ b/java/craftlabs-auth-core/src/test/resources/examples/config/wharf.bitanswer.json @@ -0,0 +1,21 @@ +{ + "schemaVersion": 1, + "provider": "bitanswer", + "scenario": "wharf", + "bitanswer": { + "url": "bit://license.example.com:8273", + "loginMode": "AUTO", + "rootPath": "/var/lib/craftlabs/bitanswer" + }, + "features": { + "container_number_detect": { "bitanswerFeatureId": 101 }, + "container_number_recognize": { "bitanswerFeatureId": 102 }, + "iso_detect": { "bitanswerFeatureId": 103 }, + "iso_recognize": { "bitanswerFeatureId": 104 } + }, + "wharf": { + "topology": "group", + "groupServiceUrl": "bit://license.example.com:8273", + "notes": "边设备集中连集团服务;特征项与控制台产品一致后替换 id。" + } +} diff --git a/java/craftlabs-auth-tests/pom.xml b/java/craftlabs-auth-tests/pom.xml index b0a58fc..94385fc 100644 --- a/java/craftlabs-auth-tests/pom.xml +++ b/java/craftlabs-auth-tests/pom.xml @@ -28,6 +28,8 @@ + + true ${project.basedir}/../../native/build diff --git a/java/pom.xml b/java/pom.xml index 0da01be..ddd2932 100644 --- a/java/pom.xml +++ b/java/pom.xml @@ -21,6 +21,10 @@ UTF-8 17 5.10.2 + 2.17.2 + 1.5.7 + + true @@ -36,6 +40,17 @@ ${junit.version} test + + com.fasterxml.jackson.core + jackson-databind + ${jackson.version} + + + com.networknt + json-schema-validator + ${json-schema-validator.version} + test + @@ -52,7 +67,39 @@ maven-surefire-plugin 3.2.5 + + org.apache.maven.plugins + maven-gpg-plugin + 3.2.7 + + + + + + release-sign + + false + + + + + org.apache.maven.plugins + maven-gpg-plugin + + + sign-release-artifacts + verify + + sign + + + + + + + + diff --git a/native/include/craftlabs_auth.h b/native/include/craftlabs_auth.h index 9c1801b..5403a1f 100644 --- a/native/include/craftlabs_auth.h +++ b/native/include/craftlabs_auth.h @@ -13,6 +13,8 @@ * FFI / Python(ctypes、cffi)说明 * ---------------------------------- * - 所有 `const char*` 均为 **UTF-8** 编码、以 `\0` 结尾。 + * - `config_json`:UTF-8 JSON,语义与仓库 `schemas/craftlabs-auth-config.schema.json` 及 `examples/config/` + * 一致;Java 侧可先经 `cn.craftlabs.auth.config.AuthConfigs` 校验后再序列化传入。 * - `AuthHandle`:不透明指针,仅由本库函数产生与销毁(`auth_initialize` / `auth_destroy`)。 * - `AuthResult`、`LicenseInfo` 使用 `stdint` 定宽字段,避免 `time_t`/`int` 宽度随平台变化导致 Python 侧 Structure 布局错误。 * - `AuthResult.message`:指向 **由本库管理的静态或内部只读缓冲区**,调用方不得 `free`;在任意后续对本库的再次调用之后视为可能失效(与其它 FFI 语言惯例一致)。 diff --git a/schemas/craftlabs-auth-config.schema.json b/schemas/craftlabs-auth-config.schema.json new file mode 100644 index 0000000..5ba7735 --- /dev/null +++ b/schemas/craftlabs-auth-config.schema.json @@ -0,0 +1,164 @@ +{ + "$schema": "https://json-schema.org/draft/2020-12/schema", + "$id": "https://craftlabs.cn/schemas/craftlabs-auth-config.json", + "title": "CraftLabs Auth — initialize(config_json)", + "type": "object", + "additionalProperties": false, + "required": ["schemaVersion", "provider", "scenario"], + "properties": { + "schemaVersion": { + "type": "integer", + "const": 1, + "description": "配置格式版本;与解析器 AuthConfigs.SCHEMA_VERSION 对齐。" + }, + "provider": { + "type": "string", + "enum": ["bitanswer", "selfhosted"], + "description": "后端实现:比特安索 native 或自研 HTTP。" + }, + "scenario": { + "type": "string", + "enum": ["wharf", "school", "floating"], + "description": "业务场景:码头集中授权、学校按边设备、流动人口按项目。" + }, + "bitanswer": { + "type": "object", + "additionalProperties": false, + "description": "provider=bitanswer 时由校验器要求非空且含 url。", + "properties": { + "url": { + "type": "string", + "description": "Bit_Login szURL:云 http(s)://、集团 bit://、本地 lic:// 等,参见比特文档。" + }, + "loginMode": { + "type": "string", + "description": "与 Bitanswer Java LoginMode 名称一致,如 AUTO、REMOTE、LOCAL。" + }, + "rootPath": { + "type": "string", + "description": "可选;对应 Bit_SetRootPath。" + }, + "sn": { + "type": "string", + "description": "可选;若激活前即固定 SN 可放此处,否则走 activate(licenseKey)。" + }, + "applicationData": { + "type": "string", + "description": "随产品绑定的 application_data,若不由库内编译常量承担。" + } + } + }, + "selfhosted": { + "type": "object", + "additionalProperties": false, + "description": "provider=selfhosted 时由校验器要求非空且含 baseUrl。", + "properties": { + "baseUrl": { + "type": "string", + "format": "uri", + "description": "自研授权服务根 URL。" + }, + "tenantKey": { + "type": "string", + "description": "可选;租户或调用方标识。" + } + } + }, + "features": { + "type": "object", + "description": "逻辑特性键 → 比特特征项 id 或名称,供 hasFeature 映射。", + "additionalProperties": { + "type": "object", + "additionalProperties": false, + "properties": { + "bitanswerFeatureId": { "type": "integer" }, + "bitanswerFeatureName": { "type": "string" } + } + } + }, + "wharf": { + "type": "object", + "additionalProperties": false, + "description": "scenario=wharf 时建议填写。", + "properties": { + "topology": { + "type": "string", + "enum": ["cloud", "group", "local_float"], + "description": "云授权、集团授权或单机浮动等拓扑提示。" + }, + "groupServiceUrl": { + "type": "string", + "description": "topology=group 时 bit://host:port。" + }, + "notes": { "type": "string" } + } + }, + "school": { + "type": "object", + "additionalProperties": false, + "description": "scenario=school 时建议填写。", + "properties": { + "edgeDeviceId": { + "type": "string", + "description": "边设备实例标识,用于运营侧对账;不参与比特 API 则仅作遥测/日志。" + }, + "tenantId": { "type": "string" } + } + }, + "floating": { + "type": "object", + "additionalProperties": false, + "description": "scenario=floating 时校验器要求 projectId。", + "properties": { + "projectId": { + "type": "string", + "minLength": 1, + "description": "项目维度授权锚点。" + }, + "projectName": { "type": "string" }, + "contractRef": { "type": "string", "description": "合同或订单引用,可选。" } + } + } + }, + "allOf": [ + { + "if": { "properties": { "provider": { "const": "bitanswer" } } }, + "then": { + "required": ["bitanswer"], + "properties": { + "bitanswer": { + "type": "object", + "required": ["url"], + "properties": { "url": { "type": "string", "minLength": 1 } } + } + } + } + }, + { + "if": { "properties": { "provider": { "const": "selfhosted" } } }, + "then": { + "required": ["selfhosted"], + "properties": { + "selfhosted": { + "type": "object", + "required": ["baseUrl"], + "properties": { "baseUrl": { "type": "string", "minLength": 1 } } + } + } + } + }, + { + "if": { "properties": { "scenario": { "const": "floating" } } }, + "then": { + "required": ["floating"], + "properties": { + "floating": { + "type": "object", + "required": ["projectId"], + "properties": { "projectId": { "type": "string", "minLength": 1 } } + } + } + } + } + ] +} diff --git a/scripts/sdk-release-checksums.sh b/scripts/sdk-release-checksums.sh new file mode 100755 index 0000000..f2719e9 --- /dev/null +++ b/scripts/sdk-release-checksums.sh @@ -0,0 +1,126 @@ +#!/usr/bin/env bash +# 生成对外发布用 SHA256SUMS(相对仓库根路径,便于客户执行 sha256sum -c)。 +# 用法见 java/RELEASING.md +set -euo pipefail + +ROOT="$(cd "$(dirname "$0")/.." && pwd)" +OUTPUT="${ROOT}/dist/sdk-release" +RUN_MVN="package" +SKIP_TESTS="-DskipTests" +NATIVE_PATH="" +SIGN="${SIGN:-0}" + +usage() { + sed -n '2,20p' "$0" | head -5 + echo "用法: $0 [--output DIR] [--verify] [--native-path DIR] [--no-mvn]" + echo " --output DIR 输出目录(默认 dist/sdk-release)" + echo " --verify 执行 mvn verify(默认为 package -DskipTests)" + echo " --native-path DIR 额外哈希该目录下 .so/.dylib/.dll" + echo " --no-mvn 不执行 Maven(假定已 package)" + echo "环境变量 SIGN=1 且已配置 gpg 时,生成 SHA256SUMS.asc" +} + +while [[ $# -gt 0 ]]; do + case "$1" in + --output) + OUTPUT="$2" + shift 2 + ;; + --verify) + RUN_MVN="verify" + SKIP_TESTS="" + shift + ;; + --native-path) + NATIVE_PATH="$2" + shift 2 + ;; + --no-mvn) + NO_MVN=1 + shift + ;; + -h|--help) + usage + exit 0 + ;; + *) + echo "未知参数: $1" >&2 + usage >&2 + exit 1 + ;; + esac +done + +mkdir -p "$OUTPUT" + +if [[ -z "${NO_MVN:-}" ]]; then + if [[ -n "$SKIP_TESTS" ]]; then + (cd "$ROOT/java" && mvn -q $SKIP_TESTS "$RUN_MVN") + else + (cd "$ROOT/java" && mvn -q "$RUN_MVN") + fi +fi + +SUMFILE="${OUTPUT}/SHA256SUMS" +: >"$SUMFILE" + +add_hash() { + local file="$1" + local rel="$2" + local h + if command -v sha256sum >/dev/null 2>&1; then + h=$(sha256sum "$file" | awk '{print $1}') + else + h=$(shasum -a 256 "$file" | awk '{print $1}') + fi + echo "$h $rel" >>"$SUMFILE" +} + +for mod in craftlabs-auth-core craftlabs-auth-bitanswer craftlabs-auth-selfhosted; do + dir="$ROOT/java/$mod/target" + [[ -d "$dir" ]] || { + echo "缺少目录: $dir(请先 mvn package)" >&2 + exit 1 + } + shopt -s nullglob + for f in "$dir"/*.jar; do + bn=$(basename "$f") + [[ "$bn" == *-sources.jar ]] && continue + [[ "$bn" == *-javadoc.jar ]] && continue + add_hash "$f" "java/$mod/target/$bn" + done + shopt -u nullglob +done + +if [[ -f "$ROOT/schemas/craftlabs-auth-config.schema.json" ]]; then + add_hash "$ROOT/schemas/craftlabs-auth-config.schema.json" "schemas/craftlabs-auth-config.schema.json" +fi + +if [[ -n "$NATIVE_PATH" ]]; then + [[ -d "$NATIVE_PATH" ]] || { + echo "无效 --native-path: $NATIVE_PATH" >&2 + exit 1 + } + find "$NATIVE_PATH" -type f \( -name '*.so' -o -name '*.dylib' -o -name '*.dll' \) | while IFS= read -r nf; do + rel="${nf#$ROOT/}" + add_hash "$nf" "$rel" + done +fi + +{ + echo "repo=$(basename "$ROOT")" + git -C "$ROOT" rev-parse HEAD 2>/dev/null || echo "git=n/a" + date -u +"%Y-%m-%dT%H:%M:%SZ" +} >"${OUTPUT}/RELEASE-MANIFEST.txt" + +echo "已写入: $SUMFILE" +wc -l "$SUMFILE" + +if [[ "$SIGN" == "1" ]] && command -v gpg >/dev/null 2>&1; then + gpg --batch --yes --armor --detach-sign --output "${SUMFILE}.asc" "$SUMFILE" + echo "已写入: ${SUMFILE}.asc" +fi + +echo "" +echo "校验(在仓库根目录执行):" +echo " cd \"$ROOT\" && sha256sum -c \"${SUMFILE#$ROOT/}\""