# beanfilter
**Repository Path**: l0km/beanfilter
## Basic Information
- **Project Name**: beanfilter
- **Description**: 基于注解(Annotation)实现的服务端(spring/thrift)对JavaBean类型数据在序列化和反序列化阶段动态字段过滤(IFieldFilter)和值过滤(IValueFilter)工具。
- **Primary Language**: Unknown
- **License**: BSD-2-Clause
- **Default Branch**: master
- **Homepage**: None
- **GVP Project**: No
## Statistics
- **Stars**: 1
- **Forks**: 1
- **Created**: 2024-06-14
- **Last Updated**: 2025-06-14
## Categories & Tags
**Categories**: utils
**Tags**: None
## README
# beanfilter
基于注解(Annotation)实现的服务端(spring/thrift)对JavaBean类型数据在序列化和反序列化阶段动态字段过滤(IFieldFilter)和值过滤(IValueFilter)工具。
为服务端提供了一个动态过滤服务方法输入输出的Java Bean参数字段的功能,字段过滤用于控制字段是否被输入/输出,值过滤用于控制字段输入/输出时的内容。
比如在字段安全保护场景,可以使用字段过滤器,将不允许用户自己修改的字段(`balance`)在作为服务方法输入参数反序列化时忽略处理,避免客户端远程修改该字段。
比如在隐私保护场景,可以使用值过滤器,将用户名字段 (`name`)在作为服务方法结果输出时加*号显示为`王*华`
## 术语
- 服务端 在此指Spring WEB或Thrift RPC服务
- 输入/输出 服务端数据反序列化/序列化
- 字段过滤 指控制 Java Bean类型特定字段在服务方法参数输入和返回值输出时是否被允许反序列化和序列化
- 值过滤 指控制 Java Bean类型特定字段在服务方法的参数输入和返回值输出时的实际反序列化和序列化的内容
- IFieldFilter 实现字段过滤的过滤器接口实例
- IValueFilter 实现值过滤的过滤器接口实例
## 特性
- 基于服务方法注解支持服务端为每个服务方法定义不同的字段过滤
- 支持jackson,fastjson的对Java Bean类型在序列化和反序列化时的字段过滤和值过滤
- 支持Thrift Struct在序列化和反序列化时的字段过滤和值过滤
- 支持过滤器动态激活
- 支持自定义字段过滤器和值过滤
- 支持自定义过滤器注解
## 快速入门
beanfilter使用非常简单,只要如下两步。
### 服务方法注解定义
需要引入依赖
```xml
com.gitee.l0km
beanfilter-annotations
0.1.0
```
如下在服务方法上定义一组过滤器注解,指定要过滤的字段,通过注解定义了服务方法上使用的过滤器
```java
public class TestService {
/**
* [序列化]字段过滤器定义:指定tokenTime字段在序列化时不输出,该方法返回的 TestUserBean 在序列化为JSON时就没有该字段
* activeOn指定了自定义的过滤器激活器,只有激活器返回true时才启用过滤器
*/
@FieldNameFilter(beanClass = TestUserBean.class,filterNames = {"tokenTime"},activeOn=MyAction.class)
/** [反序列化]字段过滤器定义:指定tokenTime字段在反序列化时不输出,该方法反序列化时得到的输入参数TestDevice的 tokenTime 不会被赋值,为null */
@FieldNameFilter(beanClass = TestDevice.class,filterNames = {"tokenTime"},serializingUsed =false,deserializingUsed = true)
/** [序列化]字段过滤器定义:为TestUserGroupBean类型数据指定使用自定义字段过滤器用于反序列化 */
@FieldFilter(filterClass=MyFieldFilter.class,beanClass=TestUserGroupBean.class)
/** [序列化]字段过滤器定义:为TestUserGroupBean类型数据指定使用自定义值过滤器用于反序列化 */
@FieldFilter(filterClass=IFieldFilter.DefaultFieldFilter.class,beanClass=TestUserGroupBean.class)
/** [序列化]值过滤器定义:指定TestUserBean的 password 字段在序列化时输出为 '***' */
@ConstantValueFilter(beanClass = TestUserBean.class,filterName = "password",consant = "***")
/** [序列化]值过滤器定义:指定TestUserBean的 name 字段在序列化时输出为 'anonymous' */
@ConstantValueFilter(beanClass = TestUserBean.class,filterName = "name",consant = "anonymous")
public TestUserBean addUserFilterOut(TestUserBean input,TestUserGroupBean group) {
return input;
}
}
```
### Spring 启动拦截器
需要引入依赖
```xml
com.gitee.l0km
beanfilter-interceptor
0.1.0
```
如下在Spring服务启动注解(`@SpringBootApplication`)上指定扫描beanfilter spring拦截器`FilterInterceptorConfig`所在包就可以启动服务方法拦截器,激活了服务方法上定义的beanfilter过滤器
```java
import org.springframework.boot.autoconfigure.SpringBootApplication;
import com.gitee.l0km.beanfilter.spring.FilterInterceptorConfig;
/**
* 应用服务启动配置
*/
@SpringBootApplication(scanBasePackageClasses = {FilterInterceptorConfig.class})
public class ApplicationBoot{
//////..../////
}
```
### Thrift 启用字段保护
引入 依赖
```xml
com.gitee.l0km
xthrift-service
1.1.0
```
如下创建服务实例时调用`setFilterable(true)`就开启了字段保护功能
```java
/**
* 创建服务实例
* @param service 服务实例,如果为{@code null}或服务已经停止则创建新的服务实例
* @param config thrift服务配置
* @return 返回{@code service}或创建的新的服务实例
*/
private ThriftServerService buildService(ThriftServerService service,ThriftServerConfig config){
return ThriftServerService.bulider()
.withServices(thriftDecorator)
.setThriftServerConfig(config)
/** 支持字段保护 */
.setFilterable(true)
.build();
}
```
## 项目结构
| 子项目 | JDK版本要求 | 依赖库 | 说明 |
| ---------------------- | ----------- | ---------------------------------------------------- | ------------------------------------------------------------ |
| beanfilter-annotations | 1.7 | 无 | 过滤器注解定义,没有任何依赖库,在需要定义服务方法注解的模组需要引用此项目 |
| beanfilter-core | 1.7 | guava | 类型定义,过滤器接口及实现,以及支持字段过滤的fastjson,jackson Java Bean序列化/反序列化器实现,需要实现自定义过滤接口的模组需要引用此项目 |
| beanfilter-interceptor | 1.8 | beanfilter-annotations,beanfilter-core,spring,casban | 服务方法拦截器实现,Spring服务启动模组需要引用此项目 |
## 过滤器说明
beanfilter-annotations定义了基本的字段过滤和值过滤器注解,可以直接用于服务方法上。
beanfilter-core中定义基本的字段过滤器和值过滤器实现类
### 内置过滤器注解
#### @FieldNameFilter
`@FieldNameFilter`是基于 [`com.gitee.l0km.beanfilter.SimpleFieldFilter`](beanfilter-core/src/main/java/com/gitee/l0km/beanfilter/SimpleFieldFilter.java) 过滤器实现的字段过滤器注解
| 注解字段名 | 类型 | 默认值 | 说明 |
| ------------------- | ------------ | ------------- | ------------------------------------------------------------ |
| beanClass | `Class>` | Object.class | 字段过滤作用的Java Bean类型,如果不指定作用于所有Java Bean类型 |
| autoCase | boolean | false | 是否将snake-case和camel-case格式的字段字视为同一字段,如`firstName`和`first_name`,为`true`时视为同一个名字。在这个字段数据库对象类型上比较有用,数据库字段一般的命名习惯是snake-case,而Java Bean的字段命名为camel-case,该字段为true,就可以避免因为定义的字段名格式不匹配而导致的过滤器失效 |
| whiteList | boolean | false | 为true时为白名单模式,filterNames指定字段名被允许输入/输出,否则为黑名单模式,filterNames指定字段名被禁止输入/输出 |
| filterNames | String[] | | 过滤的字段名列表 |
| activeOn | `Class>[]` | {} | 过滤器激活器类型列表,必须为Activation接口实现 |
| activeOnClassNames | String[] | {} | 过滤器激活器类名列表,必须为Activation接口实现类名,与`activeOn`参数作用相同,用字符器定义类名在特定场景可以减少依赖 |
| activeAnd | boolean | true | 指定多个过滤器激活器时,激活判断模式,为true为AND模式,所有激活器条件都匹配才能激活过滤器,否则为OR模式,任意一激活器条件都匹配就可以激活过滤器 |
| defaultExcludeNames | String[] | {} | 默认排除字段,黑名单模式有效,支持过滤场景前缀,参见 《阶段/场景前缀》章节,适用于以此注解为元注解设计的过滤器注解定义默认排除字段,参见 `@BaseBeanFieldNameFilter` |
| defaultIncludeNames | String[] | {} | 默认包含字段,白名单模式有效,支持过滤场景前缀,参见 《阶段/场景前缀》章节,适用于以此注解为元注解设计的过滤器注解定义默认包含字段,参见 `@BaseBeanFieldNameFilter` |
| phase | CodecPhase | SERIALIZATION | 过滤器适用编码解码阶段定义,参见《过滤器适用场景》章节 |
| scope | CodecScope | ALL | 过滤适用的编解码目标范围,参见《过滤器适用场景》章节 |
#### @ConstantValueFilter
`@ConstantValueFilter`是基于 [`com.gitee.l0km.beanfilter.SimpleValueFilter`](beanfilter-core/src/main/java/com/gitee/l0km/beanfilter/SimpleValueFilter.java) 过滤实现的值过滤器注解
| 注解字段名 | 类型 | 默认值 | 说明 |
| ------------------ | ------------ | ----------------- | -------------------- |
| beanClass | `Class>` | Object.class | 同@SimpleFieldFilter |
| autoCase | boolean | false | 同@SimpleFieldFilter |
| filterName | String | | 要过滤字段名 |
| consant | String | | 返回的字段值 |
| constantType | `Class>` | Object.class | 要求的返回字段值类型 |
| activeOn | `Class>[]` | {} | 同@SimpleFieldFilter |
| activeOnClassNames | String[] | {} | 同@SimpleFieldFilter |
| activeAnd | boolean | true | 同@SimpleFieldFilter |
| phase | `CodecPhase` | **SERIALIZATION** | 同@SimpleFieldFilter |
| scope | `CodecScope` | **ALL** | 同@SimpleFieldFilter |
#### @CoverValueFilter
`@CoverValueFilter`是基于 [`com.gitee.l0km.beanfilter.StringCoverFilter`](beanfilter-core/src/main/java/com/gitee/l0km/beanfilter/StringCoverFilter.java) 过滤实现的用于序列化的字符类型值过滤器注解
可以对字符串类型的值更灵活的控制保护输出内容
| 注解字段名 | 类型 | 默认值 | 说明 |
| ------------------ | ------------ | ----------------- | ------------------------------------------------------------ |
| beanClass | `Class>` | Object.class | 同@SimpleFieldFilter |
| autoCase | boolean | false | 同@SimpleFieldFilter |
| filterName | String | | 要过滤字段名 |
| cover | String | | 覆盖字符 |
| beginIndex | int | | 0-based 覆盖起始索引,从字符串起始位置算起开始使用覆盖字符代替的索引,小于0视为0 |
| endIndex | int | | 覆盖结束索引,从从字符串结尾位置(exclusive),向前的负索引值, 大于0 则视为固定长度覆盖 |
| split | boolean | | 是否将字符串以标点符号空格(包括中文标点符号和空格)分割为单词后处理 |
| activeOn | `Class>[]` | {} | 同@SimpleFieldFilter |
| activeOnClassNames | String[] | {} | 同@SimpleFieldFilter |
| activeAnd | boolean | true | 同@SimpleFieldFilter |
| phase | `CodecPhase` | **SERIALIZATION** | 同@SimpleFieldFilter |
| scope | `CodecScope` | **ALL** | 同@SimpleFieldFilter |
#### @PasswordValueFilter
`@UsernameValueFilter`是基于 [`com.gitee.l0km.beanfilter.StringCoverFilter`](beanfilter-core/src/main/java/com/gitee/l0km/beanfilter/StringCoverFilter.java) 过滤实现的用于序列化的用于用户名字段保护用途字符类型值过滤器注解,默认输出与密码字符串长度相同的`*`号字符串
| 注解字段名 | 类型 | 默认值 | 说明 |
| ------------------ | ------------ | ------------ | ------------------------------------------------------------ |
| beanClass | `Class>` | Object.class | 同@SimpleFieldFilter |
| autoCase | boolean | false | 同@SimpleFieldFilter |
| filterName | String | | @CoverValueFilter |
| cover | char | `*` | 覆盖字符 |
| outputLength | int | 0 | 输出的覆盖字符串长度,小于等于0输出与原字符串长度一样的覆盖字符串,大于0 则视为固定长度覆盖。 |
| activeOn | `Class>[]` | {} | 同@SimpleFieldFilter |
| activeOnClassNames | String[] | {} | 同@SimpleFieldFilter |
| activeAnd | boolean | true | 同@SimpleFieldFilter |
| scope | `CodecScope` | **ALL** | 同@SimpleFieldFilter |
#### @UsernameValueFilter
`@PasswordValueFilter`是基于 [`com.gitee.l0km.beanfilter.StringCoverFilter`](beanfilter-core/src/main/java/com/gitee/l0km/beanfilter/StringCoverFilter.java) 过滤实现的用于序列化的用于密码字段保护用途字符类型值过滤器注解,
默认输出与与原字符串长度相同,前后各保留一个字符的原文,中间部分`*`覆盖号的字符串,如`王*阳`
对于有空格标点符号(包括中文标点符号和空格)的字符串,则空格标点符号分割后作为一串单词处理:
比如`M.smith joe`输出为`*.s***h j*e`
比如`王明。《但是》`输出为`王*。《但*》`
| 注解字段名 | 类型 | 默认值 | 说明 |
| ------------------ | ------------ | ------------ | -------------------- |
| beanClass | `Class>` | Object.class | 同@SimpleFieldFilter |
| autoCase | boolean | false | 同@SimpleFieldFilter |
| filterName | String | | 同@CoverValueFilter |
| cover | String | `*` | 同@CoverValueFilter |
| beginIndex | int | 1 | 同@CoverValueFilter |
| endIndex | int | -1 | 同@CoverValueFilter |
| activeOn | `Class>[]` | {} | 同@SimpleFieldFilter |
| activeOnClassNames | String[] | {} | 同@SimpleFieldFilter |
| activeAnd | boolean | true | 同@SimpleFieldFilter |
| scope | `CodecScope` | **ALL** | 同@SimpleFieldFilter |
#### @BaseBeanFieldNameFilter
`@BaseBeanFieldNameFilter`是在`@SimpleFieldFilter`基础上定义的用于[sql2java](https://gitee.com/l0km/sql2java)生成的数据库表的Java Bean 类型的字段注解,与`@SimpleFieldFilter`相比只是少了一个`autoCase`字段(该字段默认为true)。
并定义了默认排除字段(`defaultExcludeNames`):
- JSON场景时默认排除 `initialized,modified`字段
- 反序列化阶段默认排除 `create_time,update_time`字段
#### @MultisceneNameFilter
`@MultisceneNameFilter` 设计为基于 [com.gitee.l0km.beanfilter.FlexibleContextFilter](beanfilter-annotations/src/main/java/com/gitee/l0km/beanfilter/annotations/MultisceneNameFilter.java) 过滤器实现的用于多场景的字段过滤器。
与`@FieldNameFilter`过滤器注解相比,少了`phase,scope`字段。但是`filterNames`字段定义支持基于`${phase}/${scope}:`前缀定义过滤器适用阶段和场景的的字段,允许定义多个适用不同的场景的过滤字段名。
参见 《阶段/场景前缀》章节。
### 过滤器适用场景
beanfilter通过`CodecPhase`和`CodecScope`两个枚举变量定义在两个维度定义过滤器适用场景。
`CodecPhase`用于指定过滤器适用的序列化或反序列化阶段或都适用。
`CodecScope`用于指定过滤器适用的范围,JSON,Thrift 或都适用。
#### CodecPhase
| 枚举变量 | 说明 |
| ------------------- | ------------------- |
| **NONE** | 无 |
| **BOTH** | 序列化/反序列化阶段 |
| **SERIALIZATION** | 序列化阶段 |
| **DESERIALIZATION** | 反序列化阶段 |
#### CodecScope
| 枚举变量 | 说明 |
| ------------ | --------------------------------- |
| **NONE** | 无 |
| **ALL** | 所有目标 |
| **THRIFT** | 仅用于 thrift struct 编码解码 |
| **JSON** | 仅用于 jackson和fastjson 编码解码 |
| **JACKSON** | 仅用于 jackson 编码解码 |
| **FASTJSON** | 仅用于 fastjson 编码解码 |
#### 阶段/场景前缀
阶段场景前缀定义在`,`分割的字段名列表前,用于指定该字段名列表适用的场景。
阶段场景前缀格式:
> [${phase}/${scope}:]name1[,name2...]
- phase 等价于 @FieldNameFilter 注解的 phase字段, 支持枚举变量名前两位字母缩写,如SE 等于 `SERIALIZATION`,如果不指定则默认为**BOTH**
- scope 等价于 @FieldNameFilter 注解的 scope字段, 支持枚举变量名前两位字母缩写,如TH 等于 THRIFT,如果不指定则默认为**ALL**
示例
| 字段定义 | 等价 | 说明 |
| ---------------- | ----------------------------- | ------------------------------------------------- |
| `SE/:hello` | `SERIALIZATION/ALL:hello` | `hello`字段适用于序列化阶段过滤 |
| /TH: time, user` | `BOTH/THRIFT:time,user` | `time,user`字段适用于所有阶段的THRIFT场景字段过滤 |
| `hello,world` | `BOTH/ALL:hello,world` | `hello,world`适用于所有阶段和场景字段过滤 |
| `DE/JA:jim` | `DESERIALIZATION/JACKSON:jim` | `jim`适用于反序列化阶段JACKSON场景字段过滤 |
### 自定义过滤器和注解
用户可以参照 `SimpleFieldFilter`实现 `IFieldFilter`接口来实现自定义的字段过滤器,同理可以参照 `SimpleValueFilter`实现`IValueFilter`接口来实现自定义的值过滤器。
有了自定义过滤器,用户可以参照`@FieldNameFilter`和`@ConstantValueFilter`来定义对应的注解。
以自定义字段过滤为例,如下,我们定义一个字段过滤器:
```java
public class MyFieldFilterImpl implements IFieldFilter{
final String testName;
final Class> testType;
public MyFieldFilterImpl(String testName, Class> testType) {
super();
this.testName = testName;
this.testType = testType;
}
@Override
public boolean permit(Class> clazz, String fieldName) {
return false;
}
}
```
根据上面的字段过滤器,可以定义如下注解`@MyFieldFilter`,我们希望该注解是可重复的,所以同时定义了`@MyFieldFilters`:
```java
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
@FieldFilter(filterClass = MyFieldFilterImpl.class,beanClass = Object.class)
@Repeatable(MyFieldFilters.class)
public @interface MyFieldFilter {
@CtorArg(0)
String testName();
@CtorArg(1)
Class> testType() default Object.class;
@AliasFor(annotation = FieldFilter.class,attribute = "deserializingUsed")
boolean deserializingUsed() default false;
@AliasFor(annotation = FieldFilter.class,attribute = "serializingUsed")
boolean serializingUsed() default true;
}
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface MyFieldFilters {
MyFieldFilter[] value();
}
```
上面的`@MyFieldFilter`注解中`@CtorArg`用于定义该字段是过滤器实现类`MyFieldFilterImpl`类的构造方法`MyFieldFilterImpl(String testName, Class> testType)`的参数,`@CtorArg`中定义的数字为该字段在构造方法参数列表中的索引 ,`@CtorArg(0)`即为构造方法的第一个参数,以此类推,不能搞错否则服务方法拦截器在构造过滤器实例时会抛出异常。
上面的注解中`deserializingUsed,serializingUsed`字段可以不定义,如果不定义则默认该过滤器只用于序列化。参见 [@com.gitee.l0km.beanfilter.annotations.FieldFilter](beanfilter-annotations/src/main/java/com/gitee/l0km/beanfilter/annotations/FieldFilter.java)中`deserializingUsed,serializingUsed`字段的默认值
自定义过滤器完整的示例代码参见 :
[com.gitee.l0km.beanfilter.CustomFilterTest](beanfilter-interceptor/src/test/java/com/gitee/l0km/beanfilter/CustomFilterTest.java)
### 自定义关联类型
一般情况下beanfilter可以正确找出服务方法的输入/输出参数中所有关联的Java Bean类型,并根据服务方法定义的过滤器定义对Java Bean类型安装过滤器。但是如果服务方法的返回类型中有泛型参数,且泛型参数不是泛型类定义的参数,也就是说,可能为任意类型,那么beanfilter无法确定其实际类型。
例如下面 PageInfo对象返分页内容,返回的数据可能是JavaBean列表,也可能是个整数类型列表
```java
public class PageInfo{
}
```
这时beanfilter无法正确对输出内容进行过滤。
应用层可以通过`@AssociatedTypeOfSerialization`注解定义返回类型可能关联的类型。
`@AssociatedTypeOfSerialization`注解字段说明:
| 字段名 | 默认值 | 说明 |
| ------------------- | ------ | ------------------------------------------------------------ |
| associatedTypes | {} | 关联类型列表 |
| associatedTypeNames | {} | 关联类名,与`associatedType()`作用相同, 以字符串形式提供类型,在有些场景可以减少模组耦合 |
### Activation
`Activation`为过滤器激活器接口,应用层实现此接口可以动态控制是否激活过滤器.Activation实现类必须有默认构造方法。示例如下
```java
public class TestUserActivation implements Activation{
@Override
public boolean active(FilterContext ctx) {
/** 当前令牌为用户令牌则激过滤器 */
if(Tokens.currentIsUser()){
return true;
}
return false;
}
}
```
`Activation`接口方法以`FilterContext`对象作为输入参数,向激活器提供了过滤器当前工作环境基本信息
### LocalContext
`LocalContext`为由序列化器和反序列化器提供的过滤器上下文,在实现自定义过滤时如果需要知道当前过滤器的工作环境(序列化/反序列化)以及当前序列化的实例,可以通过静态方法`com.gitee.l0km.beanfilter.context.LocalContext.getCurrentContext()`获取
## Spring服务方法拦截
beanfilter-interceptor中[com.gitee.l0km.beanfilter.spring.InstallFilterInterceptor](beanfilter-interceptor/src/main/java/com/gitee/l0km/beanfilter/spring/InstallFilterInterceptor.java) 基于`org.springframework.web.servlet.HandlerInterceptor`实现了Spring WEB请求拦截,在请求被反序列化之前,根据服务方法上的注解构建过滤器,注入Java Bean对应的序列化器和反序列化器。在请求执行完成之后,删除过滤器。
## Thrift RPC服务方法拦截
beanfilter-interceptor中[com.gitee.l0km.beanfilter.thrift.FilterableHandler](beanfilter-interceptor/src/main/java/com/gitee/l0km/beanfilter/thrift/FilterableHandler.java)基于`com.facebook.swift.service.ThriftEventHandler`实现Thrift RPC请求,在请求被反序列化之前,根据服务方法上的注解构建过滤器,注入Java Bean对应的编解码器( `com.facebook.swift.codec.FilterableThriftStructCodec`,xthrift中定义)。在请求执行完成之后,删除过滤器。
## 限制
- 如果JavaBean已经通过jackson注解(@JsonSerialize,@JsonDeserialize)指定了自定义的序列化器和反序列化器,则beanfilter对该类型无效。