SpringMvc全局id自动加密

目录
  1. 性能与安全的权衡
  2. 思路
  3. 方案
  4. 实现细节
    1. 定义个序列化器
    2. 定义一个反序列化器
    3. 定义一个Long类型解析器
    4. 修改Mvc配置
    5. 类关系图

性能与安全的权衡

对数据库Id字段的设计,各种五花八门的都有。

  • Long类型Id

数据库查询,数值比字符串性能高,正常来说会把主键设计为自增的Long类型,其他表关联也通过该字段来关联,但字段暴露到前端去,用户可以通过简单的修改id获取刷取整站的记录,安全性上存在一定的问题。

  • 字符类型Id

使用字符串如uuid之类的做主键,字段安全属性,但查询效率低下。

如何做到安全与高效兼得呢。
 ​

思路

在接口层面将所有id在输出到前端的时候自动做加密,前端提交过来的时候,再自动解密处理成数值类型。
这样是不是就能满足要求了。

  • 对前端来说是一个String,顺便解决了大整数javascript无法处理的问题。
  • 对服务端业务层来说,因框架层面做了解密处理,看到的永远是Long类型的。

需要处理的请求信息包括,query、body、header及respnose里面的id字段  

方案

对于互联网来说,任何问题都可以加入一个虚拟的层来解决。

本问题的层,可以在api层,可以在存储层。

  • 存储层

所有Id字段在数据库中是Long类型,但在代码模型上定义为String类型,对DAO做一层封装,统一对Id进行转换,如where id =“xxxxx” 会变成 where id = 111 ,where id in (“xxx”,“yyy”) 变为 where id in(111,222),查询出来的id,在装载模型的时候,自动进行加密处理。
但这存在一个问题,代码中的所有id数据都是加密的,管理后台根C端都会被加密

  • 雪花算法

类型还是Long,值空间也挺大,只要原样通过ToStringSerializer解析成字符串给前端,就能解决javascript大整数的问题。query参数转化问题,也天然支持。

  • api层

spring mvc的扩展性足够好,我们可以通过对C端的api进行扩展来满足需求。

实现细节

本文只对API层的处理,做介绍。
为了实现简单对系统做了如下约束:

  • 字段或变量名为id或Id结尾的,Long类型的id字段的充要条件。

  • id不会在header中出现,即我们可以不用处理header

    定义个序列化器

    • 重写serialize方法,从gen参数中,可以获取到对应字段的名称

    • 对接口的输出参数识别id字段,并对其进行加密处理,否则原样输出

    • 加密逻辑可以任意替换

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      @JacksonStdImpl
      public class LongStringSerializer extends ToStringSerializerBase {
      public static final LongStringSerializer instance = new LongStringSerializer();

      public LongStringSerializer() {
      super(Object.class);
      }

      public LongStringSerializer(Class<?> handledType) {
      super(handledType);
      }

      @Override
      public void serialize(Object value, JsonGenerator gen, SerializerProvider provider) throws IOException {

      if(gen.getOutputContext().getCurrentName()!=null
      && (gen.getOutputContext().getCurrentName().equals("id") ||
      gen.getOutputContext().getCurrentName().endsWith("Id"))){
      super.serialize(value, gen, provider);
      }
      else{
      gen.writeNumber((Long)value);
      }
      }

      @SneakyThrows
      public final String valueToString(Object value) {
      if(value == null ){
      return null;
      }
      return CryptUtils.encrypt(value + "", "idEn");
      }

      }

      定义一个反序列化器

    • 覆盖deserialize方法,从jsonParser参数中可获取字段的名称

    • 判断id字段,并对其进行解密操作

    • 解密方法需跟加密方法配套

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      @JacksonStdImpl
      public class LongStringDeSerializer extends JsonDeserializer<Long> {

      public static final LongStringDeSerializer instance = new LongStringDeSerializer();

      @SneakyThrows
      @Override
      public Long deserialize(JsonParser jsonParser, DeserializationContext deserializationContext) throws IOException, JsonProcessingException {
      if (jsonParser.getCurrentName() == null
      || jsonParser.getCurrentName().equals("id")
      || jsonParser.getCurrentName().endsWith("Id")) {
      return (Long.parseLong(CryptUtils.decrypt(UriEncoder.decode(jsonParser.getText()), "idEn")));
      } else {
      return Long.parseLong(jsonParser.getText());
      }
      }
      }

      定义一个Long类型解析器

    • 这个解析器主要用来解析请求参数

    • 处理Long类型即Search对象类型

    • 调用上面的 LongStringDeSerializer对字段进行解密处理

    • 这里暂时只处理了满足项目需求的场景,更多场景需自行细化

      1
      2
      3
      4
      5
      6
      7
      8
      9
      10
      11
      12
      13
      14
      15
      16
      17
      18
      19
      20
      21
      22
      23
      24
      25
      26
      27
      28
      29
      30
      31
      32
      33
      34
      35
      36
      37
      38
      39
      40
      41
      42
      43
      44
      45
      46
      47
      48
      49
      50
      51
      52
      53
      54
      55
      56
      57
      58
      59
      60
      61
      62
      public class LongArgumentResolver implements HandlerMethodArgumentResolver {
      @Override
      public boolean supportsParameter(MethodParameter parameter) {
      return parameter.getParameterType().equals(Long.class) || SearchBase.class.isAssignableFrom(parameter.getParameterType());
      }

      @Override
      public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer, NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
      ObjectMapper objectMapper = new ObjectMapper();

      SimpleModule simpleModule = new SimpleModule();
      simpleModule.addDeserializer(Long.class, LongStringDeSerializer.instance);
      simpleModule.addDeserializer(Long.TYPE, LongStringDeSerializer.instance);
      objectMapper.registerModule(simpleModule);

      if (parameter.getParameterType().equals(Long.class)) {
      var query = ((ServletWebRequest) webRequest).getRequest().getQueryString();
      var map = getQueryParams(query);
      var data = UriEncoder.decode(map.entrySet().stream().findFirst().map(Map.Entry::getValue).orElse(null));
      if (Longs.tryParse(data) != null) {
      return data;
      }
      return objectMapper.readValue(JSON.toJSONString(data)
      , parameter.getParameterType());
      }

      if (SearchBase.class.isAssignableFrom(parameter.getParameterType())) {

      if (((ServletWebRequest) webRequest).getHttpMethod().equals(HttpMethod.GET)) {
      var query = ((ServletWebRequest) webRequest).getRequest().getQueryString();
      var map = getQueryParams(query);
      return objectMapper.readValue(
      JSON.toJSONString(map), parameter.getParameterType());
      }
      }
      return null;
      }

      /**
      * Retrieve the query parameters from given url
      *
      * @return params Map with query parameters
      * @throws IOException
      */
      static public Map<String, String> getQueryParams(String query)
      throws IOException {
      Map<String, String> params = new HashMap<String, String>();
      if (query == null) {
      return params;
      }

      Arrays.stream(query.split("&")).forEach(e -> {
      var token = e.split("=");
      if (token.length == 2) {
      params.put(token[0], token[1]);
      }
      });

      return params;
      }
      }

修改Mvc配置

  • 添加HttpMessageConverter,将LongStringSerializer及LongStringDeSerializer配置为Long类型的序列化处理器
  • 添加HandlerMethodArgumentResolver,将LongArgumentResolver配置为请求参数的处理器
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    @Configuration
    public class InterceptorConfig implements WebMvcConfigurer {
    @Override
    public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
    MappingJackson2HttpMessageConverter jackson2HttpMessageConverter = new MappingJackson2HttpMessageConverter();

    ObjectMapper objectMapper = objectMapper();

    jackson2HttpMessageConverter.setObjectMapper(objectMapper);
    converters.add(jackson2HttpMessageConverter);
    }

    @Bean
    ObjectMapper objectMapper() {
    ObjectMapper objectMapper = new ObjectMapper();
    SimpleModule simpleModule = new SimpleModule();
    simpleModule.addSerializer(Long.class, LongStringSerializer.instance);
    simpleModule.addSerializer(Long.TYPE, LongStringSerializer.instance);
    simpleModule.addDeserializer(Long.class, LongStringDeSerializer.instance);
    simpleModule.addDeserializer(Long.TYPE, LongStringDeSerializer.instance);
    objectMapper.registerModule(simpleModule);
    return objectMapper;
    }

    @Bean
    public WebMvcConfigurer webMvcConfigurer() {
    return new WebMvcConfigurerAdapter() {
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
    argumentResolvers.add(new LongArgumentResolver());
    }
    };
    }
    }

类关系图

img