三、subject.execute()
// 将该subject绑定到当前线程,并更新会话的上次访问时间以及分发合适的过滤器 subject.execute(new Callable() { public Object call() throws Exception { AbstractShiroFilter.this.updateSessionLastAccessTime(request, response); AbstractShiroFilter.this.executeChain(request, response, chain); return null; } });
1、execute()
追踪execute(),来到DelegatingSubject类
public <V> V execute(Callable<V> callable) throws ExecutionException { Callable associated = this.associateWith(callable); try { return associated.call(); } catch (Throwable var4) { throw new ExecutionException(var4); } }
this.associateWith(callable)通过参数callable创建了一个SubjectCallable对象,所以我们查看SubjectCallable类中的call方法,如下:
public V call() throws Exception { Object var1; try { this.threadState.bind(); var1 = this.doCall(this.callable); } finally { this.threadState.restore(); } return var1; }
this.threadState.bind();方法将subject和securityManager绑定到当前线程的resources(一个map),如下
public void bind() { SecurityManager securityManager = this.securityManager; if (securityManager == null) { securityManager = ThreadContext.getSecurityManager(); } this.originalResources = ThreadContext.getResources(); ThreadContext.remove(); ThreadContext.bind(this.subject); if (securityManager != null) { ThreadContext.bind(securityManager); } }
doCall(this.callable)调用回调方法
2、updateSessionLastAccessTime(request, response)
更新会话上次访问时间
protected void updateSessionLastAccessTime(ServletRequest request, ServletResponse response) { if (!this.isHttpSessions()) { Subject subject = SecurityUtils.getSubject(); if (subject != null) { Session session = subject.getSession(false); if (session != null) { try { session.touch(); } catch (Throwable var6) { log.error("session.touch() method invocation has failed. Unable to update the corresponding session's last access time based on the incoming request.", var6); } } } } }
3、executeChain(request, response, chain)
将请求分发给合适的过滤器
此部分不做详解
四、认证时的createSubject()
基本认证流程如下:
Subject subject = SecurityUtils.getSubject(); subject.login(new UsernamePasswordToken("cyh", "123"));
1、getSubject()
追踪源码发现getSubject最终执行的是SecurityUtils类中的这么一个方法
public static Subject getSubject() { Subject subject = ThreadContext.getSubject(); if (subject == null) { subject = (new Builder()).buildSubject(); ThreadContext.bind(subject); } return subject; }
从上文的分析中我们知道了每一次访问都会重新建立subject然后绑定到当前线程,所以当前线程中获得subject不会是null。只有在没有配置shirofilter的应用中才会出现当前线程中subject==null的情况,所以这里就可以拿到前面过滤过程中创建的subject
2、subject.login()
随后通过当前的subject和token调用login函数
public void login(AuthenticationToken token) throws AuthenticationException { clearRunAsIdentitiesInternal(); Subject subject = securityManager.login(this, token); PrincipalCollection principals; String host = null; if (subject instanceof DelegatingSubject) { DelegatingSubject delegating = (DelegatingSubject) subject; //we have to do this in case there are assumed identities - we don't want to lose the 'real' principals: principals = delegating.principals; host = delegating.host; } else { principals = subject.getPrincipals(); } if (principals == null || principals.isEmpty()) { String msg = "Principals returned from securityManager.login( token ) returned a null or " + "empty value. This value must be non null and populated with one or more elements."; throw new IllegalStateException(msg); } this.principals = principals; this.authenticated = true; if (token instanceof HostAuthenticationToken) { host = ((HostAuthenticationToken) token).getHost(); } if (host != null) { this.host = host; } Session session = subject.getSession(false); if (session != null) { this.session = decorate(session); } else { this.session = null; } }
其在最终会通过调用realm中的认证方法获得当前用户的info
Subject loggedIn = createSubject(token, info, subject);
随后在此处又创建了一次Subject
那么两次创建subject有什么区别呢
protected Subject createSubject(AuthenticationToken token, AuthenticationInfo info, Subject existing) { SubjectContext context = createSubjectContext(); // 保存登陆状态 context.setAuthenticated(true); // 保存token context.setAuthenticationToken(token); // 保存认证的返回信息 context.setAuthenticationInfo(info); context.setSecurityManager(this); if (existing != null) { // 保存subject的信息到上下文 context.setSubject(existing); } // 开始创建subject return createSubject(context); }
可以看到此处的创建其实就是在上下文中额外增加了一些信息,之后也是通过调用上面过滤时用的那个函数createSubject(SubjectContext subjectContext)创建的subject
public Subject createSubject(SubjectContext subjectContext) { SubjectContext context = this.copy(subjectContext); context = this.ensureSecurityManager(context); context = this.resolveSession(context); context = this.resolvePrincipals(context); Subject subject = this.doCreateSubject(context); this.save(subject); return subject; }
这里着重看save函数,前面我们进入到
public Subject save(Subject subject) { if (isSessionStorageEnabled(subject)) { saveToSession(subject); } else { log.trace("Session storage of subject state for Subject [{}] has been disabled: identity and " + "authentication state are expected to be initialized on every request or invocation.", subject); } return subject; }
如果开启session,则将subject保存至session中,这里我们接着saveToSession(subject)往下追踪,来到
protected void saveToSession(Subject subject) { mergePrincipals(subject); mergeAuthenticationState(subject); }
先看mergePrincipals(subject)函数
protected void mergePrincipals(Subject subject) { PrincipalCollection currentPrincipals = null; if (subject.isRunAs() && subject instanceof DelegatingSubject) { try { Field field = DelegatingSubject.class.getDeclaredField("principals"); field.setAccessible(true); currentPrincipals = (PrincipalCollection)field.get(subject); } catch (Exception e) { throw new IllegalStateException("Unable to access DelegatingSubject principals property.", e); } } if (currentPrincipals == null || currentPrincipals.isEmpty()) { currentPrincipals = subject.getPrincipals(); } Session session = subject.getSession(false); if (session == null) { if (!isEmpty(currentPrincipals)) { session = subject.getSession(); session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals); } } else { PrincipalCollection existingPrincipals = (PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); if (isEmpty(currentPrincipals)) { if (!isEmpty(existingPrincipals)) { session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY); } } else { if (!currentPrincipals.equals(existingPrincipals)) { session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals); } } } }
其中先通过反射获取subject中的principals属性
try { Field field = DelegatingSubject.class.getDeclaredField("principals"); field.setAccessible(true); currentPrincipals = (PrincipalCollection)field.get(subject); }
显然当我们在过滤时创建的subject是没有这个属性的(除非得到了session,通过session获取保存的principals),只有在登陆认证的时候之后才有这个属性
if (!isEmpty(currentPrincipals)) { session = subject.getSession(); session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals); }
以下都是针对第一次请求,没有能够获取到cookie中的session的情况
这里表示如果currentPrincipals不为空则获取session,获取不到则创建session并保存principals,过滤时的save函数不会进去这个代码段,也就没有创建session。
到这我们就清晰了,在最开始过滤时,显然是没有principals的,虽然前面执行了resolvePrincipals(context)函数,但是此时context上下文中是没有principals的信息的,所以过滤操作时处我们并不会去创建一个session
而在认证时再次执行save操作的时候,此时已经调用了认证获得了对应的info,info中解析出了principals信息在调用resolvePrincipals(context)时就保存了该信息,所以save时就会因此创建一个session,随后存放principals信息在session中
下方的mergeAuthenticationState(subject)函数其实也同理,在认证之后会将该状态保存到session中,然后 SessionId 写到 Cookies 中(DefaultWebSessionManager的onstart方法)
subject 创建出来之后,暂且叫内部subject,就是把认证通过的内部subject的信息和session复制给我们外界使用的subject.login(token)的subject中,这个subject暂且叫外部subject,看下session的赋值,又进行了一次装饰(decorate),这次装饰则把session(类型为StoppingAwareProxiedSession,即是内部subject和session的合体)和外部subject绑定到一起。
五、用户认证后的后续请求
除了手动调用login会进入认证,其他请求过来时也会先检测当前用户是否进行了认证(即subject.authenticated是否为true),没有则先认证,有则不需要。
现在我们已经知道使用了全局的Session为我们保存了认证状态, 并且每次请求就会从这个全局Session中取出认证状态并保存到新创建的Subject中即可,不需要再执行login,也不需调用doGetAuthenticationInfo()方法
现在, 我们只需要保证前后两次请求是从同一个Session中取的认证状态即可(即两次请求在同一个会话中),这只要通过sessionId便可以实现,即
当用户访问其他接口时,经过过滤器时会创建Subject,在其中执行resovleSession时便会调用方法获取请求携带的cookie,随后通过Cookies 中的SessionId 获取到Session,并将 Session 中的信息合并到当前Subject中,此时当前线程的 Subject.authenticated = true, Subject.principals 保存了用户信息。
随后经过过滤器验证,如指定的是 authc 过滤器,则认为是 FormAuthenticationFilter 来执行此处请求的验证。而 FormAuthenticationFilter 的判断条件是 Subject.authenticated 是否为true,因为已经合并了session中保存的subject的认证信息,故校验通过。(如果没有session,则在此处需要重新执行login操作)