当前位置 博文首页 > 快乐的小三菊的博客:springboot + shiro 整合 redis 实现登出时

    快乐的小三菊的博客:springboot + shiro 整合 redis 实现登出时

    作者:[db:作者] 时间:2021-07-13 16:07

    背景:

    ? ? ? ?在实现登出操作的时候,我们发现 redis 中的当前用户的身份认证缓存并没有清理掉,虽然我们在登出的方法中已经调用了清理当前用户的身份认证权限认证的缓存信息,但是最终的结论是权限认证的缓存清理干净了,但是身份认证的缓存并没有清理掉。

    原因:

    ? ? ? ?为什么这个 key 没有清除掉呢?经过调试发现,在清理身份认证缓存的时候,调用了 CustomRealm 的?clearCachedAuthenticationInfo() 方法,最终调用到的是我们的 RedisCache 中的 remove() 方法,但是我们发现,传过来的 key 却是?User 实体,为什么会是 User 实体,是因为在?CustomRealm 的?doGetAuthenticationInfo() 方法返回值?SimpleAuthenticationInfo()?中的第一个参数传的是 User 实体,如下图所示:

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken){
    		if(StringUtils.isEmpty(authenticationToken.getPrincipal())) {
    			return null;
    		}
    		// 获取用户信息
    		String userName = authenticationToken.getPrincipal().toString();
    		User user = userService.selectByUserName(userName);
    		// 用户是否存在
    		if(user == null) {
    			throw new UnknownAccountException();
    		}
    		// 是否锁定
    		if(user!=null && user.getStatus().equals("1")){
    			throw new  LockedAccountException();
    		}
    		// 若存在将此用户存放到登录认证info中,无需做密码比对shiro会为我们进行密码比对校验
    		if(user !=null && user.getStatus().equals("0")){
    			ByteSource credentialsSalt = ByteSource.Util.bytes(user.getUserName()+ "salt");
    			SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, user.getPassword().toString(),credentialsSalt, getName());
    			return simpleAuthenticationInfo;
    		}
    		return null;
    	}

    ? ? ? ?而在删除用户权限缓存的时候,却不存在这个问题。我们研究发现,在删除缓存的时候,传入的 key 的类型为 SimplePrincipalCollection ,然后调用?getRedisKeyFromPrincipalIdField() 方法,在这个方法里面会根据你在 ShiroConfig 中配置 RedisCacheManager 指定的那个字段作为缓存的前缀,根据反射获取该字段的值并返回。如下所示:

    ? ? ? ?ShiroConfig 类中配置 RedisCacheManager 的代码如下所示,我们指定了用户名作为缓存的 key

        @Bean
    	public RedisCacheManager redisCacheManager(){
    		RedisCacheManager redisCacheManager = new RedisCacheManager();
    		redisCacheManager.setRedisManager(redisManager());
    		// redis中针对不同用户缓存
    		redisCacheManager.setPrincipalIdFieldName("userName");
    		// 用户权限信息缓存时间
    		redisCacheManager.setExpire(200000);
    		return redisCacheManager;
    	}

    解决方式一:

    ? ? ? ?在 RedisCache 类的 getStringRedisKey() 方法中加一个判断,当 key User 实体时,强转并获取用户名,代码如下所示,我这样写是因为有时会发生类加载器不同,导致 key instanceof User 返回 false ,所以加的判断。

     private String getStringRedisKey(K key) {
        	  String redisKey;
              if (key instanceof PrincipalCollection) {
                  redisKey = getRedisKeyFromPrincipalIdField((PrincipalCollection) key);
              } else if(key instanceof User){
              	redisKey = ((User)key).getUserName();
              }else if(key instanceof String){
                 redisKey = key.toString();
              }else {
              	redisKey = JSON.parseObject(JSON.toJSON(key).toString(), User.class).getUserName();
              }
              return redisKey;
        }

    解决方式二:

    ? ? ? ?修改?CustomRealm 类的?doGetAuthenticationInfo() 方法,将返回值?SimpleAuthenticationInfo 中的第一个参数换为 userName ,不要传 User 实体,代码如下所示:

    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken){
    		if(StringUtils.isEmpty(authenticationToken.getPrincipal())) {
    			return null;
    		}
    		// 获取用户信息
    		String userName = authenticationToken.getPrincipal().toString();
    		User user = userService.selectByUserName(userName);
    		// 用户是否存在
    		if(user == null) {
    			throw new UnknownAccountException();
    		}
    		// 是否锁定
    		if(user!=null && user.getStatus().equals("1")){
    			throw new  LockedAccountException();
    		}
    		// 若存在将此用户存放到登录认证info中,无需做密码比对shiro会为我们进行密码比对校验
    		if(user !=null && user.getStatus().equals("0")){
    			ByteSource credentialsSalt = ByteSource.Util.bytes(user.getUserName()+ "salt");
    			SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user.getUserName(), 
                    user.getPassword().toString(),credentialsSalt, getName());
    			return simpleAuthenticationInfo;
    		}
    		return null;
    	}

    ? ? ? ?由于修改了传入的参数,导致以前使用 User 实体的地方,都需要转换成 userName ,涉及到的模块包括?CustomRealm 的?doGetAuthorizationInfo() 方法,如下所示:

        @Override
    	protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
    		// 获取登录用户名
    		String username = (String)SecurityUtils.getSubject().getPrincipal();
    		// 查询用户名称
    		User user = userService.selectByUserName(username);
    		// 添加角色和权限
    		SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
    		List<String> roleNameList = new ArrayList<>();
    		List<String> permissionNameList = new ArrayList<>();
    
    		for (Role role : user.getRoles()) {
    			roleNameList.add(role.getRoleName());
    			for (Permission permission : role.getPermissions()) {
    				permissionNameList.add(role.getRoleName()+":"+permission.getPermissionName());
    			}
    		}
    		// 添加角色
    		simpleAuthorizationInfo.addRoles(roleNameList);
    		// 添加权限
    		simpleAuthorizationInfo.addStringPermissions(permissionNameList);
    		return simpleAuthorizationInfo;
    	}

    ? ? ? ?修改项目中其他地方使用 User 强转的代码,还需要修改 jsp 界面中的代码,将??<shiro:principal property="userName"/> 改为??<shiro:principal /> 即可。

    <shiro:user>
    	<h1>欢迎[<shiro:principal />]登录</h1>
    </shiro:user>
    <shiro:user>
    	<h1>欢迎[<shiro:principal/>]登录</h1>
    </shiro:user>
    </br>

    ? ? ? ?修改 ReidsCache 的?getStringRedisKey() 方法,直接返回 key.toString() ,下面的 getRedisKeyFromPrincipalIdField()?方法也可以直接删除了,因为不再使用它了 ,原本它存在的意义就是为了解决 principal 中放的是 User 实体。

    private String getStringRedisKey(K key) {
    		/*String redisKey;
    		if (key instanceof PrincipalCollection) {
    			redisKey = getRedisKeyFromPrincipalIdField((PrincipalCollection) key);
    		} else if(key instanceof User){
    			redisKey = ((User)key).getUserName();
    		}else if(key instanceof String){
    			redisKey = key.toString();
    		}else {
    			redisKey = JSON.parseObject(JSON.toJSON(key).toString(), User.class).getUserName();
    		}
    		return redisKey;*/
    		return key.toString();
    	}

    启动测试:

    ? ? ? ?将 redis 清空,并将浏览器缓存清除,启动项目测试,问题已解决,至于另外两个 key 都是我们自定义的功能,如果想要删除的话,直接删除 redis key 就行了,都是使用userName 拼接的 key 。如下所示,用户的身份认证的缓存已经被清除掉了。

    ?

    删除其他缓存:

    ? ? ? ?删除我们自己实现的缓存很简单,只需要调用 redisManagerdel() 方法即可,我们这里简单实现下,代码如下所示:

    	// 在登出的拦截器中注入 kickoutSessionControlFilter
        public ShiroLogoutFilter shiroLogoutFilter(){
    	    ShiroLogoutFilter shiroLogoutFilter = new ShiroLogoutFilter();
    	    //配置登出后重定向的地址,等出后配置跳转到登录接口
    	    shiroLogoutFilter.setRedirectUrl("/login");
    	    shiroLogoutFilter.setKickoutSessionControlFilter(kickoutSessionControlFilter());
    	    return shiroLogoutFilter;
    	}
    // 直接调用删除 key 的方法即可
    public class ShiroLogoutFilter extends LogoutFilter {
    
    	KickoutSessionControlFilter kickoutSessionControlFilter;
    	
    	
        public void setKickoutSessionControlFilter(KickoutSessionControlFilter kickoutSessionControlFilter) {
    		this.kickoutSessionControlFilter = kickoutSessionControlFilter;
    	}
    
        @Override
        protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
    
            Subject subject = getSubject(request,response);
            DefaultWebSecurityManager securityManager = (DefaultWebSecurityManager) SecurityUtils.getSecurityManager();
            CustomRealm shiroRealm = (CustomRealm) securityManager.getRealms().iterator().next();
            PrincipalCollection principals = subject.getPrincipals();
            shiroRealm.clearCache(principals);
            String userName = subject.getPrincipal().toString();
            // 在这进行删除缓存操作
            kickoutSessionControlFilter.removeKickoutCache(userName);
            // 登出
            subject.logout();
            // 获取登出后重定向到的地址
            String redirectUrl = getRedirectUrl(request,response,subject);
            // 重定向
            issueRedirect(request,response,redirectUrl);
            return false;
        }
    }

    ? ? ? ? 重启项目,正常登录,点击登出,此时 redis 中的缓存如下所示:

    cs