本文章是介紹SpringBoot整合Apache Shiro,并實作在項目啟動時從資料庫中讀取權限清單,在對角色進行增删改時,動态更新權限以及在分布式環境下的Session共享,Session共享使用的是shiro-redis架構,是根據真實項目寫的一個Demo。網上有很多關于Shiro相關的文章,但是大多都是零零散散的,要麼就隻介紹上述功能中的一兩個功能,要麼就是缺少配置相關的内容。是以,我整理了一下,給大家一個參考的。廢話不多說,直接上代碼。關于Shiro相關的概念,大家可以在網上自行百度。
一、使用到的相關的表
CREATE TABLE `t_user` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`username` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '使用者名',
`password` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '密碼',
`contacts` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT NULL COMMENT '聯系人',
`mobile` varchar(11) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '手機号',
`gender` tinyint(1) DEFAULT '0' COMMENT '性别',
`email` varchar(64) DEFAULT '' COMMENT '郵箱',
`role_id` bigint(20) DEFAULT '0' COMMENT '角色id',
`status` tinyint(255) DEFAULT '1' COMMENT '狀态,0:禁用 1:啟用',
`create_time` datetime DEFAULT NULL,
`creator` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP,
`updater` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=19 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='使用者表';
CREATE TABLE `t_role` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`role_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '角色名稱',
`status` tinyint(1) DEFAULT NULL COMMENT '角色狀态,0:禁用 1:啟用',
`create_time` datetime DEFAULT NULL,
`creator` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '',
`update_time` datetime DEFAULT CURRENT_TIMESTAMP,
`updater` varchar(32) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=23 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='角色表';
CREATE TABLE `t_authority` (
`id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT '主鍵',
`authority_name` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '權限名稱',
`icon` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '圖示',
`uri` varchar(255) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '請求uri',
`permission` varchar(1000) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT '' COMMENT '權限',
`p_id` bigint(20) DEFAULT NULL COMMENT '父權限id',
`type` varchar(16) CHARACTER SET utf8 COLLATE utf8_general_ci DEFAULT 'button' COMMENT '權限類型',
PRIMARY KEY (`id`) USING BTREE
) ENGINE=InnoDB AUTO_INCREMENT=17 DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='權限表';
CREATE TABLE `t_role_authority` (
`role_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '角色id',
`authority_id` bigint(20) NOT NULL DEFAULT '0' COMMENT '權限id',
PRIMARY KEY (`role_id`,`authority_id`) USING BTREE
) ENGINE=InnoDB DEFAULT CHARSET=utf8 ROW_FORMAT=DYNAMIC COMMENT='角色-權限表';
二、初始化資料
INSERT INTO `spring-boot-shiro`.`t_user`(`id`, `username`, `password`, `contacts`, `mobile`, `gender`, `email`, `role_id`, `status`, `create_time`, `creator`, `update_time`, `updater`) VALUES (1, 'admin', 'tFr8USrhfSSzG0Ya+F9dPg==', '管理者', '15921103565', 1, '[email protected]', 1, 1, NULL, '', '2019-09-06 10:19:51', '');
INSERT INTO `spring-boot-shiro`.`t_user`(`id`, `username`, `password`, `contacts`, `mobile`, `gender`, `email`, `role_id`, `status`, `create_time`, `creator`, `update_time`, `updater`) VALUES (2, 'guest', 'tFr8USrhfSSzG0Ya+F9dPg==', '普通使用者', '15821141248', 0, '[email protected]', 2, 1, NULL, '', '2019-09-06 10:19:51', '');
INSERT INTO `spring-boot-shiro`.`t_role`(`id`, `role_name`, `status`, `create_time`, `creator`, `update_time`, `updater`) VALUES (1, 'admin', 1, NULL, NULL, NULL, NULL);
INSERT INTO `spring-boot-shiro`.`t_role`(`id`, `role_name`, `status`, `create_time`, `creator`, `update_time`, `updater`) VALUES (2, '普通使用者', 1, NULL, NULL, NULL, NULL);
INSERT INTO `spring-boot-shiro`.`t_authority`(`id`, `authority_name`, `icon`, `uri`, `permission`, `p_id`, `type`) VALUES (1, '使用者管理', '', '', '', NULL, NULL);
INSERT INTO `spring-boot-shiro`.`t_authority`(`id`, `authority_name`, `icon`, `uri`, `permission`, `p_id`, `type`) VALUES (2, '角色管理', '', '', '', NULL, NULL);
INSERT INTO `spring-boot-shiro`.`t_authority`(`id`, `authority_name`, `icon`, `uri`, `permission`, `p_id`, `type`) VALUES (3, '查詢(分頁)', '', '/role/page', 'roles[admin,普通使用者]', 2, NULL);
INSERT INTO `spring-boot-shiro`.`t_authority`(`id`, `authority_name`, `icon`, `uri`, `permission`, `p_id`, `type`) VALUES (4, '新增', '', '/user/add', 'roles[admin]', 1, NULL);
INSERT INTO `spring-boot-shiro`.`t_authority`(`id`, `authority_name`, `icon`, `uri`, `permission`, `p_id`, `type`) VALUES (5, '删除', '', '/user/delete', 'roles[admin]', 1, NULL);
INSERT INTO `spring-boot-shiro`.`t_authority`(`id`, `authority_name`, `icon`, `uri`, `permission`, `p_id`, `type`) VALUES (6, '修改', '', '/user/update', 'roles[admin]', 1, NULL);
INSERT INTO `spring-boot-shiro`.`t_authority`(`id`, `authority_name`, `icon`, `uri`, `permission`, `p_id`, `type`) VALUES (7, '查詢', '', '/user/page', 'roles[admin,普通使用者]', 1, NULL);
INSERT INTO `spring-boot-shiro`.`t_authority`(`id`, `authority_name`, `icon`, `uri`, `permission`, `p_id`, `type`) VALUES (8, '查詢', '', '/role/list', 'roles[admin,普通使用者]', 2, NULL);
INSERT INTO `spring-boot-shiro`.`t_authority`(`id`, `authority_name`, `icon`, `uri`, `permission`, `p_id`, `type`) VALUES (9, '權限清單', '', '/authority/list', 'roles[admin]', 2, '');
INSERT INTO `spring-boot-shiro`.`t_authority`(`id`, `authority_name`, `icon`, `uri`, `permission`, `p_id`, `type`) VALUES (10, '新增', '', '/role/add', 'roles[admin]', 2, 'button');
INSERT INTO `spring-boot-shiro`.`t_authority`(`id`, `authority_name`, `icon`, `uri`, `permission`, `p_id`, `type`) VALUES (11, '啟用/禁用', '', '/role/updateStatus', 'roles[admin]', 2, 'button');
INSERT INTO `spring-boot-shiro`.`t_authority`(`id`, `authority_name`, `icon`, `uri`, `permission`, `p_id`, `type`) VALUES (12, '删除', '', '/role/delete', 'roles[admin,admin]', 2, 'button');
INSERT INTO `spring-boot-shiro`.`t_authority`(`id`, `authority_name`, `icon`, `uri`, `permission`, `p_id`, `type`) VALUES (13, '詳情', '', '/role/detail', 'roles[admin,admin]', 2, 'button');
INSERT INTO `spring-boot-shiro`.`t_authority`(`id`, `authority_name`, `icon`, `uri`, `permission`, `p_id`, `type`) VALUES (14, '修改', '', '/role/update', 'roles[admin,admin]', 2, 'button');
INSERT INTO `spring-boot-shiro`.`t_authority`(`id`, `authority_name`, `icon`, `uri`, `permission`, `p_id`, `type`) VALUES (15, '啟用/禁用', '', '/user/updateStatus', 'roles[admin]', 1, 'button');
INSERT INTO `spring-boot-shiro`.`t_authority`(`id`, `authority_name`, `icon`, `uri`, `permission`, `p_id`, `type`) VALUES (16, '詳情', '', '/user/detail', 'roles[admin]', 1, 'button');
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (1, 3);
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (1, 4);
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (1, 5);
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (1, 6);
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (1, 7);
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (1, 8);
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (1, 9);
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (1, 10);
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (1, 11);
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (1, 12);
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (1, 13);
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (1, 14);
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (1, 15);
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (1, 16);
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (2, 3);
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (2, 7);
INSERT INTO `spring-boot-shiro`.`t_role_authority`(`role_id`, `authority_id`) VALUES (2, 8);
三、pom.xml
<?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.example</groupId>
<artifactId>spring-boot-shiro</artifactId>
<version>0.0.1-SNAPSHOT</version>
<packaging>jar</packaging>
<name>spring-boot-shiro</name>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.9.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-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.0</version>
</dependency>
<!-- shiro -->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-core</artifactId>
<version>1.4.1</version>
</dependency>
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.0.0</version>
</dependency>
<!-- mysql -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.11</version>
</dependency>
<!-- druid -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.19</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.47</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.pagehelper</groupId>
<artifactId>pagehelper-spring-boot-starter</artifactId>
<version>1.2.12</version>
</dependency>
<dependency>
<groupId>tk.mybatis</groupId>
<artifactId>mapper</artifactId>
<version>4.1.5</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
<version>1.2.3</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.apache.logging.log4j</groupId>
<artifactId>log4j-to-slf4j</artifactId>
<version>2.10.0</version>
<scope>compile</scope>
</dependency>
<dependency>
<groupId>org.slf4j</groupId>
<artifactId>jul-to-slf4j</artifactId>
<version>1.7.25</version>
<scope>compile</scope>
</dependency>
<!-- https://mvnrepository.com/artifact/org.apache.commons/commons-lang3 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>3.8.1</version>
</dependency>
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15on</artifactId>
<version>1.55</version>
</dependency>
<dependency>
<groupId>io.jsonwebtoken</groupId>
<artifactId>jjwt</artifactId>
<version>0.9.0</version>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>com.github.ulisesbocchio</groupId>
<artifactId>jasypt-spring-boot-starter</artifactId>
<version>2.0.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
四、Shiro和自定義MessageConverter的配置Bean
@Configuration
public class ShiroConfig {
private static final String CACHE_KEY = "shiro:cache:";
private static final String SESSION_KEY = "shiro:session:";
private static final String NAME = "custom.name";
private static final String VALUE = "/";
@Bean
public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager, ShiroService shiroService) {
ShiroFilterFactoryBean shiroFilter = new ShiroFilterFactoryBean();
shiroFilter.setSecurityManager(securityManager);
Map<String, Filter> filterMap = new LinkedHashMap<>(1);
filterMap.put("roles", rolesAuthorizationFilter());
shiroFilter.setFilters(filterMap);
shiroFilter.setFilterChainDefinitionMap(shiroService.loadFilterChainDefinitions());
return shiroFilter;
}
@Bean
public CustomRolesAuthorizationFilter rolesAuthorizationFilter() {
return new CustomRolesAuthorizationFilter();
}
@Bean("securityManager")
public SecurityManager securityManager(Realm realm, SessionManager sessionManager, RedisCacheManager redisCacheManager) {
DefaultWebSecurityManager manager = new DefaultWebSecurityManager();
manager.setSessionManager(sessionManager);
manager.setCacheManager(redisCacheManager);
manager.setRealm(realm);
return manager;
}
@Bean("defaultAdvisorAutoProxyCreator")
@DependsOn("lifecycleBeanPostProcessor")
public DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator() {
DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator();
//指定強制使用cglib為action建立代理對象
defaultAdvisorAutoProxyCreator.setProxyTargetClass(true);
return defaultAdvisorAutoProxyCreator;
}
@Bean("lifecycleBeanPostProcessor")
public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() {
return new LifecycleBeanPostProcessor();
}
@Bean("delegatingFilterProxy")
public FilterRegistrationBean delegatingFilterProxy(){
FilterRegistrationBean filterRegistrationBean = new FilterRegistrationBean();
DelegatingFilterProxy proxy = new DelegatingFilterProxy();
proxy.setTargetFilterLifecycle(true);
proxy.setTargetBeanName("shiroFilter");
filterRegistrationBean.setFilter(proxy);
return filterRegistrationBean;
}
/**
* Redis叢集使用RedisClusterManager,單個Redis使用RedisManager
* @param redisProperties
* @return
*/
@Bean
public RedisClusterManager redisManager(RedisProperties redisProperties) {
RedisClusterManager redisManager = new RedisClusterManager();
redisManager.setHost(redisProperties.getHost());
redisManager.setPassword(redisProperties.getPassword());
return redisManager;
}
@Bean
public RedisCacheManager redisCacheManager(RedisClusterManager redisManager) {
RedisCacheManager redisCacheManager = new RedisCacheManager();
redisCacheManager.setRedisManager(redisManager);
redisCacheManager.setExpire(86400);
redisCacheManager.setKeyPrefix(CACHE_KEY);
return redisCacheManager;
}
@Bean
public RedisSessionDAO redisSessionDAO(RedisClusterManager redisManager) {
RedisSessionDAO redisSessionDAO = new RedisSessionDAO();
redisSessionDAO.setExpire(86400);
redisSessionDAO.setKeyPrefix(SESSION_KEY);
redisSessionDAO.setRedisManager(redisManager);
return redisSessionDAO;
}
@Bean
public DefaultWebSessionManager sessionManager(RedisSessionDAO sessionDAO, SimpleCookie simpleCookie) {
DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();
sessionManager.setSessionDAO(sessionDAO);
sessionManager.setSessionIdCookieEnabled(true);
sessionManager.setSessionIdCookie(simpleCookie);
return sessionManager;
}
@Bean
public SimpleCookie simpleCookie() {
SimpleCookie simpleCookie = new SimpleCookie();
simpleCookie.setName(NAME);
simpleCookie.setValue(VALUE);
return simpleCookie;
}
@Bean
public Realm realm(RedisCacheManager redisCacheManager) {
PasswordRealm realm = new PasswordRealm();
realm.setCacheManager(redisCacheManager);
realm.setAuthenticationCachingEnabled(false);
realm.setAuthorizationCachingEnabled(false);
return realm;
}
}
@Configuration
public class MessageConverterConfig {
@Bean
public HttpMessageConverters fastJsonHttpMessageConverter() {
//定義一個轉換消息的對象
FastJsonHttpMessageConverter converter = new FastJsonHttpMessageConverter();
//添加fastjson的配置資訊 比如 :是否要格式化傳回的json資料
FastJsonConfig config = new FastJsonConfig();
config.setSerializerFeatures(SerializerFeature.PrettyFormat);
//在轉換器中添加配置資訊
converter.setFastJsonConfig(config);
return new HttpMessageConverters(converter);
}
}
五、自定義的Realm
public class PasswordRealm extends AuthorizingRealm {
@Autowired
private RoleMapper roleMapper;
@Autowired
private UserMapper userMapper;
@Autowired
private AuthorityMapper authorityMapper;
/**
* 授權查詢回調函數, 進行鑒權但緩存中無使用者的授權資訊時調用,負責在應用程式中決定使用者的通路控制的方法
*/
@Override
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
User user = (User) principalCollection.getPrimaryPrincipal();
System.out.println(user.getUsername() + "進行授權操作");
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
Integer roleId = user.getRoleId();
Role role = roleMapper.findRoleById(roleId);
info.addRole(role.getRoleName());
List<Authority> authorities = authorityMapper.findAuthoritiesByRoleId(roleId);
if (authorities.size() == 0) {
return null;
}
return info;
}
/**
* 認證回調函數,登入資訊和使用者驗證資訊驗證
*/
@Override
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//toke強轉
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
String username = usernamePasswordToken.getUsername();
//根據使用者名查詢密碼,由安全管理器負責對比查詢出的資料庫中的密碼和頁面輸入的密碼是否一緻
User user = userMapper.findUserByUserName(username);
if (user == null) {
return null;
}
//單使用者登入
//處理session
DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
DefaultWebSessionManager sessionManager = (DefaultWebSessionManager) securityManager.getSessionManager();
//擷取目前已登入的使用者session清單
Collection<Session> sessions = sessionManager.getSessionDAO().getActiveSessions();
User temp;
for(Session session : sessions){
//清除該使用者以前登入時儲存的session,強制退出
Object attribute = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (attribute == null) {
continue;
}
temp = (User) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
if(username.equals(temp.getUsername())) {
sessionManager.getSessionDAO().delete(session);
}
}
String password = user.getPassword();
//最後的比對需要交給安全管理器,三個參數進行初步的簡單認證資訊對象的包裝,由安全管理器進行包裝運作
return new SimpleAuthenticationInfo(user, password, getName());
}
}
六、自定義的角色過濾器
public class CustomRolesAuthorizationFilter extends RolesAuthorizationFilter {
@Override
public boolean isAccessAllowed(ServletRequest req, ServletResponse resp, Object mappedValue) {
Subject subject = getSubject(req, resp);
String[] rolesArray = (String[]) mappedValue;
//如果沒有角色限制,直接放行
if (rolesArray == null || rolesArray.length == 0) {
return true;
}
for (int i = 0; i < rolesArray.length; i++) {
//若目前使用者是rolesArray中的任何一個,則有權限通路
if (subject.hasRole(rolesArray[i])) {
return true;
}
}
return false;
}
@Override
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
HttpServletRequest servletRequest = (HttpServletRequest) request;
HttpServletResponse servletResponse = (HttpServletResponse) response;
//處理跨域問題,跨域的請求首先會發一個options類型的請求
if (servletRequest.getMethod().equals(HttpMethod.OPTIONS.name())) {
return true;
}
boolean isAccess = isAccessAllowed(request, response, mappedValue);
if (isAccess) {
return true;
}
servletResponse.setCharacterEncoding("UTF-8");
Subject subject = getSubject(request, response);
PrintWriter printWriter = servletResponse.getWriter();
servletResponse.setContentType("application/json;charset=UTF-8");
servletResponse.setHeader("Access-Control-Allow-Origin", servletRequest.getHeader("Origin"));
servletResponse.setHeader("Access-Control-Allow-Credentials", "true");
servletResponse.setHeader("Vary", "Origin");
String respStr;
if (subject.getPrincipal() == null) {
respStr = JSONObject.toJSONString(new BaseResponse<>(300, "您還未登入,請先登入"));
} else {
respStr = JSONObject.toJSONString(new BaseResponse<>(403, "您沒有此權限,請聯系管理者"));
}
printWriter.write(respStr);
printWriter.flush();
servletResponse.setHeader("content-Length", respStr.getBytes().length + "");
return false;
}
}
七、ShiroService
@Service("shiroService")
public class ShiroServiceImpl implements ShiroService {
@Autowired
private AuthorityMapper authorityMapper;
/**
* 初始化權限
*/
@Override
public Map<String, String> loadFilterChainDefinitions() {
List<Authority> authorities = authorityMapper.findAuthorities();
// 權限控制map.從資料庫擷取
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
if (authorities.size() > 0) {
String uris;
String[] uriArr;
for (Authority authority : authorities) {
if (StringUtils.isEmpty(authority.getPermission())) {
continue;
}
uris = authority.getUri();
uriArr = uris.split(",");
for (String uri : uriArr) {
filterChainDefinitionMap.put(uri, authority.getPermission());
}
}
}
filterChainDefinitionMap.put("/user/login", "anon");
//配置退出 過濾器,其中的具體的退出代碼Shiro已經替我們實作了
filterChainDefinitionMap.put("/user/logout", "anon");
filterChainDefinitionMap.put("/**", "authc");
return filterChainDefinitionMap;
}
/**
* 在對角色進行增删改操作時,需要調用此方法進行動态重新整理
* @param shiroFilterFactoryBean
*/
@Override
public void updatePermission(ShiroFilterFactoryBean shiroFilterFactoryBean) {
synchronized (this) {
AbstractShiroFilter shiroFilter;
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);
}
}
}
}
其他的Service、Mapper等檔案就不貼出來了。
用guest使用者登入,調用/user/list可以查詢到資料,但是調用/role/list則會提示無權限,如下圖:

使用者登入和認證後都會在Redis中儲存相應的資料:
同時在控制台隻列印了一次xxx進行授權操作:
需要注意的是,登出時需要調用Subject.logout()方法,該方法會自動删除redis中的session和cache緩存。
使用shiro-redis做Session共享後,跟蹤源碼發現在修改角色名稱後AuthorizationInfo中的角色名稱依然是修改之前的。是以就需要使用者退出後重登才會更新認證資訊。
============================華麗的分割線===========================
為了解決上述問題,我想了兩天的時間。一開始思維進入了誤區,一心的想通過RedisCache和RedisCacheManager來删除授權相關的資訊。RedisCache提供了remove(key)方法來删除緩存,但是由于本人能力有限,實在沒看明白Redis中的key是怎麼拿到Realm類的全限定名,然後拼湊出來的。後來想到當調用subject.logout()方法會删除cache和session,于是跟蹤源碼,發現是下圖紅框中的方法删除cache的:
是以,我寫了下面的工具類,來删除cache和session:
public class ShiroUtil {
private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);
private ShiroUtil() {
}
/**
* 擷取指定使用者名的Session
* @param username
* @return
*/
private static Session getSessionByUsername(String username){
Collection<Session> sessions = redisSessionDAO.getActiveSessions();
User user;
Object attribute;
for(Session session : sessions){
attribute = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (attribute == null) {
continue;
}
user = (User) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
if (user == null) {
continue;
}
if (Objects.equals(user.getUsername(), username)) {
return session;
}
}
return null;
}
/**
* 删除使用者緩存資訊
* @param username 使用者名
* @param isRemoveSession 是否删除session,删除後使用者需重新登入
*/
public static void kickOutUser(String username, boolean isRemoveSession){
Session session = getSessionByUsername(username);
if (session == null) {
return;
}
Object attribute = session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
if (attribute == null) {
return;
}
User user = (User) ((SimplePrincipalCollection) attribute).getPrimaryPrincipal();
if (!username.equals(user.getUsername())) {
return;
}
//删除session
if (isRemoveSession) {
redisSessionDAO.delete(session);
}
DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
Authenticator authc = securityManager.getAuthenticator();
//删除cache,在通路受限接口時會重新授權
((LogoutAware) authc).onLogout((SimplePrincipalCollection) attribute);
}
}
在修改角色時,調用ShiroUtil.kickOutUser(username, isRemoveSession)方法就可以删除session和cache了。删除session主要是在禁用使用者或角色時,強制使用者退出的。如果僅僅修改角色資訊是不需要删除session的,隻需要删除cache,使使用者在通路受限接口時重新授權即可。
------
新增github下載下傳位址,我看到有人在評論區說,沒有背景頁面。是以,最近抽了個時間弄了個背景管理界面,點此體驗。管理系統代碼位址。使用者名admin/123456。擁有所有的權限,你們随便玩。我設定了每10分鐘恢複一次資料,是以,在操作時會發現過了一會,資料又回來了的情況。