解决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不匹配导致的问题,然后再清除过程中碰到的异常错误也都进行了解答。