解决Springboot整合Shiro+Redis退出登录后不清除缓存

发布于:2025-02-11 ⋅ 阅读:(56) ⋅ 点赞:(0)

解决Springboot整合Shiro+Redis退出登录后不清除缓存

问题发现

如果再使用缓存管理Shiro会话时,退出登录后缓存的数据应该清空。

依赖文件如下:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>2.7.18</version>
</dependency>
<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring-boot-web-starter</artifactId>
    <version>1.13.0</version>
</dependency>

示例代码如下:

@Controller
@RequestMapping(value = "/user")
public class UserController {
    @PostMapping("/login")
    public ModelAndView login(HttpServletRequest request, @RequestParam("username") String username
            , @RequestParam("password") String password) {
        // 提前加密,解决自定义缓存匹配时错误
        UsernamePasswordToken token = new UsernamePasswordToken(
                username,//身份信息
                password);//凭证信息
        ModelAndView modelAndView = new ModelAndView();
        // 对用户信息进行身份认证
        Subject subject = SecurityUtils.getSubject();
        if (subject.isAuthenticated() && subject.isRemembered()) {
            modelAndView.setViewName("redirect:main");
            return modelAndView;
        }
        try {
            subject.login(token);
            // 判断savedRequest不为空时,获取上一次停留页面,进行跳转
//            SavedRequest savedRequest = WebUtils.getSavedRequest(request);
//            if (savedRequest != null) {
//                String requestUrl = savedRequest.getRequestUrl();
//                modelAndView.setViewName("redirect:"+ requestUrl);
//                return modelAndView;
//            }
        } catch (AuthenticationException e) {
            e.printStackTrace();
            modelAndView.addObject("responseMessage", "用户名或者密码错误");
            modelAndView.setViewName("redirect:index");
            return modelAndView;
        }
        System.out.println(subject.getSession().getId());
        System.out.println(subject.isAuthenticated());
        modelAndView.setViewName("redirect:main");
        return modelAndView;
    }

    @GetMapping("/logout")
    public void logout() {
        SecurityUtils.getSubject().logout();
    }
}

自定义Realm,示例代码如下:

@Component
public class UserRealm extends AuthorizingRealm {

    @Autowired
    private UserService userService;
    @Autowired
    private RoleService roleService;
    @Autowired
    private PermissionService permissionService;

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        String username = (String) authenticationToken.getPrincipal();
        String password = new String((char[]) authenticationToken.getCredentials());
        User user = userService.getOne(new QueryWrapper<User>().ge("username", username));
        if (user == null) {
            throw new UnknownAccountException("账号不存在");
        }
        Sha256Hash sha256Hash = new Sha256Hash(password, username);
        if (!sha256Hash.toHex().equals(user.getPassword())) {
            throw new IncorrectCredentialsException("密码错误");
        }
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user, sha256Hash.toHex(), new ByteSourceSerializable(username), getName());
        return simpleAuthenticationInfo;
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principalCollection) {
        User user = (User) principalCollection.getPrimaryPrincipal();
        List<Role> roleList = roleService.getByUserId(user.getId());
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        roleList.forEach(item ->{
            simpleAuthorizationInfo.addRole(item.getName());
        });
        List<Integer> roleIds = roleList.stream().map(Role::getId).collect(Collectors.toList());
        List<Permission> permissions = permissionService.listByIds(roleIds);
        permissions.forEach(item->{
            simpleAuthorizationInfo.addStringPermission(item.getName());
        });
        return simpleAuthorizationInfo;
    }
}

Config配置文件如下:

package org.example.config;

import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.apache.shiro.authc.credential.HashedCredentialsMatcher;
import org.apache.shiro.cache.CacheManager;
import org.apache.shiro.cache.MemoryConstrainedCacheManager;
import org.apache.shiro.realm.Realm;
import org.apache.shiro.session.mgt.SessionManager;
import org.apache.shiro.session.mgt.eis.MemorySessionDAO;
import org.apache.shiro.session.mgt.eis.SessionDAO;
import org.apache.shiro.session.mgt.eis.SessionIdGenerator;
import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
import org.apache.shiro.spring.web.config.DefaultShiroFilterChainDefinition;
import org.apache.shiro.spring.web.config.ShiroFilterChainDefinition;
import org.apache.shiro.web.filter.authc.AnonymousFilter;
import org.apache.shiro.web.filter.authc.FormAuthenticationFilter;
import org.apache.shiro.web.mgt.CookieRememberMeManager;
import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
import org.apache.shiro.web.servlet.SimpleCookie;
import org.apache.shiro.web.session.mgt.DefaultWebSessionManager;
import org.example.realm.UserRealm;
import org.example.shiroTest.CustomSessionManager;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.JdkSerializationRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.web.client.RestTemplate;

import javax.crypto.KeyGenerator;
import javax.crypto.SecretKey;
import javax.servlet.Filter;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;

/**
 * packageName org.example.config
 *
 * @author shanchengwei
 * @className ShiroConfig
 * @date 2024/11/28
 */
@Configuration
public class ShiroConfig {
    /**
     * 核心安全过滤器对进入应用的请求进行拦截和过滤,从而实现认证、授权、会话管理等安全功能。
     */
    @Bean
    public ShiroFilterFactoryBean shiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(securityManager);
        // 当未登录的用户尝试访问受保护的资源时,重定向到这个指定的登录页面。
        shiroFilterFactoryBean.setLoginUrl("/user/index");
        // 成功后跳转地址,但是测试时未生效
        shiroFilterFactoryBean.setSuccessUrl("/user/main");
        // 当用户访问没有权限的资源时,系统重定向到指定的URL地址。
        shiroFilterFactoryBean.setUnauthorizedUrl("/user/unauth");
        // 配置拦截器链,指定了哪些路径需要认证、哪些路径允许匿名访问
        Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
        filterChainDefinitionMap.put("/user/login", "anon");
        filterChainDefinitionMap.put("/user/logout", "logout");
        filterChainDefinitionMap.put("/**", "authc");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
        return shiroFilterFactoryBean;
    }

    /**
     * 创建Shiro Web应用的整体安全管理
     */
    @Bean
    public DefaultWebSecurityManager securityManager() {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(realm());
        defaultWebSecurityManager.setSessionManager(defaultWebSessionManager()); // 注册会话管理
//        defaultWebSecurityManager.setRememberMeManager(rememberMeManager());
        // 可以添加其他配置,如缓存管理器、会话管理器等
        return defaultWebSecurityManager;
    }

    /**
     * 创建会话管理
     */
    @Bean
    public DefaultWebSessionManager defaultWebSessionManager() {
        DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
        defaultWebSessionManager.setGlobalSessionTimeout(10000);
//        defaultWebSessionManager.setSessionDAO(sessionDAO());
        defaultWebSessionManager.setCacheManager(cacheManager()); // 设置缓存管理器,自动给sessiondao赋值
        return defaultWebSessionManager;
    }

    @Bean
    public SessionDAO sessionDAO() {
        RedisSessionDao redisSessionDao = new RedisSessionDao();
        redisSessionDao.setActiveSessionsCacheName("shiro:session");
        return redisSessionDao;
    }
    
    /**
     * 指定密码加密算法类型
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher();
        hashedCredentialsMatcher.setHashAlgorithmName("SHA-256"); // 设置哈希算法
        return hashedCredentialsMatcher;
    }

    /**
     * 注册Realm的对象,用于执行安全相关的操作,如用户认证、权限查询
     */
    @Bean
    public Realm realm() {
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher()); // 为realm设置指定算法
        userRealm.setCachingEnabled(true); // 启动全局缓存
        userRealm.setAuthenticationCachingEnabled(true); // 启动验证缓存
        userRealm.setCacheManager(cacheManager());
        return userRealm;
    }

    @Bean
    public CacheManager cacheManager() {
        RedisCacheManage redisCacheManage = new RedisCacheManage(redisTemplate());
        return redisCacheManage;
    }

    @Autowired
    private RedisConnectionFactory redisConnectionFactory;

    @Bean
    public RedisTemplate<String, Object> redisTemplate() {
        RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
        redisTemplate.setConnectionFactory(redisConnectionFactory);

        Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class);
        ObjectMapper objectMapper = new ObjectMapper();
        //设置了 ObjectMapper 的可见性规则。通过该设置,所有字段(包括 private、protected 和 package-visible 等)都将被序列化和反序列化,无论它们的可见性如何。
        objectMapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
        //启用了默认的类型信息 NON_FINAL 参数表示只有非 final 类型的对象才包含类型信息,这可以帮助在反序列化时正确地将 JSON 字符串转换回对象。
        objectMapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
        jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
        StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

        //key采用String的序列化方式
        redisTemplate.setKeySerializer(stringRedisSerializer);
        //hash的key也采用String的序列化方式
        redisTemplate.setHashKeySerializer(stringRedisSerializer);
        return redisTemplate;
    }
}

当我点击退出登录后报错,如图所示:

在这里插入图片描述
后台日志报错,如图所示:

在这里插入图片描述

Redis保存数据,如图所示:

在这里插入图片描述

问题解决

根据报错可以知道,User对象无法转换为String字符串,就很神奇,存进去和删除的时候为什么参数不一致哦,然后就开启了Debug模式,一步步排查。

调用logout()方法,进入DefaultSecurityManager类,如图所示:

在这里插入图片描述

最后进入CachingRealm类,如图所示:
在这里插入图片描述
根据Debug先进入AuthorizingRealm类(前面介绍过缓存没保存授权的记录,不做讲解,参考AuthenticatingRealm),实际是再AuthenticatingRealm.doClearCache(),然后获取缓存和凭证进行删除操作,如图所示:
在这里插入图片描述

然后我们看下这个Key是如何获取的,实际上也是拿的凭证信息,如图所示:
在这里插入图片描述

然后就联想到这个凭证信息再自定义Realm中存放的,然后我就将凭证中的信息改成了username字段,示例代码如下:

SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(user.getUsername(), sha256Hash.toHex(), new ByteSourceSerializable(username), getName());

AuthenticatingRealm中的Redis数据删除后返回到AuthorizingRealm类,继续执行该类的缓存清除(虽然没有缓存数据),如图所示:

在这里插入图片描述

然后就报错了,如图所示:

在这里插入图片描述
我们可以看到又是一个类型转换错误,再getAuthorizationCacheKey()方法中直接将对象返回,如图所示:

在这里插入图片描述

解决该问题的方法有两种:

  • 方法一:子类重写该方法,自定义的Realm中去重写,示例代码如下:
@Component
public class UserRealm extends AuthorizingRealm {
	// 省略其它代码... ...
    @Override
    protected Object getAuthorizationCacheKey(PrincipalCollection principals) {
        return principals.getPrimaryPrincipal();
    }
}
  • 方法二:再Config文件中不启用授权的缓存,这样缓存为null,就不会往下走,示例代码如下:
    @Bean
    public Realm realm() {
        UserRealm userRealm = new UserRealm();
        userRealm.setCredentialsMatcher(hashedCredentialsMatcher()); // 为realm设置指定算法
        userRealm.setCachingEnabled(true); // 启动全局缓存
        userRealm.setAuthorizationCachingEnabled(false); // 启动授权缓存
        userRealm.setAuthenticationCachingEnabled(true); // 启动验证缓存
        userRealm.setAuthenticationCacheName("Authentication");
        userRealm.setCacheManager(cacheManager());
        return userRealm;
    }

这两种方式都可以解决类型转换的错误。

解决了这个删除的问题我们再回到最前面的问题:存进去和删除的时候为什么参数不一致哦?

我们进入login()方法,如图所示:
在这里插入图片描述
进入authenticate()方法,最终进入AuthenticatingRealm类的getAuthenticationInfo()方法,如图所示:
在这里插入图片描述
第一次判断缓存为空,进入自定义Realm中查询数据,然后将查询的数据再放入缓存中,如图所示:
在这里插入图片描述
我们看下getAuthenticationCacheKey()方法是如何获取key的,如图所示:

在这里插入图片描述
可以看见直接获取的参数getPrincipal()方法,也就是UsernamePasswordToken中的username字段,如图所示:

在这里插入图片描述

到此也就知道为什么存的时候和删的时候,Key值不一致的原因。

这样又带来了另外一个问题:用username当凭证就会每次都要去查询,非常的繁琐,有没有什么好的办法?还真有,我们知道它删除的时候会去获取自定义Realm中凭证信息,如图所示:

在这里插入图片描述
既然这样的话我就可以重写getAvailablePrincipal()方法,保证删除的时候和登录的凭证信息保持一致就行,示例代码如下:

@Component
public class UserRealm extends AuthorizingRealm {
	// 省略其它代码... ...
    @Override
    protected Object getAvailablePrincipal(PrincipalCollection principals) {
        User availablePrincipal = (User) super.getAvailablePrincipal(principals);
        return availablePrincipal.getUsername();
    }
}

至此退出登录时遇到的所有问题基本都解决了。

不清除缓存基本上就是key不匹配导致的问题,然后再清除过程中碰到的异常错误也都进行了解答。


网站公告

今日签到

点亮在社区的每一天
去签到