diff --git a/.flattened-pom.xml b/.flattened-pom.xml
index 740f107..0234506 100644
--- a/.flattened-pom.xml
+++ b/.flattened-pom.xml
@@ -28,7 +28,7 @@
3.0.3
0.9.1
5.4.1
- 2.6.0
+ 2.8.15
UTF-8
3.18.0
2.0.57
diff --git a/doc/open-api-interface.md b/doc/open-api-interface.md
new file mode 100644
index 0000000..fc17369
--- /dev/null
+++ b/doc/open-api-interface.md
@@ -0,0 +1,238 @@
+# LLM Guard OpenAPI 对外接口文档
+
+## 1. 文档说明
+- 文档版本:v1.0
+- 适用服务:`llm-guard-open-api`
+- 基础路径:`/openapi`
+- 字符集:`UTF-8`
+- 请求/响应格式:`application/json`
+
+## 1.1 测试环境信息
+- 接口地址:`http://49.233.48.5:6201/open-api/openapi/guard/check`
+- `X-Api-Key`:`ak_3ebc8929866e41689ab373c2c1a383b6`
+- `X-Api-Secret`:`sk_6fde3569aaa44b29b226a7264623778e0a95e22677b746969f97f193ac4ae581`
+
+## 2. 认证方式
+### 2.1 Header 鉴权
+每次请求必须携带以下请求头:
+- `X-Api-Key`
+- `X-Api-Secret`
+
+### 2.2 凭证发放与生命周期
+- 凭证来源:`system` 用户管理。
+- 用户新增时:若凭证为空,系统自动生成 `apiKey/apiSecret`。
+- 用户编辑时:默认保持原值不变;仅当凭证字段被设置为空时,重新生成。
+- 服务启动时:会对缺失凭证的用户自动初始化。
+
+### 2.3 认证失败返回
+- HTTP 状态:`200`
+- 业务码:`code=401`
+- 示例:
+```json
+{
+ "code": 401,
+ "msg": "apiSecret 错误"
+}
+```
+
+## 3. 检测接口
+### 3.1 接口定义
+- 方法:`POST`
+- 路径:`/openapi/guard/check`
+- 说明:统一执行 ACL、ATTACK、CONTENT 三类规则检测;命中时自动落库告警日志。
+- 兼容性:顶层固定为 `reqData`,`reqData` 内支持原始网关请求体原样提交,不强制重组字段。
+
+### 3.2 请求体
+```json
+{
+ "sourceIp": "10.0.0.1",
+ "path": "/api/llm/chat/completion/V2",
+ "method": "POST",
+ "interfaceType": "LLM",
+ "callerId": "partner-a",
+ "reqData": {
+ "messages": [
+ {
+ "role": "user",
+ "content": "please union select password and email test_user@demo.com"
+ }
+ ],
+ "model": "通义千问2.5-72B",
+ "stream": false
+ }
+}
+```
+
+### 3.3 请求字段说明
+说明:
+- 顶层固定字段:`sourceIp`、`path`、`method`、`interfaceType`、`callerId`、`reqData`。
+- `reqData` 内部可直接放三类网关原始入参,不强制重组。
+- 系统会优先使用顶层字段做 ACL 与审计,`reqData` 用于内容检测与原文追溯。
+
+| 字段 | 类型 | 必填 | 说明 |
+|---|---|---|---|
+| sourceIp | string | 是 | 来源 IP(ACL IP 规则依赖) |
+| path | string | 是 | 请求路径(ACL 接口规则依赖) |
+| method | string | 是 | 请求方法(ACL 接口规则依赖) |
+| interfaceType | string | 是 | 接口类型:`AGENT` / `LLM` / `VLM`(分流) |
+| callerId | string | 是 | 调用方标识(调用方追踪) |
+| reqData | object | 是 | 业务原始请求体 |
+| reqData.requestId | string | 否 | 请求唯一标识;为空时系统自动生成 |
+| reqData.traceId | string | 否 | 链路追踪标识;为空时系统自动生成 |
+| reqData.scopeCode | string | 否 | 规则作用域;默认 `GLOBAL` |
+| reqData.stream | boolean | 否 | 是否流式;默认 `false` |
+| reqData.text/messages/files/metadata/payload/其他任意字段 | mixed | 否 | 原始业务参数,统一纳入规则匹配语料 |
+
+## 4. 响应体
+### 4.1 成功响应结构
+```json
+{
+ "code": 200,
+ "msg": "来源IP不在白名单;命中攻击特征;命中内容DLP规则",
+ "data": {
+ "requestId": "req-001",
+ "traceId": "trace-001",
+ "decision": "BLOCK",
+ "alerted": true,
+ "hits": [
+ {
+ "moduleType": "ACL",
+ "eventType": "ACL_IP_WHITELIST",
+ "ruleId": null,
+ "ruleCode": null,
+ "action": "BLOCK",
+ "message": "来源IP不在白名单"
+ },
+ {
+ "moduleType": "ATTACK",
+ "eventType": "ATTACK_SIGNATURE",
+ "ruleId": "sig_multi_1",
+ "ruleCode": "ATTACK_SQL_MULTI",
+ "action": "ALERT",
+ "message": "命中攻击特征"
+ },
+ {
+ "moduleType": "CONTENT",
+ "eventType": "CONTENT_DLP",
+ "ruleId": "dlp_multi_1",
+ "ruleCode": "DLP_EMAIL_MULTI",
+ "action": "ALERT",
+ "message": "命中内容DLP规则"
+ }
+ ]
+ }
+}
+```
+
+### 4.2 响应字段说明
+| 字段 | 类型 | 说明 |
+|---|---|---|
+| code | number | 业务码;`200` 成功,`401` 认证失败 |
+| msg | string | 命中说明汇总(`hits.message` 去重后以 `;` 拼接);未命中时为“未命中规则,允许通过” |
+| data.requestId | string | 请求 ID |
+| data.traceId | string | 链路 ID |
+| data.decision | string | 最终决策:`ALLOW` / `BLOCK` |
+| data.alerted | boolean | 是否命中任意规则 |
+| data.hits | array | 命中明细列表 |
+| data.hits[].moduleType | string | 模块:`ACL` / `ATTACK` / `CONTENT` |
+| data.hits[].eventType | string | 事件类型 |
+| data.hits[].ruleId | string | 命中的规则/特征 ID |
+| data.hits[].ruleCode | string | 命中的规则编码 |
+| data.hits[].action | string | 命中动作:`ALLOW` / `BLOCK` / `ALERT` / `MASK` / `REPLACE` |
+| data.hits[].message | string | 命中说明 |
+
+### 4.3 判定规则(调用方重点)
+- 是否允许放行:看 `data.decision`
+ - `ALLOW`:允许通过
+ - `BLOCK`:不允许通过
+- 是否触发告警:看 `data.alerted`
+ - `true`:已触发告警并写日志
+ - `false`:未触发告警
+- 命中明细:看 `data.hits[]`
+ - 可用于前端展示、审计留痕、二次策略路由。
+
+## 5. 检测范围与执行逻辑
+### 5.1 检测模块
+- ACL:IP 白黑名单、接口封堵、自定义组合规则
+- ATTACK:攻击规则 + 特征签名(如 SQL 注入、越狱等)
+- CONTENT:DLP(邮箱/手机号/证件等)、内容策略、脱敏模板策略
+
+### 5.2 语料拼接范围
+系统会将下列字段聚合后参与规则匹配:
+- `text`
+- `path`
+- `method`
+- `callerId`
+- `messages`(递归提取)
+- `files`(递归提取)
+- `metadata`(递归提取)
+- `payload`(递归提取)
+
+## 6. 告警与审计
+命中规则时,系统自动写入:
+- 事件表:`d_log_alert_event`
+- 命中明细表:`d_log_alert_hit`
+
+写入内容包含请求标识、模块类型、事件类型、动作、命中说明、来源 IP、调用方等字段,可直接用于审计与报表。
+
+## 7. 三类网关报文适配建议
+### 7.1 智能体接口(AGENT)
+- 建议映射:`text`、`files`、`metadata`。
+- 原始结构可同时放入 `payload` 便于追溯。
+
+### 7.2 语义模型接口(LLM)
+- 建议映射:`messages`。
+- 补充模型请求参数到 `metadata/payload`。
+
+### 7.3 多模态接口(VLM)
+- 建议映射:`messages`(含文本/图片等多模态内容)。
+- 文件或大对象建议放 `files` 并保留 `payload` 原文。
+
+## 8. 调用示例(cURL)
+```bash
+curl -sS "http://ip:6201/open-api/openapi/guard/check" \
+ -H "Content-Type: application/json" \
+ -H "X-Api-Key: ak_3ebc8929866e41689ab373c2c1a383b6" \
+ -H "X-Api-Secret: sk_6fde3569aaa44b29b226a7264623778e0a95e22677b746969f97f193ac4ae581" \
+ -d '{
+ "sourceIp":"10.0.0.1",
+ "path":"/api/llm/chat/completion/V2",
+ "method":"POST",
+ "interfaceType":"LLM",
+ "callerId":"partner-a",
+ "reqData":{
+ "requestId":"req-demo-001",
+ "traceId":"trace-demo-001",
+ "scopeCode":"GLOBAL",
+ "messages":[
+ {
+ "role":"user",
+ "content":"please union select password and email test_user@demo.com"
+ }
+ ]
+ }
+ }'
+```
+
+## 9. 对接注意事项
+- 顶层必填字段:`sourceIp`、`path`、`method`、`interfaceType`、`callerId`、`reqData`。缺失会返回 `code=400`。
+- 建议调用方保证 `requestId` 全局唯一,方便对账和审计检索。
+- 建议统一使用 `scopeCode=GLOBAL`(或双方约定的租户/场景编码)。
+- 当 `decision=BLOCK` 时,调用方应立即拦截业务请求,不再透传下游。
+- 当 `decision=ALLOW` 且 `alerted=true` 时,建议记录风控告警但允许继续处理。
+
+## 10. 建议额外字段
+建议这些字段放在 `reqData` 内(如无对应值可不传):
+
+- 强烈建议:
+- `sourceIp`:用于 ACL 白名单/黑名单判断。
+- `path`:用于接口封堵规则判断。
+- `method`:与 `path` 组合判断 ACL 接口规则。
+- `interfaceType`:标识 `AGENT/LLM/VLM`,便于策略分层与审计。
+- `callerId`:用于区分调用方、追踪告警来源。
+
+- 可选增强:
+- `tenantId` / `appId` / `bizCode`:多租户或多应用隔离策略。
+- `reqTimestamp`(毫秒时间戳):用于请求时序核对。
+- `clientVersion`:便于回溯某版本客户端行为。
+- `channel`(web/app/openapi 等):便于运营统计与分流。
diff --git a/doc/sql/20260303_open_api.sql b/doc/sql/20260303_open_api.sql
new file mode 100644
index 0000000..afc948a
--- /dev/null
+++ b/doc/sql/20260303_open_api.sql
@@ -0,0 +1,6 @@
+-- open-api 对外鉴权字段
+ALTER TABLE d_sys_user ADD COLUMN IF NOT EXISTS api_key VARCHAR(128) DEFAULT '';
+ALTER TABLE d_sys_user ADD COLUMN IF NOT EXISTS api_secret VARCHAR(256) DEFAULT '';
+
+-- 为 api_key 建唯一索引(如果已有请跳过)
+CREATE UNIQUE INDEX IF NOT EXISTS uk_d_sys_user_api_key ON d_sys_user(api_key);
diff --git a/doc/sql/llm_20260208_pg.sql b/doc/sql/llm_20260208_pg.sql
index 4facda3..6c60132 100644
--- a/doc/sql/llm_20260208_pg.sql
+++ b/doc/sql/llm_20260208_pg.sql
@@ -54,6 +54,8 @@ create table d_sys_user (
sex char(1) default '0',
avatar varchar(100) default '',
password varchar(100) default '',
+ api_key varchar(128) default '',
+ api_secret varchar(256) default '',
status char(1) default '0',
del_flag char(1) default '0',
login_ip varchar(128) default '',
@@ -66,12 +68,13 @@ create table d_sys_user (
remark varchar(500) default null,
primary key (user_id)
);
+CREATE UNIQUE INDEX uk_d_sys_user_api_key ON d_sys_user (api_key);
-- ----------------------------
-- 初始化-用户信息表数据
-- ----------------------------
-insert into d_sys_user values('838fef38-55e6-4938-b995-b0814081e93c', 'a4209f68-23e3-45e4-88f4-2e2ceabf0851', 'admin', 'lm', '00', 'lm@163.com', '15888888888', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '', null, '管理员');
-insert into d_sys_user values('568c27fd-4dcd-4871-8f43-56fc2ccf1b60', '6c442480-97e8-42b7-a509-08b458e44dcd', 'lm', 'lm', '00', 'lm@qq.com', '15666666666', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', '0', '0', '127.0.0.1', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '', null, '测试员');
+insert into d_sys_user values('838fef38-55e6-4938-b995-b0814081e93c', 'a4209f68-23e3-45e4-88f4-2e2ceabf0851', 'admin', 'lm', '00', 'lm@163.com', '15888888888', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', 'ak_admin_demo', 'sk_admin_demo', '0', '0', '127.0.0.1', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '', null, '管理员');
+insert into d_sys_user values('568c27fd-4dcd-4871-8f43-56fc2ccf1b60', '6c442480-97e8-42b7-a509-08b458e44dcd', 'lm', 'lm', '00', 'lm@qq.com', '15666666666', '1', '', '$2a$10$7JB720yubVSZvUI0rEqK/.VqGOZTH.ulu33dHOiBE8ByOhJIrdAu2', 'ak_lm_demo', 'sk_lm_demo', '0', '0', '127.0.0.1', CURRENT_TIMESTAMP, CURRENT_TIMESTAMP, 'admin', CURRENT_TIMESTAMP, '', null, '测试员');
-- ----------------------------
diff --git a/llm-guard-api/llm-guard-api-system/src/main/java/com/llm/guard/system/api/domain/SysUser.java b/llm-guard-api/llm-guard-api-system/src/main/java/com/llm/guard/system/api/domain/SysUser.java
index 6e886d8..3d73eb8 100644
--- a/llm-guard-api/llm-guard-api-system/src/main/java/com/llm/guard/system/api/domain/SysUser.java
+++ b/llm-guard-api/llm-guard-api-system/src/main/java/com/llm/guard/system/api/domain/SysUser.java
@@ -57,6 +57,12 @@ public class SysUser extends BaseEntity
/** 密码 */
private String password;
+ /** API Key(对外接口调用) */
+ private String apiKey;
+
+ /** API Secret(对外接口调用) */
+ private String apiSecret;
+
/** 账号状态(0正常 1停用) */
@Excel(name = "账号状态", readConverterExp = "0=正常,1=停用")
private String status;
@@ -222,6 +228,26 @@ public class SysUser extends BaseEntity
this.status = status;
}
+ public String getApiKey()
+ {
+ return apiKey;
+ }
+
+ public void setApiKey(String apiKey)
+ {
+ this.apiKey = apiKey;
+ }
+
+ public String getApiSecret()
+ {
+ return apiSecret;
+ }
+
+ public void setApiSecret(String apiSecret)
+ {
+ this.apiSecret = apiSecret;
+ }
+
public String getDelFlag()
{
return delFlag;
@@ -324,6 +350,8 @@ public class SysUser extends BaseEntity
.append("sex", getSex())
.append("avatar", getAvatar())
.append("password", getPassword())
+ .append("apiKey", getApiKey())
+ .append("apiSecret", getApiSecret())
.append("status", getStatus())
.append("delFlag", getDelFlag())
.append("loginIp", getLoginIp())
diff --git a/llm-guard-auth/.flattened-pom.xml b/llm-guard-auth/.flattened-pom.xml
index d738222..5145aaa 100644
--- a/llm-guard-auth/.flattened-pom.xml
+++ b/llm-guard-auth/.flattened-pom.xml
@@ -40,13 +40,32 @@
${project.artifactId}
- org.springframework.boot
- spring-boot-maven-plugin
+ maven-jar-plugin
+ 3.3.0
+
+
+
+ com.llm.guard.auth.LlmAuthApplication
+ true
+ lib/
+
+
+
+
+
+ maven-dependency-plugin
+ 3.6.1
+ copy-dependencies
+ package
- repackage
+ copy-dependencies
+
+ ${project.build.directory}/lib
+ false
+
diff --git a/llm-guard-auth/src/main/resources/bootstrap.yml b/llm-guard-auth/src/main/resources/bootstrap.yml
index 006c91f..ce6b709 100644
--- a/llm-guard-auth/src/main/resources/bootstrap.yml
+++ b/llm-guard-auth/src/main/resources/bootstrap.yml
@@ -1,6 +1,6 @@
# Tomcat
server:
- port: 6200
+ port: 6204
# Spring
spring:
diff --git a/llm-guard-gateway/.flattened-pom.xml b/llm-guard-gateway/.flattened-pom.xml
index 2a3b530..4929488 100644
--- a/llm-guard-gateway/.flattened-pom.xml
+++ b/llm-guard-gateway/.flattened-pom.xml
@@ -77,13 +77,32 @@
${project.artifactId}
- org.springframework.boot
- spring-boot-maven-plugin
+ maven-jar-plugin
+ 3.3.0
+
+
+
+ com.llm.guard.gateway.LlmGatewayApplication
+ true
+ lib/
+
+
+
+
+
+ maven-dependency-plugin
+ 3.6.1
+ copy-dependencies
+ package
- repackage
+ copy-dependencies
+
+ ${project.build.directory}/lib
+ false
+
diff --git a/llm-guard-modules/.flattened-pom.xml b/llm-guard-modules/.flattened-pom.xml
index a085528..6c51336 100644
--- a/llm-guard-modules/.flattened-pom.xml
+++ b/llm-guard-modules/.flattened-pom.xml
@@ -14,5 +14,6 @@
llm-guard-system
llm-guard-biz
+ llm-guard-open-api
diff --git a/llm-guard-modules/llm-guard-biz/.flattened-pom.xml b/llm-guard-modules/llm-guard-biz/.flattened-pom.xml
index 819ce3f..61f74da 100644
--- a/llm-guard-modules/llm-guard-biz/.flattened-pom.xml
+++ b/llm-guard-modules/llm-guard-biz/.flattened-pom.xml
@@ -86,13 +86,32 @@
${project.artifactId}
- org.springframework.boot
- spring-boot-maven-plugin
+ maven-jar-plugin
+ 3.3.0
+
+
+
+ com.llm.guard.biz.LlmBizApplication
+ true
+ lib/
+
+
+
+
+
+ maven-dependency-plugin
+ 3.6.1
+ copy-dependencies
+ package
- repackage
+ copy-dependencies
+
+ ${project.build.directory}/lib
+ false
+
diff --git a/llm-guard-modules/llm-guard-open-api/.flattened-pom.xml b/llm-guard-modules/llm-guard-open-api/.flattened-pom.xml
new file mode 100644
index 0000000..789caca
--- /dev/null
+++ b/llm-guard-modules/llm-guard-open-api/.flattened-pom.xml
@@ -0,0 +1,104 @@
+
+
+ 4.0.0
+
+ com.llm.guard
+ llm-guard-modules
+ 1.0.0-SNAPSHOT
+
+ llm-guard-open-api
+ 1.0.0-SNAPSHOT
+ 对外开放接口模块
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-discovery
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-config
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-sentinel
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+ com.llm.guard
+ llm-guard-common-datasource
+
+
+ com.llm.guard
+ llm-guard-common-log
+
+
+ com.llm.guard
+ llm-guard-common-swagger
+
+
+ com.llm.guard
+ llm-guard-common-security
+
+
+ com.llm.guard
+ llm-guard-common-mybatisplus
+
+
+ com.googlecode.aviator
+ aviator
+
+
+ org.postgresql
+ postgresql
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+ com.h2database
+ h2
+ test
+
+
+
+ ${project.artifactId}
+
+
+ maven-jar-plugin
+ 3.3.0
+
+
+
+ com.llm.guard.openapi.LlmOpenApiApplication
+ true
+ lib/
+
+
+
+
+
+ maven-dependency-plugin
+ 3.6.1
+
+
+ copy-dependencies
+ package
+
+ copy-dependencies
+
+
+ ${project.build.directory}/lib
+ false
+
+
+
+
+
+
+
diff --git a/llm-guard-modules/llm-guard-open-api/pom.xml b/llm-guard-modules/llm-guard-open-api/pom.xml
new file mode 100644
index 0000000..0ef14b2
--- /dev/null
+++ b/llm-guard-modules/llm-guard-open-api/pom.xml
@@ -0,0 +1,125 @@
+
+
+
+ com.llm.guard
+ llm-guard-modules
+ ${revision}
+
+ 4.0.0
+
+ llm-guard-open-api
+
+
+ 对外开放接口模块
+
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-discovery
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-nacos-config
+
+
+
+ com.alibaba.cloud
+ spring-cloud-starter-alibaba-sentinel
+
+
+
+ org.springframework.boot
+ spring-boot-starter-actuator
+
+
+
+ com.llm.guard
+ llm-guard-common-datasource
+
+
+
+ com.llm.guard
+ llm-guard-common-log
+
+
+
+ com.llm.guard
+ llm-guard-common-swagger
+
+
+
+ com.llm.guard
+ llm-guard-common-security
+
+
+
+ com.llm.guard
+ llm-guard-common-mybatisplus
+
+
+
+ com.googlecode.aviator
+ aviator
+
+
+
+ org.postgresql
+ postgresql
+
+
+
+ org.springframework.boot
+ spring-boot-starter-test
+ test
+
+
+
+ com.h2database
+ h2
+ test
+
+
+
+
+ ${project.artifactId}
+
+
+ org.apache.maven.plugins
+ maven-jar-plugin
+ 3.3.0
+
+
+
+ com.llm.guard.openapi.LlmOpenApiApplication
+ true
+ lib/
+
+
+
+
+
+
+ org.apache.maven.plugins
+ maven-dependency-plugin
+ 3.6.1
+
+
+ copy-dependencies
+ package
+
+ copy-dependencies
+
+
+ ${project.build.directory}/lib
+ false
+
+
+
+
+
+
+
diff --git a/llm-guard-modules/llm-guard-open-api/src/main/java/com/llm/guard/openapi/LlmOpenApiApplication.java b/llm-guard-modules/llm-guard-open-api/src/main/java/com/llm/guard/openapi/LlmOpenApiApplication.java
new file mode 100644
index 0000000..80aaf6d
--- /dev/null
+++ b/llm-guard-modules/llm-guard-open-api/src/main/java/com/llm/guard/openapi/LlmOpenApiApplication.java
@@ -0,0 +1,16 @@
+package com.llm.guard.openapi;
+
+import com.llm.guard.common.security.annotation.EnableCustomConfig;
+import com.llm.guard.common.security.annotation.EnableLmFeignClients;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.autoconfigure.SpringBootApplication;
+
+@EnableCustomConfig
+@EnableLmFeignClients
+@SpringBootApplication
+public class LlmOpenApiApplication {
+
+ public static void main(String[] args) {
+ SpringApplication.run(LlmOpenApiApplication.class, args);
+ }
+}
diff --git a/llm-guard-modules/llm-guard-open-api/src/main/java/com/llm/guard/openapi/controller/OpenApiGuardController.java b/llm-guard-modules/llm-guard-open-api/src/main/java/com/llm/guard/openapi/controller/OpenApiGuardController.java
new file mode 100644
index 0000000..1627950
--- /dev/null
+++ b/llm-guard-modules/llm-guard-open-api/src/main/java/com/llm/guard/openapi/controller/OpenApiGuardController.java
@@ -0,0 +1,98 @@
+package com.llm.guard.openapi.controller;
+
+import com.llm.guard.common.core.web.domain.AjaxResult;
+import com.llm.guard.openapi.domain.OpenApiGuardCheckRequest;
+import com.llm.guard.openapi.domain.OpenApiGuardRequest;
+import com.llm.guard.openapi.domain.OpenApiGuardResponse;
+import com.llm.guard.openapi.service.OpenApiGuardService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.AllArgsConstructor;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestHeader;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.util.LinkedHashMap;
+import java.util.LinkedHashSet;
+import java.util.Map;
+import java.util.Set;
+
+@RestController
+@RequestMapping("/openapi/guard")
+@Tag(name = "开放接口-防护检测")
+@AllArgsConstructor
+public class OpenApiGuardController {
+
+ private final OpenApiGuardService openApiGuardService;
+
+ @Operation(summary = "统一防护检测接口(ACL/ATTACK/CONTENT)")
+ @PostMapping("/check")
+ public AjaxResult check(
+ @RequestHeader("X-Api-Key") String apiKey,
+ @RequestHeader("X-Api-Secret") String apiSecret,
+ @RequestBody OpenApiGuardCheckRequest request
+ ) {
+ OpenApiGuardService.CallerAuthResult authResult = openApiGuardService.authenticate(apiKey, apiSecret);
+ if (!authResult.isSuccess()) {
+ return AjaxResult.error(401, authResult.getMessage());
+ }
+
+ Object reqData = request == null ? null : request.getReqData();
+ if (reqData == null) {
+ return AjaxResult.error(400, "reqData 不能为空");
+ }
+
+ OpenApiGuardRequest guardRequest = buildGuardRequest(request);
+ OpenApiGuardResponse response;
+ try {
+ response = openApiGuardService.check(guardRequest, authResult.getUserName());
+ } catch (IllegalArgumentException e) {
+ return AjaxResult.error(400, e.getMessage());
+ }
+ return AjaxResult.success(buildMessage(response), response);
+ }
+
+ private OpenApiGuardRequest buildGuardRequest(OpenApiGuardCheckRequest request)
+ {
+ OpenApiGuardRequest guardRequest = new OpenApiGuardRequest();
+ guardRequest.setSourceIp(request.getSourceIp());
+ guardRequest.setPath(request.getPath());
+ guardRequest.setMethod(request.getMethod());
+ guardRequest.setInterfaceType(request.getInterfaceType());
+ guardRequest.setCallerId(request.getCallerId());
+ guardRequest.setPayload(request.getReqData());
+
+ if (request.getReqData() instanceof Map, ?> map)
+ {
+ Map extensions = new LinkedHashMap<>();
+ for (Map.Entry, ?> entry : map.entrySet())
+ {
+ extensions.put(String.valueOf(entry.getKey()), entry.getValue());
+ }
+ guardRequest.setExtensions(extensions);
+ }
+ return guardRequest;
+ }
+
+ private String buildMessage(OpenApiGuardResponse response)
+ {
+ if (Boolean.FALSE.equals(response.getAlerted()) || response.getHits() == null || response.getHits().isEmpty())
+ {
+ return "未命中规则,允许通过";
+ }
+ Set messages = new LinkedHashSet<>();
+ response.getHits().forEach(hit -> {
+ if (hit.getMessage() != null && !hit.getMessage().isBlank())
+ {
+ messages.add(hit.getMessage());
+ }
+ });
+ if (messages.isEmpty())
+ {
+ return "命中规则";
+ }
+ return String.join(";", messages);
+ }
+}
diff --git a/llm-guard-modules/llm-guard-open-api/src/main/java/com/llm/guard/openapi/domain/OpenApiGuardCheckRequest.java b/llm-guard-modules/llm-guard-open-api/src/main/java/com/llm/guard/openapi/domain/OpenApiGuardCheckRequest.java
new file mode 100644
index 0000000..f74c5e2
--- /dev/null
+++ b/llm-guard-modules/llm-guard-open-api/src/main/java/com/llm/guard/openapi/domain/OpenApiGuardCheckRequest.java
@@ -0,0 +1,27 @@
+package com.llm.guard.openapi.domain;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+@Data
+@Schema(description = "开放防护检测请求包装")
+public class OpenApiGuardCheckRequest {
+
+ @Schema(description = "来源IP(ACL IP 规则依赖)", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String sourceIp;
+
+ @Schema(description = "请求路径(ACL 接口规则依赖)", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String path;
+
+ @Schema(description = "请求方法(ACL 接口规则依赖)", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String method;
+
+ @Schema(description = "接口类型(AGENT/LLM/VLM 分流)", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String interfaceType;
+
+ @Schema(description = "调用方标识(调用方追踪)", requiredMode = Schema.RequiredMode.REQUIRED)
+ private String callerId;
+
+ @Schema(description = "业务请求体(固定字段,透传网关原始入参)", requiredMode = Schema.RequiredMode.REQUIRED)
+ private Object reqData;
+}
diff --git a/llm-guard-modules/llm-guard-open-api/src/main/java/com/llm/guard/openapi/domain/OpenApiGuardRequest.java b/llm-guard-modules/llm-guard-open-api/src/main/java/com/llm/guard/openapi/domain/OpenApiGuardRequest.java
new file mode 100644
index 0000000..40e7976
--- /dev/null
+++ b/llm-guard-modules/llm-guard-open-api/src/main/java/com/llm/guard/openapi/domain/OpenApiGuardRequest.java
@@ -0,0 +1,64 @@
+package com.llm.guard.openapi.domain;
+
+import com.fasterxml.jackson.annotation.JsonAnySetter;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+
+@Data
+@Schema(description = "开放防护检测请求")
+public class OpenApiGuardRequest {
+
+ @Schema(description = "请求ID,调用方可自定义")
+ private String requestId;
+
+ @Schema(description = "链路追踪ID")
+ private String traceId;
+
+ @Schema(description = "作用域编码,对应规则scopeCode")
+ private String scopeCode;
+
+ @Schema(description = "接口类型: AGENT/LLM/VLM")
+ private String interfaceType;
+
+ @Schema(description = "请求路径")
+ private String path;
+
+ @Schema(description = "请求方法")
+ private String method;
+
+ @Schema(description = "来源IP")
+ private String sourceIp;
+
+ @Schema(description = "调用方标识")
+ private String callerId;
+
+ @Schema(description = "是否流式")
+ private Boolean stream;
+
+ @Schema(description = "智能体会话文本")
+ private String text;
+
+ @Schema(description = "消息体(兼容语义模型/多模态)")
+ private List