[+]
1.前言
本文主要介绍使用SpringBoot与shiro实现基于数据库的细粒度动态权限管理系统实例。
使用技术:SpringBoot、mybatis、shiro、thymeleaf、pagehelper、Mapper插件、druid、dataTables、ztree、jQuery
开发工具:intellij idea
数据库:mysql、redis
基本上是基于使用SpringSecurity的demo上修改而成,地址 http://blog.csdn.net/poorcoder_/article/details/70231779
2.表结构
还是是用标准的5张表来展现权限。如下图:
分别为用户表,角色表,资源表,用户角色表,角色资源表。在这个demo中使用了mybatis-generator自动生成代码。运行mybatis-generator:generate -e 根据数据库中的表,生成 相应的model,mapper单表的增删改查。不过如果是导入本项目的就别运行这个命令了。新增表的话,也要修改mybatis-generator-config.xml中的tableName,指定表名再运行。
3.maven配置
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120 4.0.0 com.study springboot-shiro 0.0.1-SNAPSHOT jar springboot-shiro Demo project for Spring Boot org.springframework.boot spring-boot-starter-parent 1.5.2.RELEASE UTF-8 UTF-8 1.8 org.springframework.boot spring-boot-starter org.springframework.boot spring-boot-starter-test test org.springframework.boot spring-boot-starter-web org.springframework.boot spring-boot-starter-thymeleaf com.github.pagehelper pagehelper-spring-boot-starter 1.1.0 tk.mybatis mapper-spring-boot-starter 1.1.1 org.apache.shiro shiro-spring 1.3.2 com.alibaba druid 1.0.29 mysql mysql-connector-java net.sourceforge.nekohtml nekohtml 1.9.22 com.github.theborakompanioni thymeleaf-extras-shiro 1.2.1 org.crazycake shiro-redis 2.4.2.1-RELEASE org.springframework.boot spring-boot-maven-plugin org.mybatis.generator mybatis-generator-maven-plugin 1.3.5 ${basedir}/src/main/resources/generator/generatorConfig.xml true true mysql mysql-connector-java ${mysql.version} tk.mybatis mapper 3.4.0
4.配置Druid
package com.study.config;import com.alibaba.druid.support.http.StatViewServlet;import com.alibaba.druid.support.http.WebStatFilter;import org.springframework.boot.web.servlet.FilterRegistrationBean;import org.springframework.boot.web.servlet.ServletRegistrationBean;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;/** * Created by yangqj on 2017/4/19. */@Configuration public class DruidConfig { @Bean public ServletRegistrationBean druidServlet() { ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*"); //登录查看信息的账号密码. servletRegistrationBean.addInitParameter("loginUsername","admin"); servletRegistrationBean.addInitParameter("loginPassword","123456"); return servletRegistrationBean; } @Bean public FilterRegistrationBean filterRegistrationBean() { FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean(); filterRegistrationBean.setFilter(new WebStatFilter()); filterRegistrationBean.addUrlPatterns("/*"); filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*"); return filterRegistrationBean; } }12345678910111213141516171819202122232425262728293031323334353637
在application.properties中加入:
# 数据源基础配置spring.datasource.type=com.alibaba.druid.pool.DruidDataSourcespring.datasource.driver-class-name=com.mysql.jdbc.Driverspring.datasource.url=jdbc:mysql://localhost:3306/shiro spring.datasource.username=root spring.datasource.password=root# 连接池配置# 初始化大小,最小,最大spring.datasource.initialSize=1spring.datasource.minIdle=1spring.datasource.maxActive=201234567891011
配置好后,运行项目访问http://localhost:8080/druid/ 输入配置的账号密码admin,123456进入:
5.配置mybatis
使用springboot 整合mybatis非常方便,只需在application.properties
mybatis.type-aliases-package=com.study.model mybatis.mapper-locations=classpath:mapper/*.xml mapper.mappers=com.study.util.MyMapper mapper.not-empty=falsemapper.identity=MYSQL pagehelper.helperDialect=mysql pagehelper.reasonable=truepagehelper.supportMethodsArguments=truepagehelper.params=count\=countSql123456789
将相应的路径改成项目包所在的路径即可。配置文件中可以看出来还加入了pagehelper 和Mapper插件。如果不需要,把上面配置文件中的 pagehelper删除。
MyMapper:
package com.study.util;/** * Created by yangqj on 2017/4/20. */import tk.mybatis.mapper.common.Mapper; import tk.mybatis.mapper.common.MySqlMapper;public interface MyMapperextends Mapper , MySqlMapper {}1234567891011
对于Springboot整合mybatis可以参考https://github.com/abel533/MyBatis-Spring-Boot
6.thymeleaf配置
thymeleaf是springboot官方推荐的,所以来试一下。
首先加入配置:
#spring.thymeleaf.prefix=classpath:/templates/#spring.thymeleaf.suffix=.html#spring.thymeleaf.mode=HTML5#spring.thymeleaf.encoding=UTF-8# ;charset=is added#spring.thymeleaf.content-type=text/html# set to false for hot refreshspring.thymeleaf.cache=falsespring.thymeleaf.mode=LEGACYHTML512345678910
可以看到其实上面都是注释了的,因为springboot会根据约定俗成的方式帮我们配置好。所以上面注释部分是springboot自动配置的,如果需要自定义配置,只需要修改上注释部分即可。
后两行没有注释的部分,spring.thymeleaf.cache=false表示关闭缓存,这样修改文件后不需要重新启动,缓存默认是开启的,所以指定为false。但是在intellij idea中还需要按Ctrl + Shift + F9.
对于spring.thymeleaf.mode=LEGACYHTML5。thymeleaf对html中的语法要求非常严格,像我从网上找的模板,使用thymeleaf后报一堆的语法错误,后来没办法,使用弱语法校验,所以加入配置spring.thymeleaf.mode=LEGACYHTML5。加入这个配置后还需要在maven中加入
12345 net.sourceforge.nekohtml nekohtml 1.9.22
否则会报错的。
在前端页面的头部加入一下配置后,就可以使用thymeleaf了
1
不过这个项目因为使用了datatables都是使用jquery 的ajax来访问数据与处理数据,所以用到的thymeleaf语法非常少,基本上可以参考的就是js即css的导入和类似于jsp的include功能的部分页面引入。
对于静态文件的引入:
1
而文件在项目中的位置是static-css-bootstrap.min.css。为什么这样可以访问到该文件,也是因为springboot对于静态文件会自动查找/static public、/resources、/META-INF/resources下的文件。所以不需要加static.
页面引入:
局部页面如下:
...123
主体页面映入方式:
1
inclide=”文件路径::局部代码片段名称”
7.shiro配置
配置文件ShiroConfig
package com.study.config;import at.pollux.thymeleaf.shiro.dialect.ShiroDialect;import com.github.pagehelper.util.StringUtil;import com.study.model.Resources;import com.study.service.ResourcesService;import com.study.shiro.MyShiroRealm;import org.apache.shiro.authc.credential.HashedCredentialsMatcher;import org.apache.shiro.mgt.SecurityManager;import org.apache.shiro.spring.LifecycleBeanPostProcessor;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.beans.factory.annotation.Autowired;import org.springframework.beans.factory.annotation.Value;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import java.util.LinkedHashMap;import java.util.List;import java.util.Map;/** * Created by yangqj on 2017/4/23. */@Configurationpublic class ShiroConfig { @Autowired(required = false) private ResourcesService resourcesService; @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Value("${spring.redis.timeout}") private int timeout; @Bean public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * ShiroDialect,为了在thymeleaf里使用shiro的标签的bean * @return */ @Bean public ShiroDialect shiroDialect() { return new ShiroDialect(); } /** * ShiroFilterFactoryBean 处理拦截资源文件问题。 * 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在 * 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager * Filter Chain定义说明 1、一个URL可以配置多个Filter,使用逗号分隔 2、当设置多个过滤器时,全部验证通过,才视为通过 3、部分过滤器可指定参数,如perms,roles * */ @Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){ System.out.println("ShiroConfiguration.shirFilter()"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 必须设置 SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 shiroFilterFactoryBean.setLoginUrl("/login"); // 登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/usersPage"); //未授权界面; shiroFilterFactoryBean.setUnauthorizedUrl("/403"); //拦截器. MapfilterChainDefinitionMap = new LinkedHashMap (); //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了 filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/css/**","anon"); filterChainDefinitionMap.put("/js/**","anon"); filterChainDefinitionMap.put("/img/**","anon"); filterChainDefinitionMap.put("/font-awesome/**","anon"); //:这是一个坑呢,一不小心代码就不好使了; // //自定义加载权限资源关系 List resourcesList = resourcesService.queryAll(); for(Resources resources:resourcesList){ if (StringUtil.isNotEmpty(resources.getResurl())) { String permission = "perms[" + resources.getResurl()+ "]"; filterChainDefinitionMap.put(resources.getResurl(),permission); } } filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); 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; } @Bean public MyShiroRealm myShiroRealm(){ MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; } /** * 凭证匹配器 * (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了 * 所以我们需要修改下doGetAuthenticationInfo中的代码; * ) * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher(){ HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法; hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5("")); return hashedCredentialsMatcher; } /** * 开启shiro aop注解支持. * 使用代理方式;所以需要开启代码支持; * @param securityManager * @return */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager){ AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager); return authorizationAttributeSourceAdvisor; } /** * 配置shiro redisManager * 使用的是shiro-redis开源插件 * @return */ public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(host); redisManager.setPort(port); redisManager.setExpire(1800);// 配置缓存过期时间 redisManager.setTimeout(timeout); // redisManager.setPassword(password); return redisManager; } /** * cacheManager 缓存 redis实现 * 使用的是shiro-redis开源插件 * @return */ public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); return redisCacheManager; } /** * RedisSessionDAO shiro sessionDao层的实现 通过redis * 使用的是shiro-redis开源插件 */ @Bean public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); return redisSessionDAO; } /** * shiro session的管理 */ @Bean public DefaultWebSessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionDAO(redisSessionDAO()); return sessionManager; } }123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208
配置自定义Realm
package com.study.shiro;import com.study.model.Resources;import com.study.model.User;import com.study.service.ResourcesService;import com.study.service.UserService;import org.apache.shiro.SecurityUtils;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.session.Session;import org.apache.shiro.subject.PrincipalCollection;import org.apache.shiro.util.ByteSource;import javax.annotation.Resource;import java.util.HashMap;import java.util.List;import java.util.Map;/** * Created by yangqj on 2017/4/21. */public class MyShiroRealm extends AuthorizingRealm { @Resource private UserService userService; @Resource private ResourcesService resourcesService; //授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { User user= (User) SecurityUtils.getSubject().getPrincipal();//User{id=1, username='admin', password='3ef7164d1f6167cb9f2658c07d3c2f0a', enable=1} Mapmap = new HashMap (); map.put("userid",user.getId()); List resourcesList = resourcesService.loadUserResources(map); // 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission) SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); for(Resources resources: resourcesList){ info.addStringPermission(resources.getResurl()); } return info; } //认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //获取用户的输入的账号. String username = (String)token.getPrincipal(); User user = userService.selectByUsername(username); if(user==null) throw new UnknownAccountException(); if (0==user.getEnable()) { throw new LockedAccountException(); // 帐号锁定 } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user, //用户 user.getPassword(), //密码 ByteSource.Util.bytes(username), getName() //realm name ); // 当验证都通过后,把用户信息放在session里 Session session = SecurityUtils.getSubject().getSession(); session.setAttribute("userSession", user); session.setAttribute("userSessionId", user.getId()); return authenticationInfo; } }123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172
认证:
shiro的主要模块分别就是授权和认证和会话管理。
我们先讲认证。认证就是验证用户。比如用户登录的时候验证账号密码是否正确。
我们可以把对登录的验证交给shiro。我们执行要查询相应的用户信息,并传给shiro。如下代码则为用户登录:
@RequestMapping(value="/login",method=RequestMethod.POST) public String login(HttpServletRequest request, User user, Model model){ if (StringUtils.isEmpty(user.getUsername()) || StringUtils.isEmpty(user.getPassword())) { request.setAttribute("msg", "用户名或密码不能为空!"); return "login"; } Subject subject = SecurityUtils.getSubject(); UsernamePasswordToken token=new UsernamePasswordToken(user.getUsername(),user.getPassword()); try { subject.login(token); return "redirect:usersPage"; }catch (LockedAccountException lae) { token.clear(); request.setAttribute("msg", "用户已经被锁定不能登录,请与管理员联系!"); return "login"; } catch (AuthenticationException e) { token.clear(); request.setAttribute("msg", "用户或密码不正确!"); return "login"; } }123456789101112131415161718192021
可见用户登陆的代码主要就是 subject.login(token);调用后就会进去我们自定义的realm中的doGetAuthenticationInfo()方法。
//认证 @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { //获取用户的输入的账号. String username = (String)token.getPrincipal(); User user = userService.selectByUsername(username); if(user==null) throw new UnknownAccountException(); if (0==user.getEnable()) { throw new LockedAccountException(); // 帐号锁定 } SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo( user, //用户 user.getPassword(), //密码 ByteSource.Util.bytes(username), getName() //realm name ); // 当验证都通过后,把用户信息放在session里 Session session = SecurityUtils.getSubject().getSession(); session.setAttribute("userSession", user); session.setAttribute("userSessionId", user.getId()); return authenticationInfo; }12345678910111213141516171819202122
而我们在ShiroConfig中配置了凭证匹配器:
@Bean public MyShiroRealm myShiroRealm(){ MyShiroRealm myShiroRealm = new MyShiroRealm(); myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return myShiroRealm; } @Bean public HashedCredentialsMatcher hashedCredentialsMatcher(){ HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法; hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5("")); return hashedCredentialsMatcher; }12345678910111213141516
所以在认证时的密码是加过密的,使用md5散发将密码与盐值组合加密两次。则我们在增加用户的时候,对用户的密码则要进过相同规则的加密才行。
添加用户代码如下:
@RequestMapping(value = "/add") public String add(User user) { User u = userService.selectByUsername(user.getUsername()); if(u != null) return "error"; try { user.setEnable(1); PasswordHelper passwordHelper = new PasswordHelper(); passwordHelper.encryptPassword(user); userService.save(user); return "success"; } catch (Exception e) { e.printStackTrace(); return "fail"; } }12345678910111213141516
PasswordHelper:
package com.study.util;import com.study.model.User;import org.apache.shiro.crypto.RandomNumberGenerator;import org.apache.shiro.crypto.SecureRandomNumberGenerator;import org.apache.shiro.crypto.hash.SimpleHash;import org.apache.shiro.util.ByteSource;public class PasswordHelper { //private RandomNumberGenerator randomNumberGenerator = new SecureRandomNumberGenerator(); private String algorithmName = "md5"; private int hashIterations = 2; public void encryptPassword(User user) { //String salt=randomNumberGenerator.nextBytes().toHex(); String newPassword = new SimpleHash(algorithmName, user.getPassword(), ByteSource.Util.bytes(user.getUsername()), hashIterations).toHex(); //String newPassword = new SimpleHash(algorithmName, user.getPassword()).toHex(); user.setPassword(newPassword); } public static void main(String[] args) { PasswordHelper passwordHelper = new PasswordHelper(); User user = new User(); user.setUsername("admin"); user.setPassword("admin"); passwordHelper.encryptPassword(user); System.out.println(user); } }12345678910111213141516171819202122232425262728293031
授权:
接下来讲下授权。在自定义relalm中的代码为:
//授权 @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) { User user= (User) SecurityUtils.getSubject().getPrincipal();//User{id=1, username='admin', password='3ef7164d1f6167cb9f2658c07d3c2f0a', enable=1} Mapmap = new HashMap (); map.put("userid",user.getId()); List resourcesList = resourcesService.loadUserResources(map); // 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission) SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); for(Resources resources: resourcesList){ info.addStringPermission(resources.getResurl()); } return info; }1234567891011121314
从以上代码中可以看出来,我根据用户id查询出用户的权限,放入SimpleAuthorizationInfo。关联表user_role,role_resources,resources,三张表,根据用户所拥有的角色,角色所拥有的权限,查询出分配给该用户的所有权限的url。当访问的链接中配置在shiro中时,或者使用shiro标签,shiro权限注解时,则会访问该方法,判断该用户是否拥有相应的权限。
在ShiroConfig中有如下代码:
@Bean public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){ System.out.println("ShiroConfiguration.shirFilter()"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 必须设置 SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); // 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面 shiroFilterFactoryBean.setLoginUrl("/login"); // 登录成功后要跳转的链接 shiroFilterFactoryBean.setSuccessUrl("/usersPage"); //未授权界面; shiroFilterFactoryBean.setUnauthorizedUrl("/403"); //拦截器. MapfilterChainDefinitionMap = new LinkedHashMap (); //配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了 filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/css/**","anon"); filterChainDefinitionMap.put("/js/**","anon"); filterChainDefinitionMap.put("/img/**","anon"); filterChainDefinitionMap.put("/font-awesome/**","anon"); //:这是一个坑呢,一不小心代码就不好使了; // //自定义加载权限资源关系 List resourcesList = resourcesService.queryAll(); for(Resources resources:resourcesList){ if (StringUtil.isNotEmpty(resources.getResurl())) { String permission = "perms[" + resources.getResurl()+ "]"; filterChainDefinitionMap.put(resources.getResurl(),permission); } } filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; }12345678910111213141516171819202122232425262728293031323334353637383940
该代码片段为配置shiro的过滤器。以上代码将静态文件设置为任何权限都可访问,然后
ListresourcesList = resourcesService.queryAll(); for(Resources resources:resourcesList){ if (StringUtil.isNotEmpty(resources.getResurl())) { String permission = "perms[" + resources.getResurl()+ "]"; filterChainDefinitionMap.put(resources.getResurl(),permission); } }12345678
在数据中查询所有的资源,将该资源的url当作key,配置拥有该url权限的用户才可访问该url。
最后加入 filterChainDefinitionMap.put(“/*”, “authc”);表示其他没有配置的链接都需要认证才可访问。注意这个要放最后面,因为shiro的匹配是从上往下,如果匹配到就不继续匹配了,所以把 /放到最前面,则 后面的链接都无法匹配到了。
而这段代码是在项目启动的时候加载的。加载的数据是放到内存中的。但是当权限增加或者删除时,正常情况下不会重新启动来,重新加载权限。所以需要调用以下代码的updatePermission()方法来重新加载权限。其实下面的代码有些重复了,可以稍微调整下,我就先这么写了。
package com.study.shiro;import com.github.pagehelper.util.StringUtil;import com.study.model.Resources;import com.study.model.User;import com.study.service.ResourcesService;import org.apache.shiro.SecurityUtils;import org.apache.shiro.mgt.RealmSecurityManager;import org.apache.shiro.session.Session;import org.apache.shiro.spring.web.ShiroFilterFactoryBean;import org.apache.shiro.subject.SimplePrincipalCollection;import org.apache.shiro.subject.support.DefaultSubjectContext;import org.apache.shiro.web.filter.mgt.DefaultFilterChainManager;import org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver;import org.apache.shiro.web.servlet.AbstractShiroFilter;import org.crazycake.shiro.RedisSessionDAO;import org.springframework.beans.factory.annotation.Autowired;import org.springframework.stereotype.Service;import java.util.*;/** * Created by yangqj on 2017/4/30. */@Service public class ShiroService { @Autowired private ShiroFilterFactoryBean shiroFilterFactoryBean; @Autowired private ResourcesService resourcesService; @Autowired private RedisSessionDAO redisSessionDAO; /** * 初始化权限 */ public MaploadFilterChainDefinitions() { // 权限控制map.从数据库获取 Map filterChainDefinitionMap = new LinkedHashMap (); filterChainDefinitionMap.put("/logout", "logout"); filterChainDefinitionMap.put("/css/**","anon"); filterChainDefinitionMap.put("/js/**","anon"); filterChainDefinitionMap.put("/img/**","anon"); filterChainDefinitionMap.put("/font-awesome/**","anon"); List resourcesList = resourcesService.queryAll(); for(Resources resources:resourcesList){ if (StringUtil.isNotEmpty(resources.getResurl())) { String permission = "perms[" + resources.getResurl()+ "]"; filterChainDefinitionMap.put(resources.getResurl(),permission); } } filterChainDefinitionMap.put("/**", "authc"); return filterChainDefinitionMap; } /** * 重新加载权限 */ public void updatePermission() { synchronized (shiroFilterFactoryBean) { AbstractShiroFilter shiroFilter = null; try { shiroFilter = (AbstractShiroFilter) shiroFilterFactoryBean .getObject(); } catch (Exception e) { throw new RuntimeException( "get ShiroFilter from shiroFilterFactoryBean error!"); } PathMatchingFilterChainResolver filterChainResolver = (PathMatchingFilterChainResolver) shiroFilter .getFilterChainResolver(); DefaultFilterChainManager manager = (DefaultFilterChainManager) filterChainResolver .getFilterChainManager(); // 清空老的权限控制 manager.getFilterChains().clear(); shiroFilterFactoryBean.getFilterChainDefinitionMap().clear(); shiroFilterFactoryBean .setFilterChainDefinitionMap(loadFilterChainDefinitions()); // 重新构建生成 Map chains = shiroFilterFactoryBean .getFilterChainDefinitionMap(); for (Map.Entry entry : chains.entrySet()) { String url = entry.getKey(); String chainDefinition = entry.getValue().trim() .replace(" ", ""); manager.createChain(url, chainDefinition); } System.out.println("更新权限成功!!"); } } }123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899
会话管理
这个例子使用了redis保存session。这样可以实现集群的session共享。在ShiroConfig中有代码:
@Bean public SecurityManager securityManager(){ DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(); //设置realm. securityManager.setRealm(myShiroRealm()); // 自定义缓存实现 使用redis //securityManager.setCacheManager(cacheManager()); // 自定义session管理 使用redis securityManager.setSessionManager(sessionManager()); return securityManager; }1234567891011
配置了自定义session,网上已经有大神实现了 使用redis 自定义session管理,直接拿来用,引入包
123456 org.crazycake shiro-redis 2.4.2.1-RELEASE
然后再配置:
/** * 配置shiro redisManager * 使用的是shiro-redis开源插件 * @return */ public RedisManager redisManager() { RedisManager redisManager = new RedisManager(); redisManager.setHost(host); redisManager.setPort(port); redisManager.setExpire(1800);// 配置缓存过期时间 redisManager.setTimeout(timeout); // redisManager.setPassword(password); return redisManager; } /** * cacheManager 缓存 redis实现 * 使用的是shiro-redis开源插件 * @return */ public RedisCacheManager cacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(redisManager()); return redisCacheManager; } /** * RedisSessionDAO shiro sessionDao层的实现 通过redis * 使用的是shiro-redis开源插件 */ @Bean public RedisSessionDAO redisSessionDAO() { RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(redisManager()); return redisSessionDAO; } /** * shiro session的管理 */ @Bean public DefaultWebSessionManager sessionManager() { DefaultWebSessionManager sessionManager = new DefaultWebSessionManager(); sessionManager.setSessionDAO(redisSessionDAO()); return sessionManager; }1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
RedisConfig:
package com.study.config;import org.apache.log4j.Logger;import org.springframework.beans.factory.annotation.Value;import org.springframework.cache.annotation.CachingConfigurerSupport;import org.springframework.cache.annotation.EnableCaching;import org.springframework.context.annotation.Bean;import org.springframework.context.annotation.Configuration;import redis.clients.jedis.JedisPool;import redis.clients.jedis.JedisPoolConfig;/** * Created by yangqj on 2017/4/30. */@Configuration@EnableCachingpublic class RedisConfig extends CachingConfigurerSupport { @Value("${spring.redis.host}") private String host; @Value("${spring.redis.port}") private int port; @Value("${spring.redis.timeout}") private int timeout; @Value("${spring.redis.pool.max-idle}") private int maxIdle; @Value("${spring.redis.pool.max-wait}") private long maxWaitMillis; @Bean public JedisPool redisPoolFactory() { Logger.getLogger(getClass()).info("JedisPool注入成功!!"); Logger.getLogger(getClass()).info("redis地址:" + host + ":" + port); JedisPoolConfig jedisPoolConfig = new JedisPoolConfig(); jedisPoolConfig.setMaxIdle(maxIdle); jedisPoolConfig.setMaxWaitMillis(maxWaitMillis); JedisPool jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout); return jedisPool; } }1234567891011121314151617181920212223242526272829303132333435363738394041424344454647
配置文件 application.properties中加入:
#redis# Redis服务器地址spring.redis.host= localhost# Redis服务器连接端口spring.redis.port= 6379# 连接池中的最大空闲连接spring.redis.pool.max-idle= 8# 连接池中的最小空闲连接spring.redis.pool.min-idle= 0# 连接池最大连接数(使用负值表示没有限制)spring.redis.pool.max-active= 8# 连接池最大阻塞等待时间(使用负值表示没有限制)spring.redis.pool.max-wait= -1# 连接超时时间(毫秒)spring.redis.timeout= 0123456789101112131415
当然运行的时候要先启动redis。将自己的redis配置在以上配置中。这样session就存在redis中了。
上面ShiroConfig中的securityManager()方法中,我把
//securityManager.setCacheManager(cacheManager());1
这行代码注了,是这样的,因为每次在需要验证的地方,比如在subject.hasRole(“admin”) 或 subject.isPermitted(“admin”)、@RequiresRoles(“admin”) 、 shiro:hasPermission=”/users/add”的时候都会调用MyShiroRealm中的doGetAuthorizationInfo()。但是以为这些信息不是经常变的,所以有必要进行缓存。把这行代码的注释打开,的时候都会调用MyShiroRealm中的doGetAuthorizationInfo()的返回结果会被redis缓存。但是这里稍微有个小问题,就是在刚修改用户的权限时,无法立即失效。本来我是使用了ShiroService中的clearUserAuthByUserId()想清除当前session存在的用户的权限缓存,但是没有效果。不知道什么原因。希望哪个大神看到后帮忙弄个解决方法。所以我干脆就把doGetAuthorizationInfo()的返回结果通过spring cache的方式加入缓存。
@Cacheable(cacheNames="resources",key="#map['userid'].toString()+#map['type']") public ListloadUserResources(Map map) { return resourcesMapper.loadUserResources(map); }1234
这样也可以实现,然后在修改权限时加上注解
@CacheEvict(cacheNames="resources", allEntries=true)1
这样修改权限后可以立即生效。其实我感觉这样不好,因为清楚了我是清除了所有用户的权限缓存,其实只要修改当前session在线中被修改权限的用户就行了。 先这样吧,以后再研究下,修改得更好一点。
按钮控制
在前端页面,对按钮进行细粒度权限控制,只需要在按钮上加上shiro:hasPermission
1
这里的参数就是我们在ShiroConfig-shirFilter()权限加载时的过滤器 中的value,也就是资源的url。
filterChainDefinitionMap.put(resources.getResurl(),permission);1
8.效果图
9.运行、下载
下载项目后运行resources下的shiro.sql文件。需要运行redis后运行项目。访问http://localhost:8080/ 账号密码:admin admin 或user1 user1.新增的用户也可以登录。
github下载地址:https://github.com/lovelyCoder/springboot-shiro
转载请标明出处:http://blog.csdn.net/poorCoder_/article/details/71374002