解决Springboot整合Shiro自定义SessionDAO+Redis管理会话,登录后不跳转首页
- 问题发现
- 问题解决
问题发现
在Shiro框架中,SessionDAO的默认实现是MemorySessionDAO。它内部维护了一个ConcurrentMap来保存session数据,即将session数据缓存在内存中。
再使用Redis作为Session存储解决分布式系统中的Session共享问题。
依赖文件如下:
<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 {
@GetMapping("/index")
public ModelAndView index() {
Subject subject = SecurityUtils.getSubject();
System.out.println("===============index==========");
System.out.println(subject.getSession().getId());
System.out.println(subject.isAuthenticated());
if (subject.isAuthenticated() || subject.isRemembered()) {
return new ModelAndView("redirect:main");
}
return new ModelAndView("login.html");
}
@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("/main")
public String main() {
Subject subject = SecurityUtils.getSubject();
System.out.println("===============main==========");
System.out.println(subject.getSession().getId());
System.out.println(subject.isAuthenticated());
return "main.html";
}
}
自定义SessionDAO,示例代码如下:
public class RedisSessionDao extends AbstractSessionDAO {
private HashOperations<String, Object, Session> hashOperations;
private static final String key = "shiro:";
public RedisSessionDao(RedisTemplate<String, Object> redisTemplate) {
hashOperations = redisTemplate.opsForHash();
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = super.generateSessionId(session);
this.assignSessionId(session, sessionId);
this.storeSession(sessionId, session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable serializable) {
return (Session) hashOperations.get(key, serializable.toString());
}
@Override
public void update(Session session) throws UnknownSessionException {
this.storeSession(session.getId(), session);
}
@Override
public void delete(Session session) {
if (session == null) {
throw new NullPointerException("session argument cannot be null.");
} else {
Serializable id = session.getId();
if (id != null) {
hashOperations.delete(key, id.toString());
}
}
}
@Override
public Collection<Session> getActiveSessions() {
return hashOperations.values(key);
}
protected void storeSession(Serializable id, Session session) {
if (id == null) {
throw new NullPointerException("id argument cannot be null.");
} else {
this.hashOperations.putIfAbsent(key, id.toString(), session);
}
}
}
Config配置文件示例代码如下:
@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地址。
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>();
filterChainDefinitionMap.put("/user/login", "anon");
filterChainDefinitionMap.put("/**", "authc");
shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
return shiroFilterFactoryBean;
}
/**
* 创建Shiro Web应用的整体安全管理
*/
@Bean
public DefaultWebSecurityManager securityManager() {
DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
defaultWebSecurityManager.setRealm(realm());
defaultWebSecurityManager.setSessionManager(defaultWebSessionManager()); // 注册会话管理
// 可以添加其他配置,如缓存管理器、会话管理器等
return defaultWebSecurityManager;
}
/**
* 创建会话管理
*/
@Bean
public DefaultWebSessionManager defaultWebSessionManager() {
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
defaultWebSessionManager.setGlobalSessionTimeout(10000);
defaultWebSessionManager.setSessionDAO(sessionDAO());
defaultWebSessionManager.setCacheManager(cacheManager());
return defaultWebSessionManager;
}
@Bean
public SessionDAO sessionDAO() {
RedisSessionDao redisSessionDao = new RedisSessionDao(redisTemplate());
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.setAuthorizationCachingEnabled(true); // 启动授权缓存
userRealm.setAuthenticationCachingEnabled(true); // 启动验证缓存
userRealm.setCacheManager(cacheManager());
return userRealm;
}
@Bean
public CacheManager cacheManager() {
RedisCacheManage redisCacheManage = new RedisCacheManage(redisTemplate());
return redisCacheManage;
}
@Autowired
private RedisConnectionFactory redisConnectionFactory;
// redis序列化配置
@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();
redisTemplate.setHashKeySerializer(stringRedisSerializer);
redisTemplate.setHashValueSerializer(new JdkSerializationRedisSerializer());
return redisTemplate;
}
}
进入浏览器登陆成功后跳转首页,跳转过程中302,返回登录页面,如图所示:
问题解决
根据代码日志,可知道,跳转到其他页面时Session没有共享,如图所示:
最开始以为Redis中没有保存记录,其实已经保存了,如图所示:
参考网上诸多案例,似乎没什么区别,也不知道他们测过没有。
然后再Debug的时候,发现了另外一个类EnterpriseCacheSessionDAO,于是参考该类,我就把对应代码继承CachingSessionDAO,示例代码如下:
public class RedisSessionDao extends CachingSessionDAO {
private HashOperations<String, Object, Session> hashOperations;
protected Serializable doCreate(Session session) {
Serializable sessionId = this.generateSessionId(session);
this.assignSessionId(session, sessionId);
return sessionId;
}
protected Session doReadSession(Serializable sessionId) {
return null;
}
protected void doUpdate(Session session) {
}
protected void doDelete(Session session) {
}
}
Config配置文件,示例代码如下:
@Bean
public DefaultWebSessionManager defaultWebSessionManager() {
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
defaultWebSessionManager.setGlobalSessionTimeout(10000);
defaultWebSessionManager.setSessionDAO(sessionDAO());
defaultWebSessionManager.setCacheManager(cacheManager());
return defaultWebSessionManager;
}
@Bean
public SessionDAO sessionDAO() {
RedisSessionDao redisSessionDao = new RedisSessionDao();
redisSessionDao.setCacheManager(cacheManager()); // 设置缓存管理器
redisSessionDao.setActiveSessionsCacheName("shiro:session"); // 自定义redis存放的key名称
return redisSessionDao;
}
重启项目后运行,成功跳转,如图所示:
Redis中也有记录,如图所示:
至于继承AbstractSessionDAO为什么没有共享Session,大概率的原因是Redis没有被Shiro给管理导致的。
示例代码如下:
public class RedisSessionDao extends AbstractSessionDAO {
private CacheManager cacheManager;
private Cache<Serializable, Session> activeSessions;
private static final String key = "shiro:";
public RedisSessionDao() {
}
public void setCacheManager(CacheManager cacheManager) {
this.cacheManager = cacheManager;
this.activeSessions = cacheManager.getCache(key);
}
@Override
protected Serializable doCreate(Session session) {
Serializable sessionId = super.generateSessionId(session);
this.assignSessionId(session, sessionId);
this.storeSession(sessionId, session);
return sessionId;
}
@Override
protected Session doReadSession(Serializable serializable) {
return (Session) activeSessions.get(serializable);
}
@Override
public void update(Session session) throws UnknownSessionException {
this.storeSession(session.getId(), session);
}
@Override
public void delete(Session session) {
if (session == null) {
throw new NullPointerException("session argument cannot be null.");
} else {
Serializable id = session.getId();
if (id != null) {
activeSessions.remove(id);
}
}
}
@Override
public Collection<Session> getActiveSessions() {
return activeSessions.values();
}
protected void storeSession(Serializable id, Session session) {
if (id == null) {
throw new NullPointerException("id argument cannot be null.");
} else {
activeSessions.put(id, session);
}
}
}
配置文件,示例代码如下:
/**
* 创建会话管理
*/
@Bean
public DefaultWebSessionManager defaultWebSessionManager() {
DefaultWebSessionManager defaultWebSessionManager = new DefaultWebSessionManager();
defaultWebSessionManager.setGlobalSessionTimeout(10000);
defaultWebSessionManager.setSessionDAO(sessionDAO());
defaultWebSessionManager.setCacheManager(cacheManager());
return defaultWebSessionManager;
}
@Bean
public SessionDAO sessionDAO() {
RedisSessionDao redisSessionDao = new RedisSessionDao();
redisSessionDao.setCacheManager(cacheManager()); // 设置缓存管理器
return redisSessionDao;
}
经过测试也是可以成功跳转,会话共享。