spring boot shiro redis整合基于角色和权限的安全管理-Java编程

一、概述

  本博客主要讲解spring boot整合Apache的shiro框架,实现基于角色的安全访问控制或者基于权限的访问安全控制,其中还使用到分布式缓存redis进行用户认证信息的缓存,减少数据库查询的开销。Apache shiro与spring security的作用几乎一样都是简化了Java程序的权限控制开发。

二、项目


2.1首先是通过eclipse创建一个最新的spring boot项目,并添加以下依赖:

pom.xml
 复制<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>net.xqlee.project.demo.shiro</groupId> <artifactId>demo-springboot-shiro</artifactId> <version>0.0.1-SNAPSHOT</version> <packaging>jar</packaging> <name>demo-springboot-shiro-hello</name> <description>demo-springboot-shiro-hello</description> <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>1.5.4.RELEASE</version> <relativePath /> <!-- lookup parent from repository --> </parent> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <scope>runtime</scope> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-test</artifactId> <scope>test</scope> </dependency> <!-- shiro权限控制框架 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>1.3.2</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>1.3.2</version> </dependency> <!--缓存暂时用简单的 https://mvnrepository.com/artifact/org.ehcache/ehcache --> <dependency> <groupId>org.ehcache</groupId> <artifactId>ehcache</artifactId> </dependency> <!-- https://mvnrepository.com/artifact/net.sf.json-lib/json-lib --> <dependency> <groupId>net.sf.json-lib</groupId> <artifactId>json-lib</artifactId> <version>2.4</version> <classifier>jdk15</classifier> </dependency> <!-- redis缓存 --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> </dependencies> <build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build> </project> 

2.2配置redis

application.properties文件中添加配置:
 复制####################Redis 配置信息 ########################## # Redis数据库分片索引(默认为0) spring.redis.database=0 # Redis服务器地址 spring.redis.host=10.1.1.2 # Redis服务器连接端口 spring.redis.port=6379 # Redis服务器连接密码(默认为空) spring.redis.password= # 连接池最大连接数(使用负值表示没有限制) spring.redis.pool.max-active=8 # 连接池最大阻塞等待时间(使用负值表示没有限制) spring.redis.pool.max-wait=-1 # 连接池中的最大空闲连接 spring.redis.pool.max-idle=8 # 连接池中的最小空闲连接 spring.redis.pool.min-idle=0 # 连接超时时间(毫秒) spring.redis.timeout=0 #测试redis的缓存日志 logging.level.net.xqlee.project.demo.shiro.config.shiro.cache=DEBUG

Java config的redis配置
RedisConfig
 复制package net.xqlee.project.demo.shiro.config.redis;

import java.lang.reflect.Method;

import org.springframework.beans.factory.annotation.Autowired; import org.springframework.cache.CacheManager; import org.springframework.cache.annotation.CachingConfigurerSupport; import org.springframework.cache.annotation.EnableCaching; import org.springframework.cache.interceptor.KeyGenerator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.data.redis.cache.RedisCacheManager; import org.springframework.data.redis.connection.RedisConnectionFactory; import org.springframework.data.redis.core.RedisTemplate; @Configuration @EnableCaching // 继承CachingConfigurerSupport并重写方法,配合该注解实现spring缓存框架的启用 public class RedisConfig extends CachingConfigurerSupport { /** 载入通过配置文件配置的连接工场 **/ @Autowired RedisConnectionFactory redisConnectionFactory; @SuppressWarnings("rawtypes") @Autowired RedisTemplate redisTemplate; @Bean RedisTemplate<String, Object> objRedisTemplate() { RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>(); redisTemplate.setConnectionFactory(redisConnectionFactory); return redisTemplate; } /* * (non-Javadoc) * * @see org.springframework.cache.annotation.CachingConfigurerSupport# * cacheManager() */ @Bean // 必须添加此注解 @Override public CacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(redisTemplate); // 设置缓存过期时间 // redisCacheManager.setDefaultExpiration(60);//秒 return redisCacheManager; } /** * 重写缓存的key生成策略,可根据自身业务需要进行自己的配置生成条件 * * @see org.springframework.cache.annotation.CachingConfigurerSupport# * keyGenerator() */ @Bean // 必须项 @Override public KeyGenerator keyGenerator() { return new KeyGenerator() { @Override public Object generate(Object target, Method method, Object... params) { StringBuilder sb = new StringBuilder(); sb.append(target.getClass().getName()); sb.append(method.getName()); for (Object obj : params) { sb.append(obj.toString()); } return sb.toString(); } }; } } 


2.3创建shiro需要的 redis缓存器和缓存管理实现类

首先是缓存器cache的实现
RedisCache.java
 复制package net.xqlee.project.demo.shiro.config.shiro.cache;

import java.util.ArrayList;
import java.util.Collection; import java.util.List; import java.util.Set; import java.util.concurrent.TimeUnit; import org.apache.shiro.cache.Cache; import org.apache.shiro.cache.CacheException; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.data.redis.core.RedisTemplate; /** * Redis的Shiro缓存对象实现 * * @author xq * * @param <K> * @param <V> */ public class RedisCache<K, V> implements Cache<K, V> { private static final Logger logger = LoggerFactory.getLogger(RedisCache.class); private RedisTemplate<K, V> redisTemplate; private final static String PREFIX = "shiro-cache:"; private String cacheKey; private long globExpire = 30; @SuppressWarnings({ "rawtypes", "unchecked" }) public RedisCache(final String name, final RedisTemplate redisTemplate) { this.cacheKey = PREFIX + name + ":"; this.redisTemplate = redisTemplate; } @Override public V get(K key) throws CacheException { logger.debug("Shiro从缓存中获取数据KEY值["+key+"]"); redisTemplate.boundValueOps(getCacheKey(key)).expire(globExpire, TimeUnit.MINUTES); return redisTemplate.boundValueOps(getCacheKey(key)).get(); } @Override public V put(K key, V value) throws CacheException { V old = get(key); redisTemplate.boundValueOps(getCacheKey(key)).set(value); return old; } @Override public V remove(K key) throws CacheException { V old = get(key); redisTemplate.delete(getCacheKey(key)); return old; } @Override public void clear() throws CacheException { redisTemplate.delete(keys()); } @Override public int size() { return keys().size(); } @Override public Set<K> keys() { return redisTemplate.keys(getCacheKey("*")); } @Override public Collection<V> values() { Set<K> set = keys(); List<V> list = new ArrayList<>(); for (K s : set) { list.add(get(s)); } return list; } @SuppressWarnings("unchecked") private K getCacheKey(Object k) { return (K) (this.cacheKey + k); } } 
还有缓存管理器:
RedisCacheManager.java
 复制package net.xqlee.project.demo.shiro.config.shiro.cache;

import org.apache.shiro.cache.Cache;
import org.apache.shiro.cache.CacheException; import org.apache.shiro.cache.CacheManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.data.redis.core.RedisTemplate; /** * Redis的Shiro缓存管理器实现 * * @author xq * */ public class RedisCacheManager implements CacheManager { @Autowired private RedisTemplate<String, Object> redisTemplate; @Override public <K, V> Cache<K, V> getCache(String name) throws CacheException { return new RedisCache<>(name, redisTemplate); } } 

2.4自定义一个shiro的realm实现

创建realm之前需要编写一个模拟数据库查询的用户业务处理类,提供给上面的自定义realm使用

简单的用户登录对象:
 复制package net.xqlee.project.demo.shiro.pojo;

import java.util.Date;
import java.util.List; /** * 用户信息 * * @author xqlee * */ public class LoginAccount { /** 用户名 */ String loginName; List<String> roles;// 测试用 List<String> permissions;// 测试用直接放用户登录对象里面 /** 用户密码 **/ String password; boolean enabled; Date createDate; boolean isExpired; public String getLoginName() { return loginName; } public void setLoginName(String loginName) { this.loginName = loginName; } public String getPassword() { return password; } public void setPassword(String password) { this.password = password; } public boolean isEnabled() { return enabled; } public void setEnabled(boolean enabled) { this.enabled = enabled; } public Date getCreateDate() { return createDate; } public void setCreateDate(Date createDate) { this.createDate = createDate; } public boolean isExpired() { return isExpired; } public void setExpired(boolean isExpired) { this.isExpired = isExpired; } public List<String> getRoles() { return roles; } public void setRoles(List<String> roles) { this.roles = roles; } public List<String> getPermissions() { return permissions; } public void setPermissions(List<String> permissions) { this.permissions = permissions; } } 
用户模拟业务处理
 复制package net.xqlee.project.demo.shiro.service;

import java.util.ArrayList;
import java.util.HashMap; import java.util.List; import java.util.Map; import org.springframework.stereotype.Component; import net.xqlee.project.demo.shiro.pojo.LoginAccount; /** * 用户业务服务类 * * @author xqlee * */ @Component("userService") public class UserService { /** 由于重点不在数据库,这里需要使用数据库的地方全部用map代替 **/ /** 用户信息 **/ static Map<String, LoginAccount> users = new HashMap<>(); static { // 创建一个用户 LoginAccount account = new LoginAccount(); account.setLoginName("leftso"); account.setPassword("123456"); account.setEnabled(true); account.setExpired(false); // 角色添加 List<String> roles = new ArrayList<>(); roles.add("ROLE_USER"); account.setRoles(roles); List<String> permissions = new ArrayList<>(); permissions.add("query"); permissions.add("delete"); account.setPermissions(permissions); users.put(account.getLoginName(), account); // 创建一个用户 LoginAccount admin = new LoginAccount(); admin.setLoginName("admin"); admin.setPassword("123456"); admin.setEnabled(true); admin.setExpired(false); // 角色添加 roles = new ArrayList<>(); roles.add("ROLE_ADMIN"); admin.setRoles(roles); permissions = new ArrayList<>(); permissions.add("query"); permissions.add("delete"); admin.setPermissions(permissions); users.put("admin", admin); } /** * 通过用户名获取用户权限集合 * * @param loginName * 用户名 * @return 用户的权限集合 */ public List<String> getPermissionsByLoginName(String loginName) { if (users.containsKey(loginName)) { return users.get(loginName).getPermissions(); } return new ArrayList<>(); } /** * 通过用户名获取用户信息 * * @param loginName * 用户名 * @return 用户信息 */ public LoginAccount getLoginAccountByLoginName(String loginName) { if (users.containsKey(loginName)) { return users.get(loginName); } return null; } } 
 
 复制package net.xqlee.project.demo.shiro.config.shiro;

import java.util.List;

import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.AuthenticationInfo; import org.apache.shiro.authc.AuthenticationToken; import org.apache.shiro.authc.SimpleAuthenticationInfo; import org.apache.shiro.authc.UsernamePasswordToken; 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.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import org.springframework.util.StringUtils; import net.xqlee.project.demo.shiro.pojo.LoginAccount; import net.xqlee.project.demo.shiro.service.UserService; /** * 实现一个基于JDBC的Realm,继承AuthorizingRealm可以看见需要重写两个方法,doGetAuthorizationInfo和doGetAuthenticationInfo * * @author xqlee * */ @Component("JDBCShiroRealm") public class JDBCShiroRealm extends AuthorizingRealm { private static final Logger logger = LoggerFactory.getLogger(JDBCShiroRealm.class); /*** 用户业务处理类,用来查询数据库中用户相关信息 ***/ @Autowired UserService userService; /*** * 获取用户授权 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { logger.info("##################执行Shiro权限认证##################"); // 获取用户名 String loginName = (String) principalCollection.fromRealm(getName()).iterator().next(); // 判断用户名是否存在 if (StringUtils.isEmpty(loginName)) { return null; } // 查询登录用户信息 LoginAccount account = userService.getLoginAccountByLoginName(loginName); if (account == null) { logger.warn("用户[" + loginName + "]信息不存在"); return null; } // 创建一个授权对象 SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 进行权限设置 List<String> permissions = account.getPermissions(); if (permissions != null && !permissions.isEmpty()) { info.addStringPermissions(permissions); } // 角色设置 List<String> roles = account.getRoles(); if (roles != null) { info.addRoles(roles); } return info; } /** * 获取用户认证信息 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException { logger.info("##################执行Shiro登陆认证##################"); UsernamePasswordToken token = (UsernamePasswordToken) authenticationToken; // 通过表单接收的用户名 String loginName = token.getUsername(); if (loginName != null && !"".equals(loginName)) { // 模拟数据库查询用户信息 LoginAccount account = userService.getLoginAccountByLoginName(loginName); if (account != null) { // 登陆的主要信息: 可以是一个实体类的对象, 但该实体类的对象一定是根据 token 的 username 查询得到的. Object principal = token.getPrincipal(); // 创建shiro的用户认证对象 // 注意该对象的密码将会传递至后续步骤与前面登陆的subject的密码进行比对。 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, account.getPassword(), getName()); return authenticationInfo; } } return null; } } 



2.5shiro的核心配置文件

ShiroConfig.java
 复制package net.xqlee.project.demo.shiro.config.shiro;

import java.util.LinkedHashMap;
import java.util.Map; import org.apache.shiro.SecurityUtils; import org.apache.shiro.cache.CacheManager; import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.mgt.SecurityManager; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import net.xqlee.project.demo.shiro.config.shiro.cache.RedisCacheManager; /*** * shiro权限管理配置 * * @author xqlee * */ @Configuration public class ShiroConfig { /** * ehcache缓存方案<br/> * 简单的缓存,后续可更换为redis缓存,通过自己实现shiro的CacheManager接口和Cache接口 * * @return */ @Bean public CacheManager shiroEhCacheManager() { EhCacheManager cacheManager = new EhCacheManager(); cacheManager.setCacheManagerConfigFile("classpath:ehcache-shiro.xml"); return cacheManager; } /** * redis缓存方案 * * @return */ @Bean public CacheManager shiroRedisCacheManager() { return new RedisCacheManager(); } /**** * 自定义Real * * @return */ @Bean public JDBCShiroRealm jdbcShiroRealm() { JDBCShiroRealm realm = new JDBCShiroRealm(); // 根据情况使用缓存器 realm.setCacheManager(shiroRedisCacheManager());//shiroEhCacheManager() return realm; } /*** * 安全管理配置 * * @return */ @Bean public SecurityManager defaultWebSecurityManager() { // DefaultSecurityManager defaultSecurityManager = new // DefaultSecurityManager(); DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();// 注意:!!!初始化成这个将会报错java.lang.IllegalArgumentException: // SessionContext must be an HTTP compatible // implementation.:模块化本地测试shiro的一些总结 // 配置 securityManager.setRealm(jdbcShiroRealm()); // 注意这里必须配置securityManager SecurityUtils.setSecurityManager(securityManager); // 根据情况选择缓存器 securityManager.setCacheManager(shiroRedisCacheManager());//shiroEhCacheManager() return securityManager; } /** * 配置shiro的拦截器链工厂,默认会拦截所有请求,并且不可配置 * * @return */ @Bean public ShiroFilterFactoryBean shiroFilterFactoryBean() { ShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean(); // 配置安全管理(必须) filterFactoryBean.setSecurityManager(defaultWebSecurityManager()); // 配置登陆的地址 filterFactoryBean.setLoginUrl("/userNoLogin.do");// 未登录时候跳转URL,如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 filterFactoryBean.setSuccessUrl("/welcome.do");// 成功后欢迎页面 filterFactoryBean.setUnauthorizedUrl("/403.do");// 未认证页面 // 配置拦截地址和拦截器 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();// 必须使用LinkedHashMap,因为拦截有先后顺序 // authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问 filterChainDefinitionMap.put("/userNoLogin.do*", "anon");// 未登录跳转页面不设权限认证 filterChainDefinitionMap.put("/login.do*", "anon");// 登录接口不设置权限认真 filterChainDefinitionMap.put("/logout.do*", "anon");// 登出不需要认证 // 以下配置同样可以通过注解 // @RequiresPermissions("user:edit")来配置访问权限和角色注解@RequiresRoles(value={"ROLE_USER"})方式定义 // 权限配置示例,这里的配置理论来自数据库查询 filterChainDefinitionMap.put("/user/**", "roles[ROLE_USER],perms[query]");// /user/下面的需要ROLE_USER角色或者query权限才能访问 filterChainDefinitionMap.put("/admin/**", "perms[ROLE_ADMIN]");// /admin/下面的所有需要ROLE_ADMIN的角色才能访问 // 剩下的其他资源地址全部需要用户认证后才能访问 filterChainDefinitionMap.put("/**", "authc"); filterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); // 全部配置 // anon org.apache.shiro.web.filter.authc.AnonymousFilter 匿名访问 // // authc org.apache.shiro.web.filter.authc.FormAuthenticationFilter // 需要登录,不需要权限和角色可访问 // // authcBasic // org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter // // perms // org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter // 需要给定的权限值才能访问 // // port org.apache.shiro.web.filter.authz.PortFilter // // rest org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter // // roles org.apache.shiro.web.filter.authz.RolesAuthorizationFilter // 需要给定的角色才能访问 // // ssl org.apache.shiro.web.filter.authz.SslFilter // // user org.apache.shiro.web.filter.authc.UserFilter // // logout org.apache.shiro.web.filter.authc.LogoutFilter return filterFactoryBean; } } 
上面配置中有两个缓存器可以选择,一个是简单的ehcache,一个是redis,大型项目推荐使用redis


2.6编写一个测试的controller

 复制package net.xqlee.project.demo.shiro.controller;

import javax.servlet.http.HttpServletResponse;

import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.AuthenticationException; import org.apache.shiro.authc.ExcessiveAttemptsException; import org.apache.shiro.authc.IncorrectCredentialsException; import org.apache.shiro.authc.LockedAccountException; import org.apache.shiro.authc.UnknownAccountException; import org.apache.shiro.authc.UsernamePasswordToken; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import net.sf.json.JSONObject; /** * 用户登录用 * * @author xqlee * */ @RestController public class LoginController { private static final Logger logger = LoggerFactory.getLogger(LoginController.class); /**** * 用户未登录 * * @return */ @GetMapping("userNoLogin.do") public Object noLogin() { JSONObject object = new JSONObject(); object.put("message", "用户未登录"); return object; } @GetMapping(value = "/login.do") public String login(String loginName, String password) { try { // 创建shiro需要的token UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken(loginName, password.toCharArray()); usernamePasswordToken.setRememberMe(true);// 记住 try { SecurityUtils.getSubject().login(usernamePasswordToken); } catch (UnknownAccountException uae) { logger.info("对用户[" + loginName + "]进行登录验证..验证未通过,未知账户"); return "对用户[" + loginName + "]进行登录验证..验证未通过,未知账户"; } catch (IncorrectCredentialsException ice) { logger.info("对用户[" + loginName + "]进行登录验证..验证未通过,错误的凭证"); ice.printStackTrace(); return "对用户[" + loginName + "]进行登录验证..验证未通过,错误的凭证"; } catch (LockedAccountException lae) { logger.info("对用户[" + loginName + "]进行登录验证..验证未通过,账户已锁定"); return "对用户[" + loginName + "]进行登录验证..验证未通过,账户已锁定"; } catch (ExcessiveAttemptsException eae) { logger.info("对用户[" + loginName + "]进行登录验证..验证未通过,错误次数过多"); return "对用户[" + loginName + "]进行登录验证..验证未通过,错误次数过多"; } catch (AuthenticationException ae) { // 通过处理Shiro的运行时AuthenticationException就可以控制用户登录失败或密码错误时的情景 logger.info("对用户[" + loginName + "]进行登录验证..验证未通过,堆栈轨迹如下"); ae.printStackTrace(); return "用户名或密码不正确"; } return "Login Success!"; } catch (Exception e) { return "登陆时候发生异常," + e.getMessage(); } } @GetMapping("/user/hello.do") public String hello() { return "Hello User, From Server"; } @GetMapping("/admin/hello.do") public String helloAdmin() { return "Hello Admin, From Server"; } @GetMapping("/welcome.do") public String loginSuccess() { return "welcome"; } @GetMapping("/403.do") public Object error403(HttpServletResponse response) { response.setStatus(403); JSONObject object = new JSONObject(); object.put("message", "用户权限不够"); return object; } } 

2.7测试

1.通过spring bootapplication方式启动项目,项目默认监听在8080端口

首先访问需要权限的url  http://localhost:8080/user/hello.do


可以看到返回的是用户未登录提示,也就是我们在controller中写的未登录时候调用的方法返回值。

现在我们先通过用户名leftso和密码123456进行登录  localhost:8080/login.do?loginName=leftso&password=123456


可以看到登录成功,这个时候我们再次打开最初访问的:http://localhost:8080/user/hello.do

可以看到这次我们成功访问了需要ROLE_USER角色的url,

现在用这个登录的信息访问需要ROLE_ADMIN权限的地址http://localhost:8080/admin/hello.do


上面可以看到,无法访问,返回的提示是权限不足

切换为admin的用户登录,然后访问地址http://localhost:8080/admin/hello.do

首先是admin登录

然后访问admin权限的地址


上面看到admin也对应的访问成功了。

并且,切回eclipse的控制台可以看到:
redis
我们的redis缓存也起作用了。

至此spring boot shiro redis的整合角色和权限控制讲解完毕。

猜你喜欢

转载自www.cnblogs.com/telwanggs/p/10809633.html