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