springboot2 - 自定义cache

功能

首先要说明一下,这不是教你怎么写一个缓存,而是让项目中的 @Cacheable、@CacheEvict 等注解变得可用。

概要

从编码方面考虑,找到 org.springframework.cache.Cache 接口,实现这个接口就搞定了(结尾附上样例代码)。

这个过程,有很多风险项,本文重点讨论一下这些问题。

基本原理——序列化和反序列化

缓存的实现,至少包含两个技术:序列化和反序列化,也就是把对象转成字节,再将字节转成对象。

计算机专业的同学,应该非常熟悉:java 中有 Serializable 接口可以用。

使用 java 自带的序列化功能,是个很好的选择,性能不会太差,也不需要去思考更多的问题。

JSON 等新工具的反序列化问题

有野心的程序员,可能会思考:能不能用一些更先进的序列化技术?比如:fastjson、jackson。

使用 fastjson 反序列化的时候,就会想到这个函数: JSON.parseObject(String json, Class clazz)。

实际操作就会发现问题,反序列化函数是 deserialize(byte[] bytes),没有 class 这个参数的。

(用 RedisSerializer 举例,因为代码更短,更容易说明问题,spring 的 cache 有相同的问题)

没有 class 参数,那要怎么序列化?这是第一个要思考的问题。

import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONReader;
import com.alibaba.fastjson2.JSONWriter;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.SerializationException;

/**
 * Redis 使用 FastJson 序列化数据,满足所有数据类型的序列化
 *
 * @author Mr.css
 * @version 2020-01-02 11:24
 */
public class FastJsonRedisSerializer implements RedisSerializer<Object> {

    /**
     * 包含各类序列化配置
     */
    private final JSONWriter.Feature[] features;
    /**
     * 反序列化拦截器
     */
    private final JSONReader.AutoTypeBeforeHandler filter;


    /**
     * 默认序列化的时候,写入全类名
     *
     * @param names 允许自动转型的包
     */
    public FastJsonRedisSerializer(String... names) {
        this.features = new JSONWriter.Feature[]{JSONWriter.Feature.WriteClassName};
        this.filter = JSONReader.autoTypeFilter(names);
    }

    /**
     * 序列化
     *
     * @param obj 对象实体
     * @return 字节数组
     * @throws SerializationException -
     */
    @Override
    public byte[] serialize(Object obj) throws SerializationException {
        if (obj == null) {
            return new byte[0];
        } else {
            return JSON.toJSONBytes(obj, features);
        }
    }

    /**
     * 反序列化
     *
     * 不在白名单的对象,不会反序列化失败,而是返回 com.alibaba.fastjson2.JSONObject,
     *
     * @param bytes 字节数组
     * @return 对象实体
     * @throws SerializationException -
     */
    @Override
    public Object deserialize(byte[] bytes) throws SerializationException {
        if (bytes == null || bytes.length == 0) {
            return null;
        } else {
            return JSON.parseObject(bytes, Object.class, filter);
        }
    }
}

反序列程序漏洞

如果你选用的是 fastjson,很快就能找到资料,它有个 JSON.parse(String json) 函数。

这个函数不需要 class 参数,可以解决我们前面提到的问题,但是与此同时,又引出了一个值得重视的问题:

JSON.parse(String json) 只有一个参数,json 能转换成什么对象,完全由 json 自身控制。

如果掌握了 json 的规律,就能伪造 json,控制程序创建出所需的对象,这是一个十分严重的漏洞。

比如说:你的缓存用的是 redis,redis 被黑客攻破了,他就可以继续攻击你的系统,伪造一段 json,你的程序读到 json 之后,就会 new 出他所需的对象。

针对这个问题,需要考虑一份白名单设计,确定哪些对象是允许反序列化的。

(fastjson 有对应的解决方案,这里不展开说明了)

自定义Cache

CacheConfig

配置注册到容器

import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.context.annotation.Bean;

/**
 * @author Mr.css
 * @date 2019/12/23
 */
@Configuration
public class CacheConfig extends CachingConfigurerSupport {

  @Bean
  @Override
  public CacheManager cacheManager() {
    return new RedisCacheManager();
  }
}
CacheManager

缓存管理类

import org.springframework.cache.Cache;
import org.springframework.cache.support.AbstractCacheManager;

import java.util.ArrayList;
import java.util.Collection;

public class RedisCacheManager extends AbstractCacheManager {

  @Override
  protected Collection<? extends Cache> loadCaches() {
    return new ArrayList<>();
  }

  @Override
  protected Cache getMissingCache(String name) {
    return new RedisCache();
  }
}

Cache

缓存处理对象,这里展示的是伪代码,需要将 map 换成更有效的数据结构。

import cn.seaboot.admin.consts.SystemConst;
import cn.seaboot.common.core.Converter;
import org.springframework.cache.Cache;
import org.springframework.cache.support.SimpleValueWrapper;

import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.Callable;

/**
 *
 */
public class RedisCache implements Cache {

  Map<Object, Object> map = new HashMap<>();

  /**
   * 简单直白,就是获取Cache的名字
   */
  @Override
  public String getName() {
    return SystemConst.CACHE_DEF;
  }

  /**
   * 获取底层的缓存实现对象
   */
  @Override
  public Object getNativeCache() {
    return SystemConst.CACHE_DEF;
  }

  /**
   * 根据键获取值,把值包装在ValueWrapper里面,如果有必要可以附加额外信息
   */
  @Override
  public ValueWrapper get(Object key) {
    System.out.println("ValueWrapper");
    return map.containsKey(key)?new SimpleValueWrapper(map.get(key)):null;
  }

  /**
   * 这个函数在spring 4以及以上才有效,很新的api,目前我还不知道如何触发此函数。
   */
  @Override
  public <T> T get(Object key, Class<T> aClass) {
    try {
      System.out.println("get(Object o, Class<T> aClass)");
      return map.containsKey(key)?Converter.convert(map.get(key), aClass):null;
    } catch (Exception e) {
      e.printStackTrace();
    }
    return null;
  }

  /**
   * 与sync属性有关
   */
  @Override
  public <T> T get(Object key, Callable<T> valueLoader) {
    try {
      System.out.println("get");
      return valueLoader.call();
    } catch (Exception e) {
      e.printStackTrace();
    }
    return null;
  }

  /**
   * 存放键值对
   */
  @Override
  public void put(Object key, Object value) {
    System.out.println("put(Object key, Object value)");
    map.put(key, value);
  }

  /**
   * 如果键对应的值不存在,则添加键值对
   */
  @Override
  public ValueWrapper putIfAbsent(Object key, Object value) {
    System.out.println("putIfAbsent");
    map.put(key, value);
    return new SimpleValueWrapper(value);
  }

  /**
   * 移除键对应键值对
   */
  @Override
  public void evict(Object key) {
    System.out.println("evict");
    map.remove(key);
  }

  /**
   * 清空缓存
   */
  @Override
  public void clear() {
    System.out.println("clear");
    map.clear();
  }
}

posted on 2020-01-01 22:20  疯狂的妞妞  阅读(1280)  评论(0编辑  收藏  举报

导航