Compare commits

...
13 Commits
Author SHA1 Message Date
cheney 3646572581 添加 openapi 的认证头
CI / build-test (push) Has been cancelled
2026-05-28 14:56:01 +08:00
cheney 1b2825c52c fix:修改 openapi url 开头 2026-05-28 11:33:29 +08:00
waner 6ad2f46ad9 Merge remote-tracking branch 'origin/V1.00' into V1.00 2026-05-28 10:11:52 +08:00
waner f6f220be80 fix 2026-05-28 10:11:23 +08:00
waner f06c675f9b feat:根证书导入增加校验 2026-05-28 10:11:04 +08:00
xydkj f1fa2dedd9 修改PCBAgent2G 2026-05-27 16:52:45 +08:00
xydkj 195f343608 修改PCBAgent2G 2026-05-27 16:37:56 +08:00
waner cae94149cb Merge remote-tracking branch 'origin/V1.00' into V1.00 2026-05-27 13:53:01 +08:00
waner 69a740adcf fix:权限sudo 2026-05-27 13:51:31 +08:00
xydkj 563b625456 修改PBCAgent2G 2026-05-27 09:43:53 +08:00
waner 6823cc4d3e fix 2026-05-26 16:44:15 +08:00
waner 02328d1760 fix:登录流程改造1 2026-05-26 11:30:07 +08:00
waner 0f1c24caf9 fix:登录流程改造 2026-05-26 11:29:44 +08:00
43 changed files with 1163 additions and 1031 deletions
+6 -6
View File
@@ -177,7 +177,7 @@ Open:
- `POST /api/v1/crls/delete` - `POST /api/v1/crls/delete`
当前实现边界: 当前实现边界:
- 可通过 `tms.cert.trusted-root-fingerprints` 配置根 CA 证书 SHA-256 指纹白名单;配置后根 CA 导入必须命中白名单,中间 CA 仍按当前可信链校验。 - 根 CA 导入必须完成来源校验:读取配置项 `tms.cert.root-cert-source.public-key-sm3-path`、`tms.cert.root-cert-source.public-key-path`、`tms.cert.root-cert-source.signature-path` 指向的文件,默认分别为 `/home/tms/device/dev.mac.pub.sm3`、`/home/tms/device/mac_sm2_public_key.pemy` 和 `/home/tms/device/rootcert.sign`;中间 CA 仍按当前可信链校验。
- CRL 文件支持 PEM / DER 格式,导入接口会先创建后台任务并返回 `taskId`,前端通过 `POST /api/v1/crls/import-tasks/detail` 轮询 `PENDING/RUNNING/SUCCESS/FAILED` 状态。 - CRL 文件支持 PEM / DER 格式,导入接口会先创建后台任务并返回 `taskId`,前端通过 `POST /api/v1/crls/import-tasks/detail` 轮询 `PENDING/RUNNING/SUCCESS/FAILED` 状态。
- 后台导入完成后保存 CRL metadata 和 CRL 内每条 revoked certificate 明细;原始 CRL 文件只作为临时文件参与解析,任务结束后删除,不作为业务数据长期保存。 - 后台导入完成后保存 CRL metadata 和 CRL 内每条 revoked certificate 明细;原始 CRL 文件只作为临时文件参与解析,任务结束后删除,不作为业务数据长期保存。
- CRL issuer 通过可信 CA 的 issuer DN 候选和实际 CRL 签名验证解析。 - CRL issuer 通过可信 CA 的 issuer DN 候选和实际 CRL 签名验证解析。
@@ -610,8 +610,8 @@ export ALG_SM4_ECB=1025
## Current Status ## Current Status
- Structure refactored to lightweight monolith modules. - Spring Boot monolith with modular backend packages for `auth`, `init`, `device`, `mk`, `cert`, `file`, `upgrade`, `backup`, `log`, `openapi`, `sign`, and `system`.
- Internal and external API entry points separated. - Internal `/api/**` and external `/openapi/**` boundaries are separated, with session auth, role/auth-level checks, replay protection, and operation audit coverage on high-risk flows.
- Basic auth interceptors for internal/openapi are in place. - Initialization/reset, UKey authentication, master-key lifecycle, certificate/CRL management, offline upgrade, resource backup/restore, device/network/time configuration, file upload, and audit-log features all have active code and focused tests.
- `init/auth/device/sign/system` modules have runnable skeleton controllers and services. - Runtime-generated Swagger/OpenAPI docs remain the source of truth for endpoint shapes; module docs under `docs/openapi/`, `docs/deployment/`, and `docs/analysis/` provide frontend, deployment, and inspection-facing guidance.
- `init/auth/device` modules include mapper+xml+repository-impl+DDL skeleton for TiDB. - Historical implementation plans are indexed at [docs/plans/INDEX.md](/Users/waner/Work/CISD/文档/tms-framework/docs/plans/INDEX.md). Prefer the index and current code over old plan files when they disagree.
+9 -7
View File
@@ -1,8 +1,10 @@
# TMS 初始化 SQL 执行说明 # TMS 初始化 SQL 执行说明
当前推荐使用 [V1__tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/db/migration/V1__tms_schema_full.sql)。 空库初始化当前推荐使用 [V2__tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/db/migration/V2__tms_schema_full.sql)。
这份脚本已经整理成“最终库结构初始化版”,适合空库直接初始化,不再按 migration 逐步执行 `ALTER / RENAME`。 [V1__tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/db/migration/V1__tms_schema_full.sql) 仅用于旧数据库迁移:它会在已有 `tms_auth_full_account` 表上新增 `password_digest`,把固定角色席位统一重置为默认口令 `Sunyard@123`,并置为强制改密。旧库不能用 `V2` 直接覆盖执行。
这份脚本已经整理成“最终库结构初始化版”,适合空库直接初始化,不再按 migration 逐步执行 `ALTER / RENAME`,也不依赖后置 `UPDATE` 修补种子数据。
如果你在 TiDB 客户端里一次性执行整份文件,出现下面这类报错: 如果你在 TiDB 客户端里一次性执行整份文件,出现下面这类报错:
@@ -18,7 +20,7 @@ SQL Error [8130] [HY000]: client has multi-statement capability disabled
优先用客户端的 `Run Script` / `Execute Script` 模式执行: 优先用客户端的 `Run Script` / `Execute Script` 模式执行:
- [V1__tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/db/migration/V1__tms_schema_full.sql) - [V2__tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/db/migration/V2__tms_schema_full.sql)
### 方式二:用“脚本执行”而不是“单次执行” ### 方式二:用“脚本执行”而不是“单次执行”
@@ -30,13 +32,13 @@ SQL Error [8130] [HY000]: client has multi-statement capability disabled
例如: 例如:
```bash ```bash
mysql -h <host> -P <port> -u <user> -p tms < /path/to/V1__tms_schema_full.sql mysql -h <host> -P <port> -u <user> -p tms < /path/to/V2__tms_schema_full.sql
``` ```
或者先登录后: 或者先登录后:
```sql ```sql
source /path/to/V1__tms_schema_full.sql; source /path/to/V2__tms_schema_full.sql;
``` ```
### 方式四:实在只能单条执行时 ### 方式四:实在只能单条执行时
@@ -51,7 +53,7 @@ CREATE DATABASE IF NOT EXISTS tms
USE tms; USE tms;
``` ```
然后把 [V1__tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/db/migration/V1__tms_schema_full.sql) 中的语句按分号逐条执行。 然后把 [V2__tms_schema_full.sql](/Users/waner/Work/CISD/文档/tms-framework/src/main/resources/db/migration/V2__tms_schema_full.sql) 中的语句按分号逐条执行。
## 不推荐但可选 ## 不推荐但可选
@@ -72,7 +74,7 @@ SET GLOBAL tidb_multi_statement_mode='ON';
- `AUDIT_ADMIN` - `AUDIT_ADMIN`
- `OPS_ADMIN` - `OPS_ADMIN`
默认口令均为 `12345678`,其中: 默认口令均为 `Sunyard@123`,其中:
- `KEY_ADMIN` 默认 `ACTIVE` - `KEY_ADMIN` 默认 `ACTIVE`
- 其他角色默认 `UNENABLED` - 其他角色默认 `UNENABLED`
+3
View File
@@ -49,6 +49,9 @@
</dependency> </dependency>
</dependencies> </dependencies>
@@ -1,15 +1,19 @@
package cn.com.infosec.netsign.agent; package cn.com.infosec.netsign.agent;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
public class Main public class Main
{ {
public static void main(String[] args) { public static void main(String[] args) {
System.out.println("test");
PBCAgent2G signClient = new PBCAgent2G(); PBCAgent2G signClient = new PBCAgent2G();
signClient.openSignServer("192.168.100.197", 8088, "");
String dn = "CN=test108100455192,OU=Organizational-1,OU=TPC-S3,O=OCA21,C=CN";
try { try {
signClient.rawSign("test".getBytes(StandardCharsets.UTF_8), "dn"); signClient.rawSign("test".getBytes(StandardCharsets.UTF_8), dn);
// signClient.dettachedSign("test".getBytes(StandardCharsets.UTF_8), dn);
} catch (Exception e) { } catch (Exception e) {
System.out.println("签名失败:" + e.getMessage()); System.out.println("签名失败:" + e.getMessage());
@@ -3,6 +3,7 @@ package cn.com.infosec.netsign.agent;
import cn.com.infosec.netsign.agent.config.OkHttpClientHolder; import cn.com.infosec.netsign.agent.config.OkHttpClientHolder;
import cn.com.infosec.netsign.agent.dto.*; import cn.com.infosec.netsign.agent.dto.*;
import cn.com.infosec.netsign.agent.exception.BusinessException; import cn.com.infosec.netsign.agent.exception.BusinessException;
import cn.com.infosec.netsign.agent.util.HmacUtil;
import com.fasterxml.jackson.core.JsonProcessingException; import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JsonNode; import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper; import com.fasterxml.jackson.databind.ObjectMapper;
@@ -19,9 +20,13 @@ public class PBCAgent2G{
private static final MediaType JSON_MEDIA_TYPE = private static final MediaType JSON_MEDIA_TYPE =
MediaType.parse("application/json; charset=utf-8"); MediaType.parse("application/json; charset=utf-8");
private static final String APP_ID_HEADER = "X-App-Id";
private static final String TIMESTAMP_HEADER = "X-Timestamp";
private static final String NONCE_HEADER = "X-Nonce";
private static final String SIGNATURE_HEADER = "X-Signature";
private static final String baseUrl = "http://127.0.0.1:8080/api/v1/openapi"; private static final String appId = "openapi-client";
private static final String apiKey = "key"; private static final String appSecret = "openapi-secret";
private static final String rawSignPath = "/rawSign"; private static final String rawSignPath = "/rawSign";
private static final String rawVerifyPath = "/rawVerify"; private static final String rawVerifyPath = "/rawVerify";
@@ -39,6 +44,8 @@ public class PBCAgent2G{
private int orderdn; private int orderdn;
private int timeout; private int timeout;
private String baseUrl = "http://127.0.0.1:8088/openapi/v1/openapi";
private final ThreadLocal<Integer> lastReturnCode = new ThreadLocal<>(); private final ThreadLocal<Integer> lastReturnCode = new ThreadLocal<>();
public PBCAgent2G() { public PBCAgent2G() {
@@ -51,6 +58,18 @@ public class PBCAgent2G{
this.objectMapper = new ObjectMapper(); this.objectMapper = new ObjectMapper();
} }
private void addSignHeaders(Request.Builder builder) {
String timestamp = String.valueOf(System.currentTimeMillis() / 1000);
String nonce = UUID.randomUUID().toString().replace("-", "");
String content = appId + "\n" + timestamp + "\n" + nonce;
String signature = HmacUtil.hmacSha256Hex(appSecret, content);
builder.addHeader(APP_ID_HEADER, appId);
builder.addHeader(TIMESTAMP_HEADER, timestamp);
builder.addHeader(NONCE_HEADER, nonce);
builder.addHeader(SIGNATURE_HEADER, signature);
}
public void isDebug(boolean isDebug){ public void isDebug(boolean isDebug){
this.isDebug = isDebug; this.isDebug = isDebug;
} }
@@ -67,22 +86,32 @@ public class PBCAgent2G{
this.timeout = timeout; this.timeout = timeout;
} }
public int getReturnCode(){
return 1;
}
public boolean closeSignServer(){
return true;
}
public boolean openSignServer(String ip, int port, String password){ public boolean openSignServer(String ip, int port, String password){
System.out.println(ip); System.out.println("ip: " + ip);
System.out.println(port); System.out.println("port: " + port);
System.out.println(password); System.out.println("password: " + password);
baseUrl = "http://"+ ip +":" + port +"/openapi/v1/openapi";
return true; return true;
}; };
public boolean[] openSignServer(String ip, String port,String password){ public boolean[] openSignServer(String ip, String port,String password){
System.out.println(ip); System.out.println("ip: " + ip);
System.out.println(port); System.out.println("port: " + port);
System.out.println(password); System.out.println("password: " + password);
baseUrl = "http://"+ ip +":" + port +"/openapi/v1/openapi";
return new boolean[]{true}; return new boolean[]{true};
}; };
public String rawSign(byte[] origBytes, String dn) { public String rawSign(byte[] origBytes, String dn) {
System.out.println("调用rawSign, dn:" + dn); System.out.println("调用rawSign, dn:" + dn);
String sessionId = UUID.randomUUID().toString().replace("-", ""); String sessionId = UUID.randomUUID().toString().replace("-", "");
@@ -121,12 +150,12 @@ public class PBCAgent2G{
JSON_MEDIA_TYPE JSON_MEDIA_TYPE
); );
Request request = new Request.Builder() Request.Builder requestBuilder = new Request.Builder()
.url(buildUrl(rawSignPath)) .url(buildUrl(rawSignPath))
.post(requestBody) .post(requestBody)
.addHeader("Content-Type", "application/json") .addHeader("Content-Type", "application/json");
.addHeader("X-API-Key", apiKey) addSignHeaders(requestBuilder);
.build(); Request request = requestBuilder.build();
System.out.println("准备连接签名服务器"); System.out.println("准备连接签名服务器");
@@ -150,7 +179,7 @@ public class PBCAgent2G{
} }
} catch (IOException e) { } catch (IOException e) {
System.err.println("连接签名服务器异常" + e.getMessage()); System.out.println("连接签名服务器异常" + e.getMessage());
throw new RuntimeException("调用rawSign异常:" + e.getMessage(), e); throw new RuntimeException("调用rawSign异常:" + e.getMessage(), e);
} finally { } finally {
System.out.println("请求结束, 连接资源已释放"); System.out.println("请求结束, 连接资源已释放");
@@ -159,7 +188,7 @@ public class PBCAgent2G{
public boolean rawVerify(byte[] origBytes, String certStr, String dn) { public boolean rawVerify(byte[] origBytes, String certStr, String dn) {
System.out.println("调用rawVerify, dn:" + dn); System.out.println("调用rawVerify, dn:" + dn + " , certStr:" + certStr);
String sessionId = UUID.randomUUID().toString().replace("-", ""); String sessionId = UUID.randomUUID().toString().replace("-", "");
if (origBytes == null) { if (origBytes == null) {
@@ -203,12 +232,12 @@ public class PBCAgent2G{
JSON_MEDIA_TYPE JSON_MEDIA_TYPE
); );
Request request = new Request.Builder() Request.Builder requestBuilder = new Request.Builder()
.url(buildUrl(rawVerifyPath)) .url(buildUrl(rawVerifyPath))
.post(requestBody) .post(requestBody)
.addHeader("Content-Type", "application/json") .addHeader("Content-Type", "application/json");
.addHeader("X-API-Key", apiKey) addSignHeaders(requestBuilder);
.build(); Request request = requestBuilder.build();
System.out.println("准备连接签名服务器"); System.out.println("准备连接签名服务器");
try (Response response = okHttpClient.newCall(request).execute()) { try (Response response = okHttpClient.newCall(request).execute()) {
@@ -230,7 +259,7 @@ public class PBCAgent2G{
} }
} catch (IOException e) { } catch (IOException e) {
System.err.println("连接签名服务器异常" + e.getMessage()); System.out.println("连接签名服务器异常" + e.getMessage());
throw new RuntimeException("调用rawVerify异常", e); throw new RuntimeException("调用rawVerify异常", e);
} finally { } finally {
System.out.println("请求结束, 连接资源已释放"); System.out.println("请求结束, 连接资源已释放");
@@ -273,12 +302,12 @@ public class PBCAgent2G{
JSON_MEDIA_TYPE JSON_MEDIA_TYPE
); );
Request request = new Request.Builder() Request.Builder requestBuilder = new Request.Builder()
.url(buildUrl(dettachedSignPath)) .url(buildUrl(dettachedSignPath))
.post(requestBody) .post(requestBody)
.addHeader("Content-Type", "application/json") .addHeader("Content-Type", "application/json");
.addHeader("X-API-Key", apiKey) addSignHeaders(requestBuilder);
.build(); Request request = requestBuilder.build();
System.out.println("准备连接签名服务器"); System.out.println("准备连接签名服务器");
@@ -301,7 +330,7 @@ public class PBCAgent2G{
} }
} catch (IOException e) { } catch (IOException e) {
System.err.println("连接签名服务器异常" + e.getMessage()); System.out.println("连接签名服务器异常" + e.getMessage());
throw new RuntimeException("调用dettachedSign异常", e); throw new RuntimeException("调用dettachedSign异常", e);
} finally { } finally {
System.out.println("请求结束, 连接资源已释放"); System.out.println("请求结束, 连接资源已释放");
@@ -345,12 +374,12 @@ public class PBCAgent2G{
JSON_MEDIA_TYPE JSON_MEDIA_TYPE
); );
Request request = new Request.Builder() Request.Builder requestBuilder = new Request.Builder()
.url(buildUrl(dettachedVerifyPath)) .url(buildUrl(dettachedVerifyPath))
.post(requestBody) .post(requestBody)
.addHeader("Content-Type", "application/json") .addHeader("Content-Type", "application/json");
.addHeader("X-API-Key", apiKey) addSignHeaders(requestBuilder);
.build(); Request request = requestBuilder.build();
System.out.println("准备连接签名服务器"); System.out.println("准备连接签名服务器");
try (Response response = okHttpClient.newCall(request).execute()) { try (Response response = okHttpClient.newCall(request).execute()) {
@@ -374,7 +403,7 @@ public class PBCAgent2G{
} }
} catch (IOException e) { } catch (IOException e) {
System.err.println("连接签名服务器异常" + e.getMessage()); System.out.println("连接签名服务器异常" + e.getMessage());
throw new RuntimeException("调用dettachedVerify异常", e); throw new RuntimeException("调用dettachedVerify异常", e);
} finally { } finally {
System.out.println("请求结束, 连接资源已释放"); System.out.println("请求结束, 连接资源已释放");
@@ -419,12 +448,12 @@ public class PBCAgent2G{
JSON_MEDIA_TYPE JSON_MEDIA_TYPE
); );
Request request = new Request.Builder() Request.Builder requestBuilder = new Request.Builder()
.url(buildUrl(dettachedVerifySimplePath)) .url(buildUrl(dettachedVerifySimplePath))
.post(requestBody) .post(requestBody)
.addHeader("Content-Type", "application/json") .addHeader("Content-Type", "application/json");
.addHeader("X-API-Key", apiKey) addSignHeaders(requestBuilder);
.build(); Request request = requestBuilder.build();
System.out.println("准备连接签名服务器"); System.out.println("准备连接签名服务器");
try (Response response = okHttpClient.newCall(request).execute()) { try (Response response = okHttpClient.newCall(request).execute()) {
@@ -446,7 +475,7 @@ public class PBCAgent2G{
} }
} catch (IOException e) { } catch (IOException e) {
System.err.println("连接签名服务器异常" + e.getMessage()); System.out.println("连接签名服务器异常" + e.getMessage());
throw new RuntimeException("调用dettachedVerifySimple异常", e); throw new RuntimeException("调用dettachedVerifySimple异常", e);
} finally { } finally {
System.out.println("请求结束, 连接资源已释放"); System.out.println("请求结束, 连接资源已释放");
@@ -485,12 +514,12 @@ public class PBCAgent2G{
JSON_MEDIA_TYPE JSON_MEDIA_TYPE
); );
Request request = new Request.Builder() Request.Builder requestBuilder = new Request.Builder()
.url(buildUrl(uploadCertPath)) .url(buildUrl(uploadCertPath))
.post(requestBody) .post(requestBody)
.addHeader("Content-Type", "application/json") .addHeader("Content-Type", "application/json");
.addHeader("X-API-Key", apiKey) addSignHeaders(requestBuilder);
.build(); Request request = requestBuilder.build();
System.out.println("准备连接签名服务器"); System.out.println("准备连接签名服务器");
try (Response response = okHttpClient.newCall(request).execute()) { try (Response response = okHttpClient.newCall(request).execute()) {
@@ -512,7 +541,7 @@ public class PBCAgent2G{
} }
} catch (IOException e) { } catch (IOException e) {
System.err.println("连接签名服务器异常" + e.getMessage()); System.out.println("连接签名服务器异常" + e.getMessage());
throw new RuntimeException("调用uploadCert异常", e); throw new RuntimeException("调用uploadCert异常", e);
} finally { } finally {
System.out.println("请求结束, 连接资源已释放"); System.out.println("请求结束, 连接资源已释放");
@@ -526,7 +555,7 @@ public class PBCAgent2G{
private String buildUrl(String path) { private String buildUrl(String path) {
String baseUrl = "http://127.0.0.1:8080/api/v1/openapi";
if (baseUrl.endsWith("/") && path.startsWith("/")) { if (baseUrl.endsWith("/") && path.startsWith("/")) {
return baseUrl.substring(0, baseUrl.length() - 1) + path; return baseUrl.substring(0, baseUrl.length() - 1) + path;
@@ -0,0 +1,30 @@
package cn.com.infosec.netsign.agent.util;
import java.nio.charset.StandardCharsets;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
public final class HmacUtil {
private HmacUtil() {
}
public static String hmacSha256Hex(String key, String content) {
try {
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] digest = mac.doFinal(content.getBytes(StandardCharsets.UTF_8));
return toHex(digest);
} catch (Exception ex) {
throw new IllegalStateException("HMAC计算失败", ex);
}
}
private static String toHex(byte[] bytes) {
StringBuilder builder = new StringBuilder(bytes.length * 2);
for (byte b : bytes) {
builder.append(String.format("%02x", b));
}
return builder.toString();
}
}
@@ -1,4 +1,4 @@
sign-server.baseUrl=http://127.0.0.1:8080/api/v1/openapi sign-server.baseUrl=http://192.168.100.197:8088/api/v1/openapi
sign-server.apiKey=key sign-server.apiKey=key
sign-server.rawSignPath=/rawSign sign-server.rawSignPath=/rawSign
@@ -58,6 +58,7 @@ public class AuthAdminController {
@PostMapping("/roles/{roleCode}/accounts/{uid}/change-password") @PostMapping("/roles/{roleCode}/accounts/{uid}/change-password")
@Operation(summary = "管理员修改角色席位口令", description = "管理员为目标角色指定席位账号设置新口令。") @Operation(summary = "管理员修改角色席位口令", description = "管理员为目标角色指定席位账号设置新口令。")
@ReplayProtected @ReplayProtected
@Deprecated
@RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL) @RequireInternalAuth(role = RoleCode.SUPER_ADMIN, authLevel = AuthLevel.FULL)
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "管理员修改角色席位口令") @AuditedOperation(module = ModuleCode.AUTH, action = ActionType.UPDATE, summary = "管理员修改角色席位口令")
public ApiResponse<Void> changeAccountPassword( public ApiResponse<Void> changeAccountPassword(
@@ -24,6 +24,7 @@ public class AuthController {
private final AuthService authService; private final AuthService authService;
@PostMapping("/password-login") @PostMapping("/password-login")
@Deprecated
@Operation(summary = "口令登录", description = "按旧系统受限登录流程签发 LIMITED 会话。") @Operation(summary = "口令登录", description = "按旧系统受限登录流程签发 LIMITED 会话。")
@AuditedOperation(module = ModuleCode.AUTH, action = ActionType.LOGIN, summary = "口令登录") @AuditedOperation(module = ModuleCode.AUTH, action = ActionType.LOGIN, summary = "口令登录")
public ApiResponse<LoginResponse> passwordLogin(@Valid @RequestBody PasswordLoginRequest request) { public ApiResponse<LoginResponse> passwordLogin(@Valid @RequestBody PasswordLoginRequest request) {
@@ -79,9 +80,23 @@ public class AuthController {
authService.changeAccountPassword( authService.changeAccountPassword(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN), (String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN),
uid, uid,
request.getCurrentPassword(), request.getChangeToken(),
request.getNewPassword() request.getOldPasswordDynamicDigest(),
request.getNewPasswordDigest()
); );
return ApiResponse.success(); return ApiResponse.success();
} }
@PostMapping("/accounts/{uid}/change-password/prepare")
@Operation(summary = "准备修改当前登录席位口令", description = "生成改密确认令牌、旧口令挑战随机数和新口令盐值。")
@ReplayProtected
public ApiResponse<PasswordChangePrepareResponse> prepareAccountPasswordChange(
@PathVariable("uid") Integer uid,
HttpServletRequest httpRequest
) {
return ApiResponse.success(authService.prepareAccountPasswordChange(
(String) httpRequest.getAttribute(InternalApiAuthInterceptor.ATTR_SESSION_TOKEN),
uid
));
}
} }
@@ -1,32 +1,58 @@
package com.cisd.tms.modules.auth.dto; package com.cisd.tms.modules.auth.dto;
import com.fasterxml.jackson.annotation.JsonAlias;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
@Schema(description = "管理员修改账号口令请求") @Schema(description = "管理员修改账号口令请求")
public class AdminChangePasswordRequest { public class AdminChangePasswordRequest {
@NotBlank(message = "oldPassword不能为空") @NotBlank(message = "oldPasswordDigest不能为空")
@Schema(description = "旧口令SM3摘要", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b") @JsonAlias("oldPasswordDynamicDigest")
private String oldPassword; @Schema(description = "旧口令静态摘要", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b")
private String oldPasswordDigest;
@NotBlank(message = "newPassword不能为空") @NotBlank(message = "newPasswordDigest不能为空")
@Schema(description = "新口令SM3摘要", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b") @Schema(description = "新口令静态摘要", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b")
private String newPassword; private String newPasswordDigest;
public String getOldPassword() { public String getOldPassword() {
return oldPassword; return oldPasswordDigest;
} }
public void setOldPassword(String oldPassword) { public void setOldPassword(String oldPassword) {
this.oldPassword = oldPassword; this.oldPasswordDigest = oldPassword;
} }
public String getNewPassword() { public String getNewPassword() {
return newPassword; return newPasswordDigest;
} }
public void setNewPassword(String newPassword) { public void setNewPassword(String newPassword) {
this.newPassword = newPassword; this.newPasswordDigest = newPassword;
}
public String getOldPasswordDynamicDigest() {
return oldPasswordDigest;
}
public void setOldPasswordDynamicDigest(String oldPasswordDynamicDigest) {
this.oldPasswordDigest = oldPasswordDynamicDigest;
}
public String getOldPasswordDigest() {
return oldPasswordDigest;
}
public void setOldPasswordDigest(String oldPasswordDigest) {
this.oldPasswordDigest = oldPasswordDigest;
}
public String getNewPasswordDigest() {
return newPasswordDigest;
}
public void setNewPasswordDigest(String newPasswordDigest) {
this.newPasswordDigest = newPasswordDigest;
} }
} }
@@ -7,38 +7,67 @@ import jakarta.validation.constraints.NotBlank;
@Schema(description = "修改当前认证账户口令请求") @Schema(description = "修改当前认证账户口令请求")
public class ChangePasswordRequest { public class ChangePasswordRequest {
@NotBlank(message = "oldPassword不能为空") @NotBlank(message = "changeToken不能为空")
@JsonAlias("currentPassword") @Schema(description = "改密准备接口返回的确认令牌")
@Schema(description = "旧口令SM3摘要", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b") private String changeToken;
private String oldPassword;
@NotBlank(message = "newPassword不能为空") @NotBlank(message = "oldPasswordDynamicDigest不能为空")
@Schema(description = "新口令SM3摘要", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b") @JsonAlias({"oldPassword", "currentPassword"})
private String newPassword; @Schema(description = "旧口令动态摘要:SM3(oldPasswordDigest + oldServerRandom)", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b")
private String oldPasswordDynamicDigest;
@NotBlank(message = "newPasswordDigest不能为空")
@JsonAlias("newPassword")
@Schema(description = "新口令静态摘要:SM3(newPasswordSalt + newPassword)", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b")
private String newPasswordDigest;
public String getOldPassword() { public String getOldPassword() {
return oldPassword; return oldPasswordDynamicDigest;
} }
public void setOldPassword(String oldPassword) { public void setOldPassword(String oldPassword) {
this.oldPassword = oldPassword; this.oldPasswordDynamicDigest = oldPassword;
} }
@Deprecated @Deprecated
public String getCurrentPassword() { public String getCurrentPassword() {
return oldPassword; return oldPasswordDynamicDigest;
} }
@Deprecated @Deprecated
public void setCurrentPassword(String currentPassword) { public void setCurrentPassword(String currentPassword) {
this.oldPassword = currentPassword; this.oldPasswordDynamicDigest = currentPassword;
} }
public String getNewPassword() { public String getNewPassword() {
return newPassword; return newPasswordDigest;
} }
public void setNewPassword(String newPassword) { public void setNewPassword(String newPassword) {
this.newPassword = newPassword; this.newPasswordDigest = newPassword;
}
public String getChangeToken() {
return changeToken;
}
public void setChangeToken(String changeToken) {
this.changeToken = changeToken;
}
public String getOldPasswordDynamicDigest() {
return oldPasswordDynamicDigest;
}
public void setOldPasswordDynamicDigest(String oldPasswordDynamicDigest) {
this.oldPasswordDynamicDigest = oldPasswordDynamicDigest;
}
public String getNewPasswordDigest() {
return newPasswordDigest;
}
public void setNewPasswordDigest(String newPasswordDigest) {
this.newPasswordDigest = newPasswordDigest;
} }
} }
@@ -4,6 +4,7 @@ public class FullLoginAccountRequest {
private Integer uid; private Integer uid;
private String password; private String password;
private String serverRandom;
public Integer getUid() { public Integer getUid() {
return uid; return uid;
@@ -20,4 +21,12 @@ public class FullLoginAccountRequest {
public void setPassword(String password) { public void setPassword(String password) {
this.password = password; this.password = password;
} }
public String getServerRandom() {
return serverRandom;
}
public void setServerRandom(String serverRandom) {
this.serverRandom = serverRandom;
}
} }
@@ -0,0 +1,40 @@
package com.cisd.tms.modules.auth.dto;
import io.swagger.v3.oas.annotations.media.Schema;
@Schema(description = "修改口令准备响应")
public class PasswordChangePrepareResponse {
@Schema(description = "改密确认令牌")
private String changeToken;
@Schema(description = "旧口令动态摘要挑战随机数")
private String oldServerRandom;
@Schema(description = "新口令盐值,前端写入 UKey")
private String newPasswordSalt;
public String getChangeToken() {
return changeToken;
}
public void setChangeToken(String changeToken) {
this.changeToken = changeToken;
}
public String getOldServerRandom() {
return oldServerRandom;
}
public void setOldServerRandom(String oldServerRandom) {
this.oldServerRandom = oldServerRandom;
}
public String getNewPasswordSalt() {
return newPasswordSalt;
}
public void setNewPasswordSalt(String newPasswordSalt) {
this.newPasswordSalt = newPasswordSalt;
}
}
@@ -1,5 +1,6 @@
package com.cisd.tms.modules.auth.dto; package com.cisd.tms.modules.auth.dto;
import com.fasterxml.jackson.annotation.JsonAlias;
import io.swagger.v3.oas.annotations.media.Schema; import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotBlank; import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull; import jakarta.validation.constraints.NotNull;
@@ -28,8 +29,10 @@ public class UkeyLoginProof {
@NotBlank(message = "loginSignature不能为空") @NotBlank(message = "loginSignature不能为空")
private String loginSignature; private String loginSignature;
@Schema(description = "当前 UKey 固定席位对应账号口令的SM3摘要,字段名保持password兼容前端", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b") @NotBlank(message = "passwordDynamicDigest不能为空")
private String password; @JsonAlias("password")
@Schema(description = "当前 UKey 固定席位对应账号口令动态摘要:SM3(passwordDigest + serverRandom)", example = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b")
private String passwordDynamicDigest;
public String getPubKey() { public String getPubKey() {
return pubKey; return pubKey;
@@ -80,10 +83,18 @@ public class UkeyLoginProof {
} }
public String getPassword() { public String getPassword() {
return password; return passwordDynamicDigest;
} }
public void setPassword(String password) { public void setPassword(String password) {
this.password = password; this.passwordDynamicDigest = password;
}
public String getPasswordDynamicDigest() {
return passwordDynamicDigest;
}
public void setPasswordDynamicDigest(String passwordDynamicDigest) {
this.passwordDynamicDigest = passwordDynamicDigest;
} }
} }
@@ -11,7 +11,7 @@ public class AuthFullAccountEntity extends BaseEntity {
private Integer uid; private Integer uid;
private String accountName; private String accountName;
private String displayName; private String displayName;
private String passwordHash; private String passwordDigest;
private String passwordSalt; private String passwordSalt;
private String status; private String status;
private Boolean needChangePassword; private Boolean needChangePassword;
@@ -53,12 +53,12 @@ public class AuthFullAccountEntity extends BaseEntity {
this.displayName = displayName; this.displayName = displayName;
} }
public String getPasswordHash() { public String getPasswordDigest() {
return passwordHash; return passwordDigest;
} }
public void setPasswordHash(String passwordHash) { public void setPasswordDigest(String passwordDigest) {
this.passwordHash = passwordHash; this.passwordDigest = passwordDigest;
} }
public String getPasswordSalt() { public String getPasswordSalt() {
@@ -5,6 +5,7 @@ import com.cisd.tms.modules.auth.dto.CurrentUserResponse;
import com.cisd.tms.modules.auth.dto.LoginRequest; import com.cisd.tms.modules.auth.dto.LoginRequest;
import com.cisd.tms.modules.auth.dto.LoginResponse; import com.cisd.tms.modules.auth.dto.LoginResponse;
import com.cisd.tms.modules.auth.dto.PasswordLoginRequest; import com.cisd.tms.modules.auth.dto.PasswordLoginRequest;
import com.cisd.tms.modules.auth.dto.PasswordChangePrepareResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomRequest; import com.cisd.tms.modules.auth.dto.UkeyLoginRandomRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse; import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginRequest; import com.cisd.tms.modules.auth.dto.UkeyLoginRequest;
@@ -27,5 +28,13 @@ public interface AuthService {
void logout(String sessionToken); void logout(String sessionToken);
void changeAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword); PasswordChangePrepareResponse prepareAccountPasswordChange(String sessionToken, Integer uid);
void changeAccountPassword(
String sessionToken,
Integer uid,
String changeToken,
String oldPasswordDynamicDigest,
String newPasswordDigest
);
} }
@@ -16,7 +16,6 @@ import com.cisd.tms.modules.auth.repository.RoleUkeyBindingRepository;
import com.cisd.tms.modules.auth.service.AuthAdminService; import com.cisd.tms.modules.auth.service.AuthAdminService;
import com.cisd.tms.modules.auth.service.PasswordDigestValidator; import com.cisd.tms.modules.auth.service.PasswordDigestValidator;
import com.cisd.tms.modules.auth.service.PasswordHasher; import com.cisd.tms.modules.auth.service.PasswordHasher;
import com.cisd.tms.modules.auth.service.PasswordSaltGenerator;
import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket; import com.cisd.tms.modules.mk.dto.MasterKeyBackupPacket;
import com.cisd.tms.modules.mk.dto.UKeySignDTO; import com.cisd.tms.modules.mk.dto.UKeySignDTO;
import com.cisd.tms.modules.mk.dto.UKeySignEntity; import com.cisd.tms.modules.mk.dto.UKeySignEntity;
@@ -43,7 +42,6 @@ import org.springframework.stereotype.Service;
public class AuthAdminServiceImpl implements AuthAdminService { public class AuthAdminServiceImpl implements AuthAdminService {
static final String DEFAULT_PASSWORD = "Sunyard@123"; static final String DEFAULT_PASSWORD = "Sunyard@123";
private static final String DEFAULT_PASSWORD_DIGEST = PasswordDigestSupport.sm3Hex(DEFAULT_PASSWORD);
private final RoleAccountRepository roleAccountRepository; private final RoleAccountRepository roleAccountRepository;
private final AuthFullAccountRepository authFullAccountRepository; private final AuthFullAccountRepository authFullAccountRepository;
@@ -52,7 +50,6 @@ public class AuthAdminServiceImpl implements AuthAdminService {
private final LmkService lmkService; private final LmkService lmkService;
private final ObjectMapper objectMapper; private final ObjectMapper objectMapper;
private final Clock clock; private final Clock clock;
private final PasswordSaltGenerator passwordSaltGenerator;
private void enableRoleAfterUkeyBindingIfReady(RoleCode targetRole) { private void enableRoleAfterUkeyBindingIfReady(RoleCode targetRole) {
if (!hasRequiredActiveUkeyBindings(targetRole)) { if (!hasRequiredActiveUkeyBindings(targetRole)) {
@@ -105,9 +102,8 @@ public class AuthAdminServiceImpl implements AuthAdminService {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色完整账号不存在"); throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色完整账号不存在");
} }
for (AuthFullAccountEntity account : fullAccounts) { for (AuthFullAccountEntity account : fullAccounts) {
String newSalt = passwordSaltGenerator.nextSalt(); String salt = account.getPasswordSalt();
account.setPasswordSalt(newSalt); account.setPasswordDigest(passwordHasher.hash(PasswordDigestSupport.sm3Hex(salt + DEFAULT_PASSWORD), salt));
account.setPasswordHash(passwordHasher.hash(DEFAULT_PASSWORD_DIGEST, newSalt));
account.setStatus(RoleAccountStatus.ACTIVE.name()); account.setStatus(RoleAccountStatus.ACTIVE.name());
account.setNeedChangePassword(Boolean.TRUE); account.setNeedChangePassword(Boolean.TRUE);
account.setPasswordChangedAt(current); account.setPasswordChangedAt(current);
@@ -170,7 +166,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
} }
AuthFullAccountEntity account = authFullAccountRepository.findByRoleCodeAndUid(targetRoleCode, uid) AuthFullAccountEntity account = authFullAccountRepository.findByRoleCodeAndUid(targetRoleCode, uid)
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色完整账号不存在")); .orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色完整账号不存在"));
if (!passwordHasher.matches(oldPassword, account.getPasswordSalt(), account.getPasswordHash())) { if (!passwordHasher.matches(oldPassword, account.getPasswordSalt(), account.getPasswordDigest())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "旧密码错误"); throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "旧密码错误");
} }
applyAdminPasswordChange(account, newPassword); applyAdminPasswordChange(account, newPassword);
@@ -431,6 +427,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
enableRoleAfterUkeyBindingIfReady(targetRole); enableRoleAfterUkeyBindingIfReady(targetRole);
return UKeySignResult.builder() return UKeySignResult.builder()
.sign(signValue) .sign(signValue)
.passwordSalt(loadAccount(targetRole.getCode(), request.getUid()).getPasswordSalt())
.backupPacket(backupPacket) .backupPacket(backupPacket)
.build(); .build();
} }
@@ -466,9 +463,7 @@ public class AuthAdminServiceImpl implements AuthAdminService {
} }
private void applyAdminPasswordChange(AuthFullAccountEntity account, String newPassword) { private void applyAdminPasswordChange(AuthFullAccountEntity account, String newPassword) {
String newSalt = passwordSaltGenerator.nextSalt(); account.setPasswordDigest(passwordHasher.hash(newPassword, account.getPasswordSalt()));
account.setPasswordSalt(newSalt);
account.setPasswordHash(passwordHasher.hash(newPassword, newSalt));
account.setNeedChangePassword(Boolean.TRUE); account.setNeedChangePassword(Boolean.TRUE);
account.setPasswordChangedAt(now()); account.setPasswordChangedAt(now());
account.setFailedCount(0); account.setFailedCount(0);
@@ -484,6 +479,11 @@ public class AuthAdminServiceImpl implements AuthAdminService {
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色账号不存在")); .orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色账号不存在"));
} }
private AuthFullAccountEntity loadAccount(String roleCode, Integer uid) {
return authFullAccountRepository.findByRoleCodeAndUid(roleCode, uid)
.orElseThrow(() -> new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "目标角色完整账号不存在"));
}
private void validateRoleUid(RoleCode targetRole, Integer uid) { private void validateRoleUid(RoleCode targetRole, Integer uid) {
if (uid == null || uid < 1 || uid > targetRole.getRequiredUkeyCount()) { if (uid == null || uid < 1 || uid > targetRole.getRequiredUkeyCount()) {
throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "uid超出角色UKey席位要求"); throw new BizException(ErrorCode.VALIDATE_FAILED.getCode(), "uid超出角色UKey席位要求");
@@ -82,7 +82,7 @@ public class AuthSecurityResetServiceImpl implements AuthSecurityResetService {
account.setUid(seed.uid()); account.setUid(seed.uid());
account.setAccountName(seed.accountName()); account.setAccountName(seed.accountName());
account.setDisplayName(seed.displayName()); account.setDisplayName(seed.displayName());
account.setPasswordHash(seed.passwordHash()); account.setPasswordDigest(seed.passwordDigest());
account.setPasswordSalt(seed.passwordSalt()); account.setPasswordSalt(seed.passwordSalt());
account.setStatus(RoleAccountStatus.ACTIVE.name()); account.setStatus(RoleAccountStatus.ACTIVE.name());
account.setNeedChangePassword(Boolean.TRUE); account.setNeedChangePassword(Boolean.TRUE);
@@ -120,7 +120,7 @@ public class AuthSecurityResetServiceImpl implements AuthSecurityResetService {
1, 1,
"super-admin-full-01", "super-admin-full-01",
"超级管理员UKey席位一", "超级管理员UKey席位一",
"vVPlH2UGQcXUo7NblMJMaof32um6UinF8zsckZuxe9E=", "00718a8319e67ef6cf9fc389c14ce16784278d55439a7a30a63fa7be8e42bbdf",
"3df85c2988a5de2b9eeaa8109639641a" "3df85c2988a5de2b9eeaa8109639641a"
), ),
new SeedFullAccount( new SeedFullAccount(
@@ -129,7 +129,7 @@ public class AuthSecurityResetServiceImpl implements AuthSecurityResetService {
2, 2,
"super-admin-full-02", "super-admin-full-02",
"超级管理员UKey席位二", "超级管理员UKey席位二",
"25e83ejpUmjU64cYAKcDV5hhqClEu6aol8YyR3Jey7E=", "169731d7c897649a16a073cdfddf48d2e98f72f10da5cead5cbbd51f8a9ffea6",
"50de03346bb1f6afc74938acf5e88560" "50de03346bb1f6afc74938acf5e88560"
), ),
new SeedFullAccount( new SeedFullAccount(
@@ -138,7 +138,7 @@ public class AuthSecurityResetServiceImpl implements AuthSecurityResetService {
1, 1,
"key-admin-full-01", "key-admin-full-01",
"密钥管理员UKey席位", "密钥管理员UKey席位",
"l65Pr7MxOvqPOe0u6ozGGZcT4raTXUgDYMQfLTINTyE=", "6d386b2a13550befb821795dcc0e9bda3d1cffc0d565ad539de16117e28f6a81",
"073cd5fd30701edfcf033e8f501b2b43" "073cd5fd30701edfcf033e8f501b2b43"
), ),
new SeedFullAccount( new SeedFullAccount(
@@ -147,7 +147,7 @@ public class AuthSecurityResetServiceImpl implements AuthSecurityResetService {
1, 1,
"audit-admin-full-01", "audit-admin-full-01",
"审计管理员UKey席位", "审计管理员UKey席位",
"ZRwdn3TOotyXAytO4zeSpxRm/68Rm8VcB7w7VJmZPYc=", "72a53b0cb8dd15775516f5085c7a3ee09d65a130dc1b17e18545d24930826d0c",
"57e2616077bd8af5cb90fc9790f69573" "57e2616077bd8af5cb90fc9790f69573"
), ),
new SeedFullAccount( new SeedFullAccount(
@@ -156,7 +156,7 @@ public class AuthSecurityResetServiceImpl implements AuthSecurityResetService {
1, 1,
"ops-admin-full-01", "ops-admin-full-01",
"运维管理员UKey席位", "运维管理员UKey席位",
"5Wu8Mf5hC3kq4gm/62/T7pO+aZIw8Y7uYoMIztWyT1w=", "58655960343a1efb0c1b281ce4c384ac699d01ab61c2c710d35e54b798f6e4c5",
"a6bea7601138e90a540b39c73d53df81" "a6bea7601138e90a540b39c73d53df81"
) )
); );
@@ -171,7 +171,7 @@ public class AuthSecurityResetServiceImpl implements AuthSecurityResetService {
int uid, int uid,
String accountName, String accountName,
String displayName, String displayName,
String passwordHash, String passwordDigest,
String passwordSalt String passwordSalt
) { ) {
} }
@@ -10,6 +10,7 @@ import com.cisd.tms.modules.auth.dto.LoginRequest;
import com.cisd.tms.modules.auth.dto.LoginResponse; import com.cisd.tms.modules.auth.dto.LoginResponse;
import com.cisd.tms.modules.auth.dto.PasswordLoginAccountRequest; import com.cisd.tms.modules.auth.dto.PasswordLoginAccountRequest;
import com.cisd.tms.modules.auth.dto.PasswordLoginRequest; import com.cisd.tms.modules.auth.dto.PasswordLoginRequest;
import com.cisd.tms.modules.auth.dto.PasswordChangePrepareResponse;
import com.cisd.tms.modules.auth.dto.UkeyLoginProof; import com.cisd.tms.modules.auth.dto.UkeyLoginProof;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomRequest; import com.cisd.tms.modules.auth.dto.UkeyLoginRandomRequest;
import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse; import com.cisd.tms.modules.auth.dto.UkeyLoginRandomResponse;
@@ -52,6 +53,8 @@ import java.util.LinkedHashSet;
import java.util.List; import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Set; import java.util.Set;
import java.util.UUID;
import java.util.concurrent.ConcurrentHashMap;
import java.util.stream.Collectors; import java.util.stream.Collectors;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
@@ -79,6 +82,7 @@ AuthServiceImpl implements AuthService {
private final SessionTokenGenerator sessionTokenGenerator; private final SessionTokenGenerator sessionTokenGenerator;
private final PasswordSaltGenerator passwordSaltGenerator; private final PasswordSaltGenerator passwordSaltGenerator;
private final AuthPolicyService authPolicyService; private final AuthPolicyService authPolicyService;
private final Map<String, PendingPasswordChange> pendingPasswordChanges = new ConcurrentHashMap<>();
@Override @Override
public LoginResponse login(LoginRequest request) { public LoginResponse login(LoginRequest request) {
@@ -205,9 +209,7 @@ AuthServiceImpl implements AuthService {
} }
@Override @Override
public void changeAccountPassword(String sessionToken, Integer uid, String currentPassword, String newPassword) { public PasswordChangePrepareResponse prepareAccountPasswordChange(String sessionToken, Integer uid) {
PasswordDigestValidator.validate(currentPassword);
PasswordDigestValidator.validate(newPassword);
AuthSessionEntity session = requireActiveSession(sessionToken); AuthSessionEntity session = requireActiveSession(sessionToken);
if (uid == null || !containsAuthenticatedUid(session, uid)) { if (uid == null || !containsAuthenticatedUid(session, uid)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "当前会话未完成账号认证"); throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "当前会话未完成账号认证");
@@ -215,14 +217,60 @@ AuthServiceImpl implements AuthService {
AuthFullAccountEntity fullAccount = authFullAccountRepository.findByRoleCodeAndUid(session.getRoleCode(), uid) AuthFullAccountEntity fullAccount = authFullAccountRepository.findByRoleCodeAndUid(session.getRoleCode(), uid)
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "账号不存在")); .orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "账号不存在"));
validateFullAccountStatus(fullAccount); validateFullAccountStatus(fullAccount);
if (!passwordHasher.matches(currentPassword, fullAccount.getPasswordSalt(), fullAccount.getPasswordHash())) { String changeToken = UUID.randomUUID().toString().replace("-", "");
String newPasswordSalt = passwordSaltGenerator.nextSalt();
String oldServerRandom = UUID.randomUUID().toString().replace("-", "");
pendingPasswordChanges.put(changeToken, new PendingPasswordChange(
sessionToken,
session.getRoleCode(),
uid,
newPasswordSalt,
oldServerRandom,
now().plusMinutes(IDLE_TIMEOUT_MINUTES)
));
PasswordChangePrepareResponse response = new PasswordChangePrepareResponse();
response.setChangeToken(changeToken);
response.setNewPasswordSalt(newPasswordSalt);
response.setOldServerRandom(oldServerRandom);
return response;
}
@Override
public void changeAccountPassword(
String sessionToken,
Integer uid,
String changeToken,
String oldPasswordDynamicDigest,
String newPasswordDigest
) {
PasswordDigestValidator.validate(oldPasswordDynamicDigest);
PasswordDigestValidator.validate(newPasswordDigest);
AuthSessionEntity session = requireActiveSession(sessionToken);
if (uid == null || !containsAuthenticatedUid(session, uid)) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "当前会话未完成账号认证");
}
PendingPasswordChange pending = pendingPasswordChanges.remove(changeToken);
if (pending == null
|| pending.expiresAt().isBefore(now())
|| !sessionToken.equals(pending.sessionToken())
|| !session.getRoleCode().equals(pending.roleCode())
|| !uid.equals(pending.uid())) {
throw new BizException(ErrorCode.UNAUTHORIZED.getCode(), "改密确认令牌不存在或已过期");
}
AuthFullAccountEntity fullAccount = authFullAccountRepository.findByRoleCodeAndUid(session.getRoleCode(), uid)
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "账号不存在"));
validateFullAccountStatus(fullAccount);
if (!PasswordDigestSupport.matchesDynamicDigest(
fullAccount.getPasswordDigest(),
pending.oldServerRandom(),
oldPasswordDynamicDigest
)) {
onFullPasswordFailed(fullAccount); onFullPasswordFailed(fullAccount);
} }
String salt = passwordSaltGenerator.nextSalt();
LocalDateTime current = now(); LocalDateTime current = now();
fullAccount.setPasswordSalt(salt); fullAccount.setPasswordSalt(pending.newPasswordSalt());
fullAccount.setPasswordHash(passwordHasher.hash(newPassword, salt)); fullAccount.setPasswordDigest(passwordHasher.hash(newPasswordDigest, pending.newPasswordSalt()));
fullAccount.setFailedCount(0); fullAccount.setFailedCount(0);
fullAccount.setLockedUntil(null); fullAccount.setLockedUntil(null);
fullAccount.setStatus(RoleAccountStatus.ACTIVE.name()); fullAccount.setStatus(RoleAccountStatus.ACTIVE.name());
@@ -267,7 +315,7 @@ AuthServiceImpl implements AuthService {
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "账号不存在")); .orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "账号不存在"));
validateFullAccountStatus(roleAccountSeat); validateFullAccountStatus(roleAccountSeat);
PasswordDigestValidator.validate(account.getPassword()); PasswordDigestValidator.validate(account.getPassword());
if (!passwordHasher.matches(account.getPassword(), roleAccountSeat.getPasswordSalt(), roleAccountSeat.getPasswordHash())) { if (!passwordHasher.matches(account.getPassword(), roleAccountSeat.getPasswordSalt(), roleAccountSeat.getPasswordDigest())) {
onFullPasswordFailed(roleAccountSeat); onFullPasswordFailed(roleAccountSeat);
} }
validatedAccounts.add(roleAccountSeat); validatedAccounts.add(roleAccountSeat);
@@ -339,7 +387,11 @@ AuthServiceImpl implements AuthService {
.orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "完整账号不存在")); .orElseThrow(() -> new BizException(ErrorCode.UNAUTHORIZED.getCode(), "完整账号不存在"));
validateFullAccountStatus(fullAccount); validateFullAccountStatus(fullAccount);
PasswordDigestValidator.validate(account.getPassword()); PasswordDigestValidator.validate(account.getPassword());
if (!passwordHasher.matches(account.getPassword(), fullAccount.getPasswordSalt(), fullAccount.getPasswordHash())) { if (!PasswordDigestSupport.matchesDynamicDigest(
fullAccount.getPasswordDigest(),
account.getServerRandom(),
account.getPassword()
)) {
onFullPasswordFailed(fullAccount); onFullPasswordFailed(fullAccount);
} }
validatedAccounts.add(fullAccount); validatedAccounts.add(fullAccount);
@@ -365,12 +417,23 @@ AuthServiceImpl implements AuthService {
} }
FullLoginAccountRequest accountRequest = new FullLoginAccountRequest(); FullLoginAccountRequest accountRequest = new FullLoginAccountRequest();
accountRequest.setUid(uid); accountRequest.setUid(uid);
accountRequest.setPassword(proof.getPassword()); accountRequest.setPassword(proof.getPasswordDynamicDigest());
accountRequest.setServerRandom(proof.getServerRandom());
requests.add(accountRequest); requests.add(accountRequest);
} }
return requests; return requests;
} }
private record PendingPasswordChange(
String sessionToken,
String roleCode,
Integer uid,
String newPasswordSalt,
String oldServerRandom,
LocalDateTime expiresAt
) {
}
private void ensureMasterKeyReady() { private void ensureMasterKeyReady() {
MasterKeyStatus.StatusDetail status = lmkService.getMasterKeyStatus(); MasterKeyStatus.StatusDetail status = lmkService.getMasterKeyStatus();
if (status == null || status.getCode() == MasterKeyStatus.ABNORMAL.getCode()) { if (status == null || status.getCode() == MasterKeyStatus.ABNORMAL.getCode()) {
@@ -1,6 +1,7 @@
package com.cisd.tms.modules.auth.service.impl; package com.cisd.tms.modules.auth.service.impl;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import org.bouncycastle.crypto.digests.SM3Digest; import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.util.encoders.Hex; import org.bouncycastle.util.encoders.Hex;
@@ -17,4 +18,15 @@ final class PasswordDigestSupport {
digest.doFinal(result, 0); digest.doFinal(result, 0);
return Hex.toHexString(result); return Hex.toHexString(result);
} }
static boolean matchesDigest(String actual, String expected) {
byte[] actualBytes = (actual == null ? "" : actual).getBytes(StandardCharsets.UTF_8);
byte[] expectedBytes = (expected == null ? "" : expected).getBytes(StandardCharsets.UTF_8);
return MessageDigest.isEqual(actualBytes, expectedBytes);
}
static boolean matchesDynamicDigest(String storedPasswordDigest, String serverRandom, String actualDynamicDigest) {
return matchesDigest(sm3Hex((storedPasswordDigest == null ? "" : storedPasswordDigest)
+ (serverRandom == null ? "" : serverRandom)), actualDynamicDigest);
}
} }
@@ -1,36 +1,18 @@
package com.cisd.tms.modules.auth.service.impl; package com.cisd.tms.modules.auth.service.impl;
import java.nio.charset.StandardCharsets;
import java.security.GeneralSecurityException;
import java.security.MessageDigest;
import java.util.Base64;
import javax.crypto.SecretKeyFactory;
import javax.crypto.spec.PBEKeySpec;
import com.cisd.tms.modules.auth.service.PasswordHasher; import com.cisd.tms.modules.auth.service.PasswordHasher;
import org.springframework.stereotype.Component; import org.springframework.stereotype.Component;
@Component @Component
public class Pbkdf2PasswordHasher implements PasswordHasher { public class Pbkdf2PasswordHasher implements PasswordHasher {
private static final int ITERATIONS = 65_536;
private static final int KEY_LENGTH = 256;
@Override @Override
public String hash(String password, String salt) { public String hash(String passwordDigest, String salt) {
try { return passwordDigest;
PBEKeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(StandardCharsets.UTF_8), ITERATIONS, KEY_LENGTH);
SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("PBKDF2WithHmacSHA256");
return Base64.getEncoder().encodeToString(keyFactory.generateSecret(spec).getEncoded());
} catch (GeneralSecurityException ex) {
throw new IllegalStateException("密码哈希计算失败", ex);
}
} }
@Override @Override
public boolean matches(String rawPassword, String salt, String expectedHash) { public boolean matches(String actualPasswordDigest, String salt, String expectedPasswordDigest) {
byte[] actual = hash(rawPassword, salt).getBytes(StandardCharsets.UTF_8); return PasswordDigestSupport.matchesDigest(actualPasswordDigest, expectedPasswordDigest);
byte[] expected = expectedHash.getBytes(StandardCharsets.UTF_8);
return MessageDigest.isEqual(actual, expected);
} }
} }
@@ -2,22 +2,49 @@ package com.cisd.tms.modules.cert.config;
import org.springframework.boot.context.properties.ConfigurationProperties; import org.springframework.boot.context.properties.ConfigurationProperties;
import java.util.ArrayList;
import java.util.List;
@ConfigurationProperties(prefix = "tms.cert") @ConfigurationProperties(prefix = "tms.cert")
public class CertProperties { public class CertProperties {
/** private RootCertSource rootCertSource = new RootCertSource();
* 允许导入为信任锚的根 CA 证书 SHA-256 指纹。为空时保持兼容,不限制根 CA 导入。
*/
private List<String> trustedRootFingerprints = new ArrayList<>();
public List<String> getTrustedRootFingerprints() { public RootCertSource getRootCertSource() {
return trustedRootFingerprints; return rootCertSource;
} }
public void setTrustedRootFingerprints(List<String> trustedRootFingerprints) { public void setRootCertSource(RootCertSource rootCertSource) {
this.trustedRootFingerprints = trustedRootFingerprints == null ? new ArrayList<>() : trustedRootFingerprints; this.rootCertSource = rootCertSource == null ? new RootCertSource() : rootCertSource;
}
public static class RootCertSource {
private String publicKeySm3Path = "/home/tms/device/dev.mac.pub.sm3";
private String publicKeyPath = "/home/tms/device/mac_sm2_public_key.pemy";
private String signaturePath = "/home/tms/device/rootcert.sign";
public String getPublicKeySm3Path() {
return publicKeySm3Path;
}
public void setPublicKeySm3Path(String publicKeySm3Path) {
this.publicKeySm3Path = publicKeySm3Path;
}
public String getPublicKeyPath() {
return publicKeyPath;
}
public void setPublicKeyPath(String publicKeyPath) {
this.publicKeyPath = publicKeyPath;
}
public String getSignaturePath() {
return signaturePath;
}
public void setSignaturePath(String signaturePath) {
this.signaturePath = signaturePath;
}
} }
} }
@@ -0,0 +1,121 @@
package com.cisd.tms.modules.cert.service;
import com.cisd.tms.common.enums.ErrorCode;
import com.cisd.tms.common.exception.BizException;
import com.cisd.tms.modules.cert.config.CertProperties;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Security;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Locale;
@Component
public class FileRootCertSourceVerifier implements RootCertSourceVerifier {
private static final String UNTRUSTED_ROOT_CERT = "根证书来源不可信";
static {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
private final Path publicKeySm3Path;
private final Path publicKeyPath;
private final Path signaturePath;
@Autowired
public FileRootCertSourceVerifier(CertProperties certProperties) {
this(
Path.of(certProperties.getRootCertSource().getPublicKeySm3Path()),
Path.of(certProperties.getRootCertSource().getPublicKeyPath()),
Path.of(certProperties.getRootCertSource().getSignaturePath())
);
}
FileRootCertSourceVerifier(Path deviceDir) {
this(
deviceDir.resolve("dev.mac.pub.sm3"),
deviceDir.resolve("mac_sm2_public_key.pemy"),
deviceDir.resolve("rootcert.sign")
);
}
FileRootCertSourceVerifier(Path publicKeySm3Path, Path publicKeyPath, Path signaturePath) {
this.publicKeySm3Path = publicKeySm3Path;
this.publicKeyPath = publicKeyPath;
this.signaturePath = signaturePath;
}
@Override
public void verifyTrustedSource(byte[] certData) {
try {
PublicKey publicKey = loadPublicKey(publicKeyPath);
if (!isPublicKeySm3Trusted(publicKey)) {
throw untrusted();
}
byte[] signature = Files.readAllBytes(signaturePath);
if (!verifySignature(certData, signature, publicKey)) {
throw untrusted();
}
} catch (BizException ex) {
throw ex;
} catch (Exception ex) {
throw untrusted();
}
}
private boolean isPublicKeySm3Trusted(PublicKey publicKey) throws Exception {
String savedSm3 = Files.readString(publicKeySm3Path, StandardCharsets.UTF_8);
return normalizeHex(savedSm3).equals(normalizeHex(sm3Hex(publicKey.getEncoded())));
}
private boolean verifySignature(byte[] certData, byte[] sign, PublicKey publicKey) throws Exception {
Signature signature = Signature.getInstance("SM3withSM2", "BC");
signature.initVerify(publicKey);
signature.update(certData);
return signature.verify(sign);
}
private PublicKey loadPublicKey(Path publicKeyPath) throws Exception {
String pem = Files.readString(publicKeyPath, StandardCharsets.UTF_8);
String keyBase64 = pem.lines()
.filter(line -> !line.startsWith("-----"))
.map(String::trim)
.filter(StringUtils::hasText)
.reduce("", String::concat);
byte[] keyBytes = Base64.getDecoder().decode(keyBase64);
return KeyFactory.getInstance("EC", "BC").generatePublic(new X509EncodedKeySpec(keyBytes));
}
private String sm3Hex(byte[] data) {
SM3Digest digest = new SM3Digest();
digest.update(data, 0, data.length);
byte[] result = new byte[digest.getDigestSize()];
digest.doFinal(result, 0);
return Hex.toHexString(result);
}
private String normalizeHex(String value) {
if (!StringUtils.hasText(value)) {
return "";
}
return value.replaceAll("[^0-9A-Fa-f]", "").toLowerCase(Locale.ROOT);
}
private BizException untrusted() {
return new BizException(ErrorCode.BAD_REQUEST.getCode(), UNTRUSTED_ROOT_CERT);
}
}
@@ -0,0 +1,6 @@
package com.cisd.tms.modules.cert.service;
public interface RootCertSourceVerifier {
void verifyTrustedSource(byte[] certData);
}
@@ -9,7 +9,6 @@ import com.cisd.tms.modules.cert.dto.TrustedCertDeleteRequest;
import com.cisd.tms.modules.cert.dto.TrustedCertDetailResponse; import com.cisd.tms.modules.cert.dto.TrustedCertDetailResponse;
import com.cisd.tms.modules.cert.dto.TrustedCertItemResponse; import com.cisd.tms.modules.cert.dto.TrustedCertItemResponse;
import com.cisd.tms.modules.cert.dto.TrustedCertListRequest; import com.cisd.tms.modules.cert.dto.TrustedCertListRequest;
import com.cisd.tms.modules.cert.config.CertProperties;
import com.cisd.tms.modules.cert.entity.TrustedCertEntity; import com.cisd.tms.modules.cert.entity.TrustedCertEntity;
import com.cisd.tms.modules.cert.repository.CertificateRepository; import com.cisd.tms.modules.cert.repository.CertificateRepository;
import com.cisd.tms.modules.cert.repository.TrustedCertRepository; import com.cisd.tms.modules.cert.repository.TrustedCertRepository;
@@ -18,7 +17,6 @@ import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional; import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.StringUtils;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.IOException; import java.io.IOException;
@@ -28,10 +26,7 @@ import java.security.cert.X509CRL;
import java.security.cert.X509Certificate; import java.security.cert.X509Certificate;
import java.time.LocalDateTime; import java.time.LocalDateTime;
import java.time.ZoneId; import java.time.ZoneId;
import java.util.Locale;
import java.util.List; import java.util.List;
import java.util.Set;
import java.util.stream.Collectors;
@Service @Service
public class TrustedCertService { public class TrustedCertService {
@@ -47,32 +42,25 @@ public class TrustedCertService {
private final TrustedCertRepository trustedCertRepository; private final TrustedCertRepository trustedCertRepository;
private final CertificateRepository certificateRepository; private final CertificateRepository certificateRepository;
private final Set<String> trustedRootFingerprints; private final RootCertSourceVerifier rootCertSourceVerifier;
public TrustedCertService(
TrustedCertRepository trustedCertRepository,
CertificateRepository certificateRepository
) {
this(trustedCertRepository, certificateRepository, List.of());
}
@Autowired @Autowired
public TrustedCertService( public TrustedCertService(
TrustedCertRepository trustedCertRepository, TrustedCertRepository trustedCertRepository,
CertificateRepository certificateRepository, CertificateRepository certificateRepository,
CertProperties certProperties RootCertSourceVerifier rootCertSourceVerifier
) { ) {
this(trustedCertRepository, certificateRepository, certProperties.getTrustedRootFingerprints()); this.trustedCertRepository = trustedCertRepository;
this.certificateRepository = certificateRepository;
this.rootCertSourceVerifier = rootCertSourceVerifier;
} }
TrustedCertService( TrustedCertService(
TrustedCertRepository trustedCertRepository, TrustedCertRepository trustedCertRepository,
CertificateRepository certificateRepository, CertificateRepository certificateRepository
List<String> trustedRootFingerprints
) { ) {
this.trustedCertRepository = trustedCertRepository; this(trustedCertRepository, certificateRepository, certData -> {
this.certificateRepository = certificateRepository; });
this.trustedRootFingerprints = normalizeFingerprints(trustedRootFingerprints);
} }
public Page<TrustedCertItemResponse> list(TrustedCertListRequest request) { public Page<TrustedCertItemResponse> list(TrustedCertListRequest request) {
@@ -111,7 +99,7 @@ public class TrustedCertService {
} }
int caLevel = isSelfSigned(certificate) ? 0 : 1; int caLevel = isSelfSigned(certificate) ? 0 : 1;
if (caLevel == 0) { if (caLevel == 0) {
assertRootFingerprintAllowed(fingerprint); rootCertSourceVerifier.verifyTrustedSource(fileData);
// 根 CA 必须能自签自验,防止把 subject=issuer 的伪造证书放入信任锚。 // 根 CA 必须能自签自验,防止把 subject=issuer 的伪造证书放入信任锚。
certificate.verify(certificate.getPublicKey(), "BC"); certificate.verify(certificate.getPublicKey(), "BC");
} else { } else {
@@ -312,30 +300,4 @@ public class TrustedCertService {
private boolean isSelfSigned(X509Certificate certificate) { private boolean isSelfSigned(X509Certificate certificate) {
return certificate.getSubjectX500Principal().equals(certificate.getIssuerX500Principal()); return certificate.getSubjectX500Principal().equals(certificate.getIssuerX500Principal());
} }
private void assertRootFingerprintAllowed(String fingerprint) {
if (trustedRootFingerprints.isEmpty()) {
return;
}
if (!trustedRootFingerprints.contains(normalizeFingerprint(fingerprint))) {
throw new BizException(ErrorCode.BAD_REQUEST.getCode(), "根CA证书指纹不在白名单中");
}
}
private Set<String> normalizeFingerprints(List<String> fingerprints) {
if (fingerprints == null) {
return Set.of();
}
return fingerprints.stream()
.map(this::normalizeFingerprint)
.filter(StringUtils::hasText)
.collect(Collectors.toUnmodifiableSet());
}
private String normalizeFingerprint(String fingerprint) {
if (!StringUtils.hasText(fingerprint)) {
return "";
}
return fingerprint.replaceAll("[^0-9A-Fa-f]", "").toUpperCase(Locale.ROOT);
}
} }
@@ -62,7 +62,7 @@ public class TimeConfigServiceImpl implements TimeConfigService {
} }
try { try {
executeCommand("systemctl", "stop", "chronyd"); executeCommand("sudo", "systemctl", "stop", "chronyd");
} catch (Exception e){ } catch (Exception e){
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "停止chronyd服务失败"); throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "停止chronyd服务失败");
} }
@@ -72,7 +72,7 @@ public class TimeConfigServiceImpl implements TimeConfigService {
executeCommand("date", "-s", request.getDatetime()); executeCommand("date", "-s", request.getDatetime());
} catch (Exception e) { } catch (Exception e) {
// 失败后重启 chronyd // 失败后重启 chronyd
executeCommand("systemctl", "start", "chronyd"); executeCommand("sudo", "systemctl", "start", "chronyd");
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "设置时间失败,已回退并重启 chronyd 服务"); throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "设置时间失败,已回退并重启 chronyd 服务");
} }
@@ -126,7 +126,7 @@ public class TimeConfigServiceImpl implements TimeConfigService {
Files.writeString(confPath, confContent); Files.writeString(confPath, confContent);
executeCommand("systemctl", "restart", "chronyd"); executeCommand("sudo", "systemctl", "restart", "chronyd");
executeCommand("chronyc", "-a", "makestep"); executeCommand("chronyc", "-a", "makestep");
String status = executeCommand("chronyc", "tracking"); String status = executeCommand("chronyc", "tracking");
@@ -138,7 +138,7 @@ public class TimeConfigServiceImpl implements TimeConfigService {
try { try {
if (Files.exists(backupPath)) { if (Files.exists(backupPath)) {
Files.copy(backupPath, confPath, StandardCopyOption.REPLACE_EXISTING); Files.copy(backupPath, confPath, StandardCopyOption.REPLACE_EXISTING);
executeCommand("systemctl", "restart", "chronyd"); executeCommand("sudo", "systemctl", "restart", "chronyd");
} }
} catch (Exception rollbackEx) { } catch (Exception rollbackEx) {
throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "应用新的Chrony配置失败,且回滚也失败"); throw new BizException(ErrorCode.BIZ_ERROR.getCode(), "应用新的Chrony配置失败,且回滚也失败");
@@ -14,6 +14,11 @@ public class UKeySignResult {
*/ */
private String sign; private String sign;
/**
* 当前角色席位口令盐值。前端写入 UKey,登录时用于计算静态口令摘要。
*/
private String passwordSalt;
/** /**
* 当发卡流程需要同时下发主密钥备份信息时,统一通过备份包承载。 * 当发卡流程需要同时下发主密钥备份信息时,统一通过备份包承载。
*/ */
@@ -30,7 +30,7 @@ import java.util.Base64;
@RequiredArgsConstructor @RequiredArgsConstructor
@RestController @RestController
@RequestMapping("/api/v1/openapi") @RequestMapping({"/api/v1/openapi", "/openapi/v1/openapi"})
@Tag(name = "OpenApi 管理", description = "对外开放接口") @Tag(name = "OpenApi 管理", description = "对外开放接口")
public class OpenApiController { public class OpenApiController {
+9 -4
View File
@@ -48,10 +48,15 @@ tms:
# 上传文件根目录;上传接口会将文件保存到 <upload-base-dir>/<fileId>/<originalFilename>。 # 上传文件根目录;上传接口会将文件保存到 <upload-base-dir>/<fileId>/<originalFilename>。
upload-base-dir: ${TMS_FILE_STORAGE_UPLOAD_BASE_DIR:/home/tms/uploads} upload-base-dir: ${TMS_FILE_STORAGE_UPLOAD_BASE_DIR:/home/tms/uploads}
# 上传文件大小上限(字节)。 # 上传文件大小上限(字节)。
max-file-size-bytes: ${TMS_FILE_STORAGE_MAX_FILE_SIZE_BYTES:104857600} max-file-size-bytes: ${TMS_FILE_STORAGE_MAX_FILE_SIZE_BYTES:524288000}
cert: cert:
# 允许导入为根 CA 信任锚的 SHA-256 指纹白名单,多个值用英文逗号分隔;为空时不限制根 CA 导入。 root-cert-source:
trusted-root-fingerprints: ${TMS_CERT_TRUSTED_ROOT_FINGERPRINTS:} # 根证书来源校验:MAC 公钥 SM3 校验值文件。
public-key-sm3-path: ${TMS_CERT_ROOT_PUBLIC_KEY_SM3_PATH:/home/tms/device/dev.mac.pub.sm3}
# 根证书来源校验:用于验证 rootcert.sign 的 SM2 公钥文件。
public-key-path: ${TMS_CERT_ROOT_PUBLIC_KEY_PATH:/home/tms/device/dev.mac.pub}
# 根证书来源校验:根证书签名文件。
signature-path: ${TMS_CERT_ROOT_SIGNATURE_PATH:/home/tms/device/rootcert.sign}
crl: crl:
# CRL 导入临时目录;原始文件只在解析过程中暂存,导入结束后删除。 # CRL 导入临时目录;原始文件只在解析过程中暂存,导入结束后删除。
temp-dir: ${TMS_CERT_CRL_TEMP_DIR:/home/tms/tmp/crl-import} temp-dir: ${TMS_CERT_CRL_TEMP_DIR:/home/tms/tmp/crl-import}
@@ -341,7 +346,7 @@ tms:
# 是否启用 /api/** 的内部登录鉴权;开发联调时可临时关闭。 # 是否启用 /api/** 的内部登录鉴权;开发联调时可临时关闭。
enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:true} enabled: ${TMS_SECURITY_INTERNAL_AUTH_ENABLED:true}
# 关闭内部鉴权后,注入请求上下文的调试角色。 # 关闭内部鉴权后,注入请求上下文的调试角色。
debug-role-code: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_ROLE_CODE:OPS_ADMIN} debug-role-code: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_ROLE_CODE:KEY_ADMIN}
# 关闭内部鉴权后,注入请求上下文的调试认证等级。 # 关闭内部鉴权后,注入请求上下文的调试认证等级。
debug-auth-level: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_AUTH_LEVEL:FULL} debug-auth-level: ${TMS_SECURITY_INTERNAL_AUTH_DEBUG_AUTH_LEVEL:FULL}
# 关闭内部鉴权后,注入请求上下文的调试 session token。 # 关闭内部鉴权后,注入请求上下文的调试 session token。
@@ -1,12 +1,14 @@
-- ----------------------------------------------------------------------------- -- -----------------------------------------------------------------------------
-- TMS 全量库结构初始化脚本 -- TMS 旧库口令摘要迁移脚本
-- 适用范围:空库初始化 / Flyway 首次建库 -- 适用范围:已经存在旧认证表的数据库,从旧 password_hash 方案迁移到
-- password_digest = SM3(password_salt + 原始口令) 方案。
-- 数据库:TiDB / MySQL 8+ -- 数据库:TiDB / MySQL 8+
-- --
-- 说明: -- 重要说明:
-- 1. 本文件是当前仓库唯一保留的数据库初始化 SQL。 -- 1. 本文件不是空库全量初始化脚本。空库初始化请使用 V2__tms_schema_full.sql。
-- 2. 结构已按当前最终状态汇总,不再依赖历史 V1~V12 逐步迁移。 -- 2. 旧 password_hash 通常是不可逆摘要,无法还原用户原始口令后生成新摘要。
-- 3. 执行前请先选中目标数据库,本文件不再包含 CREATE DATABASE / USE。 -- 3. 因此迁移时将固定角色席位统一重置为默认口令 Sunyard@123,并置为强制改密。
-- 4. 旧 password_hash 字段保留,便于现场回滚或排查;新代码只读取 password_digest。
-- ----------------------------------------------------------------------------- -- -----------------------------------------------------------------------------
CREATE DATABASE IF NOT EXISTS TMS CREATE DATABASE IF NOT EXISTS TMS
@@ -15,764 +17,69 @@ CREATE DATABASE IF NOT EXISTS TMS
USE TMS; USE TMS;
-- ----------------------------------------------------------------------------- ALTER TABLE tms_auth_full_account
-- 初始化任务 ADD COLUMN password_digest VARCHAR(64) NULL COMMENT '口令静态摘要:SM3(password_salt + 原始口令)' AFTER password_hash;
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_init_task ( -- 默认口令摘要:SM3(password_salt + 'Sunyard@123')。
id BIGINT PRIMARY KEY, -- 迁移会把固定席位盐值同步为后端当前标准盐值,后续 issue-sign 会返回该盐值供前端写入 UKey。
task_id VARCHAR(64) NOT NULL,
task_type VARCHAR(32) NOT NULL DEFAULT 'INIT' COMMENT '任务类型:INIT/RESET',
version VARCHAR(32) NOT NULL,
status VARCHAR(32) NOT NULL,
product_type VARCHAR(16) NULL,
org_code VARCHAR(32) NULL,
mq_type VARCHAR(32) NULL,
deploy_mode VARCHAR(16) NULL,
init_plan_json LONGTEXT NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_init_task_task_id (task_id)
) COMMENT='初始化任务表';
CREATE TABLE IF NOT EXISTS tms_init_task_step (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
step_no INT NOT NULL,
step_code VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
message VARCHAR(512) NULL,
command_line VARCHAR(2048) NULL,
log_path VARCHAR(512) NULL,
exit_code INT NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_init_task_step_task_no (task_id, step_no),
KEY idx_tms_init_task_step_task_id (task_id)
) COMMENT='初始化任务步骤表';
-- -----------------------------------------------------------------------------
-- 资源备份与恢复
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_resource_backup_task (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
backup_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
product_type VARCHAR(16) NOT NULL,
mq_type VARCHAR(32) NULL,
org_code VARCHAR(32) NULL,
source_device_id VARCHAR(128) NULL,
package_name VARCHAR(255) NULL,
package_path VARCHAR(1024) NULL,
package_size BIGINT NULL,
package_hash VARCHAR(128) NULL,
storage_enc_key_fingerprint VARCHAR(128) NOT NULL,
authenticity_key_fingerprint VARCHAR(128) NOT NULL,
manifest_summary_json JSON NULL,
error_code VARCHAR(64) NULL,
error_message VARCHAR(1024) NULL,
remark VARCHAR(1024) NULL,
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
start_time DATETIME(3) NULL,
finish_time DATETIME(3) NULL,
UNIQUE KEY uk_tms_resource_backup_task_task_id (task_id),
UNIQUE KEY uk_tms_resource_backup_task_backup_id (backup_id),
KEY idx_tms_resource_backup_task_status (status),
KEY idx_tms_resource_backup_task_create_time (create_time)
) COMMENT='资源备份任务表';
CREATE TABLE IF NOT EXISTS tms_resource_restore_task (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
precheck_id VARCHAR(64) NOT NULL,
backup_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
source_device_id VARCHAR(128) NULL,
source_product_type VARCHAR(16) NOT NULL,
target_product_type VARCHAR(16) NOT NULL,
package_file_id VARCHAR(64) NOT NULL,
package_path VARCHAR(1024) NULL,
staging_dir VARCHAR(1024) NULL,
runner_state_path VARCHAR(1024) NULL,
storage_enc_key_fingerprint VARCHAR(128) NOT NULL,
authenticity_key_fingerprint VARCHAR(128) NOT NULL,
restore_plan_json JSON NULL,
consistency_report_json JSON NULL,
error_code VARCHAR(64) NULL,
error_message VARCHAR(1024) NULL,
remark VARCHAR(1024) NULL,
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
start_time DATETIME(3) NULL,
finish_time DATETIME(3) NULL,
UNIQUE KEY uk_tms_resource_restore_task_task_id (task_id),
UNIQUE KEY uk_tms_resource_restore_task_precheck_id (precheck_id),
KEY idx_tms_resource_restore_task_status (status),
KEY idx_tms_resource_restore_task_create_time (create_time)
) COMMENT='资源恢复任务表';
-- -----------------------------------------------------------------------------
-- 认证与设备基础表
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_device_node (
id BIGINT PRIMARY KEY,
node_id VARCHAR(64) NOT NULL,
host VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_device_node_node_id (node_id)
) COMMENT='设备节点表';
CREATE TABLE IF NOT EXISTS tms_file_record (
id BIGINT PRIMARY KEY,
file_id VARCHAR(64) NOT NULL,
original_filename VARCHAR(255) NOT NULL,
storage_path VARCHAR(1024) NOT NULL,
status VARCHAR(32) NOT NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_file_record_file_id (file_id)
) COMMENT='文件记录表';
-- -----------------------------------------------------------------------------
-- 设备软件版本
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_device_software_version (
id BIGINT PRIMARY KEY,
component_code VARCHAR(64) NOT NULL,
component_name VARCHAR(128) NOT NULL,
current_version VARCHAR(128) NOT NULL,
source_type VARCHAR(32) NOT NULL,
detected_at DATETIME NULL,
remarks VARCHAR(255) NULL,
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
update_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
UNIQUE KEY uk_tms_device_software_version_component_code (component_code)
) COMMENT='设备软件版本表';
INSERT INTO tms_device_software_version
(id, component_code, component_name, current_version, source_type, detected_at, remarks)
VALUES
(1001, 'RABBITMQ', '消息队列', 'V3.8.21', 'MANUAL', NOW(), '出厂预置'),
(1002, 'TLQ', '消息队列', 'V8.1.11', 'MANUAL', NOW(), '出厂预置'),
(1003, 'TIDB', '数据库', 'V8.1.0', 'MANUAL', NOW(), '出厂预置'),
(1004, 'NGINX', 'Nginx', 'V1.20', 'MANUAL', NOW(), '出厂预置'),
(1005, 'RECEIVER', '标准收发器', 'V1.3.2', 'MANUAL', NOW(), '出厂预置'),
(1006, 'TMS', 'CISD终端管理服务', 'V1.0.0', 'MANUAL', NOW(), '出厂预置')
ON DUPLICATE KEY UPDATE
component_name = VALUES(component_name),
current_version = VALUES(current_version),
source_type = VALUES(source_type),
detected_at = VALUES(detected_at),
remarks = VALUES(remarks),
update_time = CURRENT_TIMESTAMP;
-- -----------------------------------------------------------------------------
-- 升级任务
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_upgrade_task (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
upgrade_mode VARCHAR(16) NOT NULL,
task_type VARCHAR(32) NOT NULL,
product_type VARCHAR(16) NULL,
package_file_id VARCHAR(64) NOT NULL,
package_name VARCHAR(255) NOT NULL,
package_hash VARCHAR(128) NOT NULL,
current_version VARCHAR(64) NOT NULL,
target_version VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL,
verify_passed TINYINT(1) NOT NULL DEFAULT 0,
verify_message VARCHAR(1024) NULL,
manifest_json TEXT NULL,
detail_log_path VARCHAR(1024) NULL,
error_message VARCHAR(1024) NULL,
operator VARCHAR(64) NULL,
started_at DATETIME(3) NULL,
finished_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_upgrade_task_task_id (task_id),
KEY idx_tms_upgrade_task_status (status),
KEY idx_tms_upgrade_task_task_type (task_type),
KEY idx_tms_upgrade_task_create_time (create_time)
) COMMENT='升级任务表';
-- -----------------------------------------------------------------------------
-- 新认证体系
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_role_account (
id BIGINT PRIMARY KEY,
role_code VARCHAR(64) NOT NULL,
display_name VARCHAR(128) NOT NULL,
required_ukey_count INT NOT NULL,
status VARCHAR(32) NOT NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_role_account_role_code (role_code)
) COMMENT='角色账号表';
CREATE TABLE IF NOT EXISTS tms_role_ukey_binding (
id BIGINT PRIMARY KEY,
role_code VARCHAR(64) NOT NULL,
uid INT NOT NULL,
ukey_serial VARCHAR(128) NOT NULL,
ukey_pubkey TEXT NOT NULL,
issuer_sign TEXT NULL,
status VARCHAR(32) NOT NULL,
bound_at DATETIME(3) NOT NULL,
unbound_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_role_ukey_binding_role_uid_serial (role_code, uid, ukey_serial),
KEY idx_tms_role_ukey_binding_role_uid_status (role_code, uid, status),
KEY idx_tms_role_ukey_binding_role_status (role_code, status)
) COMMENT='角色UKey绑定表';
CREATE TABLE IF NOT EXISTS tms_auth_full_account (
id BIGINT PRIMARY KEY,
role_code VARCHAR(64) NOT NULL,
uid INT NOT NULL,
account_name VARCHAR(64) NOT NULL,
display_name VARCHAR(128) NOT NULL,
password_hash VARCHAR(256) NOT NULL,
password_salt VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL,
need_change_password TINYINT(1) NOT NULL DEFAULT 1,
password_changed_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
failed_count INT NOT NULL DEFAULT 0,
locked_until DATETIME(3) NULL,
last_login_at DATETIME(3) NULL,
last_active_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_auth_full_account_role_uid (role_code, uid),
UNIQUE KEY uk_tms_auth_full_account_name (account_name),
KEY idx_tms_auth_full_account_role_code (role_code)
) COMMENT='完整认证账号表';
CREATE TABLE IF NOT EXISTS tms_auth_session (
id BIGINT PRIMARY KEY,
session_token VARCHAR(128) NOT NULL,
role_code VARCHAR(64) NOT NULL,
authenticated_principals_json JSON NULL,
auth_method VARCHAR(32) NOT NULL,
auth_level VARCHAR(32) NOT NULL,
issued_at DATETIME(3) NOT NULL,
last_active_at DATETIME(3) NOT NULL,
expires_at DATETIME(3) NOT NULL,
logout_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_auth_session_token (session_token),
KEY idx_tms_auth_session_role_code (role_code)
) COMMENT='认证会话表';
-- -----------------------------------------------------------------------------
-- 防重放与安全事件
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_replay_nonce (
id BIGINT PRIMARY KEY,
scope VARCHAR(32) NOT NULL COMMENT '防重放作用域:OPENAPI / INTERNAL_API',
principal_id VARCHAR(128) NOT NULL COMMENT '主体标识:appId / sessionToken',
nonce VARCHAR(128) NOT NULL,
request_timestamp BIGINT NOT NULL,
request_method VARCHAR(16) NULL,
request_path VARCHAR(256) NULL,
body_hash VARCHAR(128) NULL,
remote_ip VARCHAR(64) NULL,
user_agent VARCHAR(512) NULL,
expires_at DATETIME(3) NOT NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_replay_nonce_scope_principal_nonce (scope, principal_id, nonce),
KEY idx_tms_replay_nonce_expires_at (expires_at),
KEY idx_tms_replay_nonce_principal_time (principal_id, create_time)
) COMMENT='防重放nonce表';
CREATE TABLE IF NOT EXISTS tms_security_event (
id BIGINT PRIMARY KEY,
event_type VARCHAR(64) NOT NULL,
scope VARCHAR(32) NOT NULL,
principal_id VARCHAR(128) NULL,
request_method VARCHAR(16) NULL,
request_path VARCHAR(256) NULL,
nonce VARCHAR(128) NULL,
remote_ip VARCHAR(64) NULL,
detail_json JSON NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
KEY idx_tms_security_event_event_type (event_type),
KEY idx_tms_security_event_scope_principal (scope, principal_id)
) COMMENT='安全事件表';
INSERT IGNORE INTO tms_role_account (
id,
role_code,
display_name,
required_ukey_count,
status,
create_time,
update_time
)
VALUES
(
11001,
'SUPER_ADMIN',
'超级管理员',
2,
'ACTIVE',
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
11002,
'KEY_ADMIN',
'密钥管理员',
1,
'UNENABLED',
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
11003,
'AUDIT_ADMIN',
'审计管理员',
1,
'UNENABLED',
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
11004,
'OPS_ADMIN',
'运维管理员',
1,
'UNENABLED',
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
);
INSERT IGNORE INTO tms_auth_full_account (
id,
role_code,
uid,
account_name,
display_name,
password_hash,
password_salt,
status,
need_change_password,
password_changed_at,
failed_count,
locked_until,
last_login_at,
last_active_at,
create_time,
update_time
)
VALUES
(
12101,
'SUPER_ADMIN',
1,
'super-admin-full-01',
'超级管理员UKey席位一',
'vVPlH2UGQcXUo7NblMJMaof32um6UinF8zsckZuxe9E=',
'3df85c2988a5de2b9eeaa8109639641a',
'ACTIVE',
1,
CURRENT_TIMESTAMP(3),
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
12102,
'SUPER_ADMIN',
2,
'super-admin-full-02',
'超级管理员UKey席位二',
'25e83ejpUmjU64cYAKcDV5hhqClEu6aol8YyR3Jey7E=',
'50de03346bb1f6afc74938acf5e88560',
'ACTIVE',
1,
CURRENT_TIMESTAMP(3),
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
12103,
'KEY_ADMIN',
1,
'key-admin-full-01',
'密钥管理员UKey席位',
'l65Pr7MxOvqPOe0u6ozGGZcT4raTXUgDYMQfLTINTyE=',
'073cd5fd30701edfcf033e8f501b2b43',
'ACTIVE',
1,
CURRENT_TIMESTAMP(3),
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
12104,
'AUDIT_ADMIN',
1,
'audit-admin-full-01',
'审计管理员UKey席位',
'ZRwdn3TOotyXAytO4zeSpxRm/68Rm8VcB7w7VJmZPYc=',
'57e2616077bd8af5cb90fc9790f69573',
'ACTIVE',
1,
CURRENT_TIMESTAMP(3),
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
),
(
12105,
'OPS_ADMIN',
1,
'ops-admin-full-01',
'运维管理员UKey席位',
'5Wu8Mf5hC3kq4gm/62/T7pO+aZIw8Y7uYoMIztWyT1w=',
'a6bea7601138e90a540b39c73d53df81',
'ACTIVE',
1,
CURRENT_TIMESTAMP(3),
0,
NULL,
NULL,
NULL,
CURRENT_TIMESTAMP(3),
CURRENT_TIMESTAMP(3)
);
-- -----------------------------------------------------------------------------
-- 兼容旧库默认口令摘要升级
-- -----------------------------------------------------------------------------
-- 旧版本默认账号保存的是 PBKDF2('Sunyard@123', password_salt)。
-- 当前版本前端提交 password=SM3('Sunyard@123'),因此默认账号统一重置为
-- 固定 salt + PBKDF2(SM3('Sunyard@123'), salt),便于旧库直接切换到摘要口径。
UPDATE tms_auth_full_account UPDATE tms_auth_full_account
SET password_hash = 'vVPlH2UGQcXUo7NblMJMaof32um6UinF8zsckZuxe9E=', SET password_salt = '3df85c2988a5de2b9eeaa8109639641a',
password_salt = '3df85c2988a5de2b9eeaa8109639641a', password_digest = '00718a8319e67ef6cf9fc389c14ce16784278d55439a7a30a63fa7be8e42bbdf',
need_change_password = 1,
failed_count = 0,
locked_until = NULL,
password_changed_at = CURRENT_TIMESTAMP(3),
update_time = CURRENT_TIMESTAMP(3) update_time = CURRENT_TIMESTAMP(3)
WHERE role_code = 'SUPER_ADMIN' WHERE role_code = 'SUPER_ADMIN'
AND uid = 1; AND uid = 1;
UPDATE tms_auth_full_account UPDATE tms_auth_full_account
SET password_hash = '25e83ejpUmjU64cYAKcDV5hhqClEu6aol8YyR3Jey7E=', SET password_salt = '50de03346bb1f6afc74938acf5e88560',
password_salt = '50de03346bb1f6afc74938acf5e88560', password_digest = '169731d7c897649a16a073cdfddf48d2e98f72f10da5cead5cbbd51f8a9ffea6',
need_change_password = 1,
failed_count = 0,
locked_until = NULL,
password_changed_at = CURRENT_TIMESTAMP(3),
update_time = CURRENT_TIMESTAMP(3) update_time = CURRENT_TIMESTAMP(3)
WHERE role_code = 'SUPER_ADMIN' WHERE role_code = 'SUPER_ADMIN'
AND uid = 2; AND uid = 2;
UPDATE tms_auth_full_account UPDATE tms_auth_full_account
SET password_hash = 'l65Pr7MxOvqPOe0u6ozGGZcT4raTXUgDYMQfLTINTyE=', SET password_salt = '073cd5fd30701edfcf033e8f501b2b43',
password_salt = '073cd5fd30701edfcf033e8f501b2b43', password_digest = '6d386b2a13550befb821795dcc0e9bda3d1cffc0d565ad539de16117e28f6a81',
need_change_password = 1,
failed_count = 0,
locked_until = NULL,
password_changed_at = CURRENT_TIMESTAMP(3),
update_time = CURRENT_TIMESTAMP(3) update_time = CURRENT_TIMESTAMP(3)
WHERE role_code = 'KEY_ADMIN' WHERE role_code = 'KEY_ADMIN'
AND uid = 1; AND uid = 1;
UPDATE tms_auth_full_account UPDATE tms_auth_full_account
SET password_hash = 'ZRwdn3TOotyXAytO4zeSpxRm/68Rm8VcB7w7VJmZPYc=', SET password_salt = '57e2616077bd8af5cb90fc9790f69573',
password_salt = '57e2616077bd8af5cb90fc9790f69573', password_digest = '72a53b0cb8dd15775516f5085c7a3ee09d65a130dc1b17e18545d24930826d0c',
need_change_password = 1,
failed_count = 0,
locked_until = NULL,
password_changed_at = CURRENT_TIMESTAMP(3),
update_time = CURRENT_TIMESTAMP(3) update_time = CURRENT_TIMESTAMP(3)
WHERE role_code = 'AUDIT_ADMIN' WHERE role_code = 'AUDIT_ADMIN'
AND uid = 1; AND uid = 1;
UPDATE tms_auth_full_account UPDATE tms_auth_full_account
SET password_hash = '5Wu8Mf5hC3kq4gm/62/T7pO+aZIw8Y7uYoMIztWyT1w=', SET password_salt = 'a6bea7601138e90a540b39c73d53df81',
password_salt = 'a6bea7601138e90a540b39c73d53df81', password_digest = '58655960343a1efb0c1b281ce4c384ac699d01ab61c2c710d35e54b798f6e4c5',
need_change_password = 1,
failed_count = 0,
locked_until = NULL,
password_changed_at = CURRENT_TIMESTAMP(3),
update_time = CURRENT_TIMESTAMP(3) update_time = CURRENT_TIMESTAMP(3)
WHERE role_code = 'OPS_ADMIN' WHERE role_code = 'OPS_ADMIN'
AND uid = 1; AND uid = 1;
ALTER TABLE tms_auth_full_account
DROP COLUMN password_hash;
-- ----------------------------------------------------------------------------- -- 如果这里失败,说明旧库中存在不属于上述固定席位的认证账号,需要先明确其迁移策略。
-- IP白名单表 ALTER TABLE tms_auth_full_account
-- ----------------------------------------------------------------------------- MODIFY COLUMN password_digest VARCHAR(64) NOT NULL COMMENT '口令静态摘要:SM3(password_salt + 原始口令)';
CREATE TABLE IF NOT EXISTS tms_access_whitelist (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
ip VARCHAR(32) NOT NULL,
mask VARCHAR(10) NOT NULL,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
) COMMENT='访问白名单表';
-- -----------------------------------------------------------------------------
-- Cert module tables
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_key_entity (
id BIGINT PRIMARY KEY,
org_code VARCHAR(64) NOT NULL COMMENT 'organization code',
org_name VARCHAR(255) NOT NULL COMMENT 'organization name',
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 / RSA',
key_len INT NOT NULL COMMENT 'SM2 fixed 256 in V1',
key_idx INT NOT NULL COMMENT 'card key index, allowed range [2..8]',
backup_data TEXT NOT NULL COMMENT 'SDFE_BackupUserKey output',
public_key VARCHAR(500) NOT NULL COMMENT 'public key, Base64/PEM',
p10_data VARCHAR(2000) NULL COMMENT 'PKCS#10 PEM content',
subject_dn VARCHAR(500) NULL COMMENT 'DN used when generating P10',
cert_mode VARCHAR(16) NULL COMMENT 'SINGLE / DOUBLE',
status VARCHAR(32) NOT NULL COMMENT 'CREATED / P10_GENERATED / CERT_IMPORTED',
allow_ips VARCHAR(500) NULL COMMENT 'allowed source IP list',
sign_data VARCHAR(512) NULL COMMENT 'reserved, not used in V1',
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_key_entity_key_idx (key_idx),
KEY idx_tms_key_entity_org_code (org_code),
KEY idx_tms_key_entity_algo (algo_type),
KEY idx_tms_key_entity_status (status),
KEY idx_tms_key_entity_create_time (create_time)
) COMMENT='密钥实体表';
CREATE TABLE IF NOT EXISTS tms_certificate (
id BIGINT PRIMARY KEY,
entity_id BIGINT NULL COMMENT 'required when cert_type=ENTITY',
cert_type VARCHAR(16) NOT NULL COMMENT 'ENTITY / USER',
subject_dn VARCHAR(500) NOT NULL,
issuer_dn VARCHAR(1024) NOT NULL,
serial_number VARCHAR(128) NOT NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 in V1',
fingerprint VARCHAR(128) NOT NULL COMMENT 'SHA-256 fingerprint',
cert_data VARCHAR(2000) NOT NULL COMMENT 'certificate PEM content',
valid_from DATETIME(3) NOT NULL,
valid_to DATETIME(3) NOT NULL,
import_time DATETIME(3) NOT NULL,
sign_data VARCHAR(512) NULL COMMENT 'reserved, not used in V1',
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_certificate_fingerprint (fingerprint),
KEY idx_tms_certificate_entity_id (entity_id),
KEY idx_tms_certificate_cert_type (cert_type),
KEY idx_tms_certificate_issuer_dn (issuer_dn(512)),
KEY idx_tms_certificate_valid_to (valid_to),
KEY idx_tms_certificate_import_time (import_time)
) COMMENT='证书表';
CREATE TABLE IF NOT EXISTS tms_trusted_cert (
id BIGINT PRIMARY KEY,
alias VARCHAR(128) NULL,
subject_dn VARCHAR(1024) NOT NULL,
issuer_dn VARCHAR(1024) NOT NULL,
serial_number VARCHAR(128) NOT NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 in V1',
fingerprint VARCHAR(128) NOT NULL COMMENT 'SHA-256 fingerprint',
cert_data VARCHAR(2000) NOT NULL COMMENT 'certificate PEM content',
ca_level TINYINT NOT NULL COMMENT '0:Root, 1:Intermediate',
valid_from DATETIME(3) NOT NULL,
valid_to DATETIME(3) NOT NULL,
import_time DATETIME(3) NOT NULL,
sign_data VARCHAR(512) NULL COMMENT 'reserved, not used in V1',
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_trusted_cert_fingerprint (fingerprint),
KEY idx_tms_trusted_cert_subject_dn (subject_dn(512)),
KEY idx_tms_trusted_cert_issuer_dn (issuer_dn(512)),
KEY idx_tms_trusted_cert_valid_to (valid_to)
) COMMENT='可信证书表';
CREATE TABLE IF NOT EXISTS tms_certificate_crl (
id BIGINT PRIMARY KEY,
issuer_dn VARCHAR(1024) NOT NULL,
crl_number VARCHAR(128) NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 in V1',
fingerprint VARCHAR(128) NOT NULL COMMENT 'SHA-256 fingerprint',
crl_data MEDIUMTEXT NOT NULL COMMENT 'reserved legacy CRL PEM content; large CRLs are not stored inline',
this_update DATETIME(3) NOT NULL,
next_update DATETIME(3) NULL,
revoked_count INT NOT NULL,
import_time DATETIME(3) NOT NULL,
create_by VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_certificate_crl_fingerprint (fingerprint),
KEY idx_tms_certificate_crl_issuer_dn (issuer_dn(512)),
KEY idx_tms_certificate_crl_this_update (this_update),
KEY idx_tms_certificate_crl_next_update (next_update)
) COMMENT='证书CRL表';
CREATE TABLE IF NOT EXISTS tms_certificate_crl_revoked (
id BIGINT PRIMARY KEY,
crl_id BIGINT NOT NULL,
issuer_dn VARCHAR(1024) NOT NULL,
serial_number VARCHAR(128) NOT NULL,
subject_dn VARCHAR(1024) NULL,
algo_type VARCHAR(16) NOT NULL COMMENT 'SM2 in V1',
crl_number VARCHAR(128) NULL,
revocation_time DATETIME(3) NOT NULL,
revocation_reason VARCHAR(64) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
KEY idx_tms_certificate_crl_revoked_crl_id (crl_id),
KEY idx_tms_certificate_crl_revoked_issuer_serial (issuer_dn(512), serial_number),
KEY idx_tms_certificate_crl_revoked_subject_dn (subject_dn(512)),
KEY idx_tms_certificate_crl_revoked_revocation_time (revocation_time)
) COMMENT='证书CRL吊销明细表';
CREATE TABLE IF NOT EXISTS tms_certificate_crl_import_task (
id BIGINT PRIMARY KEY,
task_id VARCHAR(64) NOT NULL,
original_filename VARCHAR(255) NULL,
file_size BIGINT NULL,
status VARCHAR(32) NOT NULL COMMENT 'PENDING/RUNNING/SUCCESS/FAILED',
fingerprint VARCHAR(128) NULL COMMENT 'SHA-256 fingerprint after successful parse',
crl_id BIGINT NULL,
revoked_count INT NOT NULL DEFAULT 0,
error_message VARCHAR(512) NULL,
started_at DATETIME(3) NULL,
finished_at DATETIME(3) NULL,
create_time DATETIME(3) NOT NULL,
update_time DATETIME(3) NOT NULL,
UNIQUE KEY uk_tms_certificate_crl_import_task_task_id (task_id),
KEY idx_tms_certificate_crl_import_task_status (status),
KEY idx_tms_certificate_crl_import_task_create_time (create_time)
) COMMENT='证书CRL导入任务表';
-- -----------------------------------------------------------------------------
-- 日志表
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_operation_audit_log (
id BIGINT PRIMARY KEY,
log_id VARCHAR(64) NOT NULL COMMENT '日志唯一标识',
operator_role_code VARCHAR(64) NULL COMMENT '操作角色编码',
operator_auth_level VARCHAR(32) NULL COMMENT '操作时认证等级',
module_code VARCHAR(32) NOT NULL COMMENT '业务模块',
action_type VARCHAR(32) NOT NULL COMMENT '操作动作',
remote_ip VARCHAR(64) NULL COMMENT '来源IP',
operation_result VARCHAR(16) NOT NULL COMMENT '操作结果: SUCCESS/FAILED/DENIED',
summary VARCHAR(512) NOT NULL COMMENT '操作摘要',
error_message VARCHAR(1024) NULL COMMENT '失败原因',
audit_status VARCHAR(16) NOT NULL COMMENT '审计状态: PENDING/REVIEWED',
audit_result VARCHAR(16) NULL COMMENT '审计结果: PASS/REJECT/NEED_VERIFY',
audit_comment VARCHAR(1024) NULL COMMENT '审计意见',
audited_by VARCHAR(64) NULL COMMENT '审计人',
audited_at DATETIME(3) NULL COMMENT '审计时间',
payload_hash VARCHAR(128) NOT NULL COMMENT 'SM3摘要',
sign_value TEXT NOT NULL COMMENT 'SM2签名值',
occurred_at DATETIME(3) NOT NULL COMMENT '操作发生时间',
create_time DATETIME(3) NOT NULL COMMENT '落库时间',
UNIQUE KEY uk_tms_operation_audit_log_log_id (log_id),
KEY idx_tms_operation_audit_log_role_code (operator_role_code),
KEY idx_tms_operation_audit_log_module_code (module_code),
KEY idx_tms_operation_audit_log_action_type (action_type),
KEY idx_tms_operation_audit_log_result (operation_result),
KEY idx_tms_operation_audit_log_audit_status (audit_status),
KEY idx_tms_operation_audit_log_occurred_at (occurred_at)
) COMMENT='操作审计日志表';
-- -----------------------------------------------------------------------------
-- 日志备份记录表
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_log_backup_record (
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键ID',
record_id VARCHAR(64) NOT NULL COMMENT '备份记录ID',
file_name VARCHAR(255) NOT NULL COMMENT '备份文件名',
backup_type VARCHAR(32) NOT NULL COMMENT '备份类型(如:AUTO/MANUAL)',
file_path VARCHAR(500) NOT NULL COMMENT '文件存储路径',
file_hash VARCHAR(128) NOT NULL COMMENT '文件哈希值',
file_sign VARCHAR(256) DEFAULT NULL COMMENT '文件签名',
backup_result VARCHAR(32) NOT NULL COMMENT '备份结果(SUCCESS/FAILED)',
create_time DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
sign_data TEXT DEFAULT NULL COMMENT '其他字段签名',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_record_id` (`record_id`),
KEY `idx_create_time` (`create_time`),
KEY `idx_backup_type` (`backup_type`)
) COMMENT='日志备份记录表';
-- -----------------------------------------------------------------------------
-- 日志备份配置表
-- -----------------------------------------------------------------------------
CREATE TABLE IF NOT EXISTS tms_backup_config (
id int(11) NOT NULL AUTO_INCREMENT COMMENT '主键',
enable tinyint(4) NOT NULL DEFAULT '0' COMMENT '启用状态 1:启用 0:禁用',
cron_exp varchar(64) DEFAULT NULL COMMENT 'Cron表达式',
retention_count int(11) DEFAULT '1' COMMENT '保留份数',
last_backup_time datetime DEFAULT NULL COMMENT '上次执行时间',
sign_data varchar(255) NOT NULL COMMENT '其他字段的签名值',
PRIMARY KEY (`id`)
) COMMENT='日志备份配置表';
CREATE TABLE IF NOT EXISTS tms_master_key_activate (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
ever_initialized BOOLEAN NOT NULL,
activation_status BOOLEAN NOT NULL,
create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3)
) COMMENT='主密钥激活状态表';
INSERT IGNORE INTO tms_master_key_activate (
id,
ever_initialized,
activation_status
) VALUES (1, true, true);
ALTER TABLE tms_access_whitelist ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_auth_audit_log ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_auth_challenge ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_auth_full_account ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_auth_session ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_backup_config ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_certificate ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_certificate_crl ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_certificate_crl_import_task ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_certificate_crl_revoked ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_device_node ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_device_software_version ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_file_record ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_init_task ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_init_task_step ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_key_entity ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_log_backup_record ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_master_key_activate ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_operation_audit_log ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_replay_nonce ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_resource_backup_task ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_resource_restore_task ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_role_account ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_role_ukey_binding ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_security_event ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_trusted_cert ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
ALTER TABLE tms_upgrade_task ADD signature varchar(250) NULL COMMENT '数据真实性签名值';
@@ -247,7 +247,7 @@ CREATE TABLE IF NOT EXISTS tms_auth_full_account (
uid INT NOT NULL, uid INT NOT NULL,
account_name VARCHAR(64) NOT NULL, account_name VARCHAR(64) NOT NULL,
display_name VARCHAR(128) NOT NULL, display_name VARCHAR(128) NOT NULL,
password_hash VARCHAR(256) NOT NULL, password_digest VARCHAR(64) NOT NULL,
password_salt VARCHAR(128) NOT NULL, password_salt VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL, status VARCHAR(32) NOT NULL,
need_change_password TINYINT(1) NOT NULL DEFAULT 1, need_change_password TINYINT(1) NOT NULL DEFAULT 1,
@@ -376,7 +376,7 @@ INSERT IGNORE INTO tms_auth_full_account (
uid, uid,
account_name, account_name,
display_name, display_name,
password_hash, password_digest,
password_salt, password_salt,
status, status,
need_change_password, need_change_password,
@@ -395,7 +395,7 @@ VALUES
1, 1,
'super-admin-full-01', 'super-admin-full-01',
'超级管理员UKey席位一', '超级管理员UKey席位一',
'vVPlH2UGQcXUo7NblMJMaof32um6UinF8zsckZuxe9E=', '00718a8319e67ef6cf9fc389c14ce16784278d55439a7a30a63fa7be8e42bbdf',
'3df85c2988a5de2b9eeaa8109639641a', '3df85c2988a5de2b9eeaa8109639641a',
'ACTIVE', 'ACTIVE',
1, 1,
@@ -413,7 +413,7 @@ VALUES
2, 2,
'super-admin-full-02', 'super-admin-full-02',
'超级管理员UKey席位二', '超级管理员UKey席位二',
'25e83ejpUmjU64cYAKcDV5hhqClEu6aol8YyR3Jey7E=', '169731d7c897649a16a073cdfddf48d2e98f72f10da5cead5cbbd51f8a9ffea6',
'50de03346bb1f6afc74938acf5e88560', '50de03346bb1f6afc74938acf5e88560',
'ACTIVE', 'ACTIVE',
1, 1,
@@ -431,7 +431,7 @@ VALUES
1, 1,
'key-admin-full-01', 'key-admin-full-01',
'密钥管理员UKey席位', '密钥管理员UKey席位',
'l65Pr7MxOvqPOe0u6ozGGZcT4raTXUgDYMQfLTINTyE=', '6d386b2a13550befb821795dcc0e9bda3d1cffc0d565ad539de16117e28f6a81',
'073cd5fd30701edfcf033e8f501b2b43', '073cd5fd30701edfcf033e8f501b2b43',
'ACTIVE', 'ACTIVE',
1, 1,
@@ -449,7 +449,7 @@ VALUES
1, 1,
'audit-admin-full-01', 'audit-admin-full-01',
'审计管理员UKey席位', '审计管理员UKey席位',
'ZRwdn3TOotyXAytO4zeSpxRm/68Rm8VcB7w7VJmZPYc=', '72a53b0cb8dd15775516f5085c7a3ee09d65a130dc1b17e18545d24930826d0c',
'57e2616077bd8af5cb90fc9790f69573', '57e2616077bd8af5cb90fc9790f69573',
'ACTIVE', 'ACTIVE',
1, 1,
@@ -467,7 +467,7 @@ VALUES
1, 1,
'ops-admin-full-01', 'ops-admin-full-01',
'运维管理员UKey席位', '运维管理员UKey席位',
'5Wu8Mf5hC3kq4gm/62/T7pO+aZIw8Y7uYoMIztWyT1w=', '58655960343a1efb0c1b281ce4c384ac699d01ab61c2c710d35e54b798f6e4c5',
'a6bea7601138e90a540b39c73d53df81', 'a6bea7601138e90a540b39c73d53df81',
'ACTIVE', 'ACTIVE',
1, 1,
@@ -10,7 +10,7 @@
<result property="uid" column="uid"/> <result property="uid" column="uid"/>
<result property="accountName" column="account_name"/> <result property="accountName" column="account_name"/>
<result property="displayName" column="display_name"/> <result property="displayName" column="display_name"/>
<result property="passwordHash" column="password_hash"/> <result property="passwordDigest" column="password_digest"/>
<result property="passwordSalt" column="password_salt"/> <result property="passwordSalt" column="password_salt"/>
<result property="status" column="status"/> <result property="status" column="status"/>
<result property="needChangePassword" column="need_change_password"/> <result property="needChangePassword" column="need_change_password"/>
@@ -29,7 +29,7 @@
uid, uid,
account_name, account_name,
display_name, display_name,
password_hash, password_digest,
password_salt, password_salt,
status, status,
need_change_password, need_change_password,
@@ -52,7 +52,7 @@
uid, uid,
account_name, account_name,
display_name, display_name,
password_hash, password_digest,
password_salt, password_salt,
status, status,
need_change_password, need_change_password,
@@ -73,7 +73,7 @@
uid, uid,
account_name, account_name,
display_name, display_name,
password_hash, password_digest,
password_salt, password_salt,
status, status,
need_change_password, need_change_password,
@@ -3,6 +3,8 @@ package com.cisd.tms;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import java.util.regex.Matcher; import java.util.regex.Matcher;
import java.util.regex.Pattern; import java.util.regex.Pattern;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.util.encoders.Hex;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest; import org.springframework.boot.test.context.SpringBootTest;
@@ -19,8 +21,7 @@ import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.
@AutoConfigureMockMvc @AutoConfigureMockMvc
class TmsApplicationTests { class TmsApplicationTests {
private static final String DEFAULT_PASSWORD_DIGEST = private static final String DEFAULT_PASSWORD = "Sunyard@123";
"fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b";
private static final Pattern AUTH_FULL_ACCOUNT_ROW = Pattern.compile( private static final Pattern AUTH_FULL_ACCOUNT_ROW = Pattern.compile(
"\\(\\s*1210[1-5],.*?'([^']+)'\\s*,\\s*'([0-9a-f]{32})'", "\\(\\s*1210[1-5],.*?'([^']+)'\\s*,\\s*'([0-9a-f]{32})'",
@@ -114,16 +115,23 @@ class TmsApplicationTests {
Matcher matcher = AUTH_FULL_ACCOUNT_ROW.matcher(sql); Matcher matcher = AUTH_FULL_ACCOUNT_ROW.matcher(sql);
java.util.Set<String> salts = new java.util.HashSet<>(); java.util.Set<String> salts = new java.util.HashSet<>();
com.cisd.tms.modules.auth.service.impl.Pbkdf2PasswordHasher hasher =
new com.cisd.tms.modules.auth.service.impl.Pbkdf2PasswordHasher();
int rows = 0; int rows = 0;
while (matcher.find()) { while (matcher.find()) {
rows++; rows++;
String hash = matcher.group(1); String digest = matcher.group(1);
String salt = matcher.group(2); String salt = matcher.group(2);
org.junit.jupiter.api.Assertions.assertTrue(salts.add(salt), "duplicate salt: " + salt); org.junit.jupiter.api.Assertions.assertTrue(salts.add(salt), "duplicate salt: " + salt);
org.junit.jupiter.api.Assertions.assertTrue(hasher.matches(DEFAULT_PASSWORD_DIGEST, salt, hash), "hash mismatch for salt: " + salt); org.junit.jupiter.api.Assertions.assertEquals(sm3Hex(salt + DEFAULT_PASSWORD), digest, "digest mismatch for salt: " + salt);
} }
org.junit.jupiter.api.Assertions.assertEquals(5, rows); org.junit.jupiter.api.Assertions.assertEquals(5, rows);
} }
private static String sm3Hex(String value) {
byte[] input = value.getBytes(StandardCharsets.UTF_8);
SM3Digest digest = new SM3Digest();
digest.update(input, 0, input.length);
byte[] result = new byte[digest.getDigestSize()];
digest.doFinal(result, 0);
return Hex.toHexString(result);
}
} }
@@ -32,6 +32,8 @@ class AuthControllerTest {
"0fffff81e971fa3f09107abf77931463fc0710bfb8962efeae3d5654b073bb0c"; "0fffff81e971fa3f09107abf77931463fc0710bfb8962efeae3d5654b073bb0c";
private static final String DIGEST_ABC1234 = private static final String DIGEST_ABC1234 =
"6b66c27d356b7fef3e2c2a986043031a410bb564205f22525ba022591671a7e5"; "6b66c27d356b7fef3e2c2a986043031a410bb564205f22525ba022591671a7e5";
private static final String DYNAMIC_DIGEST =
"55f6d97a52f5f3f27541e3137297a8f807d1ffdfb6596cdd562c8989bc58dc7f";
@Test @Test
void shouldLoginWithPasswordPayload() throws Exception { void shouldLoginWithPasswordPayload() throws Exception {
@@ -118,7 +120,7 @@ class AuthControllerTest {
{ {
"pubKey": "PUB-1", "pubKey": "PUB-1",
"uid": 1, "uid": 1,
"password": "%s", "passwordDynamicDigest": "%s",
"serverRandom": "RB-1", "serverRandom": "RB-1",
"issueSignature": "ISSUE-1", "issueSignature": "ISSUE-1",
"loginPayload": "LOGIN-DATA-1", "loginPayload": "LOGIN-DATA-1",
@@ -127,7 +129,7 @@ class AuthControllerTest {
{ {
"pubKey": "PUB-2", "pubKey": "PUB-2",
"uid": 2, "uid": 2,
"password": "%s", "passwordDynamicDigest": "%s",
"serverRandom": "RB-2", "serverRandom": "RB-2",
"issueSignature": "ISSUE-2", "issueSignature": "ISSUE-2",
"loginPayload": "LOGIN-DATA-2", "loginPayload": "LOGIN-DATA-2",
@@ -135,7 +137,7 @@ class AuthControllerTest {
} }
] ]
} }
""".formatted(DIGEST_12345678, DIGEST_ABC1234))) """.formatted(DYNAMIC_DIGEST, DYNAMIC_DIGEST)))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(content().string(containsString("\"token\":\"token-ukey-001\""))) .andExpect(content().string(containsString("\"token\":\"token-ukey-001\"")))
.andExpect(content().string(containsString("\"authLevel\":\"FULL\""))); .andExpect(content().string(containsString("\"authLevel\":\"FULL\"")));
@@ -220,14 +222,15 @@ class AuthControllerTest {
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(""" .content("""
{ {
"oldPassword": "%s", "changeToken": "change-token-001",
"newPassword": "%s" "oldPasswordDynamicDigest": "%s",
"newPasswordDigest": "%s"
} }
""".formatted(DIGEST_12345678, DIGEST_ABC1234))) """.formatted(DIGEST_12345678, DIGEST_ABC1234)))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(content().string(containsString("\"success\":true"))); .andExpect(content().string(containsString("\"success\":true")));
Mockito.verify(authService).changeAccountPassword("token-change-001", 1, DIGEST_12345678, DIGEST_ABC1234); Mockito.verify(authService).changeAccountPassword("token-change-001", 1, "change-token-001", DIGEST_12345678, DIGEST_ABC1234);
} }
@Test @Test
@@ -246,8 +249,8 @@ class AuthControllerTest {
.contentType(MediaType.APPLICATION_JSON) .contentType(MediaType.APPLICATION_JSON)
.content(""" .content("""
{ {
"oldPassword": "%s", "oldPasswordDigest": "%s",
"newPassword": "%s" "newPasswordDigest": "%s"
} }
""".formatted(DIGEST_12345678, DIGEST_ABC1234))) """.formatted(DIGEST_12345678, DIGEST_ABC1234)))
.andExpect(status().isOk()) .andExpect(status().isOk())
@@ -303,7 +306,7 @@ class AuthControllerTest {
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
MasterKeyBackupPacket packet = new MasterKeyBackupPacket(); MasterKeyBackupPacket packet = new MasterKeyBackupPacket();
packet.setPacketIndex(1); packet.setPacketIndex(1);
UKeySignResult response = UKeySignResult.builder().sign("ISSUE-SIGN-001").backupPacket(packet).build(); UKeySignResult response = UKeySignResult.builder().sign("ISSUE-SIGN-001").passwordSalt("SALT-001").backupPacket(packet).build();
Mockito.when(authAdminService.issueUkeyBindingSign( Mockito.when(authAdminService.issueUkeyBindingSign(
ArgumentMatchers.isNull(), ArgumentMatchers.isNull(),
ArgumentMatchers.isNull(), ArgumentMatchers.isNull(),
@@ -328,6 +331,7 @@ class AuthControllerTest {
""")) """))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-001\""))) .andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-001\"")))
.andExpect(content().string(containsString("\"passwordSalt\":\"SALT-001\"")))
.andExpect(content().string(containsString("\"packetIndex\":1"))); .andExpect(content().string(containsString("\"packetIndex\":1")));
} }
@@ -335,7 +339,7 @@ class AuthControllerTest {
void shouldIssueNormalAdminUkeyBindingSignThroughAdminEndpoint() throws Exception { void shouldIssueNormalAdminUkeyBindingSignThroughAdminEndpoint() throws Exception {
AuthService authService = Mockito.mock(AuthService.class); AuthService authService = Mockito.mock(AuthService.class);
AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class); AuthAdminService authAdminService = Mockito.mock(AuthAdminService.class);
UKeySignResult response = UKeySignResult.builder().sign("ISSUE-SIGN-KEY-001").build(); UKeySignResult response = UKeySignResult.builder().sign("ISSUE-SIGN-KEY-001").passwordSalt("SALT-KEY").build();
Mockito.when(authAdminService.issueUkeyBindingSign( Mockito.when(authAdminService.issueUkeyBindingSign(
ArgumentMatchers.eq("SUPER_ADMIN"), ArgumentMatchers.eq("SUPER_ADMIN"),
ArgumentMatchers.eq("FULL"), ArgumentMatchers.eq("FULL"),
@@ -360,7 +364,8 @@ class AuthControllerTest {
} }
""")) """))
.andExpect(status().isOk()) .andExpect(status().isOk())
.andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-KEY-001\""))); .andExpect(content().string(containsString("\"sign\":\"ISSUE-SIGN-KEY-001\"")))
.andExpect(content().string(containsString("\"passwordSalt\":\"SALT-KEY\"")));
} }
@Test @Test
@@ -34,7 +34,6 @@ import org.mockito.Mockito;
class AuthAdminServiceTest { class AuthAdminServiceTest {
private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-23T02:00:00Z"), ZoneOffset.UTC); private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-03-23T02:00:00Z"), ZoneOffset.UTC);
private static final String DEFAULT_PASSWORD_DIGEST = "fc216e5eea029a7b5c267ab13cc2e0927a3810e66a2a09b86f8fdbd8d4aa647b";
private static final String DIGEST_OLD1234 = "5237f20185252395f6ce351f8e742085d6ab1207ddda94b085b409e41026fc04"; private static final String DIGEST_OLD1234 = "5237f20185252395f6ce351f8e742085d6ab1207ddda94b085b409e41026fc04";
private static final String DIGEST_NEW1234 = "6b66c27d356b7fef3e2c2a986043031a410bb564205f22525ba022591671a7e5"; private static final String DIGEST_NEW1234 = "6b66c27d356b7fef3e2c2a986043031a410bb564205f22525ba022591671a7e5";
private static final String DIGEST_BAD_PASSWORD = "17cb3cec9c337826293158dd5623c75268a2eee06123ba53b9aef5b790c04e51"; private static final String DIGEST_BAD_PASSWORD = "17cb3cec9c337826293158dd5623c75268a2eee06123ba53b9aef5b790c04e51";
@@ -52,10 +51,10 @@ class AuthAdminServiceTest {
AuthFullAccountEntity first = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow(); AuthFullAccountEntity first = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1).orElseThrow();
AuthFullAccountEntity second = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow(); AuthFullAccountEntity second = accounts.findByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 2).orElseThrow();
Assertions.assertEquals("salt-reset-1", first.getPasswordSalt()); Assertions.assertEquals("OLD-SALT-1", first.getPasswordSalt());
Assertions.assertEquals("HASH:" + DEFAULT_PASSWORD_DIGEST + ":salt-reset-1", first.getPasswordHash()); Assertions.assertEquals("434685979fe125d142bfa1568d61ca1c6349bb54befb766617a1502d07bdc6c3", first.getPasswordDigest());
Assertions.assertEquals("salt-reset-2", second.getPasswordSalt()); Assertions.assertEquals("OLD-SALT-2", second.getPasswordSalt());
Assertions.assertEquals("HASH:" + DEFAULT_PASSWORD_DIGEST + ":salt-reset-2", second.getPasswordHash()); Assertions.assertEquals("dd6eae63d8a5776a4ed2b01cfac3d0b31ae15f331dfa507cd833da3ea5b504e4", second.getPasswordDigest());
Assertions.assertTrue(Boolean.TRUE.equals(first.getNeedChangePassword())); Assertions.assertTrue(Boolean.TRUE.equals(first.getNeedChangePassword()));
Assertions.assertTrue(Boolean.TRUE.equals(second.getNeedChangePassword())); Assertions.assertTrue(Boolean.TRUE.equals(second.getNeedChangePassword()));
} }
@@ -65,7 +64,7 @@ class AuthAdminServiceTest {
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository();
roles.save(role(RoleCode.KEY_ADMIN, RoleAccountStatus.ACTIVE)); roles.save(role(RoleCode.KEY_ADMIN, RoleAccountStatus.ACTIVE));
accounts.save(account(RoleCode.KEY_ADMIN, 1, "key-admin-01", "HASH:" + DIGEST_OLD1234 + ":SALT-OLD", "SALT-OLD")); accounts.save(account(RoleCode.KEY_ADMIN, 1, "key-admin-01", DIGEST_OLD1234, "SALT-OLD"));
service(roles, accounts, new InMemoryRoleUkeyBindingRepository(), Mockito.mock(LmkService.class)) service(roles, accounts, new InMemoryRoleUkeyBindingRepository(), Mockito.mock(LmkService.class))
.changeAccountPassword( .changeAccountPassword(
@@ -78,8 +77,8 @@ class AuthAdminServiceTest {
); );
AuthFullAccountEntity changed = accounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow(); AuthFullAccountEntity changed = accounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow();
Assertions.assertEquals("salt-reset-1", changed.getPasswordSalt()); Assertions.assertEquals("SALT-OLD", changed.getPasswordSalt());
Assertions.assertEquals("HASH:" + DIGEST_NEW1234 + ":salt-reset-1", changed.getPasswordHash()); Assertions.assertEquals(DIGEST_NEW1234, changed.getPasswordDigest());
Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword())); Assertions.assertTrue(Boolean.TRUE.equals(changed.getNeedChangePassword()));
} }
@@ -88,7 +87,7 @@ class AuthAdminServiceTest {
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository(); InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository();
roles.save(role(RoleCode.KEY_ADMIN, RoleAccountStatus.ACTIVE)); roles.save(role(RoleCode.KEY_ADMIN, RoleAccountStatus.ACTIVE));
accounts.save(account(RoleCode.KEY_ADMIN, 1, "key-admin-01", "HASH:" + DIGEST_OLD1234 + ":SALT-OLD", "SALT-OLD")); accounts.save(account(RoleCode.KEY_ADMIN, 1, "key-admin-01", DIGEST_OLD1234, "SALT-OLD"));
BizException exception = Assertions.assertThrows(BizException.class, BizException exception = Assertions.assertThrows(BizException.class,
() -> service(roles, accounts, new InMemoryRoleUkeyBindingRepository(), Mockito.mock(LmkService.class)) () -> service(roles, accounts, new InMemoryRoleUkeyBindingRepository(), Mockito.mock(LmkService.class))
@@ -132,6 +131,7 @@ class AuthAdminServiceTest {
.issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto); .issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.SUPER_ADMIN.getCode(), dto);
Assertions.assertEquals("ISSUE-SIGN-REBIND", result.getSign()); Assertions.assertEquals("ISSUE-SIGN-REBIND", result.getSign());
Assertions.assertEquals("SALT-OLD", result.getPasswordSalt());
Assertions.assertNotNull(result.getBackupPacket()); Assertions.assertNotNull(result.getBackupPacket());
List<RoleUkeyBindingEntity> activeUidBindings = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1); List<RoleUkeyBindingEntity> activeUidBindings = bindings.findActiveByRoleCodeAndUid(RoleCode.SUPER_ADMIN.getCode(), 1);
Assertions.assertEquals(2, activeUidBindings.size()); Assertions.assertEquals(2, activeUidBindings.size());
@@ -184,6 +184,7 @@ class AuthAdminServiceTest {
.issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.KEY_ADMIN.getCode(), dto); .issueUkeyBindingSign(RoleCode.SUPER_ADMIN.getCode(), AuthLevel.FULL.name(), RoleCode.KEY_ADMIN.getCode(), dto);
Assertions.assertEquals("ISSUE-SIGN-NEW", result.getSign()); Assertions.assertEquals("ISSUE-SIGN-NEW", result.getSign());
Assertions.assertEquals("SALT-OLD", result.getPasswordSalt());
Assertions.assertNull(result.getBackupPacket()); Assertions.assertNull(result.getBackupPacket());
List<RoleUkeyBindingEntity> activeBindings = bindings.findActiveByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1); List<RoleUkeyBindingEntity> activeBindings = bindings.findActiveByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1);
Assertions.assertEquals(1, activeBindings.size()); Assertions.assertEquals(1, activeBindings.size());
@@ -394,8 +395,7 @@ class AuthAdminServiceTest {
new FakePasswordHasher(), new FakePasswordHasher(),
lmkService, lmkService,
new ObjectMapper(), new ObjectMapper(),
FIXED_CLOCK, FIXED_CLOCK
new FixedSaltSupplier("salt-reset-1", "salt-reset-2", "salt-reset-3")
); );
} }
@@ -416,7 +416,7 @@ class AuthAdminServiceTest {
entity.setUid(uid); entity.setUid(uid);
entity.setAccountName(accountName); entity.setAccountName(accountName);
entity.setDisplayName(accountName); entity.setDisplayName(accountName);
entity.setPasswordHash(passwordHash); entity.setPasswordDigest(passwordHash);
entity.setPasswordSalt(passwordSalt); entity.setPasswordSalt(passwordSalt);
entity.setStatus(RoleAccountStatus.ACTIVE.name()); entity.setStatus(RoleAccountStatus.ACTIVE.name());
entity.setNeedChangePassword(Boolean.FALSE); entity.setNeedChangePassword(Boolean.FALSE);
@@ -440,29 +440,12 @@ class AuthAdminServiceTest {
private static class FakePasswordHasher implements PasswordHasher { private static class FakePasswordHasher implements PasswordHasher {
@Override @Override
public String hash(String password, String salt) { public String hash(String password, String salt) {
return "HASH:" + password + ":" + salt; return password;
} }
@Override @Override
public boolean matches(String rawPassword, String salt, String expectedHash) { public boolean matches(String rawPassword, String salt, String expectedHash) {
return hash(rawPassword, salt).equals(expectedHash); return rawPassword.equals(expectedHash);
}
}
private static class FixedSaltSupplier implements PasswordSaltGenerator {
private final List<String> salts;
private int index = 0;
private FixedSaltSupplier(String... salts) {
this.salts = List.of(salts);
}
@Override
public String nextSalt() {
if (index >= salts.size()) {
return salts.get(salts.size() - 1);
}
return salts.get(index++);
} }
} }
@@ -22,7 +22,8 @@ import org.mockito.Mockito;
class AuthSecurityResetServiceTest { class AuthSecurityResetServiceTest {
private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-05-13T08:00:00Z"), ZoneOffset.UTC); private static final Clock FIXED_CLOCK = Clock.fixed(Instant.parse("2026-05-13T08:00:00Z"), ZoneOffset.UTC);
private static final String FIRST_SUPER_ADMIN_PASSWORD_HASH = "vVPlH2UGQcXUo7NblMJMaof32um6UinF8zsckZuxe9E="; private static final String FIRST_SUPER_ADMIN_PASSWORD_DIGEST =
"00718a8319e67ef6cf9fc389c14ce16784278d55439a7a30a63fa7be8e42bbdf";
@Test @Test
void shouldClearBindingsSessionsAndRestoreSeededRolesAndPasswords() { void shouldClearBindingsSessionsAndRestoreSeededRolesAndPasswords() {
@@ -89,7 +90,7 @@ class AuthSecurityResetServiceTest {
Assertions.assertEquals("super-admin-full-01", firstSuperAdmin.getAccountName()); Assertions.assertEquals("super-admin-full-01", firstSuperAdmin.getAccountName());
Assertions.assertEquals("超级管理员UKey席位一", firstSuperAdmin.getDisplayName()); Assertions.assertEquals("超级管理员UKey席位一", firstSuperAdmin.getDisplayName());
Assertions.assertEquals("3df85c2988a5de2b9eeaa8109639641a", firstSuperAdmin.getPasswordSalt()); Assertions.assertEquals("3df85c2988a5de2b9eeaa8109639641a", firstSuperAdmin.getPasswordSalt());
Assertions.assertEquals(FIRST_SUPER_ADMIN_PASSWORD_HASH, firstSuperAdmin.getPasswordHash()); Assertions.assertEquals(FIRST_SUPER_ADMIN_PASSWORD_DIGEST, firstSuperAdmin.getPasswordDigest());
Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), firstSuperAdmin.getStatus()); Assertions.assertEquals(RoleAccountStatus.ACTIVE.name(), firstSuperAdmin.getStatus());
Assertions.assertTrue(Boolean.TRUE.equals(firstSuperAdmin.getNeedChangePassword())); Assertions.assertTrue(Boolean.TRUE.equals(firstSuperAdmin.getNeedChangePassword()));
Assertions.assertEquals(0, firstSuperAdmin.getFailedCount()); Assertions.assertEquals(0, firstSuperAdmin.getFailedCount());
@@ -114,7 +115,7 @@ class AuthSecurityResetServiceTest {
entity.setUid(uid); entity.setUid(uid);
entity.setAccountName("dirty"); entity.setAccountName("dirty");
entity.setDisplayName("dirty"); entity.setDisplayName("dirty");
entity.setPasswordHash("dirty"); entity.setPasswordDigest("dirty");
entity.setPasswordSalt("dirty"); entity.setPasswordSalt("dirty");
entity.setStatus(RoleAccountStatus.LOCKED.name()); entity.setStatus(RoleAccountStatus.LOCKED.name());
entity.setNeedChangePassword(Boolean.FALSE); entity.setNeedChangePassword(Boolean.FALSE);
@@ -36,6 +36,8 @@ import java.util.List;
import java.util.Map; import java.util.Map;
import java.util.Optional; import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentHashMap;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.util.encoders.Hex;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
@@ -99,7 +101,7 @@ class AuthServiceTest {
UkeyLoginRequest ukeyRequest = new UkeyLoginRequest(); UkeyLoginRequest ukeyRequest = new UkeyLoginRequest();
ukeyRequest.setRoleCode(RoleCode.KEY_ADMIN.getCode()); ukeyRequest.setRoleCode(RoleCode.KEY_ADMIN.getCode());
ukeyRequest.setLoginFactors(List.of(proofWithPassword("PUB-1", 1, DIGEST_ABC1234, "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1"))); ukeyRequest.setLoginFactors(List.of(proofWithPassword("PUB-1", 1, sm3Hex(DIGEST_ABC1234 + "RB-1"), "RB-1", "ISSUE-1", "LOGIN-DATA-1", "LOGIN-SIGN-1")));
LoginResponse response = service.ukeyLogin(ukeyRequest); LoginResponse response = service.ukeyLogin(ukeyRequest);
Assertions.assertEquals(AuthLevel.FULL.name(), response.getAuthLevel()); Assertions.assertEquals(AuthLevel.FULL.name(), response.getAuthLevel());
@@ -109,6 +111,34 @@ class AuthServiceTest {
); );
} }
@Test
void shouldChangePasswordWithBackendIssuedNewSaltAndDynamicOldDigest() {
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
InMemoryAuthFullAccountRepository accounts = new InMemoryAuthFullAccountRepository();
InMemoryAuthSessionRepository sessions = new InMemoryAuthSessionRepository();
roles.save(activeRole(RoleCode.KEY_ADMIN));
accounts.save(activeAccount(RoleCode.KEY_ADMIN, 1, "key-admin-01", DIGEST_OLD1234, "SALT-OLD", true));
AuthSessionEntity session = session("token-change-prepare", RoleCode.KEY_ADMIN.getCode(), AuthLevel.FULL.name());
session.setAuthenticatedPrincipalsJson("[{\"type\":\"FULL\",\"uid\":1,\"username\":\"key-admin-01\"}]");
sessions.save(session);
AuthService service = newAuthService(roles, accounts, sessions, new InMemoryRoleUkeyBindingRepository(), () -> "unused");
var prepare = service.prepareAccountPasswordChange("token-change-prepare", 1);
service.changeAccountPassword(
"token-change-prepare",
1,
prepare.getChangeToken(),
sm3Hex(DIGEST_OLD1234 + prepare.getOldServerRandom()),
DIGEST_NEW1234
);
AuthFullAccountEntity changed = accounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow();
Assertions.assertEquals("salt-test", changed.getPasswordSalt());
Assertions.assertEquals(DIGEST_NEW1234, changed.getPasswordDigest());
Assertions.assertFalse(Boolean.TRUE.equals(changed.getNeedChangePassword()));
}
@Test @Test
void shouldChangeUnifiedPasswordFromLimitedSessionAndAffectUkeyLogin() { void shouldChangeUnifiedPasswordFromLimitedSessionAndAffectUkeyLogin() {
InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository(); InMemoryRoleAccountRepository roles = new InMemoryRoleAccountRepository();
@@ -124,11 +154,18 @@ class AuthServiceTest {
AuthService service = newAuthService(roles, accounts, sessions, bindings, () -> "token-after-change"); AuthService service = newAuthService(roles, accounts, sessions, bindings, () -> "token-after-change");
service.changeAccountPassword("token-change-001", 1, DIGEST_OLD1234, DIGEST_NEW1234); var prepare = service.prepareAccountPasswordChange("token-change-001", 1);
service.changeAccountPassword(
"token-change-001",
1,
prepare.getChangeToken(),
sm3Hex(DIGEST_OLD1234 + prepare.getOldServerRandom()),
DIGEST_NEW1234
);
AuthFullAccountEntity changed = accounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow(); AuthFullAccountEntity changed = accounts.findByRoleCodeAndUid(RoleCode.KEY_ADMIN.getCode(), 1).orElseThrow();
Assertions.assertEquals("salt-test", changed.getPasswordSalt()); Assertions.assertEquals("salt-test", changed.getPasswordSalt());
Assertions.assertEquals("HASH:" + DIGEST_NEW1234 + ":salt-test", changed.getPasswordHash()); Assertions.assertEquals(DIGEST_NEW1234, changed.getPasswordDigest());
Assertions.assertFalse(Boolean.TRUE.equals(changed.getNeedChangePassword())); Assertions.assertFalse(Boolean.TRUE.equals(changed.getNeedChangePassword()));
} }
@@ -198,8 +235,8 @@ class AuthServiceTest {
UkeyLoginRequest request = new UkeyLoginRequest(); UkeyLoginRequest request = new UkeyLoginRequest();
request.setRoleCode(RoleCode.SUPER_ADMIN.getCode()); request.setRoleCode(RoleCode.SUPER_ADMIN.getCode());
request.setLoginFactors(List.of( request.setLoginFactors(List.of(
proofWithPassword("PUB-1-BACKUP", 1, DIGEST_11111111, "RB-1", "ISSUE-1B", "LOGIN-DATA-1B", "LOGIN-SIGN-1B"), proofWithPassword("PUB-1-BACKUP", 1, sm3Hex(DIGEST_11111111 + "RB-1"), "RB-1", "ISSUE-1B", "LOGIN-DATA-1B", "LOGIN-SIGN-1B"),
proofWithPassword("PUB-2-BACKUP", 2, DIGEST_22222222, "RB-2", "ISSUE-2B", "LOGIN-DATA-2B", "LOGIN-SIGN-2B") proofWithPassword("PUB-2-BACKUP", 2, sm3Hex(DIGEST_22222222 + "RB-2"), "RB-2", "ISSUE-2B", "LOGIN-DATA-2B", "LOGIN-SIGN-2B")
)); ));
LoginResponse response = service.ukeyLogin(request); LoginResponse response = service.ukeyLogin(request);
@@ -223,7 +260,7 @@ class AuthServiceTest {
BizException exception = Assertions.assertThrows(BizException.class, BizException exception = Assertions.assertThrows(BizException.class,
() -> newAuthService(roles, accounts, sessions, new InMemoryRoleUkeyBindingRepository(), () -> "unused") () -> newAuthService(roles, accounts, sessions, new InMemoryRoleUkeyBindingRepository(), () -> "unused")
.changeAccountPassword("token-expired-001", 1, DIGEST_12345678, DIGEST_ABC1234)); .prepareAccountPasswordChange("token-expired-001", 1));
Assertions.assertEquals(ErrorCode.SESSION_INVALID.getCode(), exception.getCode()); Assertions.assertEquals(ErrorCode.SESSION_INVALID.getCode(), exception.getCode());
Assertions.assertEquals("会话已过期", exception.getMessage()); Assertions.assertEquals("会话已过期", exception.getMessage());
@@ -328,7 +365,7 @@ class AuthServiceTest {
entity.setAccountName(accountName); entity.setAccountName(accountName);
entity.setDisplayName(accountName); entity.setDisplayName(accountName);
entity.setPasswordSalt(salt); entity.setPasswordSalt(salt);
entity.setPasswordHash("HASH:" + password + ":" + salt); entity.setPasswordDigest(password);
entity.setStatus(RoleAccountStatus.ACTIVE.name()); entity.setStatus(RoleAccountStatus.ACTIVE.name());
entity.setNeedChangePassword(needChangePassword); entity.setNeedChangePassword(needChangePassword);
entity.setPasswordChangedAt(LocalDateTime.of(2026, 3, 23, 1, 0)); entity.setPasswordChangedAt(LocalDateTime.of(2026, 3, 23, 1, 0));
@@ -387,15 +424,24 @@ class AuthServiceTest {
return entity; return entity;
} }
private static String sm3Hex(String value) {
byte[] input = value.getBytes(java.nio.charset.StandardCharsets.UTF_8);
SM3Digest digest = new SM3Digest();
digest.update(input, 0, input.length);
byte[] result = new byte[digest.getDigestSize()];
digest.doFinal(result, 0);
return Hex.toHexString(result);
}
private static class FakePasswordHasher implements PasswordHasher { private static class FakePasswordHasher implements PasswordHasher {
@Override @Override
public String hash(String password, String salt) { public String hash(String password, String salt) {
return "HASH:" + password + ":" + salt; return password;
} }
@Override @Override
public boolean matches(String rawPassword, String salt, String expectedHash) { public boolean matches(String rawPassword, String salt, String expectedHash) {
return hash(rawPassword, salt).equals(expectedHash); return rawPassword.equals(expectedHash);
} }
} }
@@ -0,0 +1,126 @@
package com.cisd.tms.modules.cert.service;
import com.cisd.tms.common.exception.BizException;
import org.bouncycastle.crypto.digests.SM3Digest;
import org.bouncycastle.jce.provider.BouncyCastleProvider;
import org.bouncycastle.util.encoders.Hex;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.io.TempDir;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import java.security.KeyPair;
import java.security.KeyPairGenerator;
import java.security.Security;
import java.security.Signature;
import java.security.spec.ECGenParameterSpec;
import java.util.Base64;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
class FileRootCertSourceVerifierTest {
static {
if (Security.getProvider("BC") == null) {
Security.addProvider(new BouncyCastleProvider());
}
}
@TempDir
Path deviceDir;
@Test
void shouldAcceptRootCertWhenPublicKeySm3AndSignatureAreValid() throws Exception {
KeyPair keyPair = sm2KeyPair();
byte[] certData = "trusted-root-cert".getBytes(StandardCharsets.UTF_8);
writeTrustFiles(keyPair, certData);
FileRootCertSourceVerifier verifier = new FileRootCertSourceVerifier(deviceDir);
assertDoesNotThrow(() -> verifier.verifyTrustedSource(certData));
}
@Test
void shouldReadTrustFilesFromConfiguredPaths() throws Exception {
KeyPair keyPair = sm2KeyPair();
byte[] certData = "trusted-root-cert".getBytes(StandardCharsets.UTF_8);
Path configuredSm3Path = deviceDir.resolve("custom-dev.mac.pub.sm3");
Path configuredPublicKeyPath = deviceDir.resolve("custom-mac_sm2_public_key.pemy");
Path configuredSignPath = deviceDir.resolve("custom-rootcert.sign");
Files.writeString(configuredPublicKeyPath, toPublicKeyPem(keyPair), StandardCharsets.UTF_8);
Files.writeString(configuredSm3Path, sm3Hex(keyPair.getPublic().getEncoded()), StandardCharsets.UTF_8);
Files.write(configuredSignPath, sign(certData, keyPair));
FileRootCertSourceVerifier verifier = new FileRootCertSourceVerifier(
configuredSm3Path,
configuredPublicKeyPath,
configuredSignPath
);
assertDoesNotThrow(() -> verifier.verifyTrustedSource(certData));
}
@Test
void shouldRejectRootCertWhenPublicKeySm3DoesNotMatch() throws Exception {
KeyPair keyPair = sm2KeyPair();
byte[] certData = "trusted-root-cert".getBytes(StandardCharsets.UTF_8);
writeTrustFiles(keyPair, certData);
Files.writeString(deviceDir.resolve("dev.mac.pub.sm3"), "00", StandardCharsets.UTF_8);
FileRootCertSourceVerifier verifier = new FileRootCertSourceVerifier(deviceDir);
BizException exception = assertThrows(BizException.class, () -> verifier.verifyTrustedSource(certData));
assertEquals("根证书来源不可信", exception.getMessage());
}
@Test
void shouldRejectRootCertWhenSignatureDoesNotMatch() throws Exception {
KeyPair keyPair = sm2KeyPair();
byte[] certData = "trusted-root-cert".getBytes(StandardCharsets.UTF_8);
writeTrustFiles(keyPair, certData);
FileRootCertSourceVerifier verifier = new FileRootCertSourceVerifier(deviceDir);
BizException exception = assertThrows(
BizException.class,
() -> verifier.verifyTrustedSource("tampered".getBytes(StandardCharsets.UTF_8))
);
assertEquals("根证书来源不可信", exception.getMessage());
}
private void writeTrustFiles(KeyPair keyPair, byte[] certData) throws Exception {
Files.writeString(deviceDir.resolve("mac_sm2_public_key.pemy"), toPublicKeyPem(keyPair), StandardCharsets.UTF_8);
Files.writeString(deviceDir.resolve("dev.mac.pub.sm3"), sm3Hex(keyPair.getPublic().getEncoded()), StandardCharsets.UTF_8);
Files.write(deviceDir.resolve("rootcert.sign"), sign(certData, keyPair));
}
private KeyPair sm2KeyPair() throws Exception {
KeyPairGenerator generator = KeyPairGenerator.getInstance("EC", "BC");
generator.initialize(new ECGenParameterSpec("sm2p256v1"));
return generator.generateKeyPair();
}
private byte[] sign(byte[] data, KeyPair keyPair) throws Exception {
Signature signature = Signature.getInstance("SM3withSM2", "BC");
signature.initSign(keyPair.getPrivate());
signature.update(data);
return signature.sign();
}
private String toPublicKeyPem(KeyPair keyPair) {
String base64 = Base64.getMimeEncoder(64, "\n".getBytes(StandardCharsets.UTF_8))
.encodeToString(keyPair.getPublic().getEncoded());
return "-----BEGIN PUBLIC KEY-----\n" + base64 + "\n-----END PUBLIC KEY-----\n";
}
private String sm3Hex(byte[] data) {
SM3Digest digest = new SM3Digest();
digest.update(data, 0, data.length);
byte[] result = new byte[digest.getDigestSize()];
digest.doFinal(result, 0);
return Hex.toHexString(result);
}
}
@@ -20,6 +20,7 @@ import org.mockito.Mockito;
import org.springframework.mock.web.MockMultipartFile; import org.springframework.mock.web.MockMultipartFile;
import java.math.BigInteger; import java.math.BigInteger;
import java.nio.charset.StandardCharsets;
import java.security.KeyPair; import java.security.KeyPair;
import java.security.KeyPairGenerator; import java.security.KeyPairGenerator;
import java.security.Security; import java.security.Security;
@@ -30,7 +31,6 @@ import java.time.Instant;
import java.time.temporal.ChronoUnit; import java.time.temporal.ChronoUnit;
import java.util.Date; import java.util.Date;
import java.util.List; import java.util.List;
import java.util.Locale;
import java.util.Optional; import java.util.Optional;
import javax.security.auth.x500.X500Principal; import javax.security.auth.x500.X500Principal;
@@ -195,7 +195,12 @@ class TrustedCertServiceTest {
@Test @Test
void shouldPersistRsaAlgorithmWhenImportingRsaTrustedCa() throws Exception { void shouldPersistRsaAlgorithmWhenImportingRsaTrustedCa() throws Exception {
TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class); TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class);
TrustedCertService service = new TrustedCertService(trustedCertRepository, Mockito.mock(CertificateRepository.class)); RootCertSourceVerifier rootCertSourceVerifier = Mockito.mock(RootCertSourceVerifier.class);
TrustedCertService service = new TrustedCertService(
trustedCertRepository,
Mockito.mock(CertificateRepository.class),
rootCertSourceVerifier
);
KeyPair rootKeyPair = rsaKeyPair(); KeyPair rootKeyPair = rsaKeyPair();
X509Certificate root = rsaCertificate( X509Certificate root = rsaCertificate(
rootKeyPair, rootKeyPair,
@@ -210,14 +215,16 @@ class TrustedCertServiceTest {
service.importTrusted(multipart(root), "rsa-root"); service.importTrusted(multipart(root), "rsa-root");
Mockito.verify(rootCertSourceVerifier).verifyTrustedSource(CertPemSupport.toPem(root).getBytes(StandardCharsets.UTF_8));
org.mockito.ArgumentCaptor<TrustedCertEntity> captor = org.mockito.ArgumentCaptor.forClass(TrustedCertEntity.class); org.mockito.ArgumentCaptor<TrustedCertEntity> captor = org.mockito.ArgumentCaptor.forClass(TrustedCertEntity.class);
Mockito.verify(trustedCertRepository).save(captor.capture()); Mockito.verify(trustedCertRepository).save(captor.capture());
assertEquals("RSA", captor.getValue().getAlgoType()); assertEquals("RSA", captor.getValue().getAlgoType());
} }
@Test @Test
void shouldAllowRootCaWhenFingerprintMatchesWhitelist() throws Exception { void shouldAllowRootCaWhenSourceVerifierAccepts() throws Exception {
TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class); TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class);
RootCertSourceVerifier rootCertSourceVerifier = Mockito.mock(RootCertSourceVerifier.class);
KeyPair rootKeyPair = sm2KeyPair(); KeyPair rootKeyPair = sm2KeyPair();
X509Certificate root = certificate( X509Certificate root = certificate(
rootKeyPair, rootKeyPair,
@@ -230,19 +237,21 @@ class TrustedCertServiceTest {
TrustedCertService service = new TrustedCertService( TrustedCertService service = new TrustedCertService(
trustedCertRepository, trustedCertRepository,
Mockito.mock(CertificateRepository.class), Mockito.mock(CertificateRepository.class),
List.of(colonFingerprint(CertPemSupport.sha256Fingerprint(root)).toLowerCase(Locale.ROOT)) rootCertSourceVerifier
); );
Mockito.when(trustedCertRepository.findByFingerprint(Mockito.any())).thenReturn(Optional.empty()); Mockito.when(trustedCertRepository.findByFingerprint(Mockito.any())).thenReturn(Optional.empty());
Mockito.when(trustedCertRepository.save(Mockito.any())).thenAnswer(invocation -> invocation.getArgument(0)); Mockito.when(trustedCertRepository.save(Mockito.any())).thenAnswer(invocation -> invocation.getArgument(0));
service.importTrusted(multipart(root), "root"); service.importTrusted(multipart(root), "root");
Mockito.verify(rootCertSourceVerifier).verifyTrustedSource(CertPemSupport.toPem(root).getBytes(StandardCharsets.UTF_8));
Mockito.verify(trustedCertRepository).save(Mockito.any()); Mockito.verify(trustedCertRepository).save(Mockito.any());
} }
@Test @Test
void shouldRejectRootCaWhenFingerprintDoesNotMatchWhitelist() throws Exception { void shouldRejectRootCaWhenSourceVerifierRejects() throws Exception {
TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class); TrustedCertRepository trustedCertRepository = Mockito.mock(TrustedCertRepository.class);
RootCertSourceVerifier rootCertSourceVerifier = Mockito.mock(RootCertSourceVerifier.class);
KeyPair rootKeyPair = sm2KeyPair(); KeyPair rootKeyPair = sm2KeyPair();
X509Certificate root = certificate( X509Certificate root = certificate(
rootKeyPair, rootKeyPair,
@@ -255,13 +264,16 @@ class TrustedCertServiceTest {
TrustedCertService service = new TrustedCertService( TrustedCertService service = new TrustedCertService(
trustedCertRepository, trustedCertRepository,
Mockito.mock(CertificateRepository.class), Mockito.mock(CertificateRepository.class),
List.of("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA") rootCertSourceVerifier
); );
Mockito.when(trustedCertRepository.findByFingerprint(Mockito.any())).thenReturn(Optional.empty()); Mockito.when(trustedCertRepository.findByFingerprint(Mockito.any())).thenReturn(Optional.empty());
Mockito.doThrow(new BizException(400, "根证书来源不可信"))
.when(rootCertSourceVerifier)
.verifyTrustedSource(Mockito.any());
BizException exception = assertThrows(BizException.class, () -> service.importTrusted(multipart(root), "root")); BizException exception = assertThrows(BizException.class, () -> service.importTrusted(multipart(root), "root"));
assertEquals("根CA证书指纹不在白名单中", exception.getMessage()); assertEquals("根证书来源不可信", exception.getMessage());
Mockito.verify(trustedCertRepository, Mockito.never()).save(Mockito.any()); Mockito.verify(trustedCertRepository, Mockito.never()).save(Mockito.any());
} }
@@ -349,11 +361,7 @@ class TrustedCertServiceTest {
"file", "file",
"cert.pem", "cert.pem",
"application/x-pem-file", "application/x-pem-file",
CertPemSupport.toPem(certificate).getBytes() CertPemSupport.toPem(certificate).getBytes(StandardCharsets.UTF_8)
); );
} }
private String colonFingerprint(String fingerprint) {
return fingerprint.replaceAll("(.{2})(?=.)", "$1:");
}
} }
@@ -0,0 +1,152 @@
package com.cisd.tms.modules.log.controller;
import com.cisd.tms.modules.auth.controller.AuthAdminController;
import com.cisd.tms.modules.auth.controller.AuthController;
import com.cisd.tms.modules.device.controller.DeviceController;
import com.cisd.tms.modules.device.controller.IpWhitelistController;
import com.cisd.tms.modules.device.controller.NetworkConfigController;
import com.cisd.tms.modules.device.controller.TimeConfigController;
import com.cisd.tms.modules.log.annotation.AuditedOperation;
import com.cisd.tms.modules.log.enums.ActionType;
import com.cisd.tms.modules.log.enums.ModuleCode;
import com.cisd.tms.modules.mk.controller.LmkController;
import com.cisd.tms.modules.openapi.service.controller.OpenApiController;
import com.cisd.tms.modules.upgrade.controller.UpgradeController;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.core.annotation.AnnotatedElementUtils;
import java.lang.reflect.Method;
class AuditedOperationEndpointsTest {
@Test
void shouldAuditAuthStateChanges() throws Exception {
assertAudit(AuthController.class, "passwordLogin", ModuleCode.AUTH, ActionType.LOGIN,
com.cisd.tms.modules.auth.dto.PasswordLoginRequest.class);
assertAudit(AuthController.class, "ukeyLogin", ModuleCode.AUTH, ActionType.LOGIN,
com.cisd.tms.modules.auth.dto.UkeyLoginRequest.class);
assertAudit(AuthController.class, "logout", ModuleCode.AUTH, ActionType.LOGOUT,
jakarta.servlet.http.HttpServletRequest.class);
assertAudit(AuthController.class, "changeAccountPassword", ModuleCode.AUTH, ActionType.UPDATE,
Integer.class, com.cisd.tms.modules.auth.dto.ChangePasswordRequest.class, jakarta.servlet.http.HttpServletRequest.class);
assertAudit(AuthAdminController.class, "resetPassword", ModuleCode.AUTH, ActionType.RESET,
String.class, jakarta.servlet.http.HttpServletRequest.class);
assertAudit(AuthAdminController.class, "changeAccountPassword", ModuleCode.AUTH, ActionType.UPDATE,
String.class, Integer.class, com.cisd.tms.modules.auth.dto.AdminChangePasswordRequest.class,
jakarta.servlet.http.HttpServletRequest.class);
assertAudit(AuthAdminController.class, "issueSuperAdminUkeyBindingSign", ModuleCode.AUTH, ActionType.BIND,
com.cisd.tms.modules.mk.dto.UKeySignDTO.class);
assertAudit(AuthAdminController.class, "issueUkeyBindingSign", ModuleCode.AUTH, ActionType.BIND,
String.class, com.cisd.tms.modules.mk.dto.UKeySignDTO.class, jakarta.servlet.http.HttpServletRequest.class);
}
@Test
void shouldAuditDeviceNetworkAndMasterKeyMutations() throws Exception {
assertAudit(DeviceController.class, "restart", ModuleCode.DEVICE, ActionType.RESTART,
jakarta.servlet.http.HttpServletRequest.class);
assertAudit(TimeConfigController.class, "configureTime", ModuleCode.DEVICE, ActionType.UPDATE,
com.cisd.tms.modules.device.dto.TimeConfigRequest.class);
assertAudit(NetworkConfigController.class, "configureNetworkCard", ModuleCode.NETWORK, ActionType.UPDATE,
com.cisd.tms.modules.device.dto.network.NetworkConfigureRequest.class);
assertAudit(NetworkConfigController.class, "addNetworkCard", ModuleCode.NETWORK, ActionType.CREATE,
com.cisd.tms.modules.device.dto.network.NetworkConfigureRequest.class);
assertAudit(NetworkConfigController.class, "deleteNetworkCardRecord", ModuleCode.NETWORK, ActionType.DELETE,
com.cisd.tms.modules.device.dto.network.NetworkDeleteRequest.class);
assertAudit(NetworkConfigController.class, "deleteBond", ModuleCode.NETWORK, ActionType.DELETE, String.class);
assertAudit(NetworkConfigController.class, "createBond", ModuleCode.NETWORK, ActionType.CREATE,
com.cisd.tms.modules.device.dto.network.BondCreateRequest.class);
assertAudit(NetworkConfigController.class, "configBond", ModuleCode.NETWORK, ActionType.UPDATE,
com.cisd.tms.modules.device.dto.network.BondConfigRequest.class);
assertAudit(NetworkConfigController.class, "routeAdd", ModuleCode.NETWORK, ActionType.CREATE,
com.cisd.tms.modules.device.dto.network.RouteCreateRequest.class);
assertAudit(NetworkConfigController.class, "routeUpdate", ModuleCode.NETWORK, ActionType.UPDATE,
com.cisd.tms.modules.device.dto.network.RouteUpdateRequest.class);
assertAudit(NetworkConfigController.class, "routeDelete", ModuleCode.NETWORK, ActionType.DELETE,
com.cisd.tms.modules.device.dto.network.RouteDeleteRequest.class);
assertAudit(IpWhitelistController.class, "addWhitelist", ModuleCode.DEVICE, ActionType.CREATE,
com.cisd.tms.modules.device.dto.network.IpWhitelistRequest.class);
assertAudit(IpWhitelistController.class, "updateWhitelist", ModuleCode.DEVICE, ActionType.UPDATE,
com.cisd.tms.modules.device.dto.network.IpWhitelistRequest.class);
assertAudit(IpWhitelistController.class, "deleteWhitelist", ModuleCode.DEVICE, ActionType.DELETE, Long.class);
assertAudit(LmkController.class, "initMasterKey", ModuleCode.KEY, ActionType.CREATE);
assertAudit(LmkController.class, "destroyMasterKey", ModuleCode.KEY, ActionType.DELETE);
assertAudit(LmkController.class, "recoverLmkEx", ModuleCode.KEY, ActionType.RECOVER,
com.cisd.tms.modules.mk.dto.MasterKeyRecoverPacketsRequest.class);
assertAudit(LmkController.class, "Activate", ModuleCode.DEVICE, ActionType.ENABLE,
com.cisd.tms.modules.mk.dto.MasterKeyActivateRequest.class);
}
@Test
void shouldAuditFileUpgradeBackupLogAndOpenApiMutations() throws Exception {
assertAudit(com.cisd.tms.modules.file.controller.FileController.class, "upload", ModuleCode.SYSTEM, ActionType.IMPORT,
org.springframework.web.multipart.MultipartFile.class);
assertAudit(UpgradeController.class, "uploadPackage", ModuleCode.UPGRADE, ActionType.IMPORT,
org.springframework.web.multipart.MultipartFile.class);
assertAudit(UpgradeController.class, "create", ModuleCode.UPGRADE, ActionType.CREATE,
com.cisd.tms.modules.upgrade.dto.UpgradeCreateTaskRequest.class);
assertAudit(UpgradeController.class, "execute", ModuleCode.UPGRADE, ActionType.EXECUTE, String.class);
assertAudit(UpgradeController.class, "rollback", ModuleCode.UPGRADE, ActionType.ROLLBACK, String.class);
assertAudit(BackupController.class, "backup", ModuleCode.LOG, ActionType.BACKUP);
assertAudit(BackupController.class, "downloadBackupFile", ModuleCode.LOG, ActionType.EXPORT, String.class);
assertAudit(BackupConfigController.class, "saveOrUpdateConfig", ModuleCode.LOG, ActionType.UPDATE,
com.cisd.tms.modules.log.dto.BackupConfigRequest.class);
assertAudit(OperationAuditController.class, "reviewLog", ModuleCode.LOG, ActionType.UPDATE,
String.class, com.cisd.tms.modules.log.dto.AuditLogReviewRequest.class);
assertAudit(AuditLogRestoreController.class, "restoreFromZip", ModuleCode.LOG, ActionType.RECOVER,
org.springframework.web.multipart.MultipartFile.class);
assertAudit(OpenApiController.class, "rawSign", ModuleCode.KEY, ActionType.EXECUTE,
com.cisd.tms.modules.openapi.service.dto.RawSignRequest.class);
assertAudit(OpenApiController.class, "dettachedSign", ModuleCode.KEY, ActionType.EXECUTE,
com.cisd.tms.modules.openapi.service.dto.DettachedSignRequest.class);
assertAudit(OpenApiController.class, "uploadCert", ModuleCode.KEY, ActionType.IMPORT,
com.cisd.tms.modules.openapi.service.dto.UploadCertRequest.class);
}
@Test
void shouldNotAuditReadOnlyPreviewAndVerifyEndpoints() throws Exception {
assertNoAudit(UpgradeController.class, "preview", com.cisd.tms.modules.upgrade.dto.UpgradePreviewRequest.class);
assertNoAudit(OperationAuditController.class, "queryPage",
com.cisd.tms.modules.log.dto.OperationAuditLogPageRequest.class);
assertNoAudit(OpenApiController.class, "rawVerify", com.cisd.tms.modules.openapi.service.dto.RawVerifyRequest.class);
assertNoAudit(OpenApiController.class, "dettachedVerify",
com.cisd.tms.modules.openapi.service.dto.DettachedVerifyRequest.class);
assertNoAudit(OpenApiController.class, "dettachedVerifySimple",
com.cisd.tms.modules.openapi.service.dto.DettachedVerifyRequest.class);
}
private static void assertAudit(
Class<?> controller,
String methodName,
ModuleCode moduleCode,
ActionType actionType,
Class<?>... parameterTypes
) throws Exception {
AuditedOperation annotation = auditedOperation(controller, methodName, parameterTypes);
Assertions.assertNotNull(annotation, controller.getSimpleName() + "#" + methodName);
Assertions.assertEquals(moduleCode, annotation.module(), controller.getSimpleName() + "#" + methodName);
Assertions.assertEquals(actionType, annotation.action(), controller.getSimpleName() + "#" + methodName);
}
private static void assertNoAudit(Class<?> controller, String methodName, Class<?>... parameterTypes) throws Exception {
Assertions.assertNull(auditedOperation(controller, methodName, parameterTypes),
controller.getSimpleName() + "#" + methodName);
}
private static AuditedOperation auditedOperation(Class<?> controller, String methodName, Class<?>... parameterTypes)
throws Exception {
Method method = controller.getMethod(methodName, parameterTypes);
AuditedOperation methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, AuditedOperation.class);
if (methodAnnotation != null) {
return methodAnnotation;
}
return AnnotatedElementUtils.findMergedAnnotation(controller, AuditedOperation.class);
}
}
@@ -0,0 +1,23 @@
package com.cisd.tms.modules.log.repository;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
class OperationAuditSchemaContractTest {
@Test
void shouldAllowAnonymousBootstrapAuditLogRole() throws Exception {
String fullSql = readClasspathResource("db/migration/V2__tms_schema_full.sql");
Assertions.assertTrue(fullSql.contains("operator_role_code VARCHAR(64) NULL"));
Assertions.assertFalse(fullSql.contains("operator_role_code VARCHAR(64) NOT NULL"));
}
private static String readClasspathResource(String path) throws Exception {
ClassPathResource resource = new ClassPathResource(path);
Assertions.assertTrue(resource.exists(), "Missing classpath resource: " + path);
return new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
}
}
@@ -0,0 +1,34 @@
package com.cisd.tms.schema;
import java.nio.charset.StandardCharsets;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.core.io.ClassPathResource;
class FullSchemaVersionContractTest {
@Test
void shouldRetainV1MigrationAndExposeCurrentV2FullSchema() throws Exception {
String v1Sql = readClasspathResource("db/migration/V1__tms_schema_full.sql");
String v2Sql = readClasspathResource("db/migration/V2__tms_schema_full.sql");
Assertions.assertTrue(v1Sql.contains("ALTER TABLE tms_auth_full_account"));
Assertions.assertTrue(v1Sql.contains("AFTER password_hash"));
Assertions.assertFalse(v1Sql.contains("CREATE TABLE IF NOT EXISTS tms_auth_full_account"));
Assertions.assertTrue(v2Sql.contains("CREATE TABLE IF NOT EXISTS tms_operation_audit_log"));
}
@Test
void currentV2FullSchemaShouldNotDependOnPatchStatements() throws Exception {
String v2Sql = readClasspathResource("db/migration/V2__tms_schema_full.sql");
Assertions.assertFalse(v2Sql.contains("ALTER TABLE"));
Assertions.assertFalse(v2Sql.lines().anyMatch(line -> line.stripLeading().startsWith("UPDATE ")));
}
private static String readClasspathResource(String path) throws Exception {
ClassPathResource resource = new ClassPathResource(path);
Assertions.assertTrue(resource.exists(), "Missing classpath resource: " + path);
return new String(resource.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
}
}
@@ -0,0 +1,48 @@
package com.cisd.tms.security.internal;
import com.cisd.tms.modules.auth.enums.AuthLevel;
import com.cisd.tms.modules.auth.enums.RoleCode;
import com.cisd.tms.modules.auth.security.RequireInternalAuth;
import com.cisd.tms.modules.device.controller.NetworkConfigController;
import com.cisd.tms.modules.device.controller.TimeConfigController;
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.core.annotation.AnnotatedElementUtils;
class InternalBootstrapAuthContractTest {
@Test
void shouldRequireOpsFullAfterBootstrapForNetworkAndTimeConfig() throws Exception {
RequireInternalAuth networkAuth =
AnnotatedElementUtils.findMergedAnnotation(NetworkConfigController.class, RequireInternalAuth.class);
RequireInternalAuth timeAuth = AnnotatedElementUtils.findMergedAnnotation(
TimeConfigController.class.getMethod("configureTime",
com.cisd.tms.modules.device.dto.TimeConfigRequest.class),
RequireInternalAuth.class
);
assertBootstrapOpsFull(networkAuth);
assertBootstrapOpsFull(timeAuth);
}
@Test
void shouldNotStaticallyExcludeBootstrapConfigEndpointsFromInternalAuth() throws Exception {
String source = Files.readString(
Path.of("src/main/java/com/cisd/tms/common/config/WebMvcConfig.java"),
StandardCharsets.UTF_8
);
Assertions.assertFalse(source.contains("\"/api/v1/device/time-config\""));
Assertions.assertFalse(source.contains("\"/api/v1/device/network/**\""));
}
private static void assertBootstrapOpsFull(RequireInternalAuth auth) {
Assertions.assertNotNull(auth);
Assertions.assertEquals(RoleCode.OPS_ADMIN, auth.role());
Assertions.assertEquals(AuthLevel.FULL, auth.authLevel());
Assertions.assertTrue(auth.allowBeforeMasterKeyInitialized());
}
}