关于使用shiro中Session的使用导致的Java 对象引用问题
记录一下因为增加redis账号登录互斥的需求导致的session失效的问题,引发的学习Java 对象引用问题。
问题导火索:
原来登录时直接如下两行即可设置session中的对象参数
SysUserExt user= (SysUserExt) subject.getSession().getAttribute(CommonConstant.SESSION_USER_KEY);
user.setMenuModels(menuModels);
但是加了登录账号互斥的情况下,如下,会导致上述设置失效
@Bean(name = "securityManager")public DefaultWebSecurityManager getDefaultWebSecurityManager() {DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager();dwsm.setRealm(getShiroRealm());dwsm.setRememberMeManager(rememberMeManager());dwsm.setSessionManager(sessionManager());return dwsm;}
必须这样去进行修改,才可以在session中拿到值
SysUserExt sysUserEmpExt = (SysUserExt) subject.getSession().getAttribute(CommonConstant.SESSION_USER_KEY);sysUserEmpExt.setMenuModels(menuModels);subject.getSession().setAttribute(CommonConstant.SESSION_USER_KEY, sysUserEmpExt);
发生的原因:
项目使用的是:
RedisSessionDAO
(Shiro + Redis)
这就引出了关键点:
🧠 Shiro + RedisSessionDAO 的机制
-
getAttribute(...)
:从 Redis 中取出 一个反序列化副本 -
修改这个副本,不会影响 Redis 中存储的 session
-
除非你 调用
setAttribute(...)
把它重新写回去 -
🔍 所以:
✅ 第一段代码:
SysUserExt user = (SysUserExt) subject.getSession().getAttribute(...); user.setMenuModels(...);
只是修改了内存中的一个临时副本,并不会写回 Redis 中的 session
✅ 第二段代码:
SysUserExt sysUserEmpExt = (SysUserExt) subject.getSession().getAttribute(...); sysUserEmpExt.setMenuModels(...); subject.getSession().setAttribute(..., sysUserEmpExt);
修改了之后重新调用
setAttribute(...)
,就把这个对象重新写入 Redis 中。
原因解释:
-
如果你使用的是 Shiro 的默认
ServletContainerSessionManager
(也就是用的 servlet 容器自带的 HttpSession,如 Tomcat、Jetty),Servlet 的 session 是 JVM 内存中的对象引用,修改引用内容会立即体现在 session 中。 -
RedisSessionDAO 把对象序列化进 Redis,每次
getAttribute()
都是反序列化一个副本,改了不影响原值,必须重新setAttribute()
。
下面 简单的代码示例 + 图示解释思维 来讲清楚 Java 中的对象引用。
✅ 场景一:对象引用指向同一个实例
public class ReferenceDemo {public static void main(String[] args) {Person p1 = new Person("Alice");Person p2 = p1; // p2和p1指向同一个对象p2.setName("Bob");System.out.println(p1.getName()); // 输出结果:Bob}
}
public class Person {private String name;public Person(String name) {this.name = name;}public String getName() {return name;}public void setName(String name) {this.name = name;}
}
🎯 解释:
-
p1
和p2
指向的是同一个内存地址(Person 对象) -
所以通过
p2.setName("Bob")
修改后,p1.getName()
看到的是同样的值
🧠 内存图示(堆):
+---------------------+
| Person(name=Bob)|
+---------------------+
↑ ↑
p1 p2
✅ 场景二:重新赋值引用对象
public class Demo2 {public static void main(String[] args) {Person p1 = new Person("Alice");Person p2 = new Person("Charlie");p2 = p1; // p2 now references the same object as p1p2.setName("Dave");System.out.println(p1.getName()); // Output: Dave}
}
🎯 解释:
-
原本
p2
是另一个对象,但p2 = p1;
后,两个变量都引用同一个对象了 -
修改一个,另一个也看到变化
❌ 场景三:复制对象不能共享引用
public class Demo3 {public static void main(String[] args) {Person p1 = new Person("Alice");Person p2 = new Person(p1.getName()); // 创建新对象,复制name值p2.setName("Eve"); // 修改p2的nameSystem.out.println(p1.getName()); // p1的name保持原值"Alice"}
}
🎯 解释:
-
虽然内容一样,但
p1
和p2
是两个独立的对象,修改一个不会影响另一个
✅ 扩展场景:集合中的引用
List<Person> people = new ArrayList<>();
Person person = new Person("Tom");
people.add(person);
person.setName("Jerry");
System.out.println(people.get(0).getName()); // 输出 Jerry
🎯 解释:
-
list
存的不是 Person 的副本,而是引用 -
所以即使你在外部修改
p
,集合内的对象也会变化
✅ 总结
操作 | 是否引用同一对象 | 修改是否互相影响 |
---|---|---|
Person p2 = p1 | ✅ 是 | ✅ 会 |
Person p2 = new Person(p1.getName()) | ❌ 否 | ❌ 不会 |
放入集合中 | ✅ 是引用 | ✅ 会 |
RedisSessionDAO 反序列化 | ❌ 复制品 | ❌ 不会自动同步 |
修改后的原代码如下:
一:登录的service:
private UserDTO loginAuthenticate(String loginName, String password, boolean rememberMe) throws BusiCheckException {String ip = this.getIpAddress();logger.info("用户{}尝试登录,IP: {}", loginName, ip);SysUserExt user;BackendUserToken token = new BackendUserToken(loginName, password, ip);token.setRememberMe(rememberMe);Subject subject = SecurityUtils.getSubject();subject.login(token);user = (SysUserExt) subject.getSession().getAttribute(CommonConstant.SESSION_USER_KEY);
// List<MenuModel> menuModels = iLoginService.queryPermissionList(user);List<MenuModel> menuModels = iLoginService.queryWhitelist(ip, user);if (menuModels.size()==0){throw new BusiCheckException("当前IP不允许登录!!");}if (user != null) {user.setPasswordRand(null);user.setPassword(null);// 检查公司状态是否有效this.checkCompanyStatus(user.getCompanyId());// 设置权限字符user.setMenuModels(menuModels);//【这一行很重要】}UserDTO data = new UserDTO();data.setUserInfo(user);data.setAuthorityInfo(menuModels);logger.info("------------------用户" + loginName + "登录成功-------------------");//这是用来补充的if(ObjectUtils.isNotEmpty(user)){SysUserExt sysUserEmpExt = (SysUserExt) subject.getSession().getAttribute(CommonConstant.SESSION_USER_KEY);sysUserEmpExt.setMenuModels(menuModels);subject.getSession().setAttribute(CommonConstant.SESSION_USER_KEY, sysUserEmpExt);}return data;}
二:ShiroConfiguration的代码片段
/*** 配置 RedisSessionDAO,用于 Session 持久化到 Redis*/@Beanpublic RedisSessionDAO redisSessionDAO() {RedisSessionDAO redisSessionDAO = new RedisSessionDAO();redisSessionDAO.setRedisManager(redisManager());return redisSessionDAO;}/*** 配置 Redis 管理器*/@Beanpublic org.crazycake.shiro.RedisManager redisManager() {org.crazycake.shiro.RedisManager redisManager = new org.crazycake.shiro.RedisManager();redisManager.setHost(redisHost + ":" + redisPort);redisManager.setPassword(redisPassword);redisManager.setTimeout(1800); // 单位:秒return redisManager;}/*** 配置 Session 管理器,使用 Redis 存储 Session*/@Beanpublic SessionManager sessionManager() {DefaultWebSessionManager sessionManager = new DefaultWebSessionManager();sessionManager.setSessionDAO(redisSessionDAO());sessionManager.setGlobalSessionTimeout(1200000); // 20分钟,单位:毫秒sessionManager.setSessionValidationSchedulerEnabled(true);sessionManager.setSessionIdUrlRewritingEnabled(false); // 禁用 URL 重写return sessionManager;}/*** 配置 Shiro 核心安全管理器*/@Bean(name = "securityManager")public DefaultWebSecurityManager getDefaultWebSecurityManager() {DefaultWebSecurityManager dwsm = new DefaultWebSecurityManager();dwsm.setRealm(getShiroRealm());dwsm.setRememberMeManager(rememberMeManager());dwsm.setSessionManager(sessionManager());//这一行很重要return dwsm;}