1.概述

近来咱们都在围绕着运用Spring Boot开发事务体系时怎么确保数据安全性这个主题展开总结,当下大部分的B/S架构的体系也都是依据Spring Boot + SpringMVC三层架构开发的,之前咱们总结了在接口层面怎么加固数据安全性:Spring Boot怎么高雅进步接口数据安全性,能够认为是在SpringMVC的三层架构中的controller层(逻辑操控层)对接口数据进行安全处理操作,更直接点说就是在接口请求参数传入进行逻辑处理或许呼应参数输出到页面展现之前进行数据处理的,所以仅仅在SpringMVC三层架构中的一层中进行安全加固,还不是很稳固,接下来今日咱们就再来讲讲在SpringMVC三层架构另一层中怎么进行数据安全加固,在进入今日主题之前先来看看什么是SpringMVC架构?

什么是SpringMVC三层架构?

SpringMVC的工程结构一般来说分为三层,自下而上是Modle层(模型,数据拜访层)、Cotroller层(操控,逻辑操控层)、View层(视图,页面显现层),其间Modle层分为两层:dao层、service层,MVC架构分层的首要作用是解耦。选用分层架构的长处,普遍承受的是体系分层有利于体系的保护,体系的扩展。就是增强体系的可保护性和可扩展性。关于Spring这样的结构,(View\Web)表明层调用操控层(Controller),操控层调用事务层(Service),事务层调用数据拜访层(Dao) 能够这么说,现在90%以上的事务体系都是依据该三层架构形式开发的,这种架构形式也有人说是设计形式中一种,可见其重要性显而易见,所以咱们需注重。

咱们也都知道在日常开发体系过程中,数据安全是十分重要的。特别是在当今互联网年代,个人隐私安全极其重要,一旦个人用户数据遭到攻击走漏,将会形成灾难级的事故问题。一切之前咱们依据接口层进行数据安全处理是远远不够的,今日咱们就来谈谈怎么Model层(数据拜访层)怎样做到高雅数据加密存储、含糊匹配及其脱敏展现,本文的主题:数据加密存储、含糊匹配和脱敏展现

银行体系对数据安全性的要求在事务体系中是名列前茅的,所以今日咱们就以常见的个人银行账户数据:暗码、手机号、具体地址、银行卡号等信息字段为例,进行主题的宣讲与浅析。

项目引荐:依据SpringBoot2.x、SpringCloud和SpringCloudAlibaba企业级体系架构底层结构封装,解决事务开发时常见的非功用性需求,防止重复造轮子,便利事务快速开发和企业技术栈结构统一办理。引进组件化的思维完成高内聚低耦合而且高度可配置化,做到可插拔。严格操控包依靠和统一版本办理,做到最少化依靠。注重代码标准和注释,十分适合个人学习和企业运用

Github地址:github.com/plasticene/…

Gitee地址:gitee.com/plasticene3…

微信大众号Shepherd进阶笔记

2.数据加密存储

咱们之前总结的是在接口层进行数据加解密传输,也强调过这种办法确保不了数据的肯定安全,仅仅有效进步接口数据安全性,举高数据被抓取的门槛而已。所以接下来咱们就来讲述一下怎么在数据的源头存储层确保其安全。咱们都知道一些中心私密字段,比如说暗码,手机号等在数据库层存储就不能明文存储,必须加密存储确保即使数据库走漏了也不会轻易曝光数据。

2.1 高雅完成数据库字段加解密原理

Mybatis-plus供给企业高级特性就有支撑数据加密解密,不过是收费的。。。可是咱们能够细细探究其原理进行功用的自我完成。

其实在咱们上面引荐的快速开发结构中就已经高雅整合了数据加解密功用了,EncryptTypeHandler:完成数据库的字段加密与解密。

默认供给了依据base64加密算法Base64EncryptService和AES加密算法AESEncryptService,当然事务侧也能够自定义加密算法,这需求完成接口EncryptService,并把完成类注入到容器中即可。加密功用中心逻辑

@Bean
@ConditionalOnMissingBean(EncryptService.class)
public EncryptService encryptService() {
 Algorithm algorithm = encryptProperties.getAlgorithm();
 EncryptService encryptService;
 switch (algorithm) {
  case BASE64:
   encryptService = new Base64EncryptService();
   break;
  case AES:
   encryptService = new AESEncryptService();
   break;
  default:
   encryptService = null;
  }
 return encryptService;
}

接下来就能够依据加密算法,扩展mybatis的typeHandler对实体字段数据进行加密解密了:EncryptTypeHandler

public class EncryptTypeHandler<T> extends BaseTypeHandler<T> {
​
  @Resource
  private EncryptService encryptService;
​
  @Override
  public void setNonNullParameter(PreparedStatement ps, int i, Object parameter, JdbcType jdbcType) throws SQLException {
    ps.setString(i, encryptService.encrypt((String)parameter));
   }
  @Override
  public T getNullableResult(ResultSet rs, String columnName) throws SQLException {
    String columnValue = rs.getString(columnName);
    return StrUtil.isBlank(columnValue) ? (T)columnValue : (T)encryptService.decrypt(columnValue);
   }
​
  @Override
  public T getNullableResult(ResultSet rs, int columnIndex) throws SQLException {
    String columnValue = rs.getString(columnIndex);
    return StrUtil.isBlank(columnValue) ? (T)columnValue : (T)encryptService.decrypt(columnValue);
   }
​
  @Override
  public T getNullableResult(CallableStatement cs, int columnIndex) throws SQLException {
    String columnValue = cs.getString(columnIndex);
    return StrUtil.isBlank(columnValue) ? (T)columnValue : (T)encryptService.decrypt(columnValue);
   }
}
​
​

2.2 加密与解密示例

首要创立一张user表:

CREATE TABLE `user` (
 `id` bigint(20) NOT NULL,
 `name` varchar(255) DEFAULT NULL COMMENT '姓名',
 `phone` varchar(255) DEFAULT NULL COMMENT '手机号',
 `id_card` varchar(255) DEFAULT NULL COMMENT '身份证号',
 `bank_card` varchar(255) DEFAULT NULL COMMENT '银行卡号',
 `address` varchar(255) DEFAULT NULL COMMENT '住址',
 PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

这时候咱们正常刺进一条数据:

  @Test
  public void test() {
    User user = new User();
    user.setName("shepherd");
    user.setMobile("17812345678");
    user.setIdCard("213238199601182111");
    user.setBankCard("3222022046741500");
    user.setAddress("杭州市余杭区未来科技城");
    userDAO.insert(user);
   }

数据库存储查询成果如下:

id name mobile id_card bank_card address
1567402046481436673 shepherd 17812345678 213238199601182111 3222022046741500 杭州市余杭区未来科技城

这就是咱们平常不加密存储查询的成果,这里id是经过分布式id算法主动生成的哈。

接下来咱们来看看完成对数据的加密,只需求在配置文件配置运用哪一种加密算法和在实体类的字段特点加上注解@TableField(typeHandler = EncryptTypeHandler.class)即可。

这里咱们运用aes加密算法:

ptc:
  encrypt:
   algorithm: aes

实体类:

@Data
@TableName(autoResultMap = true)
public class User {
​
  private Long id;
  private String name;
​
  @TableField(typeHandler = EncryptTypeHandler.class)
  private String mobile;
  @TableField(typeHandler = EncryptTypeHandler.class)
  private String idCard;
  @TableField(typeHandler = EncryptTypeHandler.class)
  private String bankCard;
  @TableField(typeHandler = EncryptTypeHandler.class)
  private String address;
}

再次刺进数据,数据库存储查询成果如下:

id name mobile id_card bank_card address
1567405175268642818 shepherd 9MgWngwLcd/vbYYYpG9pGQ== 97vlZQahK+y548ofQbXlW9JUwuzuj3xCkNF/is1KLa4= 2oQv5+y4+rVyN23IzudtOz+Zd7Aj1Bv2toBzmnwTXxo= 0Wj7qqLl6jWkBu+TcxuwGYcdIjv+zIJHDM7d1dU/c8D2jc2wLp+zVvpSwBKWjX44

然后咱们能够测验对这条数据进行查询:

  @Test
  public void get() {
    User user = userDAO.selectById(1567405175268642818l);
    System.out.println(user);
   }
​

成果如下:

User(id=1567405175268642818, name=shepherd, mobile=17812345678, idCard=213238199601182111, bankCard=3222022046741500, address=杭州市余杭区未来科技城)

依据以上完美展现了数据加密存储和解密查询。

2.3 数据加密后怎么进行含糊匹配

暗码、手机号、具体地址、银行卡号这些信息对加解密的要求也不一样,比如说暗码咱们需求加密存储,一般运用的都是不可逆的慢hash算法,慢hash算法能够防止暴力破解(典型的用时刻换安全性)。

在检索时咱们既不需求解密也不需求含糊查找,直接运用密文完全匹配,可是手机号就不能这样做,因为手机号咱们要检查原信息,而且对手机号还需求支撑含糊查找,因此咱们今日就针对可逆加解密的数据支撑含糊查询来看看有哪些完成办法。

咱们接下来看看惯例的做法,也是最广泛运用的办法,此类办法及满意的数据安全性,又对查询友爱。

  • 在数据库完成加密算法函数,在含糊查询的时候运用decode(key) like '%partial%

    在数据库中完成与程序共同的加解密算法,修正含糊查询条件,运用数据库加解密函数先解密再含糊查找,这样做的长处是完成成本低,开发运用成本低,只需求将以往的含糊查找略微修正一下就能够完成,可是缺陷也很明显,这样做无法运用数据库的索引来优化查询,乃至有一些数据库或许无法确保与程序完成共同的加解密算法,可是关于惯例的加解密算法都能够确保与应用程序共同。假如对查询功能要求不是特别高、对数据安全性要求一般,能够运用常见的加解密算法比如说AES、DES之类的也是一个不错的挑选。

  • 对密文数据进行分词组合,将分词组合的成果集分别进行加密,然后存储到扩展列,查询时经过key like '%partial%' [先对字符进行固定长度的分组,将一个字段拆分为多个,比如说依据4位英文字符(半角),2个中文字符(全角)为一个检索条件,举个例子

    shepherd运用4个字符为一组的加密办法,第一组shep ,第二组heph ,第三组ephe ,第四组pher … 顺次类推。

    假如需求检索一切包含检索条件4个字符的数据比如:pher ,加密字符后经过 key like “%partial%” 查库。

    分词加密完成

      public static String splitValueEncrypt(String value, int splitLength) {
        //检查参数是否合法
        if (StringUtils.isBlank(value) && splitLength <= 0) {
          return null;
         }
        String encryptValue = "";
    ​
        //获取整个字符串能够被切割成字符子串的个数
        int n = (value.length() - splitLength + 1);
    ​
        //分词(规则:分词长度依据【splitLength】且每次切割的开端跟完毕下标加一)
        for (int i = 0; i < n; i++) {
          String splitValue = value.substring(i, splitLength++);
          encryptValue += encrypt(splitValue);
         }
    ​
        return encryptValue;
       }
    ​
      /**
       * 获取加密值
       *
       * @param value 加密值
       * @return
       */
      private static String encrypt(String value) {
        // 这里进行加密
        return null;
       }
    

    依据上面分词加密保存到扩展列,同时要求对原字段的正修改查对需求对其相应的扩展列适配,还要注意因为分词之后导致扩展列的长度或许是原字段几倍乃至几十倍,所以必须在开发之前挑选和合适分词长度和加密算法,一旦加密开端之后,再更改成本就较高了。像假如手机号咱们只支撑后8位搜索、身份证号只支撑后4位搜索,这样咱们就能够经过原字段截取后边位数直接加密存储到扩展列,不需求再分词。

    3.数据脱敏

    实际的事务开发过程中,咱们常常需求对用户的隐私数据进行脱敏处理。所谓脱敏处理其实就是将数据进行混淆隐藏,例如用户手机信息展现178****5939,以免走漏个人隐私信息。

    3.1完成思路

    思路比较简单:在接口回来数据之前按要求对数据进行脱敏加工之后再回来前端。

    一开端打算用@ControllerAdvice去完成,但发现需求自己去反射类获取注解,当回来对象比较复杂,需求递归去反射,功能一会儿就会降低,所以换种思路,我想到平常运用的@JsonFormat,跟我现在的场景很类似,经过自定义注解跟字段解析器,对字段进行自定义解析。

    脱敏字段类型枚举

    public enum MaskEnum {
      /**
       * 中文名
       */
      CHINESE_NAME,
      /**
       * 身份证号
       */
      ID_CARD,
      /**
       * 座机号
       */
      FIXED_PHONE,
      /**
       * 手机号
       */
      MOBILE_PHONE,
      /**
       * 地址
       */
      ADDRESS,
      /**
       * 电子邮件
       */
      EMAIL,
      /**
       * 银行卡
       */
      BANK_CARD
    }
    

    脱敏注解类:用在脱敏字段之上

    @Retention(RetentionPolicy.RUNTIME)
    @JacksonAnnotationsInside
    @JsonSerialize(using = MaskSerialize.class)
    public @interface FieldMask {
    ​
      /**
       * 脱敏类型
       * @return
       */
      MaskEnum value();
    }
    ​
    

    脱敏序列化类

    public class MaskSerialize extends JsonSerializer<String> implements ContextualSerializer {
    ​
      /**
       * 脱敏类型
       */
      private MaskEnum type;
    ​
    ​
      @Override
      public void serialize(String s, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
        switch (this.type) {
          case CHINESE_NAME:
           {
            jsonGenerator.writeString(MaskUtils.chineseName(s));
            break;
           }
          case ID_CARD:
           {
            jsonGenerator.writeString(MaskUtils.idCardNum(s));
            break;
           }
          case FIXED_PHONE:
           {
            jsonGenerator.writeString(MaskUtils.fixedPhone(s));
            break;
           }
          case MOBILE_PHONE:
           {
            jsonGenerator.writeString(MaskUtils.mobilePhone(s));
            break;
           }
          case ADDRESS:
           {
            jsonGenerator.writeString(MaskUtils.address(s, 4));
            break;
           }
          case EMAIL:
           {
            jsonGenerator.writeString(MaskUtils.email(s));
            break;
           }
          case BANK_CARD:
           {
            jsonGenerator.writeString(MaskUtils.bankCard(s));
            break;
           }
         }
       }
    ​
      @Override
      public JsonSerializer <?> createContextual(SerializerProvider serializerProvider, BeanProperty beanProperty) throws JsonMappingException {
        // 为空直接越过
        if (beanProperty == null) {
          return serializerProvider.findNullValueSerializer(beanProperty);
         }
        // 非String类直接越过
        if (Objects.equals(beanProperty.getType().getRawClass(), String.class)) {
          FieldMask fieldMask = beanProperty.getAnnotation(FieldMask.class);
          if (fieldMask == null) {
            fieldMask = beanProperty.getContextAnnotation(FieldMask.class);
           }
          if (fieldMask != null) {
            // 假如能得到注解,就将注解的 value 传入 MaskSerialize
            return new MaskSerialize(fieldMask.value());
           }
         }
        return serializerProvider.findValueSerializer(beanProperty.getType(), beanProperty);
       }
    ​
      public MaskSerialize() {}
    ​
      public MaskSerialize(final MaskEnum type) {
        this.type = type;
       }
    }
    

    3.2运用示例

    在发送短信记载的接口上对手机号进行脱敏:

      @FieldMask(MaskEnum.MOBILE_PHONE)
      private String mobile;
    

    调用接口回来数据如下:

    {
     "code": 200,
     "msg": "OK",
     "data": {
      "list": [
        {
        "id": 1565599123774607362,
        "signId": 8389008488923136,
        "templateId": 8445337328943104,
        "templateType": 1,
        "content": "可爱的${name},博客文章已于${submitTime}上传更新,请抽空阅读。",
        "channelType": 0,
        "mobile": "178****5939",
        "sendStatus": 0,
        "receiveStatus": 0
        }
       ],
      "total": 19,
      "pages": 19
      }
    }
    

    4.总结

    依据上面内容咱们总结怎么在数据存储层进行数据安全加固来达到体系的更安全性,能够这么说没有最安全的体系只要更安全的体系。所以咱们在开发历程中都会穷极终身去加固体系安全功能。当然了,加强体系安全性的办法还有很多种,咱们最近仅仅围绕依据Spring BootSpringMVC结构中有效高雅地完成数据安全性,感兴趣的小伙伴能够自行了解其他加固办法。(近来复阳了,状况不是很好,功率不高所以有点迁延更新啦)

声明:本站所有文章,如无特殊说明或标注,均为本站原创发布。任何个人或组织,在未征得本站同意时,禁止复制、盗用、采集、发布本站内容到任何网站、书籍等各类媒体平台。如若本站内容侵犯了原著者的合法权益,可联系我们进行处理。