开心一刻
小明的朋友骨折了,小明去他家里看他。他老婆很细心的为他换药,敷药,然后出去买菜。
小明满脸羡慕地说:你特么真幸福啊,你老婆对你那么好!
朋友哭得稀里哗啦的说:兄弟你别说了,我幸福个锤子,就是她把我打骨折的。
揣摩下此刻男人的内心
前情回顾
上篇中主要讲了两点认证与授权,认证主要FormAuthenticationFilter和AnonymousFilter两个filter来控制,shiro对所有请求都会先生成ProxiedFilterChain,请求会经过ProxiedFilterChain,先执行shiro的filter链,再执行剩下的servlet Filter链,最后来到我们的Controller。
认证过程是通过filter控制实现的,我们所有的请求由shiro中3个Filter:LogoutFilter、AnonymousFilter、FormAuthenticationFilter分摊了,LogoutFilter负责/logout,AnonymousFilter负责/login和静态资源,FormAuthenticationFilter则负责剩下的(/**),三个filter只会有一个生效(注意filter的配置顺序);当FormAuthenticationFilter生效的时候会进行登录认证,认证过程:先从缓存获取authenticationInfo,没有则通过realm从数据库获取并放入缓存,然后将页面输入的用户信息(UsernamePasswordToken)与authenticationInfo进行匹配验证,认证通过会将subject中的authenticated设置成true,表示当前subject已经被认证过了。关于认证缓存,个人不建议开启,因为当修改用户信息后,不好处理缓存中的authenticationInfo,另外认证频率本来就不高,缓存的意义不大。
一般情况下授权是通过注解方式实现的,注解配合aop会在我们的业务方法前织入前置权限检查处理,检查过程与认证过程类似:从缓存中获取authorizationInfo,没有则通过realm从数据库获取,然后放入缓存,然后将authorizationInfo与@RequiresPermissions("xxx")中的xxx来进行匹配,完成权限检查,检查通过则进入我们的目标方法,不通过则抛出异常。关于权限缓存,个人建议开启,因为权限的验证还是挺频繁的,如果不开启缓存,那么会给数据库造成一定的压力。
遗留问题解答
上篇遗留问题:session过期后,我们再请求,shiro是如何处理并跳转到登录页的?回答这个问题之前,我们先看看另外一个问题:
上篇博文中讲到了登录认证成功后会将subject的authenticated设置成true,表示当前subject已经被认证过了,但是只是当前subject;
我们可以将subject看成是request,每次请求来的时候都会将request/response对封装成subject,AbstractShiroFilter的方法doFilterInternal中有这样一个调用
final Subject subject = createSubject(request, response);
我们来看看createSubject的方法描述:
Creates a WebSubject instance to associate with the incoming request/response pair which will be used throughout the request/response execution.
创建一个关联request/response对的WebSubject实例,用于后续request/response的执行
也就是目前我们还只是看到了当前请求有认证状态,当前会话还没有看到认证状态;撇开shiro,如果是我们自己实现,我们会怎么实现,肯定会在subject的authenticated设置成true的时候也将认证状态也设置在session中,至于是存储在自定义session的某个标志字段(类似subject的authenticated)中,还是存储在session的attributes中(setAttribute(Object key, Object value)进行设置),看我们的需求和个人喜好。
归纳下这个问题:shiro是如何保存当前会话认证状态的,是上述中的某种实现方式,还是shiro有另外的实现方式
shiro是如何保存会话认证状态的
每次请求都会生成新的subject,如果我们把认证状态只放到subject中,那么每次请求都需要进行认证,这显然是不合理的,我们需要将认证状态保存到会话(session)中,那么整个会话期间只需要认证一次即可。那么shiro是怎么实现的了,我们来看看源码,从我们Controller的doLogin方法开始
如果我们继续跟进s.setAttribute(attributeKey, value),会发现认证状态最终存放到了SimpleSession的attributes属性中,
private transient Map<Object, Object> attributes;
也就是说认证状态会存在session的attributes中,正是我们上面说的方式之一,shiro没有用它特有的方式,最终在session中的存在形式如下图
看过上篇博客的朋友应该会有印象:FormAuthenticationFilter的isAccessAllowed方法(从AuthenticatingFilter继承)中第一个认证的是subject的authenticated
为什么获取subject的authenticated,而不是直接获取session的认证状态,我还没弄清楚为什么,难道是为了组件的分工明确? 既然shiro这么做了肯定有它的道理,我们先不纠结这个(知道的朋友可以评论区提示下)。我们知道登录成功后,subject的authenticated会被赋值成true,但是登录成功后的其他请求,比如:http://localhost:8080/own/index,subject的authenticated是什么时候在哪被赋值成true的呢?我们已经知道session中有认证状态,那么肯定是某个时候在某个地方将session中的认证状态赋值给了subject,具体是怎么样我们来跟一下源码,还记得shiro的入口filter:SpringShiroFilter,我们从SpringShiroFilter的doFilterInternal(从AbstractShiroFilter继承)方法开始
可以看到,在创建subject的时候,会将session中的认证状态赋值给subject的authenticated。
登录时,登录成功会将认证状态(成功)存储到session的attributes中,之后的每一次请求,在创建subject的时候,都会将session中的认证状态赋值给subject的authenticated,那么FormAuthenticationFilter在认证的时候会直接返回true,继续走servlet filter链,最终来到我们的Controller。
至此,该问题就明了了,会话认证状态还是保存在session中,只是中间处理的时候会将session中的认证状态赋值给subject,由subject传递给FormAuthenticationFilter认证状态。
session过期后,我们再请求,shiro是如何处理并跳转到登录页的
如果我们明白了上个问题,那么这个问题就很好理解了。如果session过期,那么通过sessionDAO获取的session为null,subject的authenticated就会被赋值成false,那么在FormAuthenticationFilter中认证不通过,则会重定向到/login,让用户重新进行登录认证。事实是这样吗,我们来跟下源码(假设此时请求是:http://localhost:8080/own/index)
可以看到,请求进过SpringShiroFilter时,subject中authenticated被设置成false,然后生成ProxiedFilterChain
请求会来到FormAuthenticationFilter,认证不通过,重定向到/login,并返回false,表示filter链不用继续往下走了(具体可查看上篇博文)。
很多对session的操作,都会同步到缓存(或持久层),包括session刷新(touch())、设置session属性(setAttribute()等等,具体可以看AbstractNativeSessionManager,很多session操作中都会调用onChange方法
protected void onChange(Session session) {
sessionDAO.update(session);
}
shiro源码系列
系列地址
shiro源码篇 - shiro的session创建,你值得拥有
SessionManager负责session的操作,包括创建、维护、删除、失效、验证等;
AbstractNativeSessionManager的start是创建session的入口;
SimpleSession是shiro完完全全的自己实现,是shiro对session的一种拓展。但SimpleSession不对外暴露,我们一般操作的是SimpleSession的代理:DelegatingSession,或者是DelegatingSession的代理:StoppingAwareProxiedSession;对session的操作,会通过一层层代理,来到DelegatingSession,DelegatingSession将session的操作转交给sessionMananger,sessionManager通过一些校验后,最后转交给SimpleSession处理。
shiro源码篇 - shiro的session的查询、刷新、过期与删除,你值得拥有
一般操作的session是session的代理,代理将session操作委托给sessionManager,sesionManager校验之后再转交给SimpleSession;
session过期定时任务默认60分钟执行一次,所session已过期或不合法,则抛出对应的异常,上层通过捕获异常从sessionDAO中删除session;
不只定时任务做session的校验,session的基本操作都在sessionManager中有做session的校验,例如touch、setAttribute等,具体可以查看AbstractNativeSessionManager,对session的操作都是通过AbstractNativeSessionManager处理后转交给SimpleSession。
shiro源码篇 - shiro的session共享,你值得拥有
session共享实现的原理其实都是一样的,都是filter + HttpServletRequestWrapper,只是实现细节会有所区别;
shiro的session共享其实是比较简单的,重写CacheManager,将其操作指向我们的redis,然后定制CachingSessionDAO实现session缓存操作和session持久化;
如果session需要持久化,推荐自定义sessionDAO继承EnterpriseCacheSessionDAO,如果只是缓存,则推荐自定义sessionDAO继承CachingSessionDAO。
shiro源码篇 - shiro的filter,你值得拥有
SpringShiroFilter注册到spring容器,会被包装成FilterRegistrationBean,通过FilterRegistrationBean注册到servlet容器;SpringShiroFilter相当于是整个shiro的入口;
SpringShiroFilter会创建ProxiedFilterChain,代理servlet FilterChain,让请求先走shiro的filter链,让后再走servlet FilterChain。
shiro源码篇 - shiro认证与授权,你值得拥有
认证通过Filter实现,anon表示匿名访问,不需要认证,一般就是针对游客可以访问的资源,而authc则表示需要登录认证;
我们所有的请求一般由shiro中3个Filter:LogoutFilter、AnonymousFilter、FormAuthenticationFilter分摊了,LogoutFilter负责/logout,AnonymousFilter负责/login和静态资源,FormAuthenticationFilter则负责剩下的(/**);
认证由FormAuthenticationFilter实现,未登录的请求会由它重定向到/login;认证过程是将界面输入的信息(UsernamePasswordToken)与缓存(或数据库)中的authenticationInfo进行匹对验证;认证信息不建议缓存;
授权由注解方式,配合aop实现目标方法前的增强织入;认证过程是将缓存(或数据库)中的authorizationInfo与@RequiresPermissions("xxx")中的xxx进行匹配校验;认证信息建议缓存起来。
参考
《跟我学shiro》