集群环境下使用Shiro的技术方案(With Redis)
在此之前,我是使用自己编写的web安全框架完成基于角色role的权限验证的,在集群环境中只需要将session中的信息存放到redis中即可。然而换成Shiro后,我发现事情麻烦了很多,因为需要学习Shiro的文档,自定义SessionDAO实现Redis的Session存储,但是这里面还是有一些坑。
通过Reids实现Shiro Session共享
首先在集群环境下,我们不能使用 servlet 规范中的 session, 因此对于web应用,必须配置 Shiro 使用 native session(对应DefaultSessionManager类)。然后再自定义SessionDAO接口的实现RedisSessionDAO,并让 native session 使用之。对应的 Spring Bean配置如下:
@Bean
public SecurityManager securityManager() {
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
// 使用native session
securityManager.setSessionManager(sessionManager());
securityManager.setRealm(myRealm());
return securityManager;
}
@Bean
public DefaultWebSessionManager sessionManager() {
DefaultWebSessionManager manager = new DefaultWebSessionManager();
// 使用自定义的RedisSessionDAO
manager.setSessionDAO(redisSessionDAO());
manager.setGlobalSessionTimeout(1000 * 60 * 10);
return manager;
}
在RedisSessionDAO中分别实现以下方法:
- doCreate(). 该方法在用户第一次访问session时调用。
- doReadSession(). 用户读取session信息时调用
- update(). 用户向session中添加信息时调用
- delete(). session过期或用户主动删除时调用
- getActiveSessions(). 获取在线的所有session
在各自的方法里我们可以自己编写读写Redis的代码实现。但是这里有一个性能问题,如doReadSession()方法,对于以下访问session数据的代码
session.setAttribute("name", "wanghongfei");
session.setAttribute("age", "22");
Shiro会调用两次doReadSession(), 即程序会向redis做两次查询。其实Session接口在运行时只是一个代理,每次用户调用读写session的方法Shiro都会调用SessionDAO中对应的方法去获取数据,这显然是不能接受的。其实完全可以等业务逻辑方法执行完毕后再调用SessionDAO更新数据,这样整个过程只需执行一次redis写操作(还可以再优化,只有session数据发生变化时再执行写redis操作),不明白Shiro为什么没这么干。
Shiro对此的解决方案是使用 session cache, 默认为 EhCache: 即,初次从redis中拿到session后就放到 EhCache 内存缓存中,下次再访问session就不通过SessionDAO了而是直接从缓存中取。但这样就出现另外一个问题,如果用户更新了session数据,如何通知其它服务器上的 EhCache 缓存过期?如果处理不好就会出现 server A 和 server B中访问同一个 session 数据不一致的情况。一个不太优雅的方案就是,当一台server更新了session时,先更新redis,再向一个 redis channel 发送一个消息,其它 server 订阅此 channel, 收到消息后同步更新对应 session。
在这里还有一个坑:如果你用JSON序列化session,那么反序列化时会报 UnSupportedOperationException,使用二进制序列化可以解决。
搞这么复杂是何苦呢?用自己的框架就能轻松实现 redis session存储(收到请求时查redis得到session, 请求处理结束时刷新session到redis中),真是觉得用 Shiro 把问题复杂化了。
完全无状态化
服务器不存储任何 session , 完全 RESTful风格。为了让 Shiro 不创建 session ,我们需要自定义一个 Subject 工厂:
public class StatelessDefaultSubjectFactory extends DefaultWebSubjectFactory {
public Subject createSubject(SubjectContext context) {
//不创建session
context.setSessionCreationEnabled(false);
return super.createSubject(context);
}
}
具体实现方法参考 http://wiki.jikexueyuan.com/project/shiro/nostate-web.html

浙公网安备 33010602011771号