Compare commits
13
Commits
9846ff13f7
...
3646572581
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
3646572581 | ||
|
|
1b2825c52c | ||
|
|
6ad2f46ad9 | ||
|
|
f6f220be80 | ||
|
|
f06c675f9b | ||
|
|
f1fa2dedd9 | ||
|
|
195f343608 | ||
|
|
cae94149cb | ||
|
|
69a740adcf | ||
|
|
563b625456 | ||
|
|
6823cc4d3e | ||
|
|
02328d1760 | ||
|
|
0f1c24caf9 |
@@ -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.
|
||||||
|
|||||||
@@ -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`
|
||||||
|
|||||||
@@ -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席位要求");
|
||||||
|
|||||||
+7
-7
@@ -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;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 当发卡流程需要同时下发主密钥备份信息时,统一通过备份包承载。
|
* 当发卡流程需要同时下发主密钥备份信息时,统一通过备份包承载。
|
||||||
*/
|
*/
|
||||||
|
|||||||
+1
-1
@@ -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 {
|
||||||
|
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
+23
@@ -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());
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user