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>123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120
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 MyMapper<T> extends Mapper<T>, MySqlMapper<T> {}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=<encoding> 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中加入
<dependency> <groupId>net.sourceforge.nekohtml</groupId> <artifactId>nekohtml</artifactId> <version>1.9.22</version></dependency>12345
否則會報錯的。
在前端頁面的頭部加入一下配置後,就可以使用thymeleaf了
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}" />1
不過這個項目因爲使用了datatables都是使用jquery 的ajax來訪問數據與處理數據,所以用到的thymeleaf語法非常少,基本上可以參考的就是js即css的導入和類似於jsp的include功能的部分頁面引入。
對於靜態文件的引入:
<link rel="stylesheet" th:href="@{/css/bootstrap.min.css}" />1
而文件在項目中的位置是static-css-bootstrap.min.css。爲什麼這樣可以訪問到該文件,也是因爲springboot對於靜態文件會自動查找/static public、/resources、/META-INF/resources下的文件。所以不需要加static.
頁面引入:
局部頁面如下:
<div th:fragment="top"> ...</div>123
主體頁面映入方式:
<div th:include="common/top :: top"></div>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"); //攔截器. 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; } @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} 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; } //認證 @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} 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; }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"); //攔截器. 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; }12345678910111213141516171819202122232425262728293031323334353637383940
該代碼片段爲配置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); } }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 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("更新權限成功!!"); } } }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管理,直接拿來用,引入包
<dependency> <groupId>org.crazycake</groupId> <artifactId>shiro-redis</artifactId> <version>2.4.2.1-RELEASE</version></dependency> 123456
然後再配置:
/** * 配置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 List<Resources> loadUserResources(Map<String, Object> map) { return resourcesMapper.loadUserResources(map); }1234
這樣也可以實現,然後在修改權限時加上註解
@CacheEvict(cacheNames="resources", allEntries=true)1
這樣修改權限後可以立即生效。其實我感覺這樣不好,因爲清楚了我是清除了所有用戶的權限緩存,其實只要修改當前session在線中被修改權限的用戶就行了。 先這樣吧,以後再研究下,修改得更好一點。
按鈕控制
在前端頁面,對按鈕進行細粒度權限控制,只需要在按鈕上加上shiro:hasPermission
<button shiro:hasPermission="/users/add" type="button" onclick="$('#addUser').modal();" class="btn btn-info" >新增</button>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