Bootstrap

Spring Security(八)集成 Redis

博主前言:本以为这个就是代替传统 jwt 的插件,没想到复杂程度如此之高。Spring Security 本身是个高度自定义化的组件,必须花时间重点学习一下。以下为个人配置学习的流程,从零到权限管理、redis嵌入等步骤。
本文基于尚硅谷的 Spring Security 教程学习,文章与原教程有不小出入,仅供参考。
B站视频链接:尚硅谷Java项目SpringSecurity+OAuth2权限管理实战教程

这一篇最大的难点在于,为什么要集成redis,以及要替换掉 Spring Security 上的那一部分。我在网上冲浪了很久,始终不明确集成的意义。都是用一台计算机上的内存,有什么性能上的优势吗?

实际上,问题无外乎以下两点:

- 重启后数据会丢失。
- 无法在分布式环境中共享数据。

对于单部署的小应用来说,确实优势不大。但是对于微服务这种跨机器的后台网络,redis是必不可少的。因为在访问请求后,token的数据存储到本机的线程上,这使得其他机器访问不到该线程,从而拿不到用户的数据,很多业务也就无法进行。指定一个机器运行redis,让其他机器通过该机器拿用户数据,就解决了这个问题。

还有一个问题是,对于token,我们不能在上面存放太多数据,不然会变得很长——存对象的token要比只存idtoken长上数倍,而且每次访问接口都要现场解析复杂的token,浪费性能。更何况解析的时候往往产生序列相关的异常,总之就是十分麻烦。

我们可以只在token内存放id,登录的时候,不仅生成token,同时将用户数据存入redis,设id为键,这样每次访问接口,只需要快速解析出id,就能从redis获取用户数据,同时解决了复杂解析和序列化的两大难题,对于近些年来的后端程序,redis近乎是必备。

这么讲,集成redis的目的就很明确了:将原本和线程一并存储的用户数据分离,需要的时候在调用。


一、配置redis

  1. 引入依赖:
<!-- redis -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
    <version>3.3.3</version>
</dependency>
<!-- fastjson -->
<dependency>
    <groupId>com.alibaba</groupId>
    <artifactId>fastjson</artifactId>
    <version>2.0.37</version>
</dependency>
  1. 编辑配置文件(application.yml):
spring:
    data:
      redis:
        host: localhost
        port: 6379
        database: 0
        password:  # 没有请留空
        timeout: 10s
        lettuce:
          pool:
            min-idle: 0
            max-idle: 10
            max-active: 200
            max-wait: -1ms
  1. 配置redis工具类:

[!IMPORTANT]

DatabaseException()是我自定义的异常类,交由【全局异常管理】监听,读者可按需配置。

详见这篇文章:Spring Boot 全局异常拦截配置

/**
 * Redis 工具类
 *
 * @author Amane64 
@Slf4j
@Component
@RequiredArgsConstructor
public class RedisUtils {
    private final RedisTemplate<String, String> redisTemplate;
    private final JsonUtils jsonUtils;

    /**
     * 存入缓存
     *
     * @param key   键
     * @param value 值
     */
    public void set(String key, Object value) {
        this.setBySetTime(key, value, 3, TimeUnit.MINUTES);
    }

    /**
     * 存入缓存,自定义过期时间
     *
     * @param key      键
     * @param value    值
     * @param timeout  过期时间
     * @param timeUnit 时间单位
     */
    public void setBySetTime(String key, Object value, long timeout, TimeUnit timeUnit) {
        try {
            redisTemplate.opsForValue().set(key, jsonUtils.serialize(value), timeout, timeUnit);
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new DatabaseException("存入缓存数据出错:" + key);
        }
        log.debug("存入缓存数据:{}", key);
    }

    /**
     * 获取缓存数据
     *
     * @param key   键
     * @param clazz 值类型
     * @return 值
     */
    public <T> T get(String key, Class<T> clazz) {
        T res;
        try {
            res = jsonUtils.deserialize(redisTemplate.opsForValue().get(key), clazz);
        } catch (Exception e) {
            log.error(e.getMessage());
            throw new DatabaseException("获取缓存数据出错:" + key);
        }
        log.debug("获取缓存数据:{}", key);
        return res;
    }

    /**
     * 判断缓存是否存在
     *
     * @param key 键
     * @return 存在返回 true,不存在返回 false
     */
    public boolean hasKey(String key) {
        return Boolean.TRUE.equals(redisTemplate.hasKey(key));
    }

    /**
     * 删除缓存
     *
     * @param key 键
     */
    public void delete(String key) {
        if (!hasKey(key)) throw new DatabaseException("缓存数据不存在:" + key);
        redisTemplate.delete(key);
        log.debug("删除缓存数据:{}", key);
    }
}
  1. 配置序列化,供redis使用

[!IMPORTANT]

一般情况下是不需要序列化的,设置泛型为RedisTemplate<String, Object>,手动强转类型即可。

我有原创的监听Mybatis-Plus与Redis实现缓存同步的构造,必须使用双String的方式实现,故配置序列化。

对应文章传送门:[Mybatis-Plus 与 Redis 实现缓存同步]

/**
 * 序列化与反序列化转换器
 *
 * @author Amane64
 */
public class JacksonObjectMapper extends ObjectMapper {

    public JacksonObjectMapper() {
        super();
        // 前端参数蛇形命名法
        this.setPropertyNamingStrategy(PropertyNamingStrategies.SNAKE_CASE);

        // 收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        // 反序列化时,属性不存在的兼容处理
        DeserializationConfig deserializationConfig = this.getDeserializationConfig().withoutFeatures(FAIL_ON_UNKNOWN_PROPERTIES);

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(new JavaTimeModule());
    }
}

/**
 * Json 序列化工具
 *
 * @author Amane64
 */
@Component
public class JsonUtils {

    private final ObjectMapper objectMapper;

    public JsonUtils() {
        objectMapper = new JacksonObjectMapper();
    }

    public String serialize(Object object) throws Exception {
        return objectMapper.writeValueAsString(object);
    }

    public <T> T deserialize(String json, Class<T> clazz) throws Exception {
        return objectMapper.readValue(json, clazz);
    }
}
  1. 配置redisConfig类:
/**
 * redis 配置
 *
 * @author Amane64
 */
@Configuration
@Slf4j
public class RedisConfig {

    @Bean
    public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
        log.info("注册 redis 序列化器...");
        var template = new RedisTemplate<String, String>();
        template.setConnectionFactory(factory);
        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(new StringRedisSerializer());
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(new StringRedisSerializer());
        template.afterPropertiesSet();
        return template;
    }

}

更多内容请访问:个人博客传送门

;