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/>
</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>
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
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
@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
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
在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;
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.
*/
@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();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/login");
shiroFilterFactoryBean.setSuccessUrl("/usersPage");
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
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");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
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");
hashedCredentialsMatcher.setHashIterations(2);
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);
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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
- 100
- 101
- 102
- 103
- 104
- 105
- 106
- 107
- 108
- 109
- 110
- 111
- 112
- 113
- 114
- 115
- 116
- 117
- 118
- 119
- 120
- 121
- 122
- 123
- 124
- 125
- 126
- 127
- 128
- 129
- 130
- 131
- 132
- 133
- 134
- 135
- 136
- 137
- 138
- 139
- 140
- 141
- 142
- 143
- 144
- 145
- 146
- 147
- 148
- 149
- 150
- 151
- 152
- 153
- 154
- 155
- 156
- 157
- 158
- 159
- 160
- 161
- 162
- 163
- 164
- 165
- 166
- 167
- 168
- 169
- 170
- 171
- 172
- 173
- 174
- 175
- 176
- 177
- 178
- 179
- 180
- 181
- 182
- 183
- 184
- 185
- 186
- 187
- 188
- 189
- 190
- 191
- 192
- 193
- 194
- 195
- 196
- 197
- 198
- 199
- 200
- 201
- 202
- 203
- 204
- 205
- 206
- 207
- 208
配置自定義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();
Map<String,Object> map = new HashMap<String,Object>();
map.put("userid",user.getId());
List<Resources> resourcesList = resourcesService.loadUserResources(map);
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()
);
Session session = SecurityUtils.getSubject().getSession();
session.setAttribute("userSession", user);
session.setAttribute("userSessionId", user.getId());
return authenticationInfo;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
認證:
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"
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
可見用戶登陸的代碼主要就是 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()
);
Session session = SecurityUtils.getSubject().getSession();
session.setAttribute("userSession", user);
session.setAttribute("userSessionId", user.getId());
return authenticationInfo;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
而我們在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");
hashedCredentialsMatcher.setHashIterations(2);
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)
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
授權:
接下來講下授權。在自定義relalm中的代碼爲:
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
User user= (User) SecurityUtils.getSubject().getPrincipal();
Map<String,Object> map = new HashMap<String,Object>();
map.put("userid",user.getId());
List<Resources> resourcesList = resourcesService.loadUserResources(map);
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();
shiroFilterFactoryBean.setSecurityManager(securityManager);
shiroFilterFactoryBean.setLoginUrl("/login");
shiroFilterFactoryBean.setSuccessUrl("/usersPage");
shiroFilterFactoryBean.setUnauthorizedUrl("/403");
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");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
該代碼片段爲配置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.*
@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("更新權限成功!!")
}
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
- 48
- 49
- 50
- 51
- 52
- 53
- 54
- 55
- 56
- 57
- 58
- 59
- 60
- 61
- 62
- 63
- 64
- 65
- 66
- 67
- 68
- 69
- 70
- 71
- 72
- 73
- 74
- 75
- 76
- 77
- 78
- 79
- 80
- 81
- 82
- 83
- 84
- 85
- 86
- 87
- 88
- 89
- 90
- 91
- 92
- 93
- 94
- 95
- 96
- 97
- 98
- 99
會話管理
這個例子使用了redis保存session。這樣可以實現集羣的session共享。在ShiroConfig中有代碼:
@Bean
public SecurityManager securityManager(){
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
securityManager.setRealm(myShiroRealm());
securityManager.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);
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;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
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
@EnableCaching
public 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;
}
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
- 15
- 16
- 17
- 18
- 19
- 20
- 21
- 22
- 23
- 24
- 25
- 26
- 27
- 28
- 29
- 30
- 31
- 32
- 33
- 34
- 35
- 36
- 37
- 38
- 39
- 40
- 41
- 42
- 43
- 44
- 45
- 46
- 47
配置文件 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()方法中,我把
這行代碼注了,是這樣的,因爲每次在需要驗證的地方,比如在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" οnclick="$('#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