背景
最近在做新的服务端设计时,愈发感受到信息安全在实际应用中的重要性。
我们的项目中涉及大量用户信息。这些信息在应用中有的不能对外暴露,有时需要完整提供给第三方,有时只能提供脱敏信息,有时又需要根据实际请求类型来决定是否脱敏,有的数据库字段不允许用户写入。
如果完全按以前的逻辑,需要针对每个不同的场景设计不同的逻辑,这些逻辑大部分又是类似或相同的。服务端由多个成员设计,对逻辑的理解程序又可能参差不齐,最终产品的设计质量也会不同.
唉,越想越复杂,头疼。
穷则思变,难也会思变。
面对这越来越复杂的逻辑,我开始考虑设计一个独立的字段保护机制。将这些通用需求抽象化,设计为更简单更方便的形式提供给应用层实现这些逻辑控制。
其实在项目的上一个版本,我设计了一个基于正则表达定义过滤规则的字段过滤器机制,实际应用中考虑到正则表达式对于大多数程序员来说实在太过复杂,并没有真正应用到业务场景。
需求定义
下面就要进一步分析,我们到底有哪些字段保护需求呢?主要分为静态字段过滤
和动态字段过滤
.
Java Bean静态字段保护
比如下面的表中private_time
字段,它用于存储设备令牌的创建时间,用于服务端计算令牌的关键数据,对于服务端来说是绝密,不允许让服务端之外的地方知道.那这表记录在发送给客户端时就不应该出现这个字段.同时也不允许客户端修改这个字段.
CREATE TABLE IF NOT EXISTS dc_device (
`id` int(11) NOT NULL PRIMARY KEY AUTO_INCREMENT COMMENT 'X@NAME:设备id@X',
`name` varchar(32) DEFAULT NULL COMMENT 'X@NAME:设备名称@X,用户指定',
`physical_address` varchar(32) NOT NULL UNIQUE COMMENT '设备X@NAME:物理地址@X,MAC地址,IMEI或其他设备识别码',
`private_time` bigint DEFAULT 0 COMMENT 'SCOPE@LOCAL@EPOSC设备令牌创建的时间戳(毫秒),每次创建设备令牌都会修改此字段',
`os_arch` varchar(64) DEFAULT NULL COMMENT 'X@NAME:操作系统平台@X,操作系统名称及版本及硬件架构名称,例如:Windows-x86_64,Linux-x86_64,Android-arm...'
) COMMENT 'X@NAME:前端设备记录@X,前端设备基本信息' DEFAULT CHARSET=utf8;
在这种情况下,我们需要将这些需要被保护的字段在代码编译期就写死,不允许出现在网络传输的数据中.对于这种需求,我们称之为静态字段过滤.
Java Bean动态字段保护
静态字段过滤因为在编译期就写死了,不能被修改,所以安全性上是保证了字段不能被泄露出去,带来的缺点也是明显的,就是不够灵活.
所以为了服务运行时不同服务方法的个性化定制需求,还需要设计动态字段保护机制,允许针对不同服务方法制定不同的字段保护策略
字段过滤
动态字段过滤与静态字段保护的结果是一样的,就是让保护字段不出现在与客户端交互的数据流中.
比如在字段安全保护场景,将不允许用户修改的字段(balance
)在作为服务方法输入参数反序列化时忽略处理,避免无权限的客户端远程修改该字段。
值过滤
在数据对象序列化/反序列化时替换指定的字段的值就是值过滤
比如在隐私保护场景,将用户名字段 (name
)在作为服务方法结果输出时加*号显示为王*华
,就是修改了输出给客户端的数据对象的name的值.
序列化和反序列化
不论是静态字段保护还是动态字段保护都是通过作用在Java Bean数据对象的序列化或反序列化两个阶段实现的.
不论是Thrift RPC还是SpringWeb服务,服务方法的输入和输出参数都要通过网络在Server/Client之间传输。实现数据对象传输,发送端需要对数据对象进行序列化(JSON或二进制数据流),接收端需要对收到的数据反序列化还原为原始的数据对象。
服务方法接收到参数是输入,需要经过反序列化.
服务方法的返回结果是输出,则要经过序列化.
静态字段保护作用时不区分序列化和反序列化,一刀切,要保护就两个方向都保护.
但是动态字段保护可以指定保护的方向,可以要求只作用于序列化,或只作用于反序列化,也可以作用两个方向.
实现
前阵子通过对Spring-core注解工具的深入学习和解构。我对注解(Annotation)的设计和使用有了更丰富的经验。
为此我还写过相关的博客:
《java:从spring-core移植的注解(annotation)扫描工具模组common-annotutils(适用JDK 1.7)》
《spring-core:理解@AliasFor注解的作用》
《spring-core:注解扫描工具MergedAnnotations获取复合注解上的元注解》
《spring-core:注解扫描工具AnnotatedElementUtils.hasMetaAnnotationTypes方法说明》
《spring-core:注解合成(AnnotationUtils.synthesizeAnnotation)的使用示例》
开发团队成员也已经适应了Spring应用开发中大量注解的应用。所以决定基本的设计思路:设计一套独立的基于注解的服务端字段过滤机制。
静态字段保护实现
注解(Annotation)实现
Spring Web的数据序列化和反序列化是基于Jackson来实现的,Thrift 有自己的序列化和反序列实现.
可以在字段上定义注解@JsonIgnore
可以实现Jackson在序列化和反序列化时忽略该字段.
而Thrift Struct如果在字段setter/getter方法不定义@ThriftField
注解,该字段就会在序列化和反序列化时被忽略.
所以不论是对是SpringWEB服务还是Thrfit RPC服务,实现静态的字段保护就是控制字段上的对应的注解定义.这很好理解.
比如下面的Java Bean定义,privateInfo
字段定义了@JsonIgnore
,同时getter/setter方法也没有定义@ThriftField
注解,privateInfo
在Thrift RPC和Spring WEB服务端与客户端交互时就会被忽略
import com.alibaba.fastjson.annotation.JSONField;
import com.facebook.swift.codec.ThriftField;
import com.fasterxml.jackson.annotation.JsonIgnore;
public final class UserBean {
private String name;
private String id;
@JSONField(serialize = false,deserialize = false)
@JsonIgnore
private String privateInfo;
public UserBean() {
}
@ThriftField(0)
public String getName() {
return name;
}
@ThriftField(name="name")
public void setName(String name) {
this.name = name;
}
@ThriftField(1)
public String getId() {
return id;
}
@ThriftField(name="id")
public void setId(String id) {
this.id = id;
}
public String getPrivateInfo() {
return privateInfo;
}
public void setPrivateInfo(String privateInfo) {
this.privateInfo = privateInfo;
}
}
sql2java
sql2java,因为数据库访问对象类的代码都是自动生成的,没办法如上手工修改,从3.32.0版本开始,sql2java增加了静态字段过滤功能,在生成sql2java的数据库表记录对象类时,允许指定字段的可见度(ColumnVisibility)从而自动在生成的代码实现上述的注解定义。
详细说明参见我的上一篇博客 《java:sqlj2ava的静态字段保护》
动态字段保护实现
关于动态字段保护的实现,相比静态字段保护要更复杂.
涉及注解(Annotation)设计、服务请求拦截、定制Jackson,fastjson的序列化/反序列化器、过滤器注入等待环节.为此我设计了一个独立的动态字段过滤工具:beanfilter
,已经向maven发布了第一个版本,还不完善,后续会持续更新,如下为代码仓库位置:
关于beanfilter的项目介绍可以先阅读我之前的博客,再进一步看项目说明: