Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions shenyu-bootstrap/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -263,6 +263,14 @@
</dependency>
<!--Ai token limiter Plugin end-->

<!--Sensitive word Plugin Start-->
<dependency>
<groupId>org.apache.shenyu</groupId>
<artifactId>shenyu-spring-boot-starter-plugin-ai-sensitive-word</artifactId>
<version>${project.version}</version>
</dependency>
<!--Sensitive word Plugin end-->

<!--Ai request transformer Plugin Start-->
<dependency>
<groupId>org.apache.shenyu</groupId>
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,111 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.shenyu.common.dto.convert.rule;

/**
* The sensitive word rule handle, it tells the plugin where the dictionary lives and how long a
* loaded dictionary may be reused.
*/
public class SensitiveWordHandle {

/**
* The default redis key holding the sensitive word set.
*/
public static final String DEFAULT_REDIS_KEY = "shenyu:sensitive:words";

/**
* The redis key of the sensitive word set, every rule may point to its own dictionary.
*/
private String redisKey = DEFAULT_REDIS_KEY;

/**
* How long, in seconds, a loaded dictionary is reused before it is read from redis again.
* Zero or a negative value reads the dictionary on every request.
*/
private long refreshIntervalSeconds = 300L;

/**
* Whether the request must be rejected when the dictionary is unavailable. It defaults to
* false, which means such a request is passed through: a broken redis must not take the
* traffic down. Deployments with a hard compliance requirement can opt into blocking.
*/
private boolean failClosed;

/**
* get redis key.
*
* @return redis key
*/
public String getRedisKey() {
return redisKey;
}

/**
* set redis key.
*
* @param redisKey redis key
*/
public void setRedisKey(final String redisKey) {
this.redisKey = redisKey;
}

/**
* get refresh interval seconds.
*
* @return refresh interval in seconds
*/
public long getRefreshIntervalSeconds() {
return refreshIntervalSeconds;
}

/**
* set refresh interval seconds.
*
* @param refreshIntervalSeconds refresh interval in seconds
*/
public void setRefreshIntervalSeconds(final long refreshIntervalSeconds) {
this.refreshIntervalSeconds = refreshIntervalSeconds;
}

/**
* whether the request must be rejected when the dictionary is unavailable.
*
* @return true when the request must be rejected
*/
public boolean isFailClosed() {
return failClosed;
}

/**
* set whether the request must be rejected when the dictionary is unavailable.
*
* @param failClosed true to reject the request
*/
public void setFailClosed(final boolean failClosed) {
this.failClosed = failClosed;
}

/**
* new default instance.
*
* @return the default handle
*/
public static SensitiveWordHandle newDefaultInstance() {
return new SensitiveWordHandle();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -287,6 +287,11 @@ public enum PluginEnum {
*/
AI_TOKEN_LIMITER(171, 0, "aiTokenLimiter"),

/**
* Sensitive-word plugin enum.
*/
SENSITIVE_WORD(197, 0, "sensitiveWord"),

/**
* Mcp-server plugin enum.
*/
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
/*
* Licensed to the Apache Software Foundation (ASF) under one or more
* contributor license agreements. See the NOTICE file distributed with
* this work for additional information regarding copyright ownership.
* The ASF licenses this file to You under the Apache License, Version 2.0
* (the "License"); you may not use this file except in compliance with
* the License. You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/

package org.apache.shenyu.common.dto.convert.rule;

import org.junit.jupiter.api.Test;

import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertTrue;

/**
* Test cases for {@link SensitiveWordHandle}.
*/
public final class SensitiveWordHandleTest {

@Test
public void testDefaultInstance() {
SensitiveWordHandle handle = SensitiveWordHandle.newDefaultInstance();
assertEquals(SensitiveWordHandle.DEFAULT_REDIS_KEY, handle.getRedisKey());
assertEquals(300L, handle.getRefreshIntervalSeconds());
}

@Test
public void testFailClosed() {
SensitiveWordHandle handle = new SensitiveWordHandle();
assertFalse(handle.isFailClosed());
handle.setFailClosed(true);
assertTrue(handle.isFailClosed());
}

@Test
public void testSetter() {
SensitiveWordHandle handle = new SensitiveWordHandle();
handle.setRedisKey("custom:sensitive:words");
handle.setRefreshIntervalSeconds(30L);
assertEquals("custom:sensitive:words", handle.getRedisKey());
assertEquals(30L, handle.getRefreshIntervalSeconds());
}
}
1 change: 1 addition & 0 deletions shenyu-plugin/shenyu-plugin-ai/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@
<module>shenyu-plugin-ai-request-transformer</module>
<module>shenyu-plugin-ai-proxy</module>
<module>shenyu-plugin-ai-response-transformer</module>
<module>shenyu-plugin-ai-sensitive-word</module>
</modules>

</project>
Original file line number Diff line number Diff line change
@@ -0,0 +1,60 @@
# shenyu-plugin-ai-sensitive-word

The sensitive word plugin rejects a request whose body contains a word of the dictionary
configured for the matched rule. The dictionary lives in a **redis set**, so it can be maintained
by the operations team without redeploying shenyu.

It is aimed at the content compliance of an AI gateway, where a prompt must not reach the model
with forbidden content, but it only inspects the request body, so it also applies to plain HTTP
routes.

## How it works

- The dictionary is read with `SMEMBERS <rule.redisKey>` and compiled into an
[Aho-Corasick](../main/java/org/apache/shenyu/plugin/ai/sensitive/word/ac/AhoCorasick.java)
automaton, which reports **every** matching word, including nested and overlapping ones, for
example both `中国` and `中国银行` for the text `中国银行`.
- The body is read with the shared `ServerWebExchangeUtils#rewriteRequestBody`, the whole path is
reactive and the automaton is compiled on a bounded elastic thread, so the gateway event loop is
never blocked.
- A compiled dictionary is reused for `refreshIntervalSeconds` and then read from redis again, so
a dictionary update takes effect within that interval. Updating the rule in the admin console
drops the cached dictionary immediately.
- If redis is unreachable the request is **passed through** (fail open, a warning is logged): a
broken dictionary must not take the traffic down.

## Configuration

Plugin level (`config` of the plugin, the redis client used to read the dictionaries):

```json
{
"url": "127.0.0.1:6379",
"password": "",
"database": 0,
"mode": "standalone",
"maxIdle": 8,
"minIdle": 0,
"maxActive": 8
}
```

Rule level (`handle` of the rule):

| field | type | default | description |
| --- | --- | --- | --- |
| `redisKey` | string | `shenyu:sensitive:words` | the redis set holding the dictionary of this rule |
| `refreshIntervalSeconds` | long | `300` | how long a compiled dictionary is reused, `0` reads it on every request |

## Dictionary format

A redis set of words, one word per member, for example:

```
SADD shenyu:sensitive:words "bad word 1"
SADD shenyu:sensitive:words "bad word 2"
```

The word list itself is **not** part of this repository: every deployment is expected to provide
its own dictionary, because the content of such a list depends on the country, the business and
the compliance rules that apply to it.
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
<?xml version="1.0" encoding="UTF-8"?>
<!--
~ Licensed to the Apache Software Foundation (ASF) under one or more
~ contributor license agreements. See the NOTICE file distributed with
~ this work for additional information regarding copyright ownership.
~ The ASF licenses this file to You under the Apache License, Version 2.0
~ (the "License"); you may not use this file except in compliance with
~ the License. You may obtain a copy of the License at
~
~ http://www.apache.org/licenses/LICENSE-2.0
~
~ Unless required by applicable law or agreed to in writing, software
~ distributed under the License is distributed on an "AS IS" BASIS,
~ WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
~ See the License for the specific language governing permissions and
~ limitations under the License.
-->

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<groupId>org.apache.shenyu</groupId>
<artifactId>shenyu-plugin-ai</artifactId>
<version>2.7.2-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>shenyu-plugin-ai-sensitive-word</artifactId>

<dependencies>
<dependency>
<groupId>org.apache.shenyu</groupId>
<artifactId>shenyu-plugin-base</artifactId>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>org.apache.shenyu</groupId>
<artifactId>shenyu-infra-redis</artifactId>
<version>${project.version}</version>
</dependency>
</dependencies>

</project>
Loading
Loading