Springboot怎么快速集成Redis?

Posted 凡夫贩夫

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了Springboot怎么快速集成Redis?相关的知识,希望对你有一定的参考价值。

前言

其实在Springboot中集成redis是一个非常简单的事情,但是为什么要单独输出一篇文章来记录这个过程呢?第一个原因是,我记性不是太好,这次把这个过程记录下,在新的项目搭建的时候或者需要在本地集成redis做一些其他相关联技术的测试分析的时候,可以很快找到集成方法;第二个原因是,最早我记得Spring项目里集成redis的时候,用的是jedis作为客户端,而在Springboot2.0后,这一事实改变了,默认的是lettuce。作为一个成熟的程序员来说,我是乐于拥抱变化的,改变意味着新的可能。

文章示例环境配置信息
jdk版本:1.8
开发工具:Intellij iDEA 2020.1
springboot:2.3.9.RELEASE

依赖配置

Springboot本身已经提供好了关于redis的starter,即spring-boot-starter-data-redis,在使用redis连接池的时候会依赖到commons-pool2包。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
    <groupId>org.apache.commons</groupId>
    <artifactId>commons-pool2</artifactId>
</dependency>

application.properties配置

单机模式

redis单机模式下,配置的连接参数是比较简单的,有主机ip、端口、连接验证密码,如果使用了连接池,再加连接池的几个参数就算配置完成了;

#单机模式
#redis主机ip
spring.redis.host=localhost
#redis对外提供服务端口
spring.redis.port=6379
#redis连接验证密码
spring.redis.password=fanfu123
#redis连接池配置-最大保持闲置的连接数
spring.redis.lettuce.pool.max-idle=16
#redis连接池配置-最大活跃的连接数
spring.redis.lettuce.pool.max-active=20
#redis连接池配置-最大等待时间,单位毫秒
spring.redis.lettuce.pool.max-wait=1

集群模式

集群模式下与单机模式的连接参数配置最大的区别就在于,这里配置的是多个节点主机ip+端口,多个节点之间英文逗号隔开;另外就是一些集群模式特有的参数,如重定向次数、集群刷新等;

#集群模式
#集群节点ip+端口,多个节点之间以英文逗号隔开
spring.redis.cluster.nodes=172.18.229.61:6380,172.18.229.61:6381,172.18.229.61:6382,172.18.229.61:6383,172.18.229.61:6384,172.18.229.61:6385;
#密码
spring.redis.password=fanfu123
#最大重定向次数
spring.redis.cluster.max-redirects=5
#是否开启集群刷新功能
spring.redis.lettuce.cluster.refresh.adaptive=false
#集群刷新间隔
spring.redis.lettuce.cluster.refresh.period=10M

redis密码更新

redis的配置文件,我本机操作系统是windows,使用了redis.windows.conf配置文件,更改内容如下:

requirepass fanfu123

单元测试

1、在redis.windows.conf配置文件中配置好密码,然后启动redis,这里我写了一个windwos上的批处理文件start.bat来启动redis,start.bat内容如下:

title redis-6385
redis-server.exe redis.windows.conf

2、在项目里增加依赖配置spring-boot-starter-data-redis和commons-pool2,并在application.properties中配置好redis的连接信息;

3、编写单元测试,使用redisTemplate对象往redis中设置一个key-value的键值对并读取;RedisTemplate是spring-data-redis提供的一个操作reids的类,可以直接注入单元测试或其他spring的bean中使用;

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class RedisTest 
    @Autowired
    private RedisTemplate redisTemplate;
    @Test
    public void test() 
        redisTemplate.opsForValue().set("username", "凡夫");
        String name = redisTemplate.opsForValue().get("name").toString();
        Assert.isTrue(name.equals("凡夫"), "xxxx");
    

单元测试执行结果,发现key是这样的“\\xAC\\xED\\x00\\x05t\\x00\\x08username”,出现这样类似乱码的现象是因为注入的redisTemplate对象使用了默认的序列化方式JdkSerializationRedisSerializer,具体就是使用redisTemplate对象把key存储到redis中的时候,就会先使用JdkSerializationRedisSerializer对key进行序列化,序列后的结果存储在redis服务端;

这样在观察redis中存储的key时不太直观,为了解决redis中key使用默认序列化产生类似乱码的现象,需要重新定义redisTemplate对象并注入到Spring容器中,这里key的序列化使用StringRedisSerializer的方式,就可以消除上面类似乱码的现象;value类型是String,默认的序列化方式是StringRedisSerializer;value的类型是object,默认的序列化方式是JdkSerializationRedisSerializer;而一般经常使用的对象序列化方式是Jackson2JsonRedisSerializer;

这里再简单梳理一下redis本身提供的几种序列化方式 :

GenericToStringSerializer: 可以将任何对象泛化为字符串并序列化

Jackson2JsonRedisSerializer: 序列化object对象为json字符串

JdkSerializationRedisSerializer: 序列化java对象

StringRedisSerializer: 简单的字符串序列化

@Configuration
public class RedisConfig 
        @Bean(name = "redisTemplate")
        public RedisTemplate<String, Object> getRedisTemplate(RedisConnectionFactory factory) 
            RedisTemplate<String, Object> redisTemplate = new RedisTemplate<String, Object>();
            redisTemplate.setConnectionFactory(factory);
            //key采用string的序列化方式
            StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
            redisTemplate.setKeySerializer(stringRedisSerializer);
            Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
            ObjectMapper objectMapper = new ObjectMapper();
            objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
            objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance ,
                    ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY);
            jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
            jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
            //value的序列化方式采用jackson
            redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
            //hash的key也采用string的序列化方式
            redisTemplate.setHashKeySerializer(stringRedisSerializer);
            //hash的value也采用jackson的序列化方式
            redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);
            redisTemplate.afterPropertiesSet();
            return redisTemplate;
        

工具类封装

redis有五种数据类型,分别是String、hash、list、set、zset,spring-data-redis中的redisTemplate针对每种类型value的操作命令都进行封装,如下:

ValueOperations valueOperations = redisTemplate.opsForValue();//string
HashOperations hashOperations = redisTemplate.opsForHash();//hash
ListOperations listOperations = redisTemplate.opsForList();//list
SetOperations setOperations = redisTemplate.opsForSet();//set
ZSetOperations zSetOperations = redisTemplate.opsForZSet();//zset

为了方便使用,可以单独封装成一个工具类,使用的时候直接通过工具类来操作,如下:

@Component
public class RedisService 

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 给一个指定的 key 值附加过期时间
     * @param key
     * @param time
     * @return
     */
    public boolean expire(String key, long time) 
        return redisTemplate.expire(key, time, TimeUnit.SECONDS);
    

    /**
     * 根据key 获取过期时间
     * @param key
     * @return
     */
    public long getTime(String key) 
        return redisTemplate.getExpire(key, TimeUnit.SECONDS);
    

    /**
     * 判断指定的key是否存在
     * @param key
     * @return
     */
    public boolean hasKey(String key) 
        return redisTemplate.hasKey(key);
    

    /**
     * 移除指定key的过期时间
     * @param key
     * @return
     */
    public boolean persist(String key) 
        return redisTemplate.boundValueOps(key).persist();
    

    /**
     * 根据key获取值
     * @param key 键
     * @return 值
     */
    public Object get(String key) 
        return key == null ? null : redisTemplate.opsForValue().get(key);
    

    /**
     * 将值放入缓存
     * @param key   键
     * @param value 值
     * @return true成功 false 失败
     */
    public void set(String key, String value) 
        redisTemplate.opsForValue().set(key, value);
    

    /**
     * 将值放入缓存并设置时间
     * @param key   键
     * @param value 值
     * @param time  时间(秒) -1为无期限
     * @return true成功 false 失败
     */
    public void set(String key, String value, long time) 
        if (time > 0) 
            redisTemplate.opsForValue().set(key, value, time, TimeUnit.SECONDS);
         else 
            redisTemplate.opsForValue().set(key, value);
        
    

    /**
     * 批量添加 key (重复的键会覆盖)
     * @param keyAndValue
     */
    public void batchSet(Map<String, String> keyAndValue) 
        redisTemplate.opsForValue().multiSet(keyAndValue);
    

    /**
     * 批量添加 key-value 只有在键不存在时,才添加
     * map 中只要有一个key存在,则全部不添加
     * @param keyAndValue
     */
    public void batchSetIfAbsent(Map<String, String> keyAndValue) 
        redisTemplate.opsForValue().multiSetIfAbsent(keyAndValue);
    

    /**
     * 对一个 key-value 的值进行加减操作,
     * 如果该 key 不存在 将创建一个key 并赋值该 number
     * 如果 key 存在,但 value 不是长整型 ,将报错
     * @param key
     * @param number
     */
    public Long increment(String key, long number) 
        return redisTemplate.opsForValue().increment(key, number);
    

    /**
     * 对一个 key-value 的值进行加减操作,
     * 如果该 key 不存在 将创建一个key 并赋值该 number
     * 如果 key 存在,但 value 不是 纯数字 ,将报错
     * @param key
     * @param number
     */
    public Double increment(String key, double number) 
        return redisTemplate.opsForValue().increment(key, number);
    

    /**
     * 将数据放入set缓存
     * @param key 键
     * @return
     */
    public void sSet(String key, String value) 
        redisTemplate.opsForSet().add(key, value);
    

    /**
     * 获取变量中的值
     * @param key 键
     * @return
     */
    public Set<Object> members(String key) 
        return redisTemplate.opsForSet().members(key);
    

    /**
     * 随机获取变量中指定个数的元素
     * @param key   键
     * @param count 值
     * @return
     */
    public List randomMembers(String key, long count) 
        List list = redisTemplate.opsForSet().randomMembers(key, count);
        return list;
    

    /**
     * 随机获取变量中的元素
     * @param key 键
     * @return
     */
    public Object randomMember(String key) 
        return redisTemplate.opsForSet().randomMember(key);
    

    /**
     * 弹出变量中的元素
     * @param key 键
     * @return
     */
    public Object pop(String key) 
        return redisTemplate.opsForSet().pop("setValue");
    

    /**
     * 获取变量中值的长度
     * @param key 键
     * @return
     */
    public long setSize(String key) 
        return redisTemplate.opsForSet().size(key);
    

    /**
     * 根据value从一个set中查询,是否存在
     * @param key   键
     * @param value 值
     * @return true 存在 false不存在
     */
    public boolean sHasKey(String key, Object value) 
        return redisTemplate.opsForSet().isMember(key, value);
    

    /**
     * 检查给定的元素是否在变量中
     * @param key 键
     * @param obj 元素对象
     * @return
     */
    public boolean isMember(String key, Object obj) 
        return redisTemplate.opsForSet().isMember(key, obj);
    

    /**
     * 转移变量的元素值到目的变量
     * @param key     键
     * @param value   元素对象
     * @param destKey 元素对象
     * @return
     */
    public boolean move(String key, String value, String destKey) 
        return redisTemplate.opsForSet().move(key, value, destKey);
    

    /**
     * 批量移除set缓存中元素
     * @param key    键
     * @param values 值
     * @return
     */
    public void remove(String key, Object... values) 
        redisTemplate.opsForSet().remove(key, values);
    

    /**
     * 通过给定的key求2个set变量的差值
     * @param key     键
     * @param destKey 键
     * @return
     */
    public Set difference(String key, String destKey) 
        return redisTemplate.opsForSet().difference(key, destKey);
    

    /**
     * 加入缓存
     * @param key 键
     * @param map 键
     * @return
     */
    public void add(String key, Map<String, String> map) 
        redisTemplate.opsForHash().putAll(key, map);
    

    /**
     * 获取 key 下的 所有  hashkey 和 value
     * @param key 键
     * @return
     */
    public Map<Object, Object> getHashEntries(String key) 
        return redisTemplate.opsForHash().entries(key);
    

    /**
     * 验证指定 key 下 有没有指定的 hashkey
     * @param key
     * @param hashKey
     * @return
     */
    public boolean hashKey(String key, String hashKey) 
        return redisTemplate.opsForHash().hasKey(key, hashKey);
    

    /**
     * 获取指定key的值string
     * @param key     键
     * @param hashKey 键
     * @return
     */
    public String getMapString(String key, String hashKey) 
        return redisTemplate.opsForHash().get(key, hashKey).toString();
    

    /**
     * 获取指定的值Int
     * @param key     键
     * @param hashKey 键
     * @return
     */
    public Integer getMapInt(String key, String hashKey) 
        return (Integer) redisTemplate.opsForHash().get(key, hashKey);
    

    /**
     * 弹出元素并删除
     * @param key 键
     * @return
     */
    public String popValue(String key) 
        return redisTemplate.opsForSet().pop(key).toString();
    

    /**
     * 删除指定 hash 的 HashKey
     * @param key
     * @param hashKeys
     * @return 删除成功的 数量
     */
    public Long delete(String key, String... hashKeys) 
        return redisTemplate.opsForHash().delete(key, hashKeys);
    

    /**
     * 给指定 hash 的 hashkey 做增减操作
     * @param key
     * @param hashKey
     * @param number
     * @return
     */
    public Long increment(String key, String hashKey, long number) 
        return redisTemplate.opsForHash().increment(key, hashKey, number);
    

    /**
     * 给指定 hash 的 hashkey 做增减操作
     * @param key
     * @param hashKey
     * @param number
     * @return
     */
    public Double increment(String key, String hashKey, Double number) 
        return redisTemplate.opsForHash().increment(key, hashKey, number);
    

    /**
     * 获取 key下的所有hashkey字段
     * @param key
     * @return
     */
    public Set<Object> hashKeys(String key) 
        return redisTemplate.opsForHash().keys(key);
    

    /**
     * 获取指定hash下面的键值对数量
     *
     * @param key
     * @return
     */
    public Long hashSize(String key) 
        return redisTemplate.opsForHash().size(key);
    


    /**
     * 在变量左边添加元素值
     *
     * @param key
     * @param value
     * @return
     */
    public void leftPush(String key, Object value) 
        redisTemplate.opsForList().leftPush(key, value);
    

    /**
     * 获取集合指定位置的值。
     *
     * @param key
     * @param index
     * @return
     */
    public Object index(String key, long index) 
        return redisTemplate.opsForList().index("list", 1);
    

    /**
     * 获取指定区间的值。
     *
     * @param key
     * @param start
     * @param end
     * @return
     */
    public List<Object> range(String key, long start, long end) 
        return redisTemplate.opsForList().range(key, start, end);
    

    /**
     * 如果pivot值存在于集合内,把一个value值放到第一个出现的pivot值的前面
     *
     * @param key
     * @param pivot
     * @param value
     * @return
     */
    public void leftPush(String key, String pivot, String value) 
        redisTemplate.opsForList().leftPush(key, pivot, value);
    

    /**
     * 向左边批量添加参数元素。
     *
     * @param key
     * @param values
     * @return
     */
    public void leftPushAll(String key, String... values) 
        redisTemplate.opsForList().leftPushAll(key, values);
    

    /**
     * 向集合最右边添加元素。
     *
     * @param key
     * @param value
     * @return
     */
    public void leftPushAll(String key, String value) 
        redisTemplate.opsForList().rightPush(key, value);
    

    /**
     * 向左边批量添加参数元素。
     *
     * @param key
     * @param values
     * @return
     */
    public void rightPushAll(String key, String... values) 
        redisTemplate.opsForList().rightPushAll(key, values);
    

    /**
     * 向已存在的集合中添加元素。
     *
     * @param key
     * @param value
     * @return
     */
    public void rightPushIfPresent(String key, Object value) 
        redisTemplate.opsForList().rightPushIfPresent(key, value);
    

    /**
     * 向已存在的集合中添加元素。
     *
     * @param key
     * @return
     */
    public long listLength(String key) 
        return redisTemplate.opsForList().size(key);
    

    /**
     * 移除集合中的左边第一个元素。
     *
     * @param key
     * @return
     */
    public void leftPop(String key) 
        redisTemplate.opsForList().leftPop(key);
    

    /**
     * 移除集合中左边的元素在等待的时间里,如果超过等待的时间仍没有元素则退出。
     *
     * @param key
     * @return
     */
    public void leftPop(String key, long timeout, TimeUnit unit) 
        redisTemplate.opsForList().leftPop(key, timeout, unit);
    

    /**
     * 移除集合中右边的元素。
     *
     * @param key
     * @return
     */
    public void rightPop(String key) 
        redisTemplate.opsForList().rightPop(key);
    

    /**
     * 在等待的时间里移除集合中右边的元素,如果超过等待的时间仍没有元素则退出。
     *
     * @param key
     * @return
     */
    public void rightPop(String key, long timeout, TimeUnit unit) 
        redisTemplate.opsForList().rightPop(key, timeout, unit);
    

Jedis和Lettuce

jedis和Lettuce都是Redis的客户端,它们都可以连接Redis服务器,但是在SpringBoot2.0之后默认都是使用的Lettuce这个客户端连接Redis服务器。因为当使用Jedis客户端连接Redis服务器的时候,每个线程都要拿自己创建的Jedis实例去连接Redis客户端,当有很多个线程的时候,不仅开销大需要反复的创建关闭一个Jedis连接,而且也是线程不安全的,一个线程通过Jedis实例更改Redis服务器中的数据之后会影响另一个线程。

但是如果使用Lettuce这个客户端连接Redis服务器的时候,就不会出现上面的情况,Lettuce底层使用的是Netty,当有多个线程都需要连接Redis服务器的时候,可以保证只创建一个Lettuce连接,使所有的线程共享这一个Lettuce连接,这样可以减少创建关闭一个Lettuce连接时候的开销;而且这种方式也是线程安全的,不会出现一个线程通过Lettuce更改Redis服务器中的数据之后而影响另一个线程的情况

总结

通过这篇文章可以了解到什么呢?

1、springboot集成redis时的依赖引入、连接参数是如何配置的?

2、如何选择redisTemplate中key-value序列化方式;

3、redis的五种数据类型常用操作对应的java封装;

SpringBoot项目+Shiro(权限框架)+Redis(缓存)集成

项目是SpringCloud框架,分布式项目,包括Eureka、Zuul、Config、User-Svr(用户管理的服务,既是服务端也是客户端);

SpringCloud框架的SpringBoot 的项目搭建就不再赘述,这里重点介绍如何引入集成 Shiro 框架:

Apache Shiro是一个强大且易用的Java安全框架,执行身份验证、授权、密码学和会话管理。使用Shiro的易于理解的API,您可以快速、轻松地获得任何应用程序,从最小的移动应用程序到最大的网络和企业应用程序。

一、数据库设计

  这里数据库表为5个分别是: 用户表、角色表、权限表、用户角色关系表、角色权限资源关系表

 

 

 

遵循三步走:导包,配置,写代码

二、导包(引入依赖)

 <!-- shiro -->
      <dependency>
          <groupId>org.apache.shiro</groupId>
          <artifactId>shiro-ehcache</artifactId>
          <version>1.4.2</version>
      </dependency>

      <!-- https://mvnrepository.com/artifact/com.sun.xml.fastinfoset/FastInfoset -->
      <dependency>
          <groupId>org.apache.shiro</groupId>
          <artifactId>shiro-spring</artifactId>
          <scope>compile</scope>
          <version>1.4.2</version>
      </dependency>
      <!-- shiro+redis缓存插件 -->
      <dependency>
          <groupId>org.crazycake</groupId>
          <artifactId>shiro-redis</artifactId>
          <version>2.4.2.1-RELEASE</version>
          <scope>compile</scope>
      </dependency>

 三、创建ShiroConfig配置ShiroServerConfig、ShiroAnnotionConfig

package com.iot.microservice.shiroconfig;

import org.apache.shiro.mgt.SecurityManager;
import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.crazycake.shiro.RedisCacheManager;
import org.crazycake.shiro.RedisManager;
import org.crazycake.shiro.RedisSessionDAO;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.LinkedHashMap;
import java.util.Map;

/**
 * Created by IntelliJ IDEA
 * 这是一个神奇的Class
 *
 * @author zhz
 * @date 2019/12/13 16:31
 */

@Configuration
public class ShiroServerConfig {



        @Bean
    public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        // 必须设置 SecurityManager
        shiroFilterFactoryBean.setSecurityManager(securityManager);

        // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面
        //访问的是后端url的地址,这里要写base 服务的公用登录接口。
        shiroFilterFactoryBean.setLoginUrl("http://localhost:18900/base/loginpage");

        // 登录成功后要跳转的链接;现在应该没用
        //shiroFilterFactoryBean.setSuccessUrl("/index");

        // 未授权界面;可以写个公用的403页面
        shiroFilterFactoryBean.setUnauthorizedUrl("/403");

        // private Map<String, Filter> filters;  shiro有一些默认的拦截器 比如auth,它就是FormAuthenticationFilter表单拦截器  <取名,拦截器地址>,可以自定义拦截器放在这
        //private Map<String, String> filterChainDefinitionMap; <url,拦截器名>哪些路径会被此拦截器拦截到
        //Map<String, Filter> filters = shiroFilterFactoryBean.getFilters();
        //AdminFilter ad=new AdminFilter();
        //filters.put("ad", ad);

        // 拦截器.
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
        // 配置不会被拦截的链接 顺序判断
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/login", "anon");
        filterChainDefinitionMap.put("/loginpage", "anon");
        filterChainDefinitionMap.put("/swagger-ui.html#", "anon");

        filterChainDefinitionMap.put("/base/test", "authc");

        // 配置退出过滤器,其中的具体的退出代码Shiro已经替我们实现了,加上这个会导致302,请求重置,暂不明白原因
        //filterChainDefinitionMap.put("/logout", "logout");
        //配置某个url需要某个权限码
        //filterChainDefinitionMap.put("/hello", "perms[how_are_you]");

        // 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边
        // <!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问;user:remember me的可以访问-->
//        filterChainDefinitionMap.put("/fine", "user");
        //filterChainDefinitionMap.put("/focus/**", "ad");

        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        System.out.println("Shiro拦截器工厂类注入成功");
        return shiroFilterFactoryBean;
    }

    @Bean
    public SecurityManager securityManager() {
        DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
        // 设置realm.
        securityManager.setRealm(myShiroRealm());
        // 自定义缓存实现 使用redis
        securityManager.setCacheManager(cacheManager());
        // 自定义session管理 使用redis
        securityManager.setSessionManager(sessionManager());
        return securityManager;
    }

    /**
     * 身份认证realm; (这个需要自己写,账号密码校验;权限等)
     *
     * @return
     */
    @Bean
    public ShiroServerRealm myShiroRealm() {
        ShiroServerRealm myShiroRealm = new ShiroServerRealm();
        return myShiroRealm;
    }

    /**
     * cacheManager 缓存 redis实现
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    public RedisCacheManager cacheManager() {
        RedisCacheManager redisCacheManager = new RedisCacheManager();
        redisCacheManager.setRedisManager(redisManager());
        return redisCacheManager;
    }

    /**
     * 配置shiro redisManager
     * 使用的是shiro-redis开源插件
     *
     * @return
     */
    @Bean
    public RedisManager redisManager() {
        RedisManager redisManager = new MyRedisManager();
//        RedisManager redisManager = new RedisManager();
//        redisManager.setHost(host);
//        redisManager.setPort(port);
//        // 配置缓存过期时间
//        redisManager.setExpire(expireTime);
//        redisManager.setTimeout(timeOut);
        // redisManager.setPassword(password);
        return redisManager;
    }



//    /**
//     * 配置shiro redisManager
//     * 网上的一个 shiro-redis 插件,实现了shiro的cache接口、CacheManager接口就
//     * @return
//     */
//    @Bean
//    public RedisManager redisManager() {
//        RedisManager redisManager = new RedisManager();
//        redisManager.setHost("localhost");
//        redisManager.setPort(6379);
//        redisManager.setExpire(18000);// 配置过期时间
//        // redisManager.setTimeout(timeout);
//        // redisManager.setPassword(password);
//        return redisManager;
//    }

    /**
     * Session Manager
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public DefaultWebSessionManager sessionManager() {
        DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
        sessionManager.setSessionDAO(redisSessionDAO());
        return sessionManager;
    }

    /**
     * RedisSessionDAO shiro sessionDao层的实现 通过redis
     * 使用的是shiro-redis开源插件
     */
    @Bean
    public RedisSessionDAO redisSessionDAO() {
        RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
        redisSessionDAO.setRedisManager(redisManager());
        return redisSessionDAO;
    }

//    /**
//     * 限制同一账号登录同时登录人数控制
//     *
//     * @return
//     */
//    @Bean
//    public KickoutSessionControlFilter kickoutSessionControlFilter() {
//        KickoutSessionControlFilter kickoutSessionControlFilter = new KickoutSessionControlFilter();
//        kickoutSessionControlFilter.setCacheManager(cacheManager());
//        kickoutSessionControlFilter.setSessionManager(sessionManager());
//        kickoutSessionControlFilter.setKickoutAfter(false);
//        kickoutSessionControlFilter.setMaxSession(1);
//        kickoutSessionControlFilter.setKickoutUrl("/auth/kickout");
//        return kickoutSessionControlFilter;
//    }


    /***
     * 授权所用配置
     *
     * @return
     */
    @Bean
    public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
        DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
        return defaultAdvisorAutoProxyCreator;
    }

    /***
     * 使授权注解起作用不如不想配置可以在pom文件中加入
     * <dependency>
     *<groupId>org.springframework.boot</groupId>
     *<artifactId>spring-boot-starter-aop</artifactId>
     *</dependency>
     * @param securityManager
     * @return
     */
    @Bean
    public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){
        AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
        authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
        return authorizationAttributeSourceAdvisor;
    }

}
package com.iot.microservice.shiroconfig;

import org.apache.shiro.spring.LifecycleBeanPostProcessor;
import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.DependsOn;

@Configuration
public class ShiroAnnotionConfig {

      /**
     * Shiro生命周期处理器
     * @return
     */
    @Bean
    public LifecycleBeanPostProcessor lifecycleBeanPostProcessor(){
        return new LifecycleBeanPostProcessor();
    }
    /**
     * 开启Shiro的注解(如@RequiresRoles,@RequiresPermissions),需借助SpringAOP扫描使用Shiro注解的类,并在必要时进行安全逻辑验证
     * 配置以下两个bean(DefaultAdvisorAutoProxyCreator(可选)和AuthorizationAttributeSourceAdvisor)即可实现此功能
     * @return
     */
    @Bean
    @DependsOn({"lifecycleBeanPostProcessor"})
    public DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator(){
        DefaultAdvisorAutoProxyCreator advisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
        advisorAutoProxyCreator.setProxyTargetClass(true);
        return advisorAutoProxyCreator;
    }
}

四、自定义Realm  ShiroServerRealm

package com.iot.microservice.shiroconfig;

import com.keenyoda.iot.microservice.userservice.PrivilegeService;
import com.keenyoda.iot.microservice.userservice.UserService;
import com.keenyoda.iot.pojos.rbac.ResourceVo;
import com.keenyoda.iot.pojos.user.UserEntity;
import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;
import org.apache.shiro.util.ByteSource;
import org.springframework.beans.factory.annotation.Autowired;

import java.util.List;


/**
 * Created by IntelliJ IDEA
 * 这是一个神奇的Class
 *
 * @author zhz
 * @date 2019/12/13 16:31
 */
public class ShiroServerRealm extends AuthorizingRealm {

    Boolean cachingEnabled=true;

    @Autowired
    private PrivilegeService privilegeService;

    @Autowired
    private UserService userService;


    /**
     * 1.授权方法,在请求需要操作码的接口时会执行此方法。不需要操作码的接口不会执行
     * 2.实际上是 先执行 AuthorizingRealm,自定义realm的父类中的 getAuthorizationInfo方法,
     * 逻辑是先判断缓存中是否有用户的授权信息(用户拥有的操作码),如果有 就直返回不调用自定义 realm的授权方法了,
     * 如果没缓存,再调用自定义realm,去数据库查询。
     * 用库查询一次过后,如果 在安全管理器中注入了 缓存,授权信息就会自动保存在缓存中,下一次调用需要操作码的接口时,
     * 就肯定不会再调用自定义realm授权方法了。   网上有分析AuthorizingRealm,shiro使用缓存的过程
     * 3.AuthorizingRealm 有多个实现类realm,推测可能是把 自定义realm注入了安全管理器,所以才调用自定义的
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        SimpleAuthorizationInfo simpleAuthorInfo = new SimpleAuthorizationInfo();

        UserEntity userEntity=(UserEntity) principals.getPrimaryPrincipal();

        List<ResourceVo> resourceVos = privilegeService.selectResourceVoListByUserId(userEntity.getId());
        if(resourceVos!=null){
            for (ResourceVo resourceVo:resourceVos) {
                simpleAuthorInfo.addStringPermission(resourceVo.getResource());
            }
        }
        return simpleAuthorInfo;
    }

    /**
     * 1.和授权方法一样,AuthenticatingRealm的getAuthenticationInfo,先判断缓存是否有认证信息,没有就调用
     * 但试验,登录之后,再次登录,发现还是调用了认证方法,说明第一次认证登录时,没有将认证信息存到缓存中。不像授权信息,
     * 将缓存注入安全管理器,就自动保存了授权信息。 难道无法 防止故意多次登录 ,按理说不应该啊?
     * 2  可以在登录controller简单用session是否有key 判断是否登录?
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(
            AuthenticationToken authcToken) throws AuthenticationException {
        //获取基于用户名和密码的令牌
        //实际上这个authcToken是从LoginController里面currentUser.login(token)传过来的
        UsernamePasswordToken token = (UsernamePasswordToken) authcToken;
        String account = token.getUsername();
        UserEntity user = userService.findUserUserId(account);
        if(user==null){throw new AuthenticationException("用户不存在");}

        //进行认证,将正确数据给shiro处理
        //密码不用自己比对,AuthenticationInfo认证信息对象,一个接口,new他的实现类对象SimpleAuthenticationInfo
        /*    第一个参数随便放,可以放user对象,程序可在任意位置获取 放入的对象
         * 第二个参数必须放密码,
         * 第三个参数放 当前realm的名字,因为可能有多个realm*/
        UserEntity baseUserVM = EntityUtils.entity2VM(user, UserEntity.class, "");
        SimpleAuthenticationInfo authcInfo=new SimpleAuthenticationInfo(baseUserVM, user.getPwd(), this.getName());

        //密码凭证器加盐
        authcInfo.setCredentialsSalt(ByteSource.Util.bytes(user.getId()));
        //清缓存中的授权信息,保证每次登陆 都可以重新授权。因为AuthorizingRealm会先检查缓存有没有 授权信息,再调用授权方法
        super.clearCachedAuthorizationInfo(authcInfo.getPrincipals());

        return authcInfo;
        //返回给安全管理器,securityManager,由securityManager比对数据库查询出的密码和页面提交的密码
        //如果有问题,向上抛异常,一直抛到控制器
    }
}

工具类

package com.iot.microservice.shiroconfig;

import com.github.pagehelper.Page;
import org.springframework.beans.BeanUtils;
import org.springframework.util.CollectionUtils;

import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;

public class EntityUtils {

    /**
     * 实体列表转Vm
     *
     * @param source           原列表
     * @param vmClass          vm类
     * @param ignoreProperties 忽略的字段
     * @param <T>              泛型
     * @return vm列表
     */
    public static <T> List<T> entity2VMList(List<?> source, Class<T> vmClass, String... ignoreProperties) {
        List<T> target = (source instanceof Page ? new Page<T>() : new ArrayList<T>());
        if (source instanceof Page) {
            BeanUtils.copyProperties(source, target);
        }
        if (CollectionUtils.isEmpty(source)) {
            return target;
        }
        source.forEach(e -> {
            target.add(entity2VM(e, vmClass, ignoreProperties));
        });
        return target;
    }

    /**
     * 实体转VM
     *
     * @param source           原对象
     * @param vmClass          要转换的对象
     * @param ignoreProperties 忽略的属性
     * @param <T>              泛型
     * @return 转换后对象
     * @author Say
     */
    public static <T> T entity2VM(Object source, Class<T> vmClass, String... ignoreProperties) {
        if (null == source) {
            return null;
        }
        try {
            T target = vmClass.newInstance();
            BeanUtils.copyProperties(source, target, ignoreProperties);
            return target;
        } catch (InstantiationException | IllegalAccessException e) {
            e.printStackTrace();
        }
        return null;
    }

    /**
     * VM转实体
     * 底层用的vm2Entity,只是方法名做区分
     *
     * @param source           vm
     * @param entClass         实体
     * @param ignoreProperties 忽略的属性
     * @param <T>              泛型
     * @return 转换后的对象
     * @author Say
     */
    public static <T> T vm2Entity(Object source, Class<T> entClass, String... ignoreProperties) {
        return entity2VM(source, entClass, ignoreProperties);
    }

    /**
     * VM转实体集合
     * 底层用的entity2VMList,只是方法名做区分
     *
     * @param source           原对象
     * @param entClass         实体
     * @param ignoreProperties 忽略的属性
     * @param <T>              泛型
     * @return 转换后的对象
     * @author Say
     */
    public static <T> List<T> vm2EntityList(List<?> source, Class<T> entClass, String... ignoreProperties) {
        return entity2VMList(source, entClass, ignoreProperties);
    }

    /**
     * Entity VM 互转
     *
     * @param object      数据源
     * @param laterObject 转换对象
     * @param <T>         泛型
     */
    public static <T> void copyProperties(final T object, T laterObject) {

        if (null == object || null == laterObject) {
            return;
        }

        ConcurrentHashMap<String, Method> getMethods = findGetMethods(object.getClass().getMethods());

        ConcurrentHashMap<String, Method> setMethods = findSetMethods(laterObject.getClass().getDeclaredMethods());

        Iterator<Map.Entry<String, Method>> iterator = getMethods.entrySet().iterator();

        while (iterator.hasNext()) {
            Map.Entry<String, Method> entry = iterator.next();
            String methodName = entry.getKey();
            Method getMethod = entry.getValue();
            Method setMethod = setMethods.get(methodName);
            if (null == setMethod) {
                continue;
            }
            try {
                Object value = getMethod.invoke(object, new Object[]{});
                setMethod.invoke(laterObject, value);
            } catch (IllegalAccessException e) {
                e.printStackTrace();
            } catch (IllegalArgumentException e) {
                e.printStackTrace();
            } catch (InvocationTargetException e) {
                e.printStackTrace();
            }
        }
    }

    /**
     * 获取所有的get方法
     *
     * @param methods 所有的方法
     * @return 所有的get方法
     */
    private static ConcurrentHashMap<String, Method> findGetMethods(Method[] methods) {
        ConcurrentHashMap<String, Method> getMethodsMap = new ConcurrentHashMap<>();
        for (Method method : methods) {
            if (isGetMethod(method.getName())) {
                getMethodsMap.put(getMethodName(method.getName()), method);
            }
        }
        return getMethodsMap;
    }

    /**
     * 获取所有的set方法
     *
     * @param methods 所有的方法
     * @return 所有的set方法
     */
    private static ConcurrentHashMap<String, Method> findSetMethods(Method[] methods) {
        ConcurrentHashMap<String, Method> setMethodsMap = new ConcurrentHashMap<>();
        for (Method method : methods) {
            if (isSetMethod(method.getName())) {
                setMethodsMap.put(getMethodName(method.getName()), method);
            }
        }
        return setMethodsMap;
    }


    /**
     * 取方法名
     *
     * @param getMethodName 方法名称
     * @return 去掉get set的方法名
     */
    private static String getMethodName(String getMethodName) {
        String fieldName = getMethodName.substring(3, getMethodName.length());
        return fieldName;
    }

    /**
     * 判断是否是get方法
     *
     * @param methodName
     * @return
     */
    private static boolean isGetMethod(String methodName) {
        int index = methodName.indexOf("get");
        if (index == 0) {
            return true;
        }
        return false;
    }

    /**
     * 判断是否是set方法
     *
     * @param methodName 方法名
     * @return 是否为set 方法
     */
    private static boolean isSetMethod(String methodName) {
        int index = methodName.indexOf("set");
        if (index == 0) {
            return true;
        }
        return false;
    }

}

 

五、异常处理类,拦截未授权页面(未授权页面有三种实现方式,我这里使用异常处理)

package com.iot.microservice.shiroconfig;

import com.iot.commons.Message;
import com.iot.commons.enumpackage.ErrorCodeEnum;
import org.apache.shiro.authz.AuthorizationException;
import org.apache.shiro.authz.UnauthorizedException;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;

import javax.servlet.http.HttpServletRequest;

/**
 * Created by IntelliJ IDEA
 * 这是一个神奇的Class
 * 全局捕捉无权限异常
 *
 * @author zhz
 * @date 2019/12/13 15:40
 */
@ControllerAdvice
public class GlobalDefaultExceptionHandler {

    @ExceptionHandler(UnauthorizedException.class)
    @ResponseBody
    public Message defaultExceptionHandler(HttpServletRequest req,Exception e){

        return new Message(ErrorCodeEnum.UNAUTHORIZED.getValue(),"对不起,你没有访问权限!");
    }

    @ExceptionHandler(AuthorizationException.class)
    @ResponseBody
    public Message throwAuthenticationException(HttpServletRequest req,Exception e){
        return new Message(ErrorCodeEnum.AUTHENTICATION_EXCEPTION.getValue(),"账号验证异常,请重新登录!");
    }
}

六、因为不想我这里把redis单独做成了一个服务,为了不用多次配置,重写RedisManager 中的两个方法

package com.iot.microservice.shiroconfig;

import com.iot.microservice.redisservice.RedisService;
import org.crazycake.shiro.RedisManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Base64;

/**
 * Created by IntelliJ IDEA
 * 这是一个神奇的Class
 *
 * @author zhz
 * @date 2019/12/13 16:31
 */
@Component
public class MyRedisManager extends RedisManager {

    @Autowired
    RedisService redisService;
    

    @Override
    public byte[] set(byte[] key, byte[] value, int expire) {
        String val = Base64.getEncoder().encodeToString(value);
        expire=12000;
        redisService.set(new String(key),val,expire);
        return value;
    }


    @Override
    public byte[] get(byte[] key){
        String s = redisService.get(new String(key));
        if (s == null){
            return null;
        }
        return Base64.getDecoder().decode(s);
    }


    public static void main(String[] args) {
        String a = null;
        System.out.println(Base64.getDecoder().decode(a));
    }
}

七、登录部分代码

/**
     * 用户登录
     * zhz
     *
     * @param loginUser 
     */
    @RequestMapping("login") 
    @ResponseBody
    public Message<String> login(LoginUserVM loginUser) throws IncorrectCredentialsException {
        Asserts.notEmpty(loginUser,"登录用户不能为空");
        String account=loginUser.getLoginName();
        String password=loginUser.getPassword();
        UsernamePasswordToken token = new UsernamePasswordToken(account,password,false);
        token.setRememberMe(true);

        Subject currentUser = SecurityUtils.getSubject();
        try {
            currentUser.login(token);
        } catch(IncorrectCredentialsException e){
            return Message.ok("密码错误",500);
        } catch (AuthenticationException e) {
//            return Message.ok("登录失败");
            return Message.ok(e.getMessage(),500);
        }

        return Message.ok(FocusMicroBaseConstants.SUCCESS);
    }
 private Message getUserToken(UserEntity userEntity, UserInfo userInfo) {
        UsernamePasswordToken userToken = new UsernamePasswordToken(userEntity.getId(), userEntity.getPwd(), false);
        userToken.setRememberMe(true);
        Subject currentUser = SecurityUtils.getSubject();
        try {
            currentUser.login(userToken);
        } catch (IncorrectCredentialsException e) {
            return <

以上是关于Springboot怎么快速集成Redis?的主要内容,如果未能解决你的问题,请参考以下文章

SpringBoot 集成Redisson 提示:java.lang.ClassNotFoundException: **.redis.connection.ReactiveRedisConnec

springboot 怎么集成redis

springboot集成redis(mybatis分布式session)

SpringBoot整合redis(源码)

SpringBoot——SpringBoot集成Redis

SpringBoot项目+Shiro(权限框架)+Redis(缓存)集成