shiro|shiro中session实现的简单分析

前阵子对shiro进行分布式环境下的改造时跟了一遍源码,当时只是使用了思维带图简要的记录了一下方法的调用过程。最近有空了决定用博客详细的记录分析一下这个流程,以帮助自己更好的理解。
###配置
首先看看shiro在web.xml文件中的配置
shiroFilter org.springframework.web.filter.DelegatingFilterProxy shiroFilter /*

可以看到使用的标签是Spring的代理过滤器,那么它是如何代理shiro的过滤器的呢?看看DelegatingFilterProxy的源码
@Override protected void initFilterBean() throws ServletException { synchronized (this.delegateMonitor) { if (this.delegate == null) { // If no target bean name specified, use filter name. //如果没有delegate则根据去Spring容器中寻找对应的bean if (this.targetBeanName == null) { this.targetBeanName = getFilterName(); } // Fetch Spring root application context and initialize the delegate early, // if possible. If the root application context will be started after this // filter proxy, we'll have to resort to lazy initialization. WebApplicationContext wac = findWebApplicationContext(); if (wac != null) { this.delegate = initDelegate(wac); } } } }

于是Spring中应该配置了name为shiroFilter的bean,下面看看Spring中与shiro相关的配置
/user/loginpage = anon /user/login = anon /* = authc /user/perms1 = perms["user:delete"] /user/perms2 = perms["user:select"] /user/admin = roles["admin"] #自定义的过滤器,只要多个权限中有一个满足即可 /user/users = rolesOr["admin","user"]

ShiroFilterFactoryBean的源码这里不进行讨论,先看看ShiroFilterFactoryBean.class中生成ShiroFilter的createInstance()方法
protected AbstractShiroFilter createInstance() throws Exception {log.debug("Creating Shiro Filter instance."); SecurityManager securityManager = getSecurityManager(); if (securityManager == null) { String msg = "SecurityManager property must be set."; throw new BeanInitializationException(msg); }if (!(securityManager instanceof WebSecurityManager)) { String msg = "The security manager does not implement the WebSecurityManager interface."; throw new BeanInitializationException(msg); }FilterChainManager manager = createFilterChainManager(); //Expose the constructed FilterChainManager by first wrapping it in a // FilterChainResolver implementation. The AbstractShiroFilter implementations // do not know about FilterChainManagers - only resolvers: PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver(); chainResolver.setFilterChainManager(manager); //Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built //FilterChainResolver.It doesn't matter that the instance is an anonymous inner class //here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts //injection of the SecurityManager and FilterChainResolver: return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver); }

spring的DelegatingFilterProxy由此获得了对AbstractShiroFilter的代理。下面我们在DelegatingFilterProxy的doFilter方法上打上断点,跟踪shiro在一次登录请求中都会做哪些处理。
断点调试
1.入口:invokeDelegate(delegateToUse, request, response, filterChain)
// Let the delegate perform the actual doFilter operation.
2.接着执行OncePerRequestFilter的doFilter方法
public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws ServletException, IOException { String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName(); if ( request.getAttribute(alreadyFilteredAttributeName) != null ) { log.trace("Filter '{}' already executed.Proceeding without invoking this filter.", getName()); filterChain.doFilter(request, response); } else //noinspection deprecation if (/* added in 1.2: */ !isEnabled(request, response) || /* retain backwards compatibility: */ shouldNotFilter(request) ) { log.debug("Filter '{}' is not enabled for the current request.Proceeding without invoking this filter.", getName()); filterChain.doFilter(request, response); } else { // Do invoke this filter... log.trace("Filter '{}' not yet executed.Executing now.", getName()); request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE); try { doFilterInternal(request, response, filterChain); } finally { // Once the request has finished, we're done and we don't // need to mark as 'already filtered' any more. request.removeAttribute(alreadyFilteredAttributeName); } } }

可以看到这个方法会先判断请求是否是过滤的了,在最后一个分支调用了doFilterInternal(request, response, filterChain); 这个方法,我们跟进方法中看看。
3.断点跳进了AbstractShiroFilter中,观察这个类,发现他继承了OncePerRequestFilter 并重写了其中的doFilterInternal
public abstract class AbstractShiroFilter extends OncePerRequestFilter

接下来看看doFilterInternal中的逻辑
protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain) throws ServletException, IOException {Throwable t = null; try { final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain); final ServletResponse response = prepareServletResponse(request, servletResponse, chain); final Subject subject = createSubject(request, response); //noinspection unchecked subject.execute(new Callable() { public Object call() throws Exception { updateSessionLastAccessTime(request, response); executeChain(request, response, chain); return null; } }); } catch (ExecutionException ex) { t = ex.getCause(); } catch (Throwable throwable) { t = throwable; }if (t != null) { if (t instanceof ServletException) { throw (ServletException) t; } if (t instanceof IOException) { throw (IOException) t; } //otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one: String msg = "Filtered request failed."; throw new ServletException(msg, t); } }

主要的逻辑就是创建一个subject,并在创建完成后异步执行一个callable任务用于更新 updateSessionLastAccessTime。接下里看看subject的创建过程。
4.createSubject()
protected WebSubject createSubject(ServletRequest request, ServletResponse response) { return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject(); }

使用建造者模式建造了一个WebSubject对象,继续跟进
Builder的构造方法
public Builder(SecurityManager securityManager, ServletRequest request, ServletResponse response) { super(securityManager); if (request == null) { throw new IllegalArgumentException("ServletRequest argument cannot be null."); } if (response == null) { throw new IllegalArgumentException("ServletResponse argument cannot be null."); } setRequest(request); setResponse(response); }

build方法
public WebSubject buildWebSubject() { //调用父类的buildSubject() Subject subject = super.buildSubject(); if (!(subject instanceof WebSubject)) { String msg = "Subject implementation returned from the SecurityManager was not a " + WebSubject.class.getName() + " implementation.Please ensure a Web-enabled SecurityManager " + "has been configured and made available to this builder."; throw new IllegalStateException(msg); } return (WebSubject) subject; }

可以看到WebSubject的build方法最终调用了父类的buildSubject方法,跟进这个方法。
5.跟进父类Subject的buildSubject方法
public Subject buildSubject() { return this.securityManager.createSubject(this.subjectContext); }

发现调用的是securityManager的createSubject方法,继续跟进
public Subject createSubject(SubjectContext subjectContext) { //create a copy so we don't modify the argument's backing map: SubjectContext context = copy(subjectContext); //ensure that the context has a SecurityManager instance, and if not, add one: context = ensureSecurityManager(context); //Resolve an associated Session (usually based on a referenced session ID), and place it in the context before //sending to the SubjectFactory.The SubjectFactory should not need to know how to acquire sessions as the //process is often environment specific - better to shield the SF from these details: context = resolveSession(context); //Similarly, the SubjectFactory should not require any concept of RememberMe - translate that here first //if possible before handing off to the SubjectFactory: context = resolvePrincipals(context); Subject subject = doCreateSubject(context); //save this subject for future reference if necessary: //(this is needed here in case rememberMe principals were resolved and they need to be stored in the //session, so we don't constantly rehydrate the rememberMe PrincipalCollection on every operation). //Added in 1.2: save(subject); return subject; }

方法的的作用注释已经说明的很清楚,我们需要注意的是**doCreateSubject(context)**这个方法,securityManger通过这个方法根据传入的subjectContext构建了一个Subject对象。
protected Subject doCreateSubject(SubjectContext context) { return getSubjectFactory().createSubject(context); }

跟进发现securityManger使用了内部的subjectFactoy对象进行subject的创建。
public Subject createSubject(SubjectContext context) { if (!(context instanceof WebSubjectContext)) { return super.createSubject(context); } WebSubjectContext wsc = (WebSubjectContext) context; SecurityManager securityManager = wsc.resolveSecurityManager(); Session session = wsc.resolveSession(); boolean sessionEnabled = wsc.isSessionCreationEnabled(); PrincipalCollection principals = wsc.resolvePrincipals(); boolean authenticated = wsc.resolveAuthenticated(); String host = wsc.resolveHost(); ServletRequest request = wsc.resolveServletRequest(); ServletResponse response = wsc.resolveServletResponse(); return new WebDelegatingSubject(principals, authenticated, host, session, sessionEnabled, request, response, securityManager); }

方法首先对传入的SubjectContext的类型做了一个判断,我们只关心wsc的情况。发现方法通过wsc获取了shiro框架中一系列重要对象如principal,session后构建了一个WebDelegatingSubject对象。先看看resolveSession这个方法。
6.SecurityManaget中的resolveSession
protected SubjectContext resolveSession(SubjectContext context) { if (context.resolveSession() != null) { log.debug("Context already contains a session.Returning."); return context; } try { //Context couldn't resolve it directly, let's see if we can since we have direct access to //the session manager: Session session = resolveContextSession(context); if (session != null) { context.setSession(session); } } catch (InvalidSessionException e) { log.debug("Resolved SubjectContext context session is invalid.Ignoring and creating an anonymous " + "(session-less) Subject instance.", e); } return context; }

首先试图从SubjectContext中获取session,因此让我跟进一下这个方法:
sc中的resolveSession方法
//SubjectContext.class public Session resolveSession() { Session session = getSession(); if (session == null) { //try the Subject if it exists: Subject existingSubject = getSubject(); if (existingSubject != null) { session = existingSubject.getSession(false); } } return session; }

首先会检查subjectContext中的session是否是null。因为此时session还没有与sc做绑定因此getSession方法必定返回null,跳入第二个分支试图从与sc绑定的subject中获取,同理此时subject也为null,因此return session 返回的一定是一个null对象。让我们回到securityManager中的resolveSession方法,接下来会执行session为null的那一个分支。即
try { //Context couldn't resolve it directly, let's see if we can since we have direct access to //the session manager: Session session = resolveContextSession(context); if (session != null) { context.setSession(session); } } catch (InvalidSessionException e) { log.debug("Resolved SubjectContext context session is invalid.Ignoring and creating an anonymous " + "(session-less) Subject instance.", e); }

这个代码片段。通过resolveContextSession(context)方法获取session并在获取成功之后与context进行绑定(因此接下来如果再调用这个方法可以直接走从Context获取的分支)。于是我们分析的重点就转移到了**resolveContextSession(context)**这个方法上。
7.resolveContextSession(context)
protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException { SessionKey key = getSessionKey(context); if (key != null) { return getSession(key); } return null; }

【shiro|shiro中session实现的简单分析】这里通过context获取了一个新的对象SessionKey,只有一个方法getSessionId(),通过注释可以得知通过SessionKey可以找到唯一指定的session。弄清楚SessionKey的作用后我们开始分析getSessionKey方法
//DefaultWebSecurityManager.class protected SessionKey getSessionKey(SubjectContext context) { //首先判断是否是web环境 if (WebUtils.isWeb(context)) { //获取sessionId Serializable sessionId = context.getSessionId(); ServletRequest request = WebUtils.getRequest(context); ServletResponse response = WebUtils.getResponse(context); return new WebSessionKey(sessionId, request, response); } else { return super.getSessionKey(context); } }

context.getSessionId()
public Serializable getSessionId() { return getTypedValue(SESSION_ID, Serializable.class); }

从context中根据键SESSION_ID进行取值,因为还没有进行设置因此返回的sessionId为null。
getSessionKey最终构造了一个WebSessionKey对象并返回。因此resolveContextSession方法走入执行getSession(key)方法的分支。
getSession(key)
//SessionSecurityManager public Session getSession(SessionKey key) throws SessionException { return this.sessionManager.getSession(key); }

继续跟进SessionManager的getSession(key)
//AbstractNativeSessionManager.class public Session getSession(SessionKey key) throws SessionException { //首先根据key寻找session Session session = lookupSession(key); return session != null ? createExposedSession(session, key) : null; }

lookupSession(key)
private Session lookupSession(SessionKey key) throws SessionException { if (key == null) { throw new NullPointerException("SessionKey argument cannot be null."); } return doGetSession(key); }

doGetSession(key)
@Override protected final Session doGetSession(final SessionKey key) throws InvalidSessionException { enableSessionValidationIfNecessary(); log.trace("Attempting to retrieve session with key {}", key); Session s = retrieveSession(key); if (s != null) { validate(s, key); } return s; }

重点关注retrieveSession(key); 这个方法,使用该方法获取session后使用validate方法校验后即可返回。
retrieveSession(key)
//这里使用的是我在CustomeSessionManager中覆写的retrieveSession @Override protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException { //通过SessionKey对象获取sessionId Serializable sessionId = getSessionId(sessionKey); ServletRequest request = null; if(sessionKey instanceof WebSessionKey){ request = ((WebSessionKey)sessionKey).getServletRequest(); } //尝试从request中取而不是每次都请求数据库 if(request !=null && sessionId !=null){ Session session = (Session) request.getAttribute(sessionId.toString()); if(session != null){ return session; } } //如果request中没有session则从数据库中请求并把请求结果设置给sessionKey Session session = super.retrieveSession(sessionKey); if(request !=null && sessionId != null){ request.setAttribute(sessionId.toString(),session); } return session; }

首先关注getSessionId(sessionKey)这个方法,因为到目前为止我们的sessionKey对象中的sessionId属性仍然是空的。
getSessionId(sessionKey)
//DefaultWebSessionManager.class public Serializable getSessionId(SessionKey key) { Serializable id = super.getSessionId(key); if (id == null && WebUtils.isWeb(key)) { ServletRequest request = WebUtils.getRequest(key); ServletResponse response = WebUtils.getResponse(key); id = getSessionId(request, response); } return id; }

super.getSessionId(key); 的逻辑很简单,就是获取传入sessionKey的sessionId属性,获取到的当然是空值因此走入if分支。从sessionKey中获取request和response对象然后通过getSessionId(request, response)方法生成sessionId。
getSessionId(request, response)
protected Serializable getSessionId(ServletRequest request, ServletResponse response) { return getReferencedSessionId(request, response); }

getReferencedSessionId(request, response)
private Serializable getReferencedSessionId(ServletRequest request, ServletResponse response) { //试图从cookie中获取sessionId(这里已经可以看出shiro的session实现原理也是基于cookie的) String id = getSessionIdCookieValue(request, response); if (id != null) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE); } else {//cookie被禁用的情况,使用url后缀裹挟sessionId的方式实现session //not in a cookie, or cookie is disabled - try the request URI as a fallback (i.e. due to URL rewriting)://try the URI path segment parameters first: id = getUriPathSegmentParamValue(request, ShiroHttpSession.DEFAULT_SESSION_ID_NAME); if (id == null) { //not a URI path segment parameter, try the query parameters: String name = getSessionIdName(); id = request.getParameter(name); if (id == null) { //try lowercase: id = request.getParameter(name.toLowerCase()); } } if (id != null) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.URL_SESSION_ID_SOURCE); } } if (id != null) { request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, id); //automatically mark it valid here.If it is invalid, the //onUnknownSession method below will be invoked and we'll remove the attribute at that time. request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); }// always set rewrite flag - SHIRO-361 request.setAttribute(ShiroHttpServletRequest.SESSION_ID_URL_REWRITING_ENABLED, isSessionIdUrlRewritingEnabled()); return id; }

getSessionIdCookieValue()
private String getSessionIdCookieValue(ServletRequest request, ServletResponse response) { if (!isSessionIdCookieEnabled()) { log.debug("Session ID cookie is disabled - session id will not be acquired from a request cookie."); return null; } if (!(request instanceof HttpServletRequest)) { log.debug("Current request is not an HttpServletRequest - cannot get session ID cookie.Returning null."); return null; } HttpServletRequest httpRequest = (HttpServletRequest) request; return getSessionIdCookie().readValue(httpRequest, WebUtils.toHttp(response)); }

readValue()
//SimpleCookie.class public String readValue(HttpServletRequest request, HttpServletResponse ignored) { String name = getName(); String value = https://www.it610.com/article/null; javax.servlet.http.Cookie cookie = getCookie(request, name); if (cookie != null) { // Validate that the cookie is used at the correct place. String path = StringUtils.clean(getPath()); if (path != null && !pathMatches(path, request.getRequestURI())) { log.warn("Found '{}' cookie at path '{}', but should be only used for '{}'", new Object[] { name, request.getRequestURI(), path}); } else { value = https://www.it610.com/article/cookie.getValue(); log.debug("Found '{}' cookie value [{}]", name, value); } } else { log.trace("No '{}' cookie value", name); }return value; }

readValue()的逻辑很清晰不多做解释
此时结果一番折腾终于获得了sessionId,让我们回到retrieveSession方法继续往下执行
Session session = super.retrieveSession(sessionKey);

super.retrieveSession(sessionKey)
//DefaultWebSessionManager.class protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException { //虽然此时sessionKey中的sessionId依然是null但由于我们在 //getReferencedSessionId方法中获取到sessionId后将sessionId存在了 //request对象中,因此sessionId的值是从request中获取的 Serializable sessionId = getSessionId(sessionKey); if (sessionId == null) { log.debug("Unable to resolve session ID from SessionKey [{}].Returning null to indicate a " + "session could not be found.", sessionKey); return null; } //根据sessionId去数据源取相应的session Session s = retrieveSessionFromDataSource(sessionId); if (s == null) { //session ID was provided, meaning one is expected to be found, but we couldn't find one: String msg = "Could not find session with ID [" + sessionId + "]"; throw new UnknownSessionException(msg); } return s; }

这里需要重视 Session s = retrieveSessionFromDataSource(sessionId)这句代码,通过sessionId去相应的数据源获取对应的session,跟进一下。
retrieveSessionFromDataSource(sessionId)
protected Session retrieveSessionFromDataSource(Serializable sessionId) throws UnknownSessionException { return sessionDAO.readSession(sessionId); }

可以看出调用了SessionDAO的readSession方法,由于sessionDAO是可以自由定义与替换的,所以我们可以根据实际场景更换相应的SessionDao。那么到这了就取得了session。可以看到shiro框架内部自身实现了一套session机制,因此shiro的session是可以脱离web容器使用的。
###总结
我们从一次请求开始简单分析了shiro框架对于session的处理流程,下一篇博客准备以同样的模式分析shiro对于身份验证以及权限认证的处理流程。(最近比较懒散更新时间待定哈哈)。

    推荐阅读