Apache Shiro:开启rememberMe后重置密码如何强制全设备登出
解决Apache Shiro重置密码后强制失效所有设备RememberMe令牌的问题
你已经实现了失效用户的所有活跃会话,但因为Shiro的RememberMe令牌默认是无状态的(存储在客户端Cookie),服务器端没有跟踪所有有效令牌的机制,所以旧的RememberMe令牌仍然能创建新会话。要解决这个问题,需要通过凭证版本号的方式来校验RememberMe令牌的有效性。
核心思路
给用户添加一个credential_version(凭证版本号)字段,每次重置密码时递增该版本号。在生成RememberMe令牌时嵌入这个版本号,验证令牌时对比当前用户的版本号与令牌中的版本号——不一致则判定令牌失效。
具体实现步骤
1. 扩展用户实体,添加凭证版本号
在用户表中新增credential_version字段,默认值设为1:
public class User { // 原有字段 private String username; private String password; // 新增凭证版本号 private Integer credentialVersion = 1; // getter、setter方法 }
2. 重置密码时更新版本号
在重置密码的业务逻辑中,更新用户密码后同时递增版本号:
// 重置密码示例 public void resetPassword(String username, String newPassword) { User user = userRepository.findByUsername(username); // 加密新密码 user.setPassword(passwordEncoder.encode(newPassword)); // 递增凭证版本号 user.setCredentialVersion(user.getCredentialVersion() + 1); userRepository.save(user); }
3. 自定义RememberMeManager,嵌入并校验版本号
继承Shiro默认的CookieRememberMeManager,重写序列化/反序列化逻辑,把版本号嵌入令牌并验证:
public class VersionedCookieRememberMeManager extends CookieRememberMeManager { // 注入用户DAO或服务,用于查询当前用户的版本号 @Autowired private UserRepository userRepository; // 序列化时,将用户名和版本号封装为自定义对象 @Override protected byte[] serialize(Serializable principal) { // 假设你的principal是用户名(String类型),如果是User对象可直接获取版本号 String username = (String) principal; User user = userRepository.findByUsername(username); RememberMeTokenInfo tokenInfo = new RememberMeTokenInfo(username, user.getCredentialVersion()); return super.serialize(tokenInfo); } // 反序列化时,校验版本号是否一致 @Override protected Serializable deserialize(byte[] serialized) { RememberMeTokenInfo tokenInfo = (RememberMeTokenInfo) super.deserialize(serialized); User currentUser = userRepository.findByUsername(tokenInfo.getUsername()); // 版本号不一致,抛出异常,Shiro会自动失效该RememberMe令牌 if (currentUser == null || !currentUser.getCredentialVersion().equals(tokenInfo.getCredentialVersion())) { throw new InvalidCookieException("RememberMe令牌已失效,请重新登录"); } // 返回原principal(用户名),适配原有逻辑 return tokenInfo.getUsername(); } // 内部类:封装RememberMe令牌所需的信息 private static class RememberMeTokenInfo implements Serializable { private String username; private Integer credentialVersion; public RememberMeTokenInfo(String username, Integer credentialVersion) { this.username = username; this.credentialVersion = credentialVersion; } // getter方法 public String getUsername() { return username; } public Integer getCredentialVersion() { return credentialVersion; } } }
4. 配置Shiro使用自定义的RememberMeManager
在Shiro配置类中替换默认的RememberMeManager:
@Configuration public class ShiroConfig { // 注入密码加密器 @Autowired private PasswordEncoder passwordEncoder; // 自定义RememberMeManager @Bean public RememberMeManager rememberMeManager() { VersionedCookieRememberMeManager manager = new VersionedCookieRememberMeManager(); // 配置加密密钥(必须设置,默认密钥不安全) manager.setCipherKey(Base64.decode("your-32-byte-encryption-key-here")); // 配置RememberMe Cookie SimpleCookie rememberMeCookie = new SimpleCookie("rememberMe"); rememberMeCookie.setHttpOnly(true); // 防止JS窃取 rememberMeCookie.setMaxAge(30 * 24 * 60 * 60); // 30天过期 manager.setCookie(rememberMeCookie); return manager; } // 配置SecurityManager @Bean public DefaultSecurityManager securityManager(Realm userRealm, RememberMeManager rememberMeManager) { DefaultSecurityManager securityManager = new DefaultSecurityManager(userRealm); securityManager.setRememberMeManager(rememberMeManager); // 其他配置(如SessionManager等) return securityManager; } // 其他Shiro配置(如Realm、Filter等) }
5. 保留原有失效活跃会话的逻辑
你之前实现的失效活跃会话代码可以继续保留,处理当前在线的会话,结合版本号校验,就能覆盖活跃会话+RememberMe令牌的全场景强制登出。
补充说明
- Shiro的RememberMe默认是无状态设计,服务器端不存储令牌信息,因此无法直接主动失效客户端的令牌,通过版本号校验是业内通用的解决方案。
- 加密密钥必须设置为32字节(256位)的字符串,避免使用默认密钥导致安全问题。
内容的提问来源于stack exchange,提问作者amru
相关产品推荐
相关产品推荐

