原文:https://blog.csdn.net/qq_34021712/article/details/84722724
如果是跟着我的shiro系列博客敲下来的,其实还有一个bug,这是一个网友遇到的,他在登出的时候,发现redis中当前用户身份认证缓存没有清理掉,之前在 springboot整合shiro-ehcache缓存(五) 中测试添加权限之后,清理的是所有用户的缓存,所以没有发现这个问题。
还记得上一篇博客: springboot整合shiro-实现自己登出(十六), 我们在登出方法中,清理了当前用户的 身份认证
和 权限认证
的 缓存信息,最后发现有一个key 没有清理掉,如下图:
为什么该key没有清除掉呢?经过debug发现,在清理 身份认证 缓存的时候,调用了ShiroRealm
的clearCachedAuthenticationInfo
最终调用到 RedisCache
的 remove
方法,但是传过来key 却是 User实体,为什么会是User实体,就是因为在 ShiroRealm
的 doGetAuthenticationInfo
方法返回值 SimpleAuthenticationInfo
中,第一个参数 传的是 User实体,具体 debug细节 如下:
而在删除 用户 权限缓存时,却没有这个问题,删除缓存时,传入的key为SimplePrincipalCollection
最终调用getRedisKeyFromPrincipalIdField
根据你在 ShiroConfig
中 配置 RedisCacheManager
指定的那个字段作为缓存的前缀,根据反射获取该字段的值 并返回,具体的配置信息,可以参考之前博客的源码:
解决方案
第一种,判断key为User实体,强转并获取用户名
第二种,将ShiroRealm 的 doGetAuthenticationInfo 方法返回值 SimpleAuthenticationInfo 中,第一个参数,传username,不要传User实体
第一步:修改ShiroRealm
修改shiroRealm
中的 doGetAuthenticationInfo
验证用户身份的最后一句 返回值
return new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(),new MyByteSource(user.getUsername()),getName());
protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
//获取用户名密码 第一种方式
//String username = (String) authenticationToken.getPrincipal();
//String password = new String((char[]) authenticationToken.getCredentials());
//获取用户名 密码 第二种方式
UsernamePasswordToken usernamePasswordToken = (UsernamePasswordToken) authenticationToken;
String username = usernamePasswordToken.getUsername();
String password = new String(usernamePasswordToken.getPassword());
//从数据库查询用户信息
User user = this.userMapper.findByUserName(username);
//可以在这里直接对用户名校验,或者调用 CredentialsMatcher 校验
if (user == null) {
throw new UnknownAccountException("用户名或密码错误!");
}
//这里将 密码对比 注销掉,否则 无法锁定 要将密码对比 交给 密码比较器
//if (!password.equals(user.getPassword())) {
// throw new IncorrectCredentialsException("用户名或密码错误!");
//}
if ("1".equals(user.getState())) {
throw new LockedAccountException("账号已被锁定,请联系管理员!");
}
SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user.getUsername(), user.getPassword(),new MyByteSource(user.getUsername()),getName());
return info;
}
并且修改shiroRealm
中 的 doGetAuthorizationInfo
方法,SecurityUtils.getSubject().getPrincipal()
之前 返回的是User实体,现在 就是我们上一步中 放进去的用户名。并且这里需要再单独根据 username
去数据库查询 User
protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
System.out.println("查询权限方法调用了!!!");
//这里获取到的就是 上面方法放进去的username了
String username = (String)SecurityUtils.getSubject().getPrincipal();
//需要单独根据 username 从数据库查询用户信息
User user = this.userMapper.findByUserName(username);
//获取用户角色
Set<Role> roles =this.roleMapper.findRolesByUserId(user.getUid());
//添加角色
SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
for (Role role : roles) {
authorizationInfo.addRole(role.getRole());
}
//获取用户权限
Set<Permission> permissions = this.permissionMapper.findPermissionsByRoleId(roles);
//添加权限
for (Permission permission:permissions) {
authorizationInfo.addStringPermission(permission.getPermission());
}
return authorizationInfo;
}
第二步:修改项目中 其他地方 使用(User)SecurityUtils.getSubject().getPrincipal();的 代码
因为 我们在第一部中 已经将 Principal
从 User实体 改为了 username
所以这些相应的都需要修改,这里要看你们代码中都是在那里使用的了。
第三步:修改之前 index.html页面中的34行 <shiro:principal property=“username”/>
因为 principal
为 用户名,已经不是实体了,这里再指定 property
就会报如下 异常:
2018/11/30 15:13:28.861 org.thymeleaf.TemplateEngine [] ERROR [THYMELEAF][http-nio-9090-exec-5] Exception processing template "index": Error during execution of processor 'at.pollux.thymeleaf.shiro.processor.element.HasPermissionElementProcessor' (index:15)
2018/11/30 15:13:28.862 o.s.web.servlet.DispatcherServlet [] DEBUG Error rendering view [org.thymeleaf.spring4.view.ThymeleafView@7578de10] in DispatcherServlet with name 'dispatcherServlet'
org.thymeleaf.exceptions.TemplateProcessingException: Error during execution of processor 'at.pollux.thymeleaf.shiro.processor.element.HasPermissionElementProcessor' (index:15)
具体如下图,只需要使用 <shiro:principal></shiro:principal>
就可以了。
第四步:修改RedisCache 类中的 getStringRedisKey 方法
直接返回 key.toString
下面的 getRedisKeyFromPrincipalIdField
方法 也可以直接删除了,因为不再使用它了 ,原本 它存在的意义 就是为了解决 principal
放的是 User实体。
第五步:启动测试
将redis清空, 并将浏览器缓存清除,启动项目测试,问题已解决,至于另外两个key都是我们自定义的功能,如果想要删除的话,直接删除redis的key就行了 都是使用username
拼接的key。
管理员清理其他用户的缓存
还有这样一种情况,有test
和 admin
两名用户, admin是管理员,在给 test用户分配新的权限之后,需要清除该用户的 权限缓存信息 ,这里有一种笨方法,如下 ,添加一个 删除用户缓存的方法, 只有 userInfo:clearCache
权限才可以执行此操作 ,并将 该权限给admin用户,在admin用户 给test用户 分配新的权限之后,可以立即清除test用户的权限缓存。
/**
* 将该权限赋给 admin用户 使用admin用户清理其他用户的 权限缓存
* @param username
* @return
*/
@RequiresPermissions("userInfo:clearCache")
@RequestMapping(value = "/clearCache",method = RequestMethod.GET)
@ResponseBody
public String clearCache(String username) {
String[] keys = new String[3];
keys[0] = "shiro:cache:authenticationCache:"+username;
keys[1] = "shiro:cache:authorizationCache:"+username;
keys[2] = "shiro:cache:retrylimit:"+username;
//原子性 命令 删除多个key
shiroRedisTemplate.delete(CollectionUtils.arrayToList(keys));
return "删除"+username+"权限成功";
}
我们将在下一篇讨论 :SimpleAuthenticationInfo 应该 使用username 还是User实体。