一. 背景
最近,为了响应国家数据安全的号召,要求应用系统中的数据,在入库时,以最小的代价对敏感数据(电话号码、身份证号等)加密;在以往的系统设计中,并未考虑这一环节。为了减少改造工作量和对业务系统影响最小,本文的实现方式采用注解和拦截器来实现。
二. 实现方式
采用SpringBoot + mybatis的方式实现,此处,只介绍对字符串类型的字段进行加密,后续需要对其它类型的字段加密,可以在此基础上进行扩展。
加密算法:我们使用AES加密算法实现。
使用注解@Intercepts 开启拦截器,@Signature 定义拦截器的具体类型,其中type属性指定当前拦截器使用StatementHandler、ResultSetHandler、ParameterHandler、Executor的一种;method属性指定使用type指定类型的具体方法;args指定预编译语句。
利用反射机制
- 定义敏感数据类注解
SensitiveInfoClazz; - 定义敏感数据类属性注解
SensitiveInfoField; - 入库时加密拦截器
EncryptInterceptor; - 出库时解密拦截器
DecryptInterceptor;
最终的项目层级如下图所示:
三. 实现步骤
-
在自己的数据库中,执行如下
SQL语句,创建一张测试表,表名为:user_infoCREATE TABLE `user_info` ( `id` int NOT NULL AUTO_INCREMENT, `username` varchar(255) DEFAULT NULL, `identity_no` varchar(255) DEFAULT NULL, `real_name` varchar(255) DEFAULT NULL, `mobile` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb3; -
使用
IDEA创建一个SpringBoot项目,在pom.xml文件中引入如下配置<dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.mybatis.spring.boot</groupId> <artifactId>mybatis-spring-boot-starter</artifactId> <version>2.3.0</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.30</version> </dependency> <dependency> <groupId>commons-codec</groupId> <artifactId>commons-codec</artifactId> </dependency> <dependency> <groupId>org.projectlombok</groupId> <artifactId>lombok</artifactId> </dependency> </dependencies> -
在
resource目录下,配置mybatis的配置信息<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <properties> <property name="dialect" value="mysql" /> </properties> <settings> <!-- 开启驼峰匹配 --> <setting name="mapUnderscoreToCamelCase" value="true"/> <!-- 这个配置使全局的映射器启用或禁用缓存。系统默认值是true,设置只是为了展示出来 --> <setting name="cacheEnabled" value="true" /> <!-- 全局启用或禁用延迟加载。当禁用时,所有关联对象都会即时加载。 系统默认值是true,设置只是为了展示出来 --> <setting name="lazyLoadingEnabled" value="true" /> <!-- 允许或不允许多种结果集从一个单独的语句中返回(需要适合的驱动)。 系统默认值是true,设置只是为了展示出来 --> <setting name="multipleResultSetsEnabled" value="true" /> <!--使用列标签代替列名。不同的驱动在这方便表现不同。参考驱动文档或充分测试两种方法来决定所使用的驱动。 系统默认值是true,设置只是为了展示出来 --> <setting name="useColumnLabel" value="true" /> <!--允许 JDBC 支持生成的键。需要适合的驱动。如果设置为 true 则这个设置强制生成的键被使用,尽管一些驱动拒绝兼容但仍然有效(比如 Derby)。 系统默认值是false,设置只是为了展示出来 --> <setting name="useGeneratedKeys" value="false" /> <!--配置默认的执行器。SIMPLE 执行器没有什么特别之处。REUSE 执行器重用预处理语句。BATCH 执行器重用语句和批量更新 系统默认值是SIMPLE,设置只是为了展示出来 --> <setting name="defaultExecutorType" value="SIMPLE" /> <!--设置超时时间,它决定驱动等待一个数据库响应的时间。 系统默认值是null,设置只是为了展示出来 --> <setting name="defaultStatementTimeout" value="25000" /> </settings> </configuration>UserInfoMapper.xml<?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="cn.com.ethan.dao.IUserInfoMapper"> <insert id="save" parameterType="cn.com.ethan.entity.UserInfoEntity"> INSERT INTO user_info (username, identity_no, real_name, mobile) VALUES (#{username}, #{identityNo}, #{realName}, #{mobile}); </insert> <select id="findUserInfo" parameterType="integer" resultType="cn.com.ethan.entity.UserInfoEntity"> SELECT id, username, identity_no AS identityNo, real_name AS realName, mobile FROM user_info WHERE id = #{id} </select> </mapper> -
在application.yml文件中配置数据源,秘钥等
spring: datasource: driver-class-name: com.mysql.cj.jdbc.Driver url: jdbc:mysql://127.0.0.1:3306/dev username: 数据库账号 password: 数据库密码 mybatis: mapper-locations: classpath:mapper/*.xml type-aliases-package: cn.com.ehtan.entity config-location: classpath:mybatis/mybatis-config.xml aes: private_key: 1qaz2wsx3edc4rfv
-
创建敏感信息类注解:
SensitiveInfoClazz@Inherited @Target({ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) public @interface SensitiveInfoClazz { } -
创建敏感信息类属性注解
SensitiveInfoField@Inherited @Target({ElementType.FIELD}) @Retention(RetentionPolicy.RUNTIME) public @interface SensitiveInfoField { } -
加密拦截器
EncryptInterceptor,实现Interceptor接口@Component @Intercepts({@Signature(type = ParameterHandler.class, method = "setParameters", args = PreparedStatement.class)}) public class EncryptInterceptor implements Interceptor { @Resource private EncryptUtilsImpl encryptUtils; @Override public Object intercept(final Invocation invocation) throws Throwable { final ParameterHandler parameterHandler = (ParameterHandler) invocation.getTarget(); final Field declaredField = parameterHandler.getClass().getDeclaredField("parameterObject"); declaredField.setAccessible(true); final Object obj = declaredField.get(parameterHandler); if (Objects.nonNull(obj)) { final Class<?> clazz = obj.getClass(); final SensitiveInfoClazz sensitiveClazz = AnnotationUtils.findAnnotation(clazz, SensitiveInfoClazz.class); if (Objects.nonNull(sensitiveClazz)) { final Field[] fields = clazz.getDeclaredFields(); encryptUtils.encrypt(fields, obj); } } return invocation.proceed(); } @Override public Object plugin(final Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(final Properties properties) { Interceptor.super.setProperties(properties); } } -
解密拦截器
DecryptInterceptor,需要实现Interceptor接口@Component @Intercepts(@Signature(type = ResultSetHandler.class, method = "handleResultSets", args = {Statement.class})) public class DecryptInterceptor implements Interceptor { @Resource private DecryptUtilsImpl decryptUtils; @Override public Object intercept(final Invocation invocation) throws Throwable { final Object proceed = invocation.proceed(); if (Objects.isNull(proceed)) { return null; } if (proceed instanceof ArrayList) { final ArrayList resultList = (ArrayList) proceed; if (!CollectionUtils.isEmpty(resultList) && isDecrypt(resultList.get(0))) { for (final Object obj : resultList) { decryptUtils.decrypt(obj); } } } else { if (isDecrypt(proceed)) { decryptUtils.decrypt(proceed); } } return proceed; } @Override public Object plugin(final Object target) { return Plugin.wrap(target, this); } @Override public void setProperties(final Properties properties) { Interceptor.super.setProperties(properties); } private boolean isDecrypt(final Object obj) { final Class<?> clazz = obj.getClass(); final SensitiveInfoClazz sensitiveClazz = AnnotationUtils.findAnnotation(clazz, SensitiveInfoClazz.class); return Objects.nonNull(sensitiveClazz); } } -
加密工具类接口
IEncryptUtils与实现类EncryptUtilsImpl创建public interface IEncryptUtils { /** * 加密 * @param fieldList paramsObject中声明的字段 * @param paramsObject mapper中paramsType的实例 * @return {@link T} * @param <T> */ <T> T encrypt(final Field[] fieldList, final T paramsObject) throws IllegalAccessException; }@Component public class EncryptUtilsImpl implements IEncryptUtils { @Value("${aes.private_key}") private String privateKey; /** * 加密 * * @param fieldList paramsObject中声明的字段 * @param paramsObject mapper中paramsType的实例 * @return {@link T} */ @Override public <T> T encrypt(Field[] fieldList, T paramsObject) throws IllegalAccessException { final Base64.Decoder decoder = Base64.getDecoder(); for (final Field field : fieldList) { final SensitiveInfoField sensitiveField = field.getAnnotation(SensitiveInfoField.class); if (Objects.nonNull(sensitiveField)) { field.setAccessible(true); final Object obj = field.get(paramsObject); if (obj instanceof String) { final String val = (String) obj; final String value = AES.encrypt(val, privateKey); field.set(paramsObject, value); } } } return paramsObject; } } -
解密工具接口类
IDecryptUtils与实现类DecryptUtilsImpl创建public interface IDecryptUtils { /** * 解密 * @param result resultType实例 * @return {@link T} * @param <T> */ <T> T decrypt(final T result) throws IllegalAccessException; }@Component public class DecryptUtilsImpl implements IDecryptUtils { @Value("${aes.private_key}") private String publicKey; /** * 解密 * * @param result resultType实例 * @return {@link T} */ @Override public <T> T decrypt(final T result) throws IllegalAccessException { final Class<?> clazz = result.getClass(); final Field[] fields = clazz.getDeclaredFields(); for (final Field field : fields) { final SensitiveInfoField sensitiveField = field.getAnnotation(SensitiveInfoField.class); if (Objects.nonNull(sensitiveField)) { field.setAccessible(true); final Object obj = field.get(result); if (obj instanceof String) { final String val = (String) obj; final String value = AES.decrypt(val, publicKey); field.set(result, value); } } } return result; } } -
创建常规的
controller和service类@Service("userInfoServiceImpl") public class UserInfoServiceImpl implements IUserInfoService { @Resource private IUserInfoMapper userInfoMapper; @Override public void save(UserInfoEntity userInfoEntity) { userInfoMapper.save(userInfoEntity); } @Override public UserInfoEntity findUserInfo(final Integer id) { return userInfoMapper.findUserInfo(id); } }UserInfoController.java@RestController public class UserInfoController { @Resource(name = "userInfoServiceImpl") private IUserInfoService userInfoService; @PostMapping(value = "/user/save") public String save() { final UserInfoEntity userInfoEntity = new UserInfoEntity(); userInfoEntity.setUsername("Ethan"); userInfoEntity.setRealName("张三"); userInfoEntity.setIdentityNo("511324199310033223"); userInfoEntity.setMobile("18782975188"); userInfoService.save(userInfoEntity); return userInfoEntity.getUsername(); } @GetMapping(value = "/user/find/{id}") public String findUserInfo(@PathVariable final Integer id) { final UserInfoEntity userInfo = userInfoService.findUserInfo(id); return userInfo.toString(); } } -
在
postman中请求接口:http://localhost:8080/user/save,然后在数据库中查询数据,结果如下
-
在
postman中请求接口:http://localhost:8080/user/find/33,得到如下结果,已经实现了入库加密,出库解密的效果
四. 总结
- 敏感数据入库加密的方式有很多种,本文只是介绍了一下,自己采取的实现方式,可以很快实现用户需求,但也有不足,入库和出库的加密与解密,会影响接口响应时间。
- 使用本文的这种方式,需要了解Java的反射机制、明白拦截器等实现方式。