SpringBoot整合實現基於數據庫的細粒度動態權限管理系統實例

SpringBoot整合mybatis、shiro、redis實現基於數據庫的細粒度動態權限管理系統實例


SpringBoot整合實現基於數據庫的細粒度動態權限管理系統實例


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張表來展現權限。如下圖:

SpringBoot整合實現基於數據庫的細粒度動態權限管理系統實例

分別為用戶表,角色表,資源表,用戶角色表,角色資源表。在這個demo中使用了mybatis-generator自動生成代碼。運行mybatis-generator:generate -e 根據數據庫中的表,生成 相應的model,mapper單表的增刪改查。不過如果是導入本項目的就別運行這個命令了。新增表的話,也要修改mybatis-generator-config.xml中的tableName,指定表名再運行。

3.maven配置

<project>

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>

<properties>

<project.build.sourceencoding>UTF-8/<project.build.sourceencoding>

<project.reporting.outputencoding>UTF-8/<project.reporting.outputencoding>

<java.version>1.8/<java.version>

<dependencies>

<dependency>

<groupid>org.springframework.boot/<groupid>

<artifactid>spring-boot-starter/<artifactid>

<dependency>

<groupid>org.springframework.boot/<groupid>

<artifactid>spring-boot-starter-test/<artifactid>

<scope>test/<scope>

<dependency>

<groupid>org.springframework.boot/<groupid>

<artifactid>spring-boot-starter-web/<artifactid>

<dependency>

<groupid>org.springframework.boot/<groupid>

<artifactid>spring-boot-starter-thymeleaf/<artifactid>

<dependency>

<groupid>com.github.pagehelper/<groupid>

<artifactid>pagehelper-spring-boot-starter/<artifactid>

<version>1.1.0/<version>

<dependency>

<groupid>tk.mybatis/<groupid>

<artifactid>mapper-spring-boot-starter/<artifactid>

<version>1.1.1/<version>

<dependency>

<groupid>org.apache.shiro/<groupid>

<artifactid>shiro-spring/<artifactid>

<version>1.3.2/<version>

<dependency>

<groupid>com.alibaba/<groupid>

<artifactid>druid/<artifactid>

<version>1.0.29/<version>

<dependency>

<groupid>mysql/<groupid>

<artifactid>mysql-connector-java/<artifactid>

<dependency>

<groupid>net.sourceforge.nekohtml/<groupid>

<artifactid>nekohtml/<artifactid>

<version>1.9.22/<version>

<dependency>

<groupid>com.github.theborakompanioni/<groupid>

<artifactid>thymeleaf-extras-shiro/<artifactid>

<version>1.2.1/<version>

<dependency>

<groupid>org.crazycake/<groupid>

<artifactid>shiro-redis/<artifactid>

<version>2.4.2.1-RELEASE/<version>

<build>

<plugins>

<plugin>

<groupid>org.springframework.boot/<groupid>

<artifactid>spring-boot-maven-plugin/<artifactid>

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

<dependencies>

<dependency>

<groupid>mysql/<groupid>

<artifactid>mysql-connector-java/<artifactid>

<version>${mysql.version}/<version>

<dependency>

<groupid>tk.mybatis/<groupid>

<artifactid>mapper/<artifactid>

<version>3.4.0/<version>

/<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 {

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

}

}

在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進入:

SpringBoot整合實現基於數據庫的細粒度動態權限管理系統實例


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

extends Mapper, MySqlMapper {

}

對於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/<encoding>

#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>

否則會報錯的。

在前端頁面的頭部加入一下配置後,就可以使用thymeleaf了

<link>

不過這個項目因為使用了datatables都是使用jquery 的ajax來訪問數據與處理數據,所以用到的thymeleaf語法非常少,基本上可以參考的就是js即css的導入和類似於jsp的include功能的部分頁面引入。

對於靜態文件的引入:

<link>

而文件在項目中的位置是static-css-bootstrap.min.css。為什麼這樣可以訪問到該文件,也是因為springboot對於靜態文件會自動查找/static public、/resources、/META-INF/resources下的文件。所以不需要加static.

頁面引入:

局部頁面如下:

...

主體頁面映入方式:

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.

*/

@Configuration

public 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> filterChainDefinitionMap = new LinkedHashMap<string>();/<string>/<string>

//配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了

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();/<resources>

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;

}

}

配置自定義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> map = new HashMap<string>();/<string>/<string>

map.put("userid",user.getId());

List<resources> resourcesList = resourcesService.loadUserResources(map);/<resources>

// 權限信息對象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;

}

}

認證:

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()方法。

//認證

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

}

而我們在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;

}

所以在認證時的密碼是加過密的,使用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中的代碼為:

@Override

protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {

User user= (User) SecurityUtils.getSubject().getPrincipal();//User{id=1, username='admin', password='3ef7164d1f6167cb9f2658c07d3c2f0a', enable=1}

Map<string> map = new HashMap<string>();/<string>/<string>

map.put("userid",user.getId());

List<resources> resourcesList = resourcesService.loadUserResources(map);/<resources>

// 權限信息對象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中有如下代碼:

@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> filterChainDefinitionMap = new LinkedHashMap<string>();/<string>/<string>

//配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實現了

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();/<resources>

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();/<resources>

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 {

@Autowired

private ShiroFilterFactoryBean shiroFilterFactoryBean;

@Autowired

private ResourcesService resourcesService;

@Autowired

private RedisSessionDAO redisSessionDAO;

/**

* 初始化權限

*/

public Map<string> loadFilterChainDefinitions() {/<string>

// 權限控制map.從數據庫獲取

Map<string> filterChainDefinitionMap = new LinkedHashMap<string>();/<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();/<resources>

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> chains = shiroFilterFactoryBean/<string>

.getFilterChainDefinitionMap();

for (Map.Entry<string> entry : chains.entrySet()) {/<string>

String url = entry.getKey();

String chainDefinition = entry.getValue().trim()

.replace(" ", "");

manager.createChain(url, chainDefinition);

}

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

}

}

}

會話管理

這個例子使用了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;

}

配置了自定義session,網上已經有大神實現了 使用redis 自定義session管理,直接拿來用,引入包

<dependency>

<groupid>org.crazycake/<groupid>

<artifactid>shiro-redis/<artifactid>

<version>2.4.2.1-RELEASE/<version>

然後再配置:

/**

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

配置文件 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> map) {/<string>/<resources>

return resourcesMapper.loadUserResources(map);

}

這樣也可以實現,然後在修改權限時加上註解

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

這樣修改權限後可以立即生效。其實我感覺這樣不好,因為清楚了我是清除了所有用戶的權限緩存,其實只要修改當前session在線中被修改權限的用戶就行了。 先這樣吧,以後再研究下,修改得更好一點。

按鈕控制

在前端頁面,對按鈕進行細粒度權限控制,只需要在按鈕上加上shiro:hasPermission

<button>新增/<button>

這裡的參數就是我們在ShiroConfig-shirFilter()權限加載時的過濾器 中的value,也就是資源的url。

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

8.效果圖

SpringBoot整合實現基於數據庫的細粒度動態權限管理系統實例


SpringBoot整合實現基於數據庫的細粒度動態權限管理系統實例


9.運行、下載

下載項目後運行resources下的shiro.sql文件。需要運行redis後運行項目。訪問http://localhost:8080/ 賬號密碼:admin admin 或user1 user1.新增的用戶也可以登錄。

———————————–分割線 2018-01-21——————————–

之前說了一個問題,是將doGetAuthorizationInfo()的返回結果存放在redis 無效,然後改成了spring cache的方式。後來因為換工作的原因,一直沒時間去弄這個。在此謝謝 noWayBinding 這位同學在評論出,給出瞭解決方法。我今天修改了下重新上傳了。

還有同學說redis 連接不上,我的項目因為引用了別人寫好的shiro-redis。所以代碼裡的RedisConfig 其實並沒有用上,大家要加上密碼的話,應該在ShiroConfig 中設置redisManager.setPassword(password); 。

————————————————

更多幹貨請關注我,免費Java架構視頻資料私信


分享到:


相關文章: