SpringBoot整合mybatis、shiro、redis实现基于数据库的细粒度动态权限管理系统实例

shiro

目录(?)[+]

  1. 前言
  2. 表结构
  3. maven配置
  4. 配置Druid
  5. 配置mybatis
    1. MyMapper
  6. thymeleaf配置
  7. shiro配置
    1. 配置文件ShiroConfig
    2. 配置自定义Realm
    3. 认证
    4. 授权
  8. 会话管理
  9. 按钮控制
  10. 效果图
  11. 运行下载

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配置

<?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>com.study</groupId><artifactId>springboot-shiro</artifactId><version>0.0.1-SNAPSHOT</version><packaging>jar</packaging>

<name>springboot-shiro</name><description>Demo project for Spring Boot</description>

<parent><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-parent</artifactId><version>1.5.2.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</artifactId></dependency>

<dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-test</artifactId><scope>test</scope></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-web</artifactId></dependency><dependency><groupId>org.springframework.boot</groupId><artifactId>spring-boot-starter-thymeleaf</artifactId></dependency><dependency><groupId>com.github.pagehelper</groupId><artifactId>pagehelper-spring-boot-starter</artifactId><version>1.1.0</version></dependency><dependency><groupId>tk.mybatis</groupId><artifactId>mapper-spring-boot-starter</artifactId><version>1.1.1</version></dependency><dependency><groupId>org.apache.shiro</groupId><artifactId>shiro-spring</artifactId><version>1.3.2</version></dependency><dependency><groupId>com.alibaba</groupId><artifactId>druid</artifactId><version>1.0.29</version></dependency><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId></dependency><dependency><groupId>net.sourceforge.nekohtml</groupId><artifactId>nekohtml</artifactId><version>1.9.22</version></dependency><dependency><groupId>com.github.theborakompanioni</groupId><artifactId>thymeleaf-extras-shiro</artifactId><version>1.2.1</version></dependency><dependency><groupId>org.crazycake</groupId><artifactId>shiro-redis</artifactId><version>2.4.2.1-RELEASE</version></dependency></dependencies><build><plugins><plugin><groupId>org.springframework.boot</groupId><artifactId>spring-boot-maven-plugin</artifactId></plugin><plugin><groupId>org.mybatis.generator</groupId><artifactId>mybatis-generator-maven-plugin</artifactId><version>1.3.5</version><configuration><configurationFile>${basedir}/src/main/resources/generator/generatorConfig.xml</configurationFile><overwrite>true</overwrite><verbose>true</verbose></configuration><dependencies><dependency><groupId>mysql</groupId><artifactId>mysql-connector-java</artifactId><version>${mysql.version}</version></dependency><dependency><groupId>tk.mybatis</groupId><artifactId>mapper</artifactId><version>3.4.0</version></dependency></dependencies></plugin></plugins></build>

</project>

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 {@Beanpublic ServletRegistrationBean druidServlet() {

ServletRegistrationBean servletRegistrationBean = new ServletRegistrationBean(new StatViewServlet(), "/druid/*");//登录查看信息的账号密码.

servletRegistrationBean.addInitParameter("loginUsername","admin");servletRegistrationBean.addInitParameter("loginPassword","123456");return servletRegistrationBean;}@Beanpublic FilterRegistrationBean filterRegistrationBean() {FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();filterRegistrationBean.setFilter(new WebStatFilter());filterRegistrationBean.addUrlPatterns("/*");filterRegistrationBean.addInitParameter("exclusions", "*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*");return filterRegistrationBean;}
}

在application.properties中加入:

# 数据源基础配置
spring.datasource.type=com.alibaba.druid.pool.DruidDataSource
spring.datasource.driver-class-name=com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://localhost:3306/shiro
spring.datasource.username=root
spring.datasource.password=root# 连接池配置# 初始化大小,最小,最大
spring.datasource.initialSize=1
spring.datasource.minIdle=1
spring.datasource.maxActive=20

配置好后,运行项目访问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=false
mapper.identity=MYSQL
pagehelper.helperDialect=mysql
pagehelper.reasonable=true
pagehelper.supportMethodsArguments=true

pagehelper.params=count\=countSql

将相应的路径改成项目包所在的路径即可。配置文件中可以看出来还加入了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 MyMapper<T> extends Mapper<T>, MySqlMapper<T> {
}

对于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=<encoding> is added#spring.thymeleaf.content-type=text/html# set to false for hot refresh
spring.thymeleaf.cache=false
spring.thymeleaf.mode=LEGACYHTML5

可以看到其实上面都是注释了的,因为springboot会根据约定俗成的方式帮我们配置好。所以上面注释部分是springboot自动配置的,如果需要自定义配置,只需要修改上注释部分即可。
后两行没有注释的部分,spring.thymeleaf.cache=false表示关闭缓存,这样修改文件后不需要重新启动,缓存默认是开启的,所以指定为false。但是在intellij idea中还需要按Ctrl + Shift + F9.
对于spring.thymeleaf.mode=LEGACYHTML5。thymeleaf对html中的语法要求非常严格,像我从网上找的模板,使用thymeleaf后报一堆的语法错误,后来没办法,使用弱语法校验,所以加入配置spring.thymeleaf.mode=LEGACYHTML5。加入这个配置后还需要在maven中加入

<dependency><groupId>net.sourceforge.nekohtml</groupId><artifactId>nekohtml</artifactId><version>1.9.22</version></dependency>

否则会报错的。
在前端页面的头部加入一下配置后,就可以使用thymeleaf了

<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}" />

不过这个项目因为使用了datatables都是使用jquery 的ajax来访问数据与处理数据,所以用到的thymeleaf语法非常少,基本上可以参考的就是js即css的导入和类似于jsp的include功能的部分页面引入。
对于静态文件的引入:

 <link rel="stylesheet" th:href="@{/css/bootstrap.min.css}" />

而文件在项目中的位置是static-css-bootstrap.min.css。为什么这样可以访问到该文件,也是因为springboot对于静态文件会自动查找/static public、/resources、/META-INF/resources下的文件。所以不需要加static.

页面引入:
局部页面如下:

<div  th:fragment="top">...
</div>

主体页面映入方式:

<div th:include="common/top :: top"></div>

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;

@Beanpublic static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {return new LifecycleBeanPostProcessor();}/*** ShiroDialect,为了在thymeleaf里使用shiro的标签的bean* @return*/@Beanpublic ShiroDialect shiroDialect() {return new ShiroDialect();}/*** ShiroFilterFactoryBean 处理拦截资源文件问题。* 注意:单独一个ShiroFilterFactoryBean配置是或报错的,因为在* 初始化ShiroFilterFactoryBean的时候需要注入:SecurityManager*Filter Chain定义说明1、一个URL可以配置多个Filter,使用逗号分隔2、当设置多个过滤器时,全部验证通过,才视为通过3、部分过滤器可指定参数,如perms,roles**/@Beanpublic ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){System.out.println("ShiroConfiguration.shirFilter()");ShiroFilterFactoryBean shiroFilterFactoryBean  = new ShiroFilterFactoryBean();

// 必须设置 SecurityManagershiroFilterFactoryBean.setSecurityManager(securityManager);// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面shiroFilterFactoryBean.setLoginUrl("/login");// 登录成功后要跳转的链接shiroFilterFactoryBean.setSuccessUrl("/usersPage");//未授权界面;shiroFilterFactoryBean.setUnauthorizedUrl("/403");//拦截器.Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();

//配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了filterChainDefinitionMap.put("/logout", "logout");filterChainDefinitionMap.put("/css/**","anon");filterChainDefinitionMap.put("/js/**","anon");filterChainDefinitionMap.put("/img/**","anon");filterChainDefinitionMap.put("/font-awesome/**","anon");//<!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;//<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->//自定义加载权限资源关系List<Resources> 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;}@Beanpublic SecurityManager securityManager(){DefaultWebSecurityManager securityManager =  new DefaultWebSecurityManager();//设置realm.securityManager.setRealm(myShiroRealm());// 自定义缓存实现 使用redis//securityManager.setCacheManager(cacheManager());// 自定义session管理 使用redissecurityManager.setSessionManager(sessionManager());return securityManager;}

@Beanpublic MyShiroRealm myShiroRealm(){MyShiroRealm myShiroRealm = new MyShiroRealm();myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());return myShiroRealm;}

/*** 凭证匹配器* (由于我们的密码校验交给Shiro的SimpleAuthenticationInfo进行处理了*  所以我们需要修改下doGetAuthenticationInfo中的代码;* )* @return*/@Beanpublic HashedCredentialsMatcher hashedCredentialsMatcher(){HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();

hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));

return hashedCredentialsMatcher;}/***  开启shiro aop注解支持.*  使用代理方式;所以需要开启代码支持;* @param securityManager* @return*/@Beanpublic 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开源插件*/@Beanpublic RedisSessionDAO redisSessionDAO() {RedisSessionDAO redisSessionDAO = new RedisSessionDAO();redisSessionDAO.setRedisManager(redisManager());return redisSessionDAO;}/*** shiro session的管理*/@Beanpublic DefaultWebSessionManager sessionManager() {DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();sessionManager.setSessionDAO(redisSessionDAO());return sessionManager;}}
配置自定义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 {

@Resourceprivate UserService userService;

@Resourceprivate ResourcesService resourcesService;//授权@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {User user= (User) SecurityUtils.getSubject().getPrincipal();//User{id=1, username='admin', password='3ef7164d1f6167cb9f2658c07d3c2f0a', enable=1}Map<String,Object> map = new HashMap<String,Object>();map.put("userid",user.getId());List<Resources> resourcesList = resourcesService.loadUserResources(map);// 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();for(Resources resources: resourcesList){info.addStringPermission(resources.getResurl());}return info;}//认证@Overrideprotected 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;}

}
认证:

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";}}

可见用户登陆的代码主要就是 subject.login(token);调用后就会进去我们自定义的realm中的doGetAuthenticationInfo()方法。

 //认证@Overrideprotected 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;}

而我们在ShiroConfig中配置了凭证匹配器:

@Beanpublic MyShiroRealm myShiroRealm(){MyShiroRealm myShiroRealm = new MyShiroRealm();myShiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());return myShiroRealm;}

@Beanpublic HashedCredentialsMatcher hashedCredentialsMatcher(){HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();

hashedCredentialsMatcher.setHashAlgorithmName("md5");//散列算法:这里使用MD5算法;hashedCredentialsMatcher.setHashIterations(2);//散列的次数,比如散列两次,相当于 md5(md5(""));return hashedCredentialsMatcher;}

所以在认证时的密码是加过密的,使用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";}}

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);}
}
授权:

接下来讲下授权。在自定义relalm中的代码为:

 //授权@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {User user= (User) SecurityUtils.getSubject().getPrincipal();//User{id=1, username='admin', password='3ef7164d1f6167cb9f2658c07d3c2f0a', enable=1}Map<String,Object> map = new HashMap<String,Object>();map.put("userid",user.getId());List<Resources> resourcesList = resourcesService.loadUserResources(map);// 权限信息对象info,用来存放查出的用户的所有的角色(role)及权限(permission)SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();for(Resources resources: resourcesList){info.addStringPermission(resources.getResurl());}return info;}

从以上代码中可以看出来,我根据用户id查询出用户的权限,放入SimpleAuthorizationInfo。关联表user_role,role_resources,resources,三张表,根据用户所拥有的角色,角色所拥有的权限,查询出分配给该用户的所有权限的url。当访问的链接中配置在shiro中时,或者使用shiro标签,shiro权限注解时,则会访问该方法,判断该用户是否拥有相应的权限。

在ShiroConfig中有如下代码:

 @Beanpublic ShiroFilterFactoryBean shirFilter(SecurityManager securityManager){System.out.println("ShiroConfiguration.shirFilter()");ShiroFilterFactoryBean shiroFilterFactoryBean  = new ShiroFilterFactoryBean();

// 必须设置 SecurityManagershiroFilterFactoryBean.setSecurityManager(securityManager);// 如果不设置默认会自动寻找Web工程根目录下的"/login.jsp"页面shiroFilterFactoryBean.setLoginUrl("/login");// 登录成功后要跳转的链接shiroFilterFactoryBean.setSuccessUrl("/usersPage");//未授权界面;shiroFilterFactoryBean.setUnauthorizedUrl("/403");//拦截器.Map<String,String> filterChainDefinitionMap = new LinkedHashMap<String,String>();//配置退出 过滤器,其中的具体的退出代码Shiro已经替我们实现了filterChainDefinitionMap.put("/logout", "logout");filterChainDefinitionMap.put("/css/**","anon");filterChainDefinitionMap.put("/js/**","anon");filterChainDefinitionMap.put("/img/**","anon");filterChainDefinitionMap.put("/font-awesome/**","anon");//<!-- 过滤链定义,从上向下顺序执行,一般将 /**放在最为下边 -->:这是一个坑呢,一不小心代码就不好使了;//<!-- authc:所有url都必须认证通过才可以访问; anon:所有url都都可以匿名访问-->//自定义加载权限资源关系List<Resources> 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;}

该代码片段为配置shiro的过滤器。以上代码将静态文件设置为任何权限都可访问,然后

 List<Resources> resourcesList = resourcesService.queryAll();for(Resources resources:resourcesList){

if (StringUtil.isNotEmpty(resources.getResurl())) {String permission = "perms[" + resources.getResurl()+ "]";filterChainDefinitionMap.put(resources.getResurl(),permission);}}

在数据中查询所有的资源,将该资源的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 {@Autowiredprivate ShiroFilterFactoryBean shiroFilterFactoryBean;@Autowiredprivate ResourcesService resourcesService;@Autowiredprivate RedisSessionDAO redisSessionDAO;/*** 初始化权限*/public Map<String, String> loadFilterChainDefinitions() {// 权限控制map.从数据库获取Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();filterChainDefinitionMap.put("/logout", "logout");filterChainDefinitionMap.put("/css/**","anon");filterChainDefinitionMap.put("/js/**","anon");filterChainDefinitionMap.put("/img/**","anon");filterChainDefinitionMap.put("/font-awesome/**","anon");List<Resources> 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<String, String> chains = shiroFilterFactoryBean.getFilterChainDefinitionMap();for (Map.Entry<String, String> entry : chains.entrySet()) {String url = entry.getKey();String chainDefinition = entry.getValue().trim().replace(" ", "");manager.createChain(url, chainDefinition);}

System.out.println("更新权限成功!!");}}

}
会话管理

这个例子使用了redis保存session。这样可以实现集群的session共享。在ShiroConfig中有代码:

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

配置了自定义session,网上已经有大神实现了 使用redis 自定义session管理,直接拿来用,引入包

<dependency><groupId>org.crazycake</groupId><artifactId>shiro-redis</artifactId><version>2.4.2.1-RELEASE</version></dependency>  

然后再配置:

 /*** 配置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开源插件*/@Beanpublic RedisSessionDAO redisSessionDAO() {RedisSessionDAO redisSessionDAO = new RedisSessionDAO();redisSessionDAO.setRedisManager(redisManager());return redisSessionDAO;}/*** shiro session的管理*/@Beanpublic DefaultWebSessionManager sessionManager() {DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();sessionManager.setSessionDAO(redisSessionDAO());return sessionManager;}

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;

@Beanpublic 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;}}

配置文件 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= 0


当然运行的时候要先启动redis。将自己的redis配置在以上配置中。这样session就存在redis中了。
上面ShiroConfig中的securityManager()方法中,我把

//securityManager.setCacheManager(cacheManager());

这行代码注了,是这样的,因为每次在需要验证的地方,比如在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 List<Resources> loadUserResources(Map<String, Object> map) {return resourcesMapper.loadUserResources(map);}

这样也可以实现,然后在修改权限时加上注解

 @CacheEvict(cacheNames="resources", allEntries=true)

这样修改权限后可以立即生效。其实我感觉这样不好,因为清楚了我是清除了所有用户的权限缓存,其实只要修改当前session在线中被修改权限的用户就行了。 先这样吧,以后再研究下,修改得更好一点。

按钮控制

在前端页面,对按钮进行细粒度权限控制,只需要在按钮上加上shiro:hasPermission

  <button shiro:hasPermission="/users/add" type="button"  onclick="$('#addUser').modal();" class="btn btn-info" >新增</button>

这里的参数就是我们在ShiroConfig-shirFilter()权限加载时的过滤器 中的value,也就是资源的url。

  filterChainDefinitionMap.put(resources.getResurl(),permission);

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

阅读全文
 
 

上一篇SSM整合SpringSecurity实现权限管理实例 javaconfig配置方式 下一篇MySQL EXPLAIN详解

SpringBoot整合mybatis、shiro、redis实现基于数据库的细粒度动态权限管理系统实例(转)...相关推荐

  1. SpringBoot整合mybatis、shiro、redis实现基于数据库的细粒度动态权限管理系统实例...

    SpringBoot整合mybatis.shiro.redis实现基于数据库的细粒度动态权限管理系统实例 shiro 目录(?)[+] 1.前言 本文主要介绍使用SpringBoot与shiro实现基 ...

  2. 【SpringBoot整合Mybatis】数据库某字段值为空时,接口未返回该字段 解决办法

    [SpringBoot整合Mybatis]数据库字段为空时,接口不返回该字段 解决办法 问题描述: 排查问题: 解决问题: 测试: 参考资料: 今天整合项目的时候,发现了SpringBoot整合Myb ...

  3. SpringBoot整合Mybatis(高级)

    SpringBoot整合Mybatis(高级) 文章目录 SpringBoot整合Mybatis(高级) 前言 基础环境配置 增删改查 ResultMap 复杂查询 多对一 一对多 动态SQL if ...

  4. springboot整合mybatis

    3.springboot整合mybatis 首先新建一个项目,勾选上我们需要的 1.springboot配置数据库连接池druid druid学习地址 https://github.com/aliba ...

  5. Springboot整合mybatis plus生成代码

    一.Springboot整合mybatis plus生成代码 1.介绍 1.1.前言 从零开始搭建一个项目骨架,最好选择合适熟悉的技术,并且在未来易拓展,适合微服务化体系等.所以一般以Springbo ...

  6. mybatis plugins_[MyBatis] SpringBoot 整合Mybatis

    现在基本上搭建一个简单的工程都是三剑客 springboot+mybatis+redis 之前整合Mybatis 都是按照SSM来,所以,这一次带来SpringBoot+MyBatis 的快速整合 p ...

  7. SpringBoot整合Mybatis超详细流程

    SpringBoot整合Mybatis超详细流程 文章目录 SpringBoot整合Mybatis超详细流程 前言 详细流程 0.引入Mybatis 1.创建数据 2.创建程序目录 3.理解后台访问流 ...

  8. 3、SpringBoot整合MyBatis注解版及配置文件版

    目录 1.配置pom.xml 2.配置application.yml 3.配置DruidConfig关联yml的配置文件spring.datasource 4.创建数据库及数据库表结构 5.创建对应的 ...

  9. (一)SpringBoot 整合 MyBatis

    一.工具 IDE:idea.DB:mysql 二.创建SpringBoot工程 在Idea中使用SpringInitializr模板创建SpringBoot工程,依赖选择如下: 这里也可以不选JDBC ...

最新文章

  1. google breakpad native crash分析工具
  2. 客户端如何连接 DataSnap Server 调用服务的方法
  3. 程序员心中都有一个江湖,java世界,就是一个江湖!
  4. Tomcat Server 配置
  5. android平板值得买吗,2021年一月更新1000-2000价位最全平板选购指南
  6. python日期时间
  7. 知识表示之二——产生式规则表示法
  8. 2018杭州云栖大会,梁胜博士的演讲PPT来啦!
  9. 研发工程师如何转型项目经理
  10. 构造函数和复制函数java_什么是Java构造函数?
  11. 优衣库KAWS联名款遭哄抢 大打出手场面惨烈 是我不懂时尚了吗?
  12. 高通发布《5G经济》研究:将催生12万亿美元市场
  13. 循环冗余校验(CRC,模2运算)
  14. 区间直觉模糊集相似度及matlab应用
  15. matlab调和均值滤波_求matlab均值滤波、中值滤波和领域平均滤波算法
  16. CTA策略01_dualThrust
  17. 压缩包文件解压找回密码
  18. ingress 七层负载均衡器
  19. 万测试验机软件,万测关注检查井盖质量检测
  20. [Oracle 11g r2(11.2.0.4.0)]集群守护进程gpnp介绍

热门文章

  1. Android ANR产生的原理和如何避免
  2. hdoj 1025 Constructing Roads In JGShining's Kingdom(最长上升子序列+二分)
  3. Android的消息机制(2)
  4. 数据库防护技术对比分析
  5. 谷歌Nexus 3开售时间曝光
  6. 致远互联“平台+生态”抢占数字化升级新赛
  7. 法总统:英国若“无协议脱欧” 将成最大输家
  8. C# 图片旋转360度程序
  9. Java 并发编程中使用 ReentrantLock 替代 synchronized 关键字原语
  10. ActionScript 3.0 Step By Step系列(五):走在面向对象开发的路上,以类为基础去思考编程问题...