本文章是介绍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, '995284121@qqcom', 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, '995284121@qqcom', 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
4.0.0
com.example
spring-boot-shiro
0.0.1-SNAPSHOT
jar
spring-boot-shiro
Demo project for Spring Boot
org.springframework.boot
spring-boot-starter-parent
1.5.9.RELEASE
UTF-8
UTF-8
1.8
org.springframework.boot
spring-boot-starter-web
org.mybatis.spring.boot
mybatis-spring-boot-starter
2.1.0
org.apache.shiro
shiro-spring
1.4.1
org.apache.shiro
shiro-core
1.4.1
org.crazycake
shiro-redis
3.0.0
mysql
mysql-connector-java
8.0.11
com.alibaba
druid
1.1.19
org.springframework.boot
spring-boot-starter-data-redis
com.alibaba
fastjson
1.2.47
org.springframework.boot
spring-boot-starter-test
test
com.github.pagehelper
pagehelper-spring-boot-starter
1.2.12
tk.mybatis
mapper
4.1.5
ch.qos.logback
logback-classic
1.2.3
compile
org.apache.logging.log4j
log4j-to-slf4j
2.10.0
compile
org.slf4j
jul-to-slf4j
1.7.25
compile
org.apache.commons
commons-lang3
3.8.1
org.bouncycastle
bcprov-jdk15on
1.55
io.jsonwebtoken
jjwt
0.9.0
org.springframework.boot
spring-boot-starter-aop
com.github.ulisesbocchio
jasypt-spring-boot-starter
2.0.0
org.springframework.boot
spring-boot-maven-plugin
四、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 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 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 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 loadFilterChainDefinitions() {
List authorities = authorityMapper.findAuthorities();
// 权限控制map.从数据库获取
Map 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 chains = shiroFilterFactoryBean.getFilterChainDefinitionMap();
for (Map.Entry 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 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分钟恢复一次数据,所以,在操作时会发现过了一会,数据又回来了的情况。