feat(sdk): AuthConfigs, JSON Schema, examples, and release checksum CI

Add craftlabs-auth-config.schema.json, Java AuthConfigs model with tests,
example configs aligned to BP-10, C/Java/auth-config documentation,
native header notes, RELEASING guide, and workflow to verify SDK
artifact checksums on release tags.

Made-with: Cursor
This commit is contained in:
2026-04-06 21:05:12 +08:00
parent 65eb983035
commit f94f03bcc2
31 changed files with 1219 additions and 22 deletions
@@ -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
+35
View File
@@ -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` 或递增版本的前提下扩展字段即可。
+71 -10
View File
@@ -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/](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=0szReserved=NULL, …),当需要登录包含指定特征项的授权时才需要调用Bit_LoginEx。
@@ -50,6 +50,7 @@ Bit_Login 等价于 Bit_LoginEx(…, featureId=0szReserved=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-IDBIT-ID硬件,类似于加密的USB设备)** | 格式:#`<序号>`#bitid:`<序号>` 说明:`<序号>` 为 BIT-ID 的索引号,从 0 开始递增 示例:#0#1、#`bitid:0、#bitid:1` | 使用BIT-ID授权 |
| **激活口令(帐号授权密码或SN激活口令)** | <`xxx`> 通过“<>”包裹起来,中间部分为密码 示例:<1234>, <1233>65447> | 激活口令,是指授权码激活时,需要输入的一种口令,激活后不再需要,目前仅支持浮动授权 使用帐号授权时保存的是帐号授权的密码(已经不推荐),其它授权代表激活口令 |
| **既输入SN又输入激活口令** | `xxx<password>`,授权码后边紧跟“<>” 示例:`JNLTGZSE********`<123> | |
| **既输入SN又输入激活口令** | `xxx<password>`,授权码后边紧跟“<>” 示例:`JNLTGZSE******`**<123> | |
- **featureId** - [IN] 登录授权所需要包含的特征项ID。
@@ -73,6 +74,7 @@ lic:///tmp,lic:///data,bit://127.0.0.1:8273
<feature id="" name="" ver=""/>
</scope>
```
- **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 (
<!-- 或者直接输入:'1.1' -->
```
- **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(
<!-- 或者直接输入:'1.1' -->
```
- **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(
……
</features>
```
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(
</groups>
```
> 说明:通过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 id='10'/>
</features>
```
当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(
<type>ws</type> <!-- type:归还类型,ws表示归还到云授权服务,默认是归还到集团授权服务 -->
</scope>
```
用于指定借出的参数,格式: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] 借出请求串。
+61 -9
View File
@@ -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/](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 等价于 LoginExfeatureId=0);当需要登录包含指定特征项的授权时才需要调用LoginEx。
@@ -75,6 +74,7 @@ Login 等价于 LoginExfeatureId=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可以兼容该接口。
### 参数
+17
View File
@@ -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"
}
}
+18
View File
@@ -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"
}
}
+16
View File
@@ -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"
}
}
+21
View File
@@ -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。"
}
}
+29 -2
View File
@@ -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} 后初始化并完成许可校验。
*
* <p>编译时需将 {@code craftlabs-auth-core} 与 Jackson 置于 classpath(与 Maven 模块依赖一致)。
*
* <p>版权所有 © 广州创飞人工智能技术有限公司
*
@@ -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());
+93
View File
@@ -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 公钥指纹** 与下载说明
---
版权所有 © 广州创飞人工智能技术有限公司(以项目实际声明为准)。
+17
View File
@@ -13,4 +13,21 @@
<artifactId>craftlabs-auth-core</artifactId>
<name>CraftLabs Auth — core API</name>
<packaging>jar</packaging>
<dependencies>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<scope>test</scope>
</dependency>
</dependencies>
</project>
@@ -3,6 +3,9 @@ package cn.craftlabs.auth;
/**
* 授权能力的统一契约:初始化、激活、校验许可、查询特性与释放等生命周期方法。
*
* <p>{@link #initialize(String)} 的 JSON 建议使用 {@link cn.craftlabs.auth.config.AuthConfigs#parse(String)}
* 先校验后再传入,格式见仓库 {@code schemas/craftlabs-auth-config.schema.json} 与 {@code examples/config/}。
*
* <p>实现类负责加载对应 native 或远端适配器;调用方应在不再使用时调用 {@link #close()} 释放底层资源。
*
* <p>版权所有 © 广州创飞人工智能技术有限公司
@@ -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} 对齐。
*
* <p>版权所有 © 广州创飞人工智能技术有限公司
*
* @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<String, FeatureMapping> 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;
}
}
@@ -0,0 +1,19 @@
package cn.craftlabs.auth.config;
/**
* {@link AuthConfigs#parse(String)} 或校验失败时抛出。
*
* <p>版权所有 © 广州创飞人工智能技术有限公司
*
* @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);
}
}
@@ -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 配置。
*
* <p>版权所有 © 广州创飞人工智能技术有限公司
*
* @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<String> 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();
}
}
@@ -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) {}
@@ -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) {}
@@ -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) {}
@@ -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) {}
@@ -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) {}
@@ -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) {}
@@ -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);
}
}
}
@@ -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<Path> stream = Files.list(examplesDir)) {
stream.filter(p -> p.toString().endsWith(".json")).forEach(jsonFile -> {
try {
JsonNode instance = MAPPER.readTree(jsonFile.toFile());
Set<ValidationMessage> 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"));
}
}
@@ -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"
}
}
@@ -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"
}
}
@@ -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。"
}
}
+2
View File
@@ -28,6 +28,8 @@
</dependencies>
<properties>
<!-- 测试模块不发布;跳过 GPG 以免生成多余 .asc -->
<gpg.skip>true</gpg.skip>
<native.library.path>${project.basedir}/../../native/build</native.library.path>
</properties>
+47
View File
@@ -21,6 +21,10 @@
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.release>17</maven.compiler.release>
<junit.version>5.10.2</junit.version>
<jackson.version>2.17.2</jackson.version>
<json-schema-validator.version>1.5.7</json-schema-validator.version>
<!-- 默认跳过 GPG;发布签名:mvn -Prelease-sign verify(见 RELEASING.md -->
<gpg.skip>true</gpg.skip>
</properties>
<dependencyManagement>
@@ -36,6 +40,17 @@
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
<version>${jackson.version}</version>
</dependency>
<dependency>
<groupId>com.networknt</groupId>
<artifactId>json-schema-validator</artifactId>
<version>${json-schema-validator.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</dependencyManagement>
@@ -52,7 +67,39 @@
<artifactId>maven-surefire-plugin</artifactId>
<version>3.2.5</version>
</plugin>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<version>3.2.7</version>
</plugin>
</plugins>
</pluginManagement>
</build>
<!-- 对发布 JAR 做 GPG 分离签名(.asc);显式 -Prelease-sign 时启用 -->
<profiles>
<profile>
<id>release-sign</id>
<properties>
<gpg.skip>false</gpg.skip>
</properties>
<build>
<plugins>
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-gpg-plugin</artifactId>
<executions>
<execution>
<id>sign-release-artifacts</id>
<phase>verify</phase>
<goals>
<goal>sign</goal>
</goals>
</execution>
</executions>
</plugin>
</plugins>
</build>
</profile>
</profiles>
</project>
+2
View File
@@ -13,6 +13,8 @@
* FFI / Pythonctypes、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 语言惯例一致)。
+164
View File
@@ -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 } }
}
}
}
}
]
}
+126
View File
@@ -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/}\""