mybatis 加密,mybatis字段加密解密
00-1010 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.5映射器接口文件4.6映射器映射文件。实现数据加密和解密的方法多种多样,在mybatis环境下数据加密和解密变得非常简单易用。本文旨在提供参考。在生产中,应尽可能完成单元测试,并进行足够的覆盖测试,以验证可靠性、可用性和安全性。
00-1010 * *原要求:* *数据保存时加密,取出时解密,避免拖库时泄露敏感信息。
* *初步分析:* *数据来自前端,到达后端,经过业务逻辑后存储在数据库中,要经过三个步骤:
1.前端和后端之间的传输加密了吗?如果需要加密,传输前需要加密前端,暂时可以用HTTPS代替;2.到了后端,数据通常需要一些逻辑判断,所以加密没有意义,反而会带来不必要的麻烦;3.入库,这是最后一步。数据通过insert的sql或update语句存储在仓库中,之前需要加密;
* *核心要求:* *入库前的最后一步是完成数据加密。达到的目的是在数据库暴露的情况下,在一定程度上保证数据的安全性,也可以防止有数据操作权限的人泄露数据。
* *加密算法:* *对称和非对称算法都可以使用。考虑到加密和解密的效率以及场景,可以考虑选择对称算法AES进行加密。
**ORM环境:**mybatis
* *加密字段:* *加密字段是不确定的,所以在设计数据库表的时候要确定敏感字段,也就是加密字段是可以定制的。
应注意的细节:
1.某个字段加密后,其字段的访问性能下降。加密字段越多,性能下降越多,没有具体指标;2.字段加密后,这个字段的索引就没有太大的意义了。例如,当手机号码字段被加密时,它可能被设计为唯一的索引,以防止号码被重复。加密后密文性能下降,比较结果不直观,没有大量数据验证,理论上密文不会一样;3.有些SQL比较是不能直接实现的,比如手机号匹配查询。在开发和运维中,需要考虑后续工作中敏感领域的可操作性;4.原字段长度需要扩展,密文肯定比原文长;5.不要加密主键(真的,有人会这么做);6.有时候为了减少关联查询,我们会在表上做冗余字段,比如把姓名字段放到业务表中。如果name字段是加密的,那么冗余表需要同步加密。所以在需要数据加密的时候,要考虑全局。
最后:数据加密用来提高安全性的同时,必然会牺牲整个程序性能和易用性。
00-1010在mybatis的依赖环境中,至少有两种自动加密的方式:
1.使用拦截器拦截insert和update语句,获取要加密的字段,加密后存储在数据库中。读取时拦截查询,解密后存入结果对象;2.请使用类型转换器TypeHandler来实现它。
目录
00-1010由于mybatis拦截器会拦截所有符合签名的请求,为了提高效率,定义了标签接口非常重要。既然有接口,不如把需要加密的字段信息加入到接口中,也可以根据实际场景来设计。
/* * * @ author : Xu . DM * @ since : 2022/3/8 16336030 *该接口用于标记需要加密的实体类,通过getEncryptFields返回具体的加密内容字段。*注意:getEncryptFields和@Encrypt批注可以一起使用,也可以互斥使用,可以根据具体需求实现。* */公共接口加密{/* *
00-1010主要针对一些场景直接标记实体类的字段,直观的表示该字段是加密的。一些业务逻辑也可以依靠这个标志进行进一步的操作。一句话,根据场景改编设计。
/* * * @ author : Xu . DM * @ since 3360 2022/3/8 *标识加密评论,值暂时不可用。
用,根据需要可以考虑采用的加密方式与算法等 * 注意: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("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; /** * email */ 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<EncryptType> { 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 */@Datapublic 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; /** * email */ 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<TestUser> queryByMobile(EncryptType mobile); /** * 通过ID查询单条数据 * * @param id 主键 * @return 实例对象 */ TestUser queryById(Long id); /** * 查询所有数据,根据入参,决定是否模糊查询 * * @param testUser 查询条件 * * @return 对象列表 */ List<TestUser> queryByBlurry(TestUser testUser); /** * 统计总行数 * * @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
类型的使用。
<?xml version="1.0" encoding="UTF-8"?><!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"><mapper namespace="com.wood.system.dao.TestUserDao"> <resultMap type="com.wood.system.entity.TestUser" id="TestUserMap"> <result property="id" column="id" jdbcType="INTEGER"/> <result property="mobile" column="mobile" jdbcType="VARCHAR"/> <result property="username" column="username" jdbcType="VARCHAR"/> <result property="name" column="name" jdbcType="VARCHAR"/> <result property="password" column="password" jdbcType="VARCHAR"/> <result property="email" column="email" jdbcType="VARCHAR"/> <result property="state" column="state" jdbcType="VARCHAR"/> <result property="level" column="level" jdbcType="VARCHAR"/> <result property="companyId" column="company_id" jdbcType="INTEGER"/> <result property="deptId" column="dept_id" jdbcType="INTEGER"/> <result property="createTime" column="create_time" jdbcType="TIMESTAMP"/> <result property="updateTime" column="update_time" jdbcType="TIMESTAMP"/> </resultMap> <!--查询单个--> <select id="queryById" resultMap="TestUserMap"> select id, mobile, username, name, password, email, state, level, company_id, dept_id, create_time, update_time from test_user where id = #{id} </select> <!--查询指定行数据--> <select id="queryByBlurry" resultMap="TestUserMap"> select id, mobile, username, name, password, email, state, level, company_id, dept_id, create_time, update_time from test_user <where> <if test="id != null"> and id = #{id} </if> <if test="mobile != null and mobile != "> and mobile = #{mobile} </if> <if test="username != null and username != "> and username = #{username} </if> <if test="name != null and name != "> and name = #{name} </if>... ... </where> </select> <select id="queryMobileById" resultType="com.wood.encryption.type.EncryptType"> select mobile from test_user where id = #{id} </select> <select id="queryByMobile" resultType="com.wood.system.entity.TestUser"> select * from test_user where mobile = #{mobile} </select> <!--新增所有列--> <insert id="insert" keyProperty="id" useGeneratedKeys="false"> insert into test_user(id, mobile, username, name, password, email, state, level, company_id, dept_id, create_time, update_time) values (#{id}, #{mobile}, #{username}, #{name}, #{password}, #{email}, #{state}, #{level}, #{companyId}, #{deptId}, #{createTime}, #{updateTime}) </insert> <!--通过主键修改数据--> <update id="update"> update test_user <set> <if test="mobile != null and mobile != "> mobile = #{mobile}, </if> <if test="username != null and username != "> username = #{username}, </if> <if test="name != null and name != "> name = #{name}, </if> <if test="email != null and email !=
郑重声明:本文由网友发布,不代表盛行IT的观点,版权归原作者所有,仅为传播更多信息之目的,如有侵权请联系,我们将第一时间修改或删除,多谢。