Shiro源码分析② :AbstractShiroFilter
发布日期:2021-05-10 01:56:40 浏览次数:12 分类:精选文章

本文共 32115 字,大约阅读时间需要 107 分钟。

文章目录

一、前言

由于之前没有使用过 Shiro,最近开始使用,故对其部分流程和源码进行了阅读,大体总结了一些内容记录下来。本系列并不会完完全全分析 Shiro 的全部代码,仅把主(我)要(用)流(到)程(的) 简单分析一下。由于本系列大部分为个人内容理解 并且 个人学艺实属不精,故难免出现 “冤假错乱”。如有发现,感谢指正,不胜感激。


Shiro 源码分析全集:


在使用 Shiro 时,当一个请求请求服务端时,首先需要通过的一关就是 AbstractShiroFilter。AbstractShiroFilter 的主要作用有两个 :

  1. 负责创建一个和当前线程绑定的Subject,并且将Session 中的缓存信息填充到 Subject属性中 (如果存在Session 缓存信息的话)。这样进行一些校验时便可以通过Session缓存来进行判断。
  2. 将请求分发给合适的过滤器来进行处理,比如authc的请求分发给 FormAuthenticationFilter 过滤器来处理。

二、AbstractShiroFilter

在整篇文章开始之前,我们首先来看看一个过滤器AbstractShiroFilter,其实AbstractShiroFilter 还有一个子类 ShiroFilter,不过其也就初始化了一下信息,更多的事情是在 AbstractShiroFilter 中处理,所以这里来看 AbstractShiroFilterAbstractShiroFilter如下:

在这里插入图片描述
以我目前对Shiro 有限的认知来说,个人觉着这个过滤器还是非常重要的。AbstractShiroFilter 继承了 OncePerRequestFilteOncePerRequestFilter 提供了 doFilterInternal 方法,保证每次一次请求仅经过一次过滤(如部分转发请求会经过多次过滤器。详参 : )。

那么这个过滤器做了什么呢?直白的说,在每次请求过来的时候,AbstractShiroFilter 会去创建一个Subject并将其绑定到当前请求的线程上,在创建Subject的时候会去读取缓存的用户信息,并填充到Subject 中,这样我们便能随时随地的通过 Subject subject = SecurityUtils.getSubject(); 来获取Subject。


下面来简单看看Subject的绑定过程。

由于AbstractShiroFilter 继承了 OncePerRequestFilter ,所以我们这里直接来看 AbstractShiroFilter#doFilterInternal 方法的实现。

protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)          throws ServletException, IOException {       		// ... 异常处理    		       	// 转换 request  和 response           final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);           final ServletResponse response = prepareServletResponse(request, servletResponse, chain);		//  1. 创建 Subject            final Subject subject = createSubject(request, response);           //noinspection unchecked           // 2. 将Subject 绑定到当前线程上。同时调用 回调方法           subject.execute(new Callable() {                  public Object call() throws Exception {                  		// 更新session 会话时间                   updateSessionLastAccessTime(request, response);                   // 执行过滤器链,分发请求                   executeChain(request, response, chain);                   return null;               }           });		// ... 对异常信息的处理	}

这里我们可以看到两个关键方法:

  • createSubject(request, response); : 见名知意,在这个方法中创建了 Subject
  • subject.execute : 在这里将 Subject绑定到线程上,同时执行了updateSessionLastAccessTimeexecuteChain 两个回调方法。

下面,我尝试在我有限的理解里讲清楚这两个方法。。。。

1. createSubject(request, response)

目前我所知 Subject的创建的场景有两个

  • 第一种是调用 DelegatingSubject#login 方法时会在内部创建一个 Subject,这个是我们自己在代码中通过Subject进行验证时进行的主动调用,如下:

    UsernamePasswordToken usernamePasswordToken = new UsernamePasswordToken("张三", "123456");     	// 这里通过 SecurityUtils.getSubject(); 获取到的Subject就是本篇中AbstractShiroFilter 创建的Subject        Subject subject = SecurityUtils.getSubject();        // subject.login 中还会创建一个Subject,不过个人理解,这个Subject仅仅用来承载数据,即作为一个暂时的数据保存作用。        subject.login(usernamePasswordToken);
  • 第二种即是当前场景下,当一个请求过来时,AbstractShiroFilter 在处理请求的过程中,会通过AbstractShiroFilter#createSubject 方法会创建一个 Subject

    // org.apache.shiro.web.servlet.AbstractShiroFilter#createSubject    protected WebSubject createSubject(ServletRequest request, ServletResponse response) {         	// 在Builder 方法中会将 request,response 赋值给         return new WebSubject.Builder(getSecurityManager(), request, response).buildWebSubject();    }

上面的内容,我们分成两句来看

  1. new WebSubject.Builder(getSecurityManager(), request, response) 如下,这里就是将 request 和response 填充到 SubjectContext 上下文中。
    private final SubjectContext subjectContext; 		        public Builder(SecurityManager securityManager, ServletRequest request, ServletResponse response) {                 super(securityManager); 			//... 参数校验            // subjectContext  中保存 request、response            setRequest(request);            setResponse(response);        }
  2. buildWebSubject() : 这里开始通过 SubjectContext 来创建 Subject了
    public WebSubject buildWebSubject() {     		// 会调用  this.securityManager.createSubject(this.subjectContext); 来创建 Subject           Subject subject = super.buildSubject();           if (!(subject instanceof WebSubject)) {                  // 抛出异常           }           return (WebSubject) subject;       }
    关于 super.buildSubject(); 的内容,其调用了 this.securityManager.createSubject(this.subjectContext); 来完成 Subject的创建。这一部分比较复杂,后面单独讲解。

2. subject.execute

这里则是将 Subject 绑定到当前线程的部分,并且将请求分发给匹配的过滤器。首先我们先看 subject.execute 的代码

subject.execute(new Callable() {               public Object call() throws Exception {               	// 回调方法                updateSessionLastAccessTime(request, response);                executeChain(request, response, chain);                return null;            }        });

2.1 线程绑定

我们可以看到 subject.execute 的实现方法 DelegatingSubject#execute(java.util.concurrent.Callable<V>) 的具体逻辑

public 
V execute(Callable
callable) throws ExecutionException { Callable
associated = associateWith(callable); try { return associated.call(); } catch (Throwable t) { throw new ExecutionException(t); } }

这里通过 associateWith(callable) 返回的类型是 SubjectCallable。所以我们这里来看 SubjectCallable#call()

public V call() throws Exception {           try {           	// 将Subject 绑定到当前线程            threadState.bind();            // 调用回调方法            return doCall(this.callable);        } finally {               threadState.restore();        }    }

threadState.bind(); 的方法 SubjectThreadState#bind 如下:

public void bind() {           SecurityManager securityManager = this.securityManager;        if ( securityManager == null ) {               //try just in case the constructor didn't find one at the time:            securityManager = ThreadContext.getSecurityManager();        }        this.originalResources = ThreadContext.getResources();        ThreadContext.remove();		// 将Subject绑定到当前线程        ThreadContext.bind(this.subject);        if (securityManager != null) {           	// 将 securityManager 绑定到当前线程            ThreadContext.bind(securityManager);        }    }

上面可以看到绑定的工作都是通过 ThreadContext.bind 方法来完成的,所以 我们 再来看 ThreadContext 部分的实现如下:

public static final String SECURITY_MANAGER_KEY = ThreadContext.class.getName() + "_SECURITY_MANAGER_KEY";    public static final String SUBJECT_KEY = ThreadContext.class.getName() + "_SUBJECT_KEY";    private static final ThreadLocal
> resources = new InheritableThreadLocalMap
>(); public static void bind(Subject subject) { if (subject != null) { // 保存到 resources 中 put(SUBJECT_KEY, subject); } } public static void bind(SecurityManager securityManager) { if (securityManager != null) { put(SECURITY_MANAGER_KEY, securityManager); } }

至此,我们可以知道,Shiro 为每个 线程分配了一个 Map<Object, Object> 来保存信息,由于resourcesThreadLocal<Map<Object, Object>> 类型,所以可以保证每个线程独享一份内容。

2.2 updateSessionLastAccessTime(request, response);

更新会话上次访问时间

protected void updateSessionLastAccessTime(ServletRequest request, ServletResponse response) {           if (!isHttpSessions()) {    //'native' sessions            Subject subject = SecurityUtils.getSubject();            //Subject should never _ever_ be null, but just in case:            if (subject != null) {                   Session session = subject.getSession(false);                if (session != null) {                       try {                       	// 刷新 Session                        session.touch();                    } catch (Throwable t) {                         	// ...                     }                }            }        }    }

2.3 executeChain(request, response, chain);

这部分内容也是非常关键,将请求分发给合适的过滤器。下面我们来看详细代码

protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)            throws IOException, ServletException {            // 筛选出匹配当前请求的 Filter        FilterChain chain = getExecutionChain(request, response, origChain);        chain.doFilter(request, response);    }
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {           FilterChain chain = origChain;		// 默认是  org.apache.shiro.web.filter.mgt.PathMatchingFilterChainResolver 类型        FilterChainResolver resolver = getFilterChainResolver();        if (resolver == null) {               log.debug("No FilterChainResolver configured.  Returning original FilterChain.");            return origChain;        }		// 交由过滤链处理器来进行处理        FilterChain resolved = resolver.getChain(request, response, origChain);        if (resolved != null) {               log.trace("Resolved a configured FilterChain for the current request.");            chain = resolved;        } else {               log.trace("No FilterChain configured for the current request.  Using the default.");        }        return chain;    }

我们直接进去看一下 resolver.getChain(request, response, origChain); 的实现 PathMatchingFilterChainResolver#getChain

从下面可以看到,过滤器的作用和添加顺序有关系,对于一个路径匹配多个过滤器先添加的先生效,后添加的后生效

public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {      		// 获取过滤链路管理器,这里保存着所有的Filter 以及 filterChains        FilterChainManager filterChainManager = getFilterChainManager();        if (!filterChainManager.hasChains()) {               return null;        }		// 获取当前请求路径        String requestURI = getPathWithinApplication(request);        // 获取所有filterChain 中的keySet        for (String pathPattern : filterChainManager.getChainNames()) {   			// 进行路径匹配            if (pathMatches(pathPattern, requestURI)) {               	// 获取 filterChain  中 pathPattern 对应的value(也即是过滤器),并包装成 ProxiedFilterChain 类型返回。                return filterChainManager.proxy(originalChain, pathPattern);            }        }        return null;    }

这里通过截图更直白的看到 FilterChainManager 所包含的内容:

在这里插入图片描述


这里我们来看看 filterChainManager.proxy(originalChain, pathPattern); 的实现如下:

public FilterChain proxy(FilterChain original, String chainName) {   	    //  return this.filterChains.get(chainName);        NamedFilterList configured = getChain(chainName);        if (configured == null) {               String msg = "There is no configured chain under the name/key [" + chainName + "].";            throw new IllegalArgumentException(msg);        }        return configured.proxy(original);    }

这里可以看到 getChain(chainName); 直接获取的 filterChain 中key对应的Filter

在这里插入图片描述


也即是说,在 executeChain(request, response, chain); 方法中,通过当前请求路径和 filterChain 的映射关系来找到对应的过滤器,再调用该过滤器的doFilter 方法。

2.3.1 Shiro 默认的过滤器

public enum DefaultFilter {       anon(AnonymousFilter.class),    authc(FormAuthenticationFilter.class),    authcBasic(BasicHttpAuthenticationFilter.class),    logout(LogoutFilter.class),    noSessionCreation(NoSessionCreationFilter.class),    perms(PermissionsAuthorizationFilter.class),    port(PortFilter.class),    rest(HttpMethodPermissionFilter.class),    roles(RolesAuthorizationFilter.class),    ssl(SslFilter.class),    user(UserFilter.class);}

三、this.securityManager.createSubject(this.subjectContext);

该方法来实现了Subject的创建,是非常重要的方法。具体实现为

DefaultSecurityManager#createSubject(org.apache.shiro.subject.SubjectContext) ,需要注意的是,默认情况下我们这里 的 securityManager 实际类型为DefaultWebSecurityManager


下面我们来详细分析一下代码:

public Subject createSubject(SubjectContext subjectContext) {           //create a copy so we don't modify the argument's backing map:        // 拷贝一个  SubjectContext  副本        SubjectContext context = copy(subjectContext);        //ensure that the context has a SecurityManager instance, and if not, add one:        // 1. 确保上下文具有SecurityManager实例,如果没有,添加一个        context = ensureSecurityManager(context);		// 2. 解析session。        context = resolveSession(context);		// 3. 解析 Principals        context = resolvePrincipals(context);		// 4. 创建 一个全新的 Subject        Subject subject = doCreateSubject(context);		// 5. 保存(缓存) subject         save(subject);        return subject;    }

1 ensureSecurityManager(context)

这一步是为了保证上下文中已经存在 SecurityManager。

如果存在则直接返回。不存在则将当前的 SecurityManager 设值进来,然后保存。

protected SubjectContext ensureSecurityManager(SubjectContext context) {           if (context.resolveSecurityManager() != null) {               log.trace("Context already contains a SecurityManager instance.  Returning.");            return context;        }        log.trace("No SecurityManager found in context.  Adding self reference.");        context.setSecurityManager(this);        return context;    }

2 resolveSession(context);

这一步是为了填充上下文中的 session,如果上下文中没有session则进行解析。需要注意的是,在这一步中,并没有Session的创建过程,也就是说,如果当前会话存在Session, 则进行了一层包装包返回,如果没有Session,则返回的是null,也就不会向SubjectContext中注入session。

这里解析session的目的是为了尝试根据Cookies 中的sessionId 获取 Session 会话,获取Session中缓存的之前请求的内容(比如登录后会将登录信息保存到Session中,后续请求通过session获取到Session 从而解析出来登录验证的结果信息)。

protected SubjectContext resolveSession(SubjectContext context) {       	// 如果已经存在 解析的Session则直接返回        if (context.resolveSession() != null) {                return context;        }        try {             	// 到达这里说明 context 中不能直接获取 session,需要进一步解析            Session session = resolveContextSession(context);            if (session != null) {                   context.setSession(session);            }        } catch (InvalidSessionException e) {            	...        }        return context;    }	...		// 这里就打算从缓存中获取session    protected Session resolveContextSession(SubjectContext context) throws InvalidSessionException {       	// 1. 解析 SessionKey        SessionKey key = getSessionKey(context);        if (key != null) {           	// 2. 获取 session            return getSession(key);        }        return null;    }

2.1 getSessionKey(context);

@Override    protected SessionKey getSessionKey(SubjectContext context) {       	// 判断条件是 context 是 RequestPairSource 实现类  &&  context.ServletRequest 和 ServletResponse 不为空        if (WebUtils.isWeb(context)) {               Serializable sessionId = context.getSessionId();            ServletRequest request = WebUtils.getRequest(context);            ServletResponse response = WebUtils.getResponse(context);            // 封装成 SessionKey 类型            return new WebSessionKey(sessionId, request, response);        } else {               return super.getSessionKey(context);        }    }

这里 WebUtils.isWeb(context) 的成立条件是 context 是 RequestPairSource 实现类 && context.ServletRequest 和 ServletResponse 不为空。我们在 上面讲解 AbstractShiroFilter 流程的时候特意强调过 此时的 上下文 SubjectContext 是被填充了request 和 response 的,所以这里会进入if分支。封装成 WebSessionKey 返回,值得一提的是,此时的sessionId为null。

额外的 super.getSessionKey(context) 调用的是 DefaultSecurityManager#getSessionKey。实现如下:

protected SessionKey getSessionKey(SubjectContext context) {           Serializable sessionId = context.getSessionId();        if (sessionId != null) {               return new DefaultSessionKey(sessionId);        }        return null;    }

2.2 getSession(key);

getSession(key) 方法会调用 this.sessionManager.getSession(key),这里会出现两种截然不同的逻辑,我们继续分析。

由于 SessionManager 有多个实现类

  • DefaultSessionManager: JavaSE环境
  • ServletContainerSessionManager: Web环境,直接使用servlet容器会话
  • DefaultWebSessionManager:用于Web环境的实现,可以替第二个,自己维护着会话,直接废弃了Servlet容器的会话管理

这里我们只关注后两种。这两种的实现逻辑并不相同。

2.2.1 ServletContainerSessionManager#getSession

上面的已经介绍了 ServletContainerSessionManager 纯粹依赖于Servlet 容器来管理会话,所以获取session的方式也是直接通过Servlet 的方式来获取。如下,可以看到直接通过 request.getSession(false) 的方式来获取,获取不到则封装一个HttpServletSession 返回 :

public Session getSession(SessionKey key) throws SessionException {           if (!WebUtils.isHttp(key)) {               String msg = "SessionKey must be an HTTP compatible implementation.";            throw new IllegalArgumentException(msg);        }        HttpServletRequest request = WebUtils.getHttpRequest(key);        Session session = null;		        HttpSession httpSession = request.getSession(false);        if (httpSession != null) {               session = createSession(httpSession, request.getRemoteHost());        }        return session;    }    	...		protected Session createSession(HttpSession httpSession, String host) {           return new HttpServletSession(httpSession, host);    }

2.2.2 DefaultWebSessionManager #getSession

DefaultWebSessionManager 的实现是在其父类中 AbstractNativeSessionManager#getSession,如下

public Session getSession(SessionKey key) throws SessionException {           Session session = lookupSession(key);        return session != null ? createExposedSession(session, key) : null;    }

这里的调用顺序你是 AbstractNativeSessionManager#lookupSession =》AbstractNativeSessionManager#doGetSession =》AbstractValidatingSessionManager#retrieveSession

其中 AbstractNativeSessionManager#doGetSession 中通过了 SessionValidationScheduler 的来决定是否启用session会话验证。

这里我们直接来看 AbstractValidatingSessionManager#retrieveSession

protected Session retrieveSession(SessionKey sessionKey) throws UnknownSessionException {       	// 获取 sessionId        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。在这里面调用了SessionDao 来处理。        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;    }

篇幅问题,就不再追下去了,

我们只需要注意下面两点内容:

  1. Serializable sessionId = getSessionId(sessionKey); 通过获取 客户端的 Cookies 来获取到了 SessionId。
  2. Session s = retrieveSessionFromDataSource(sessionId); 通过SessionDao 根据 sessionId 获取到了Session

注:Shiro 默认是通过 ServletContainerSessionManager#getSession 的方式来获取session。因为session 的会话需要客户端Cookies的支持,大致逻辑就是 服务端创建Session后会将sessionId 保存到客户端的Cookies 中,默认Cookies 的名称是 JSESSIONID。如下(看了好久都想不通ServletContainerSessionManager 模式下 Cookies 的生成,最后一步一步断点发现是request 中生成的。。。 ):

在这里插入图片描述

3 resolvePrincipals(context);

这一步其实很简单,填充上下文中的 principals ,不存在则从 RememberedMe 中获取(不保存一定存在)。

protected SubjectContext resolvePrincipals(SubjectContext context) {   		// 从上下文中获取 principals ,这里会尝试从Session中获取        PrincipalCollection principals = context.resolvePrincipals();		// 如果 上下文中 的 principals  为空则从 RememberedMe 中获取缓存的 principals         if (isEmpty(principals)) {           	// 这里通过 RememberMeManager 来尝试获取 principals            principals = getRememberedIdentity(context);            if (!isEmpty(principals)) {                   context.setPrincipals(principals);            } else {                   log.trace("No remembered identity found.  Returning original context.");            }        }        return context;    }

这里可以看到,这里整个逻辑都是为了获取principals而奋斗。

  1. 直接从上下文中获取 principals
  2. 从上下文中 AuthenticationInfo ,从AuthenticationInfo 中获取 principals
  3. 从上下文中 Subject ,从Subject 中获取 principals
  4. 从上下文中 获取Session,从 Session 中获取 principals
  5. 最后从 RememberMeManager 中来获取 principals

3.1 context.resolvePrincipals()

context.resolvePrincipals() 的实现在DefaultSubjectContext#resolvePrincipals 中,目的就是解析出来 Principals 信息,详细代码如下:

public PrincipalCollection resolvePrincipals() {   	 	// 从缓存中获取principals。这是直接从上下文中获取,获取不到开始其他的获取        PrincipalCollection principals = getPrincipals();		        if (isEmpty(principals)) {               //check to see if they were just authenticated:            // 从缓存中获取 AuthenticationInfo ,从 AuthenticationInfo  中获取 principals             AuthenticationInfo info = getAuthenticationInfo();            if (info != null) {                   principals = info.getPrincipals();            }        }        if (isEmpty(principals)) {           	// 从缓存中获取 Subject,从 subject 中获取 principals             Subject subject = getSubject();            if (subject != null) {                   principals = subject.getPrincipals();            }        }        if (isEmpty(principals)) {               //try the session:            // 从 session 中获取 principals             Session session = resolveSession();            if (session != null) {                   principals = (PrincipalCollection) session.getAttribute(PRINCIPALS_SESSION_KEY);            }        }        return principals;    }		...	// 这里从 backingMap 中尝试获取  PRINCIPALS。 backingMap实际上是 上下文中的缓存,在 上下文初始化的时候会将其保存的属性信息保存到  backingMap 中    public PrincipalCollection getPrincipals() {           return getTypedValue(PRINCIPALS, PrincipalCollection.class);    }		public AuthenticationInfo getAuthenticationInfo() {           return getTypedValue(AUTHENTICATION_INFO, AuthenticationInfo.class);    }        public Subject getSubject() {           return getTypedValue(SUBJECT, Subject.class);    }

这里需要注意,context获取 Principals 的方式 有两种 : context.resolvePrincipals() 和 context.getPrincipals() 两者并不完全相通,context.getPrincipals() 直接通过上下文获取,而 context.resolvePrincipals() 在上下文直接获取不到时会通过AuthenticationInfo、Subject 中获取。如下:

在这里插入图片描述

实际上 Shiro 这一步只是确定了 context.resolvePrincipals() 能解析出来 Principals ,如果能解析出来,不需要做处理,如果解析不到,则通过其他途径获取 Principals 并通过到 context.setPrincipals() 直接赋值到上下文中,以保证context.resolvePrincipals() 能解析出来 Principals。

4 doCreateSubject(context);

上面已经做了 SecurityManager、Session、PrincipalCollection 的解析工作,不管是否获取到了,准备工作在上面已经结束,这里便开始创建 Subject。其实就是将上面的解析出来的各个属性填充到新创建的 Subject 中。

public Subject createSubject(SubjectContext context) {           SecurityManager securityManager = context.resolveSecurityManager();        Session session = context.resolveSession();        boolean sessionCreationEnabled = context.isSessionCreationEnabled();        PrincipalCollection principals = context.resolvePrincipals();        boolean authenticated = context.resolveAuthenticated();        String host = context.resolveHost();        return new DelegatingSubject(principals, authenticated, host, session, sessionCreationEnabled, securityManager);    }

5 save(subject);

在上一步结束,我们终于创建出来了一个 Subject。为了更好的使用这个 Subject,这里将Subject 中的部分信息(PrincipalCollection 和 AuthenticationState) 保存到Session 中,Session的创建也是在这一步完成的。下面我们来看代码:

save(subject) 的实现是在 DefaultSecurityManager#save 中。我们可以看到直接委托给了 subjectDAO 来处理Subject。

protected void save(Subject subject) {           this.subjectDAO.save(subject);    }

DefaultSubjectDAO#save 代码如下 :

public Subject save(Subject subject) {     		// 通过 SessionStorageEvaluator 判断是否启用 Session 缓存        if (isSessionStorageEnabled(subject)) {           	// 如果启用了 session缓存,则进行处理            saveToSession(subject);        } else {               //... 打印日志        }        return subject;   }    protected void saveToSession(Subject subject) {           //performs merge logic, only updating the Subject's session if it does not match the current state:        // 执行合并逻辑,仅在主题的会话与当前状态不匹配时才更新        // 这里实际上就是将当前的 subject 中的 principals 同步到 session 中        // key 为 PRINCIPALS_SESSION_KEY : value 为当前的 principals (会将session中已存在的 principals  移除,替换成当前的 principals )        mergePrincipals(subject);        // 同理,同步验证状态 true or false 。表验证是否通过        mergeAuthenticationState(subject);    }

我们这里可以看到,需要合并(缓存到Session)的数据只有 principals 和 AuthenticationState ,其中AuthenticationState 就是一个状态表明当前验证已经通过。也只有需要缓存数据到Session 的情况下,才有可能主动去创建Session。

这里的合并情况:当第一次登录请求已经通过,第二次其他请求过来时,会获取当会话的Session,第二次请求也会获取到 principals 和 AuthenticationState 信息,这是需要进行一个合并缓存。

5.1 mergePrincipals(subject);

合并 Principals 状态,这里需要注意的是Subject 获取 session的方式有三种

  • subject.getSession(); :等同于 subject.getSession(true);。获取 session ,不存在session则创建一个session
  • subject.getSession(true); :获取 session ,不存在session则创建一个session
  • subject.getSession(false); :获取 session ,不存在session则返回 null
// org.apache.shiro.subject.support.DefaultSubjectContext_PRINCIPALS_SESSION_KEY	public static final String PRINCIPALS_SESSION_KEY = DefaultSubjectContext.class.getName() + "_PRINCIPALS_SESSION_KEY";  	protected void mergePrincipals(Subject subject) {           PrincipalCollection currentPrincipals = null;        // 根据官方注释,这里两种获取 Principals  的方式应该是为了上下版本兼容               if (subject.isRunAs() && subject instanceof DelegatingSubject) {               try {               	// 通过反射获取 principals 属性                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 session = subject.getSession(false);		        if (session == null) {           	// 如果获取不到session  &&  currentPrincipals 不为空(表明需要缓存),则需要自己创建一个sesson            if (!isEmpty(currentPrincipals)) {               	// 创建一个session                session = subject.getSession();                // 将 currentPrincipals 保存到 session中                session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);            }            // otherwise no session and no principals - nothing to save        } else {           	// 如果session不为空,先尝试获取 session中缓存的 existingPrincipals 。这里需要将原先的 existingPrincipals  和 currentPrincipals  进行一个状态合并(说白了,就是以currentPrincipals 为准)            PrincipalCollection existingPrincipals =                    (PrincipalCollection) session.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);			            if (isEmpty(currentPrincipals)) {                   if (!isEmpty(existingPrincipals)) {                       session.removeAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);                }                // otherwise both are null or empty - no need to update the session            } else {                   if (!currentPrincipals.equals(existingPrincipals)) {                       session.setAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY, currentPrincipals);                }                // otherwise they're the same - no need to update the session            }        }    }

5.2 mergeAuthenticationState(subject);

mergeAuthenticationState(subject); 的实现和上面类似,也是为了合并当前的 AuthenticationState,这个状态将在登录后的请求中作为一个判断,表明当前会话是否已经通过登录验证。

// org.apache.shiro.subject.support.DefaultSubjectContext_AUTHENTICATED_SESSION_KEY	public static final String AUTHENTICATED_SESSION_KEY = DefaultSubjectContext.class.getName() + "_AUTHENTICATED_SESSION_KEY";	 	protected void mergeAuthenticationState(Subject subject) {           Session session = subject.getSession(false);        if (session == null) {               if (subject.isAuthenticated()) {                   session = subject.getSession();                session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);            }            //otherwise no session and not authenticated - nothing to save        } else {               Boolean existingAuthc = (Boolean) session.getAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);            if (subject.isAuthenticated()) {                   if (existingAuthc == null || !existingAuthc) {                       session.setAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY, Boolean.TRUE);                }                //otherwise authc state matches - no need to update the session            } else {                   if (existingAuthc != null) {                       //existing doesn't match the current state - remove it:                    session.removeAttribute(DefaultSubjectContext.AUTHENTICATED_SESSION_KEY);                }                //otherwise not in the session and not authenticated - no need to update the session            }        }    }

5.3 subject.getSession();

我们这里需要额外提及一下 mergePrincipals(subject);mergeAuthenticationState(subject); 中调用的 subject.getSession(); 方法。

subject.getSession(); 方法创建了一个全新的Session。需要注意的是,在创建Session的同时还向Cookies 中写入了SessionId

subject.getSession();调用 DelegatingSubject#getSession(boolean),如下

public Session getSession(boolean create) {                  if (this.session == null && create) {               //added in 1.2:            if (!isSessionCreationEnabled()) {              		//... 抛出异常            }            log.trace("Starting session for host {}", getHost());            SessionContext sessionContext = createSessionContext();            Session session = this.securityManager.start(sessionContext);            this.session = decorate(session);        }        return this.session;    }

this.securityManager.start(sessionContext); 实现如下,可以看到还是通过SessionManager 来进行创建:

public Session start(SessionContext context) throws AuthorizationException {           return this.sessionManager.start(context);    }

5.3.1 ServletContainerSessionManager#start

ServletContainerSessionManager 是直接依赖于Servlet 来管理Session,所以创建过程全权委托给了Request。

public Session start(SessionContext context) throws AuthorizationException {           return createSession(context);    }    protected Session createSession(SessionContext sessionContext) throws AuthorizationException {           if (!WebUtils.isHttp(sessionContext)) {               String msg = "SessionContext must be an HTTP compatible implementation.";            throw new IllegalArgumentException(msg);        }        HttpServletRequest request = WebUtils.getHttpRequest(sessionContext);		// request在创建session的时候会自动创建一个名字为 JSESSIONID 的 Cookies ,并将SessionId写入        HttpSession httpSession = request.getSession();        //SHIRO-240: DO NOT use the 'globalSessionTimeout' value here on the acquired session.        //see: https://issues.apache.org/jira/browse/SHIRO-240        String host = getHost(sessionContext);        return createSession(httpSession, host);    }

5.3.2 DefaultWebSessionManager#start

相较于 ServletContainerSessionManager#start 直接托管给Servlet 来实现,Cookies 的保存,DefaultWebSessionManager则需要我们自己来实现。DefaultWebSessionManager#start 实现是在AbstractNativeSessionManager#start 中,其作用是创建一个新的会话。

public Session start(SessionContext context) {           Session session = createSession(context);        // 应用 设置的全局Session过期时间        applyGlobalSessionTimeout(session);        // 在这里将SessionId 写入到Cookies 中        onStart(session, context);        // 通知 SessionListener 监听器onStart 事件        notifyStart(session);        //Don't expose the EIS-tier Session object to the client-tier:        // 包装成 DelegatingSession 暴露出去        return createExposedSession(session, context);    }	protected void onStart(Session session, SessionContext context) {           super.onStart(session, context);        if (!WebUtils.isHttp(context)) {               log.debug("SessionContext argument is not HTTP compatible or does not have an HTTP request/response " +                    "pair. No session ID cookie will be set.");            return;        }        HttpServletRequest request = WebUtils.getHttpRequest(context);        HttpServletResponse response = WebUtils.getHttpResponse(context);		// 如果启用cookie存储SessionId        if (isSessionIdCookieEnabled()) {           	// 获取SessionId            Serializable sessionId = session.getId();            // 将SessionId 写入Cookies 中,可以通过自己设置 SimpleCookie 来控制cookie的name,有效期等信息            storeSessionId(sessionId, request, response);        } else {               log.debug("Session ID cookie is disabled.  No cookie has been set for new session with id {}", session.getId());        }        request.removeAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE);        request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_IS_NEW, Boolean.TRUE);    }

我们这里只关注 Session session = createSession(context);

protected Session createSession(SessionContext context) throws AuthorizationException {           enableSessionValidationIfNecessary();        return doCreateSession(context);    }	...	protected Session doCreateSession(SessionContext context) {   		// 直接new一个 SimpleSession,如果有host,就赋值host        Session s = newSessionInstance(context);        if (log.isTraceEnabled()) {               log.trace("Creating session for host {}", s.getHost());        }        create(s);        return s;    }		....    protected void create(Session session) {           if (log.isDebugEnabled()) {               log.debug("Creating new EIS record for new session instance [" + session + "]");        }        // 调用了sessionDao 来对Session进行一个管理        // 这里用的如果是CacheSessionDao,则会将这个Session 缓存起来        sessionDAO.create(session);    }

以上:内容部分参考网络

如有侵扰,联系删除。 内容仅用于自我记录学习使用。如有错误,欢迎指正

上一篇:Spring 源码分析补充篇一 :DeferredImportSelector 的处理
下一篇:Shiro源码分析④ :鉴权流程

发表评论

最新留言

网站不错 人气很旺了 加油
[***.192.178.218]2025年04月18日 06时05分17秒