Java查询时间段(startTime
653
2022-08-20
mybatis中数据加密与解密的实现
目录1、需求2、解决方案3、使用拦截器方式3.1定义加密接口3.2定义加密注解3.3拦截器加密数据3.4拦截器解密数据3.5解密工具类3.6实体类样例4、使用类型转换器4.1定义加密类型4.2定义类型转换处理器4.3配置类型转换器的包路径4.4测试用的实体类4.5mapper接口文件4.6mapper映射文件
数据加解密的实现方式多种多样,在mybatis环境中数据加解密变得非常简单易用,本文旨在提供参考,在生产中应尽可能完成单元测试,开展足够的覆盖测试,以验证可靠性、可用性、安全性。
1、需求
**原始需求:**数据在保存时进行加密,取出时解密,避免被拖库时泄露敏感信息。
**初始分析:**数据从前端过来,到达后端,经过业务逻辑后存入数据库,其中经历三大环节:
1、前端与后端之间传输,是否加密,如果需要加密则前端传输前就需要加密,暂时可以用HTTPS代替;2、到达后端,此时数据通常需要经过一些逻辑判断,所以加密没有意义,反而会带来不必要的麻烦;3、入库,这个是最后环节,数据经过insert的sql或者update语句入库,在此前需要加密;
**核心需求:**入库前最后一步完成数据加密,达成的目的是如果数据库被暴露,一定程度上保障数据的安全,也可以防止有数据操作权限的人将数据泄露。
**加密算法:**对称和非对称算法均可,考虑加密和解密的效率以及场景,考虑选用对称算法AES加密。
**ORM环境:**mybatis
**加密字段:**加密字段不确定,应该在数据库表设计的时候确定敏感字段,即加密字段可定制。
应注意的细节:
最后:数据加密用来提高安全性的同时,必然会牺牲整个程序性能和易用性。
2、解决方案
在mybatis的依赖环境下,至少有两种自动加密的方式:
1、使用拦截器,对insert和update语句拦截,获取需加密字段,加密后存入数据库。读取时拦截query,解密后存入result对象; 2、使用类型转换器TypeHandler来实现。
3、使用拦截器方式
3.1 定义加密接口
因为mybatis拦截器会拦截所有符合签名的请求,为了提高效率定义一个标记接口非常重要,既然有接口不如就在接口里加入需要加密的字段信息,当然也可以不加,根据实际场景来设计。
/**
* @author: xu.dm
* @since: 2022/3/8 16:30
* 该接口用于标记实体类需要加密,具体的加密内容字段通过getEncryptFields返回.
* 注意:getEncryptFields与@Encrypt注解可配合使用也可以互斥使用,根据具体的需求实现。
**/
public interface Encrypted {
/**
* 实现该接口,返回需要加密的字段名数组,需与类中字段完全一致,区分大小写
* @return 返回需要加密的字段
*/
default String[] getEncryptFields() {
return new String[0];
}
}
3.2 定义加密注解
主要为了某些场景,直接在实体类的字段打标记,直观的说明该字段是加密字段,某些业务逻辑也可以依赖此标记做进一步操作,一句话,根据场景来适配和设计。
/**
* @author : xu.dm
* @since : 2022/3/8
* 标识加密的注解,value值暂时没用,根据需要可以考虑采用的加密方式与算法等
* 注意:Encrypted接口的getEncryptFields与@Encrypt注解可配合使用也可以互斥使用,根据具体的需求实现。
*/
@Target(ElementType.FIELD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypt {
String value() default "";
}
3.3 拦截器加密数据
初始拦截器定义是相对单一的场景,利用反射遍历需加密的字段,对字段的字符加密,也就是待加密字段最好是字符串类型,并且,没有对父类反射遍历,如果有继承情况,并且父类也有需要加密的字段,需根据场景调整代码,对父类递归,直到根父类。在当前设计中Encrypted接口和@Encrypt只会生效一种,并且以接口优先。
/**
* @author: xu.dm
* @since: 2022/3/8
* 拦截所有实现Encrypted接口的实体类insert和update操作
* 如果接口的getEncryptFields返回数组长度大于0,则使用该参数进行加密,
* 否则检查实体类中带@Encrypt注解,对该标识字段加密,
* 注意:待加密的字段最好是字符串,加密调用的是标识对象的ToString()结果进行加密,
*
**/
@Component
@Slf4j
@Intercepts({
@Signature(method = "update", type = Executor.class, args = {MappedStatement.class, Object.class})
})
public class EncryptionInterceptor implements Interceptor {
public EncryptionInterceptor() {
}
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object[] args = invocation.getArgs();
SqlCommandType sqlCommandType = null;
for (Object object : args) {
// 从MappedStatement参数中获取到操作类型
if (object instanceof MappedStatement) {
MappedStatement ms = (MappedStatement) object;
sqlCommandType = ms.getSqlCommandType();
log.debug(vCukbfvWo"Encryption interceptor 操作类型: {}", sqlCommandType);
continue;
}
log.debug("Encryption interceptor 操作参数:{}",object);
// 判断参数
if (object instanceof Encrypted) {
if (SqlCommandType.INSERT == sqlCommandType) {
encryptField((Encrypted)object);
continue;
}
if (SqlCommandType.UPDATE == sqlCommandType) {
encryptField((Encrypted)object);
log.debug("Encryption interceptor update operation,encrypt field: {}",object.toString());
}
}
}
return invocation.proceed();
}
/**
* @param object 待检查的对象
* @throws IllegalAccessException
* 通过查询注解@Encrypt或者Encrypted返回的字段,进行动态加密
* 两种方式互斥
*/
private void encryptField(Encrypted object) throws IllegalAccessException, NoSuchFieldException {
String[] encryptFields = object.getEncryptFields();
String factor = "xu.dm118dAADF!@$";
Class> clazz = object.getClass();
if(encryptFields.length==0){
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
Encrypt encrypt = field.getAnnotation(Encrypt.class);
if(encrypt!=null) {
String encryptString = AesUtils.encrypt(field.get(object).toString(), factor);
field.set(object,encryptString);
log.debug("Encryption interceptor,encrypt field: {}",field.getName());
}
}
}else {
for (String fieldName : encryptFields) {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
String encryptString = AesUtils.encrypt(field.get(object).toString(), factor);
field.set(object,encryptString);
log.debug("Encryption interceptor,encrypt field: {}",field.getName());
}
}
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
}
3.4 拦截器解密数据
解密时拦截query方法,只对结果集判断,结果属于Encrypted接口或者结果结果集第一条数据属于Encrypted接口则进入解密流程。
解密失败或者解密方法返回空串后,不会修改原本字段数据。
/**
* @author: xu.dm
* @since: 2022/3/9 11:39
* 解密数据,返回结果为list集合时,应保证集合里都是同一类型的元素。
* 解密失败时返回为null,或者返回为空串时,不对原数据操作。
**/
@Component
@Slf4j
@Intercepts({
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class}),
@Signature(type = Executor.class, method = "query", args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class, CacheKey.class, BoundSql.class}),
})
public class DecryptionInterceptor implements Interceptor {
@Override
public Object intercept(Invocation invocation) throws Throwable {
Object result = invocation.proceed();
if(result instanceof ArrayList) {
@SuppressWarnings("rawtypes")
ArrayList list = (ArrayList) result;
if(list.size() == 0) {
return result;
}
if(list.get(0) instanceof Encrypted) {
for (Object item : list) {
decryptField((Encrypted) item);
}
}
return result;
}
if(result instanceof Encrypted) {
decryptField((Encrypted) result);
}
return result;
}
@Override
public Object plugin(Object target) {
return Plugin.wrap(target, this);
}
@Override
public void setProperties(Properties properties) {
}
/**
* @param object 待检查的对象
* @throws IllegalAccessException
* 通过查询注解@Encrypt或者Encrypted返回的字段,进行解密
* 两种方式互斥
*/
private void decryptField(Encrypted object) throws IllegalAccessException, NoSuchFieldException {
String[] encryptFields = object.getEncryptFields();
String factor = "xu.dm118dAADF!@$";
Class> clazz = object.getClass();
if(encryptFields.length==0){
Field[] fields = clazz.getDeclaredFields();
for (Field field : fields) {
field.setAccessible(true);
Encrypt encrypt = field.getAnnotation(Encrypt.class);
if(encrypt!=null) {
String encryptString = AesUtils.decrypt(field.get(object).toString(), factor);
if(encryptString!=null){
field.set(object,encryptString);
log.debug("Encryption interceptor,encrypt field: {}",field.getName());
}
}
}
}else {
for (String fieldName : encryptFields) {
Field field = clazz.getDeclaredField(fieldName);
field.setAccessible(true);
String encryptString = AesUtils.decrypt(field.get(object).toString(), factor);
if(encryptString!=null && encryptString.length() > 0){
field.set(object,encryptString);
log.debug("Encryption interceptor,encrypt field: {}",field.getName());
}
}
}
}
}
3.5 解密工具类
解密工具类可根据场景进一步优化,例如:可考虑解密类实例化后常驻内存,以减少CPU负载。
/**
* @author: xu.dm
* @since: 2018/11/24 22:26
*
*/
public class AesUtils {
private static final String ALGORITHM = "AES/ECB/PKCS5Padding";
public static String encrypt(String content, String key) {
try {
//获得密码的字节数组
byte[] raw = key.getBytes();
//根据密码生成AES密钥
SecretKeySpec keySpec = new SecretKeySpec(raw, "AES");
//根据指定算法ALGORITHM自成密码器
Cipher cipher = Cipher.getInstance(ALGORITHM);
//初始化密码器,第一个参数为加密(ENCRYPT_MODE)或者解密(DECRYPT_MODE)操作,第二个参数为生成的AES密钥
cipher.init(Cipher.ENCRYPT_MODE, keySpec);
//获取加密内容的字节数组(设置为utf-8)不然内容中如果有中文和英文混合中文就会解密为乱码
byte [] contentBytes = content.getBytes(StandardCharsets.UTF_8);
//密码器加密数据
byte [] encodeContent = cipher.doFinal(contentBytes);
//将加密后的数据转换为字符串返回
return Base64.encodeBase64String(encodeContent);
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("AesUtils加密失败");
}
}
public static String decrypt(String encryptStr, String decryptKey) {
try {
//获得密码的字节数组
byte[] raw = decryptKey.getBytes();
//根据密码生成AES密钥
SecretKeySpec keySpec = new SecretKeySpec(raw, "AES");
//根据指定算法ALGORITHM自成密码器
Cipher cipher = Cipher.getInstance(ALGORITHM);
//初始化密码器,第一个参数为加密(ENCRYPT_MODE)或者解密(DECRYPT_MODE)操作,第二个参数为生成的AES密钥
cipher.init(Cipher.DECRYPT_MODE, keySpec);
//把密文字符串转回密文字节数组
byte [] encodeContent = Base64.decodeBase64(encryptStr);
//密码器解密数据
byte [] byteContent = cipher.doFinal(encodeContent);
//将解密后的数据转换为字符串返回
return new String(byteContent, StandardCharsets.UTF_8);
} catch (Exception e) {
// e.printStackTrace();
// 解密失败暂时返回null,可以抛出runtime异常
return null;
}
}
}
3.6 实体类样例
/**
* (SysUser)实体类
*
* @author xu.dm
* @since 2020-05-02 09:34:53
*/
@EqualsAndHashCode(callSuper = true)
@Data
@NoArgsConstructor
@AllArgsConstructor
@ToString(exclude = {"password","username"},callSuper = true)
public class SysUser extends BaseDO implements Serializable, Encrypted {
private static final long serialVersionUID = 100317866935565576L;
/**
* ID 转换成字符串给前端,否则js会出现精度问题
* 对于前后台传参Long类型64位而言,当前端超过53位后会丢失精度,超过的部分会以00的形式展示.
* 可以使用 @JsonSerialize(using = ToStringSerializer.class)
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/**
* 手机号码
*/
@Encrypt
private String mobile;
/**
* 用户登录名称
*/
private String username;
private String name;
/**
* 密码
*/
@JsonIgnore
private String password;
/**
*/
private String email;
@Override
public String[] getEncryptFields() {
return new String[]{"mobile","name"};
}
}
4、使用类型转换器
在mybatis中使用类型转换器,本质上就是就自定义一个类型(本质就是一个类),通过mybatis提供的TypeHandler接口扩展,对数据类型转换,在这个过程中加入加密和解密业务逻辑实现数据存储和查询的加解密功能。
4.1 定义加密类型
这个类型就直接理解成类似java.lang.String。如果对加密的方式有多种需求,可扩N种EncryptType类型。
/**
* @author: xu.dm
* @since: 2022/3/9 16:54
* 自定义类型,用于在mybatis中表示加密类型
* 需要加密的字段使用EncryptType声明
**/
public class EncryptType {
private String value;
public EncryptType() {
}
public EncryptType(String value) {
this.value = value;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
@Override
public String toString() {
return value;
}
}
4.2 定义类型转换处理器
AesUtils工具类见上文描述。
转换器继承自mybatis的BaseTypeHandler,重写值设置和值获取的方法,在其过程中加入加密和解密逻辑。
/**
* @author: xu.dm
* @since: 2022/3/9 16:21
* 类型转换器,处理EncryptType类型,用于数据加解密
**/
@MappedJdbcTypes(JdbcType.VARCHAR)
@MappedTypes(EncryptType.class)
public class EncryptTypeHandler extends BaseTypeHandler
private String factor = "xu.dm118dAADF!@$";
@Override
public void setNonNullParameter(PreparedStatement ps, int i, EncryptType parameter, JdbcType jdbcType) throws SQLException {
if (parameter == null || parameter.getValue() == null) {
ps.setString(i, null);
return;
}
String encrypt = AesUtils.encrypt(parameter.getValue(),factor);
ps.setString(i, encrypt);
}
@Override
public EncryptType getNullableResult(ResultSet rs, String columnName) throws SQLException {
String decrypt = AesUtils.decrypt(rs.getString(columnName), factor);
if(decrypt==null || decrypt.length()==0){
decrypt = rs.getString(columnName);
}
return new EncryptType(decrypt);
}
@Override
public EncryptType getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
String decrypt = AesUtils.decrypt(rs.getString(columnIndex), factor);
if(decrypt==null || decrypt.length()==0){
decrypt = rs.getString(columnIndex);
}
return new EncryptType(decrypt);
}
@Override
public EncryptType getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
String decrypt = AesUtils.decrypt(cs.getString(columnIndex), factor);
if(decrypt==null || decrypt.length()==0){
decrypt = cs.getString(columnIndex);
}
return new EncryptType(decrypt);
}
}
4.3 配置类型转换器的包路径
这个配置是可选的,因为可以在mapper的映射xml文件中指定。
mybatis:
#xml映射版才需要配置,纯注解版本不需要
mapper-locations: classpath*:mapper/*.xml #多模块指定sql映射文件的位置,需要在classpath后面多加一个星号
type-handlers-package: com.wood.encryption.handler
4.4 测试用的实体类
截取了部分代码,关注代码中使用EncryptType类型的字段name和mobile。
/**
* (TestUser)实体类
*
* @author xu.dm
* @since 2022-03-10 11:31:54
*/
@Data
public class TestUser extends BaseDO implements Serializable {
private static final long serialVersionUID = -53491943096074862L;
/**
* ID
*/
private Long id;
/**
* 手机号码
*/
private EncryptType mobile;
/**
* 用户登录名称
*/
private String username;
/**
* 用户名或昵称
*/
private EncryptType name;
/**
* 密码
*/
private String password;
/**
*/
private String email;
... ...
}
4.5 mapper接口文件
这个类没有本质的变化,截取了部分代码,注意EncryptType类型的使用。
/**
* (TestUser)表数据库访问层
*
* @author xu.dm
* @since 2022-03-10 11:31:54
*/
public interface TestUserDao {
/**
* 查询手机号码,通过主键
*
* @param id 主键
* @return 手机号码
*/
EncryptType queryMobileById(Long id);
/**
* 通过手机号码查询单条数据
*
* @param mobile 手机号码
* @return 实例对象
*/
List
/**
* 通过ID查询单条数据
*
* @param id 主键
* @return 实例对象
*/
TestUser queryById(Long id);
/**
* 查询所有数据,根据入参,决定是否模糊查询
*
* @param testUser 查询条件
*
* @return 对象列表
*/
List
/**
* 统计总行数
*
* @param testUser 查询条件
* @return 总行数
*/
long count(TestUser testUser);
/**
* 新增数据
*
* @param testUser 实例对象
* @return 影响行数
*/
int insert(TestUser testUser);
/**
* 修改数据
*
* @param testUser 实例对象
* @return 影响行数
*/
int update(TestUser testUser);
}
4.6 mapper映射文件
没有本质变化,截取了部分代码,注意EncryptType类型的使用。
select
id, mobile, username, name, password, email, state, level, company_id, dept_id, create_time, update_time
from test_user
where id = #{id}
select
id, mobile, username, name, password, email,vCukbfvWo state, level, company_id, dept_id, create_time, update_time
from test_user
and id = #{id}
and mobile = #{mobile}
and username = #{username}
and name = #{name}
... ...
select mobile from test_user where id = #{id}
select * from test_user where mobile = #{mobile}
insert into test_user(id, mobile, username, name, password, email, state, level, company_id, dept_id, create_time, update_time)
values (#{id}, #{mobile}, #{username}, #{http://name}, #{password}, #{email}, #{state}, #{level}, #{companyId}, #{deptId}, #{createTime}, #{updateTime})
update test_user
mobile = #{mobile},
username = #{username},
name = #{name},
email = #{email},
state = #{state},
level = #{level},
company_id = #{companyId},
dept_id = #{deptId},
update_time = #{updateTime},
where id = #{id}
版权声明:本文内容由网络用户投稿,版权归原作者所有,本站不拥有其著作权,亦不承担相应法律责任。如果您发现本站中有涉嫌抄袭或描述失实的内容,请联系我们jiasou666@gmail.com 处理,核实后本网站将在24小时内删除侵权内容。
发表评论
暂时没有评论,来抢沙发吧~