SpringShiroFilter原理

前言

Spring 或 SpringBoot 集成 Shiro 后,无论在 Shiro 配置文件中配置了多少个 Shiro 过滤器(包括 Shiro 已实现的或自定义实现的),这些过滤器在ApplicationFilterChain(tomcat定义的过滤器链)中只以“一个过滤器”的身份存在。这个过滤器的类型是SpringShiroFilter,过滤器的名字取决于我们注入这个 Bean 时设置的 BeanName。

这篇博客主要介绍 SpringShiroFilter 的创建和工作原理。

以下展示源码的版本是 Shiro 1.3.2。

创建原理

Shiro 过滤器是由ShiroFilterFactoryBean生成并初始化。在 Shiro 的配置文件中,我们一定会配置这个 ShiroFilterFactoryBean 的 Bean 注入。如下配置示例代码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36

// 配置SecurityManager
@Bean
public DefaultSecurityManager securityManager(CustomRealm customRealm) { // CustomRealm是自定义Realm类,securityManager所需
DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(customRealm);
SecurityUtils.setSecurityManager(securityManager);
return securityManager;
}

// 配置ShiroFilterFactoryBean
@Bean
public ShiroFilterFactoryBean shiroFilter(DefaultSecurityManager securityManager) {
ShiroFilterFactoryBean factory = new ShiroFilterFactoryBean();
factory.setSecurityManager(securityManager);

factory.setLoginUrl("/login"); // 登录页URI
factory.setUnauthorizedUrl("/unauth"); // 403页面URI

// 添加自定义Filter
Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
filtersMap.put("permissionCheckFilter", new PermissionCheckFilter());
factory.setFilters(filtersMap);

Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
// 匹配上的URI走对应的Filter,从上到下匹配
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/login/submit", "anon");
filterChainDefinitionMap.put("/getRSAKey", "anon");
filterChainDefinitionMap.put("/unauth", "anon");
filterChainDefinitionMap.put("/*.ico", "anon");
filterChainDefinitionMap.put("/**", "authc");
System.out.println(filterChainDefinitionMap);
factory.setFilterChainDefinitionMap(filterChainDefinitionMap);
return factory;
}

虽然在配置代码中注入的 Bean 类型是”ShiroFilterFactoryBean”,但其实是”SpringShiroFilter”。这是由于 ShiroFilterFactoryBean 实现了FactoryBean接口,FactoryBean的特点是注册这个 Bean 时,注册的不是自己本类的 Bean,而是getObject方法返回的 Bean,且 Bean 的类型在getObjectType指定。

ShiroFilterFactoryBean 还实现了BeanPostProcessor接口,BeanPostProcessor的特点是,在“Bean 的生命周期——Bean 初始化”阶段,在调用某个 Bean 的初始化方法(afterPropertiesSet 和 init-method)前后,遍历调用所有 BeanPostProcessor 的实现类的2个方法。

ShiroFilterFactoryBean 实现这2个接口的部分源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {

public Object getObject() throws Exception {
if (instance == null) {
instance = createInstance(); // 下面分析
}
return instance;
}

public Class getObjectType() {
return SpringShiroFilter.class;
}

public boolean isSingleton() {
return true;
}

public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}

public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
// 只处理过滤器Bean
if (bean instanceof Filter) {
log.debug("Found filter chain candidate filter '{}'", beanName);
Filter filter = (Filter) bean;

// 根据过滤器的类型设置不同的URL
// AccessControlFilter 类型的,设置 loginUrl
// AuthenticationFilter 类型的,设置 successUrl
// AuthorizationFilter 类型的,设置 unauthorizedUrl
applyGlobalPropertiesIfNecessary(filter);

// 添加到成员Map<String, Filter> filters
getFilters().put(beanName, filter);
} else {
log.trace("Ignoring non-Filter bean '{}'", beanName);
}
return bean;
}
// ...
}

从以上源码可以看出,ShiroFilterFactoryBean 调用自己的createInstance方法创建 SpringShiroFilter Bean,最终注入到 IoC 容器。在postProcessBeforeInitialization方法中搜集 IoC 容器中所有 Filter Bean(包括不是 Shiro 实现的 Filter),添加到成员filters中,而成员 filters 中也包含我们基于 Shiro 过滤器基类实现的 filter(如配置示例代码中的”PermissionCheckFilter”)。

根据 postProcessBeforeInitialization 方法可以推测,自定义的 Shiro 过滤器既可以在配置代码中手动添加(配置示例就是采用这个方式),也可以直接注入 Bean,两种方式最终都会添加到 ShiroFilterFactoryBean 中。

createInstance方法就是负责创建 SpringShiroFilter,一共有3个步骤:

  1. 创建FilterChainManager,实现类是DefaultFilterChainManager
  2. 创建PathMatchingFilterChainResolver,并把上一步创建的 FilterChainManager 作为它的成员
  3. 创建 SpringShiroFilter,并把上一步创建的 PathMatchingFilterChainResolver 和 ShiroFilterFactoryBean 的成员 securityManager(等于配置代码中的”DefaultWebSecurityManager”)作为它的成员

需要重点讲解的是第一步:创建FilterChainManager的过程,原因是 FilterChainManager 是 SpringShiroFilter 的间接成员,是它工作过程中的关键类之一,它保存了 url 与过滤器链的映射关系,即配置代码中的filterChainDefinitionMap中的内容。

FilterChainManager

ShiroFilterFactoryBean 中创建 FilterChainManager 的方法源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
protected FilterChainManager createFilterChainManager() {

// 返回对象。刚创建时,这个manager内包含了DefaultFilter类中的所有过滤器
DefaultFilterChainManager manager = new DefaultFilterChainManager();
Map<String, Filter> defaultFilters = manager.getFilters(); // key=filterName

// 根据过滤器的类型设置不同的url,同"postProcessBeforeInitialization"方法
for (Filter filter : defaultFilters.values()) {
applyGlobalPropertiesIfNecessary(filter);
}

// 取ShiroFilterFactoryBean的成员filters(包括自定义的ShiroFilter和容器中其他非Shiro的Filter)
Map<String, Filter> filters = getFilters();
if (!CollectionUtils.isEmpty(filters)) {
for (Map.Entry<String, Filter> entry : filters.entrySet()) {
String name = entry.getKey();
Filter filter = entry.getValue();
applyGlobalPropertiesIfNecessary(filter);
if (filter instanceof Nameable) {
((Nameable) filter).setName(name);
}
// 自定义filter添加到manager中,但没有初始化(即没有调用Filter.init方法)
// 此时manager内包含了defaultFilters和自定义filters
manager.addFilter(name, filter, false);
}
}

// 这个chains是在ShiroFilterFactoryBean配置的filterChainDefinitionMap,如<"/role", "authc,roles[admin]">
Map<String, String> chains = getFilterChainDefinitionMap();
if (!CollectionUtils.isEmpty(chains)) {
for (Map.Entry<String, String> entry : chains.entrySet()) {
String url = entry.getKey();
String chainDefinition = entry.getValue();
manager.createChain(url, chainDefinition); // 下面分析
}
}

return manager;
}

在这个方法的最后一段,遍历我们配置的filterChainDefinitionMap,调用manager.createChain()处理这个 Map 中每个键值对(key=url,value=url对应的过滤器配置如”roles[admin,user]”)。

DefaultFilterChainManager的部分源码(主要关注 createChain 方法):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
public class DefaultFilterChainManager implements FilterChainManager {

private Map<String, Filter> filters;// key=filterName, 其中包含DefaultFilter类中所有过滤器和所有自定义filters

private Map<String, NamedFilterList> filterChains;// key=url

public void createChain(String chainName, String chainDefinition) {
// 2个方法形参即为filterChainDefinitionMap的key、value
// 一些校验代码...

// 对chainDefinition的一些处理,效果如下面的注释所说
// Input: "authc, roles[admin,user], perms[file:edit]"
// Result: [ "authc", "roles[admin,user]", "perms[file:edit]" ]
String[] filterTokens = splitChainDefinition(chainDefinition);

// 遍历filterTokens
for (String token : filterTokens) {
// 对token的一些处理,效果如下面的注释所说
// Input: foo[bar, baz]
// Result: returned[0] == foo returned[1] == bar, baz
String[] nameConfigPair = toNameConfigPair(token);

// 见下面
addToChain(chainName, nameConfigPair[0], nameConfigPair[1]);
}
}

public void addToChain(String chainName, String filterName, String chainSpecificFilterConfig) {
if (!StringUtils.hasText(chainName)) {
throw new IllegalArgumentException("chainName cannot be null or empty.");
}
Filter filter = getFilter(filterName); // 从成员filters中取
if (filter == null) {
throw new IllegalArgumentException("....略");
}
// 见下方说明
applyChainConfig(chainName, filter, chainSpecificFilterConfig);

// 从成员filterChains中取(chainName=url),若没有就初始化一个
// 把当前filter添加到URL对于的chain中
NamedFilterList chain = ensureChain(chainName);
chain.add(filter);
}

// ...
}

applyChainConfig(chainName, filter, chainSpecificFilterConfig);这行代码的内容是:当且仅当 filter 是PathMatchingFilter(Shiro 过滤器基类之一)的子类时,将 chainName、chainSpecificFilterConfig 设置到 PathMatchingFilter 的成员Map<String, Object> appliedPaths中。

整个DefaultFilterChainManager.createChain()方法的作用是,以<”/“, “authc, roles[admin,user], perms[file:edit]”>这个 filterChainDefinitionMap 中的键值对为例,执行完整个 createChain 方法后,manager 的 filterChains 成员中,key=”/“ 对应的 chain 中包含”authc”, “roles”, “perms”这3个过滤器(按定义顺序添加),这3个过滤器的配置,即”[admin,user]”, “[file:edit]”,分别被添加到了”roles”, “perms”过滤器的成员 appliedPaths 中。

DefaultFilterChainManager 的 filterChains 成员中,key 即 url,对应一条过滤器链。因为 filterChainDefinitionMap 类型是 LinkedHashMap,所以过滤器链中的过滤器顺序与配置的顺序一致。

总结

综上所述,ShiroFilterFactoryBean 创建了 SpringShiroFilter Bean,并给它设置了成员 DefaultWebSecurityManager 和 PathMatchingFilterChainResolver,PathMatchingFilterChainResolver 中的成员 DefaultFilterChainManager 中保存了 url 与过滤器链的关系,这部分数据来自于配置的 filterChainDefinitionMap。

工作原理

前面说过,无论在 Shiro 配置文件中配置了多少个 Shiro 过滤器,这些过滤器在ApplicationFilterChain(tomcat定义的过滤器链)中只以SpringShiroFilter存在。原因就是 SpringShiroFilter 的间接成员 DefaultFilterChainManager 保存了 url 与 Shiro 过滤器链的关系。SpringShiroFilter 会根据不同的 url 取对应的过滤器链来处理请求,主要逻辑在它的 doFilterInternal 方法内(被 doFilter 方法调用)。

SpringShiroFilter.doFilterInternal() 源码:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
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);

// 创建Subject对象
final Subject subject = createSubject(request, response);

// 调用subject.call()方法,里面会调用此处声明的call方法
subject.execute(new Callable() {
public Object call() throws Exception {
// 在指定条件下,更新session中用户最近一次请求的时间
updateSessionLastAccessTime(request, response);
// 取url对应的过滤器链并执行
executeChain(request, response, chain);
return null;
}
});
} catch (ExecutionException ex) {
t = ex.getCause();
} catch (Throwable throwable) {
t = throwable;
}

// ...
}

这个方法的步骤如注释所述,下面分别讲解这些步骤内的原理。

创建Subject对象

创建 Subject 对象的主要方法是:securityManager.createSubject(subjectContext)。这里的”securityManager”就是我们配置的DefaultWebSecurityManager,”subjectContext”的类型是DefaultWebSubjectContext。从 SpringShiroFilter 过来时,subjextContext 是新创建的对象,里面只保存了 servletRequest、servletResponse 以及 securityManager。

DefaultWebSubjectContext 介绍

DefaultWebSubjectContext 继承自DefaultSubjectContext,保存数据的方式是把它们保存在成员Map<String, Object> backingMap中,每种数据的 KEY 均有定义。

DefaultSubjectContext 实现SubjectContext接口,继承MapContext类。SubjectContext 接口继承 Map 接口并定义了额外方法,MapContext 类定义了成员Map<String, Object> backingMap并实现了 Map 接口,所以 DefaultSubjectContext 类只需实现 SubjectContext 接口中的方法。


下面来看securityManager.createSubject(subjectContext)方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
public Subject createSubject(SubjectContext subjectContext) {
SubjectContext context = copy(subjectContext); // 1

context = ensureSecurityManager(context); // 2

context = resolveSession(context); // 3

context = resolvePrincipals(context); // 4

Subject subject = doCreateSubject(context); // 5

save(subject); // 6

return subject;
}

上面注释中的序号就是创建 subject 对象的步骤:

  1. 用入参复制出另一个 context 对象,下面都是对这个 context 对象操作
  2. 若 context 中没有设置过 securityManager,就把当前的 securityManager 设置到 context 中
  3. 在 context 中解析 session
  4. 在 context 中解析 principals
  5. 根据 context 创建 subject 对象
  6. 保存新创建的 subject 对象。如果用户已登录,在这步中还会将用户登录后的 sessionId 放入 servletResponse 的请求头中

下面的小节会详细解释第三至第六步骤的实现原理。

解析session

这节讲的是创建 subject 对象的第3步:在 context 中解析 session。

这里解析出的”session”,接口类型就是 Session,但需要注意这个 Session 接口是 Shiro 定义的,不是 Tomcat 定义的。实现类有 SimpleSession、HttpServletSession 等。

这一步的主要逻辑(去掉方法嵌套、异常处理和日志打印)如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
// context 是否已经保存了 session(保存在 backingMap 里),已保存则直接返回
if (context.resolveSession() != null) {
return context;
}

// 从 context 中查询 sessionId(保存在 backingMap 里),构造 sessionKey
SessionKey key = null;
if (WebUtils.isWeb(context)) { // context=DefaultWebSubjectContext返回true
Serializable sessionId = context.getSessionId();
ServletRequest request = WebUtils.getRequest(context);
ServletResponse response = WebUtils.getResponse(context);
key = new WebSessionKey(sessionId, request, response);
} else {
Serializable sessionId = context.getSessionId();
if (sessionId != null) {
key = new DefaultSessionKey(sessionId);
}
}

// 用 sessionKey 从 sessionManager 中查询 session
Session session = null;
if (key != null) {
session = sessionManager.getSession(key);
}

// 把 session 保存到 context
if (session != null) {
context.setSession(session);
}

// 方法最后返回 context
return context;

这一步的工作就是,在设置了启用 session 的情况下(默认启用),它会尝试根据请求从服务器内存中取出已保存的当前用户的 session 对象,设置到 context 中,方便后续步骤使用。

如上,若 context 中还没有保存 session 对象,则先从 context 中取 sessionId 封装成SessionKey,再用 sessionKey 从sessionManager中取出 session,设置到 context 中。当请求从 SpringShiroFilter 过来,context 是新创建的,里面只保存了 servletRequest、servletResponse 以及 securityManager,没有保存 session 和 sessionId。所以获取 session 只能通过 sessionManager

sessionManager是当前 securityManager 的成员,类型就是SessionManager接口,实现类有ServletContainerSessionManager(默认)、DefaultWebSessionManager(这个类可以设置 session 过期时间)等。这2种实现类获取 session 的实现不同:

  1. ServletContainerSessionManager获取 session 的方法是调用HttpServletRequest.getSession(false),将javax.servlet.http.HttpSession封装成HttpServletSession返回。
  2. DefaultWebSessionManager获取 session 的方法是通过它的成员SessionDAO sessionDAO,使用的实现类是MemorySessionDAO。MemorySessionDAO 是把 session 保存在 ConcurrentHashMap<Serializable, Session> 中(key=sessionId)。

补充说明:DefaultWebSessionManager 在通过 sessionDAO 取 session 之前,要先从 sessionKey 中取 sessionId,若取不到,则会调用 HttpServletRequest.getCookies(),取出 name=”JSESSIONID” 的 cookie 值,若还是取不到(如用户禁用了cookie),再从 requestURL 中取。这部分代码见DefaultWebSessionManager.getSessionId()方法。

综上,只要当前用户请求的 Cookie 或 URI 中带了”JSESSIONID=XXX”,这一步就可以解析出 session 对象并保存到 context 中。

用户的 session 是在何时被创建并被保存的?如何设置启用或禁用 session?见“创建subject对象并保存”一节。

解析principals

这节讲的是创建 subject 对象的第4步:在 context 中解析 principals。

解析出的 principals 对象,类型是PrincipalCollection,它是用户身份的象征,这个“身份”可以是一个字符串,也可以是一个对象。又因为是个集合类型,表示可以有多个身份,可用其getPrimaryPrincipal方法取首要身份。

在从 context 中解析这个对象时,首先从 backingMap 中直接取,没取到则依次从AuthenticationInfoSubjectSession取(这些对象也保存在 backingMap 中),只要能从其中一个对象中取到,就不再继续了,也有可能一直都没取到。

首先说明一点,现在介绍的securityManager.createSubject(subjectContext)方法,不只是在用户请求经过 SpringShiroFilter 时被调用,还会在用户的登录请求经过Authenticator.authenticate()方法认证后被调用。也就是说,如果用户请求的接口是登录认证接口,那么请求经过 SpringShiroFilter 时会调用securityManager.createSubject(subjectContext)创建第一个 subject 对象,在用户认证成功后,还会调用这个方法,创建第二个 subject 对象。最后,第二个 subject 对象中的 principals 对象会更新到第一个 subject 对象中。第二个 subject 对象的 principals 的来源就是这个步骤。

AuthenticationInfo就是Authenticator.authenticate()方法的返回对象,之后被保存到了 context 中,它包含了 principals 属性。

此处出现的 Subject,是指上面说的“第一个 subject 对象”。它在这个步骤的作用不大,因为如果是上面所说的“第一次调用”,那么 context 中也没有这个对象;如果是“第二次调用”,那么 context 中既有 AuthenticationInfo,也有它。此时 context 已经可以从 AuthenticationInfo 中获得 principals,不会经过它。所以在这个步骤,无论什么情况 context 都不会用到它。

最后关于 Session,它只能依靠上个步骤得来(来源于用户当前请求)。如果上一步可以解析出 session 对象,那么这一步 context 就有保存 session 对象,可以尝试从 session 中取 principals。

在这一步中,如果从 context 中没有解析出 principals ,那么会尝试使用”RememberMe”功能取 principals 再设置到 context 中。因”RememberMe”功能不在这篇博客讨论范围内,所以忽略。

经过这一步之后,若 context 中直接或间接保存了 principals,这个 principals 对象会保存到下一步创建的 subject 对象中,并且会更新到 session 中。详情见下一节。

创建subject对象并保存

终于来到最后的两个步骤:根据 context 创建 subject 对象,保存新创建的 subject 对象。

根据 context 创建 subject 对象依靠 securityManager 成员SubjectFactory,使用的实现类是DefaultWebSubjectFactory,创建的 subject 对象类型是WebDelegatingSubject。创建出的 subject 对象内容如下,都是从 context 中取的:

1
2
3
4
5
6
7
8
SecurityManager securityManager = wsc.resolveSecurityManager(); // wsc就是context
Session session = wsc.resolveSession();
boolean sessionEnabled = wsc.isSessionCreationEnabled();
PrincipalCollection principals = wsc.resolvePrincipals();
boolean authenticated = wsc.resolveAuthenticated(); // 已认证标志,当context中有AuthenticationInfo为TRUE,或取context中的session中的这个属性值
String host = wsc.resolveHost();
ServletRequest request = wsc.resolveServletRequest();
ServletResponse response = wsc.resolveServletResponse();

保存新创建的 subject 对象依靠 securityManager 成员SubjectDAO,默认使用实现类DefaultSubjectDAO,这也是 Shiro 唯一的实现类。

SubjectDAO接口只定义了2个方法:save 和 delete,方法参数都是 subject 对象。

DefaultSubjectDAO.save() 方法中,先判断是否需要保存 session,这个判断由它的成员SessionStorageEvaluator负责,默认使用DefaultSessionStorageEvaluator实现类。DefaultSessionStorageEvaluator 的判断方法是:要保存的 subject 中有 session 对象,或属性sessionStorageEnabled=true(默认值)。

如果把 DefaultSessionStorageEvaluator 的属性 sessionStorageEnabled 置为 false,那么在用户登录成功后也不会保存 session(用户登录成功前本来也没有 session),则用户请求中一直不会有 session,相当于禁用 session。

判断“是否需要保存 session”为是时,依次执行mergePrincipalsmergeAuthenticationState方法。 mergePrincipals 方法逻辑如下图,mergeAuthenticationState 方法逻辑相同,只是把”principals”换成”authenticated(已认证标志)”。


下面讲解 subject 是如何新建 session 对象的(以 WebDelegatingSubject 为例)。

WebDelegatingSubject 创建 session 是调用securityManager.start(sessionContext)方法,创建出的 session 类型是StoppingAwareProxiedSession。其中”sessionContext”是一个新建的空对象,类型是DefaultWebSessionContext。它和SubjectContext相似,也继承自 MapContext。

securityManager = DefaultWebSecurityManager,在它的 start 方法中,又调用sessionManager.start(context)来创建 session。如在“解析session”一节中所述,这个 sessionManager 是 securityManager 的成员,实现类是ServletContainerSessionManagerDefaultWebSessionManager。这2种实现类创建 session 的实现不同:

  1. ServletContainerSessionManager 创建 session 的方法是调用HttpServletRequest.getSession(),将HttpSession(javax.servlet.http)封装成HttpServletSession返回。
  2. DefaultWebSessionManager 创建 session 的方法是通过它的成员SessionFactory sessionFactory,使用的实现类是SimpleSessionFactory,创建的 session 类型是SimpleSession(内容只有”host”)。创建好 session 后,DefaultWebSessionManager 会把它保存到成员SessionDAO sessionDAO,实现类是MemorySessionDAO。MemorySessionDAO 将创建的 session 保存到 ConcurrentHashMap<Serializable, Session> 中(sessionId自动生成)。保存好 session 后,若属性sessionIdCookieEnabled=true(默认),DefaultWebSessionManager 会将 sessionId 写入 HttpServletResponse 的响应头”Set-Cookie:JSESSIONID=XXX”中。

subject 创建好 session 后,会把自己的 principals 和 authenticated 设置到 session 中。HttpServletSession 保存数据的方式是保存到HttpSession(javax.servlet.http)的attributes,SimpleSession 是保存到自己的Map<Object, Object> attributes


小总结:

  1. 创建 Subject 对象的是SubjectFactory,保存 Subject 的是DefaultSubjectDAO,在保存 Subject 时可能会调用到SessionManager创建 Session。
  2. DefaultSubjectDAO 保存和删除 subject 对象,并不是把它保存到什么位置,或从什么位置删除。保存时,是把 subject 对象中的 principals、authenticated(已认证标志) 更新到此 subject 的 session 对象中。删除时,就是从 subject 中的 session(如果有的话)中删除这两个元素。
  3. 当且仅当 subject 中存在 principals 或 authenticated=true 时,subject 才会在没有 session 的情况下新建一个 session 对象。

总结

上面的三个小节所描述的步骤,代码上看起来完全没有关联,做到了解耦,但在逻辑上是紧密相连的,形成了一个闭环:
第一步“解析session”完全依赖用户请求;第二步“解析principals”既依赖第一步解析的 session,也依赖用户是否认证成功;最后一步“创建subject对象并保存”,创建的 subject 对象中是否包含 principals、是否新建 session 完全依赖第二步,并且这一步新建的 session 又是用户请求中的 session 的来源。

根据以上结论,可以推理出下面的情况。


当未登录用户请求非认证接口:

  1. 解析session:因为请求中没有 sessionId,所以没有解析出 session
  2. 解析principals:因为 context 中没有 AuthenticationInfo、subject、session,所以没有解析出 principals
  3. 创建新的subject对象:创建出的 subject 对象只有 context 中原有的 securityManager、httpServletRequest、httpServletResponse,无 session 也无 principals
  4. 保存新的subject对象:因为 subject 对象中无 session 也无 principals,所以这一步什么也没做

当未登录用户请求认证接口,在用户认证成功后,会再次经过这些步骤创建 subject 对象:

  1. 解析session:因为请求中没有 sessionId,所以没有解析出 session
  2. 解析principals:因为 context 中有 AuthenticationInfo,所以解析出 principals
  3. 创建新的subject对象:创建出的 subject 对象除了 securityManager、httpServletRequest、httpServletResponse 以外,还会有 principals 和 authenticated(已认证标志),但没有 session
  4. 保存新的subject对象:因为 subject 对象中无 session,但有 principals 和 authenticated(已认证标志),所以 subject 创建了 session,并把 principals 和 authenticated 保存在 session 中。如果用的是DefaultWebSessionManager,session 对象保存在MemorySessionDAO中,且 sessionId 写入响应头”Set-Cookie:JSESSIONID=XXX”

当已登录用户请求非认证接口:

  1. 解析session:因为请求头中带有 sessionId,如果用的是DefaultWebSessionManager,会从请求中取 sessionId,从 MemorySessionDAO 中取已保存的 session 对象,这个 session 对象中又包含了 principals 和 authenticated。这个 session 保存在 context 中
  2. 解析principals:因为 context 中有 session,所以从 session 中解析出 principals
  3. 创建新的subject对象:创建出的 subject 对象除了 securityManager、httpServletRequest、httpServletResponse 以外,还会有 principals、authenticated(从 session 中取出的)、session
  4. 保存新的subject对象:因为 subject 对象中既有 session 也有 principals,所以 session 中的 principals 更新为 subject 中的 principals(因为 subject 中的其实也来自于 session,所以这两个 principals 内容一致)。authenticated 也是如此更新

当已登录用户请求认证接口,在第一次经过这些步骤时,情况如上一个所述。在用户认证成功后,第二次经过这些步骤时:

  1. 解析session:因为 context 中保存了用户“认证成功前”的 subject 对象,而这个(旧)subject 对象有 session,所以相当于 context 中也保存了 session
  2. 解析principals:因为 context 中有 AuthenticationInfo,所以解析出 principals
  3. 创建新的subject对象:创建出的 subject 对象除了 securityManager、httpServletRequest、httpServletResponse 以外,还会有 principals 和 authenticated,还有 session【是从 context 中的(旧)subject 对象取的
  4. 保存新的subject对象:因为 subject 对象中既有 session 也有 principals,所以 session 中的 (旧)principals 更新为 subject 中的 (新)principals。authenticated 也是如此更新【注意:如果用的是DefaultWebSessionManager,被更新的 session 对象依然是在MemorySessionDAO中保存的那个对象。sessionId 会再次写入响应头”Set-Cookie:JSESSIONID=XXX”

取url对应的过滤器链

准备

经过上面的步骤,若是已登录用户的请求,创建的 subject 对象会包含 principals、session 等信息。创建后,调用subject.execute()方法。

subject.execute()的方法参数是Callable,在这里可以理解为一个回调方法,这个回调方法的内容就是这两行:

1
2
3
4
// 在指定条件下,更新session中用户最近一次请求的时间
updateSessionLastAccessTime(request, response);
// 取url对应的过滤器链并执行
executeChain(request, response, chain);

实际上,Callable 一般和 FutureTask 配合,用于多线程编程。它和 Runnable 的区别在于,它的 call 方法可以获取线程执行后的返回值,而 Runnable 的 run 方法无返回值。在当前场景下,Callable 作为回调方法使用,是同步执行的。

subject.execute()方法中,先把Callable封装成SubjectCallable,再调用 SubjectCallable.call() 方法。SubjectCallable 源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
public class SubjectCallable<V> implements Callable<V> {

protected final ThreadState threadState;
private final Callable<V> callable;

public SubjectCallable(Subject subject, Callable<V> delegate) {
this(new SubjectThreadState(subject), delegate);
}

protected SubjectCallable(ThreadState threadState, Callable<V> delegate) {
if (threadState == null) {
throw new IllegalArgumentException("ThreadState argument cannot be null.");
}
this.threadState = threadState;
if (delegate == null) {
throw new IllegalArgumentException("Callable delegate instance cannot be null.");
}
this.callable = delegate;
}

public V call() throws Exception {
try {
threadState.bind();
return doCall(this.callable);
} finally {
threadState.restore();
}
}

protected V doCall(Callable<V> target) throws Exception {
return target.call();
}
}

SubjectCallable 的成员”Callable callable”就是上面声明的回调方法,”ThreadState threadState”在构造方法中被赋值SubjectThreadState。在 call 方法中,先后调用了这两个成员的方法。

第一个方法threadState.bind()内部调用了ThreadContext.bind(subject),subject 即为上个步骤创建的 subject 对象,ThreadContext 将这个对象保存在ThreadLocal<Map<Object, Object>>中。在这之后用SecurityUtils.getSubject()来获取 subject 时,就可以从 ThreadContext 中取到 subject。

第二个方法target.call(),就是执行了上面给出的回调方法的两行内容,一是更新 session 时间(当使用的 sessionManager=ServletContainerSessionManager 时才会更新),二是取 url 对应的过滤器链并执行。

在这两个方法执行完后,即过滤器链也执行完了,进入 finally 块,清空 ThreadContext 中的 ThreadLocal 内容。

取过滤器链并执行

这一节讲 SpringShiroFilter 中的 executeChain 方法,也是上面回调方法中的第二个方法。在这个方法中,首先要取 url 对应的过滤器链,就是下面源码所展示的:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
FilterChain chain = origChain;

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;
}

从以上源码可以看出,负责“取 url 对应的过滤器链”这项工作的是 SpringShiroFilter 的成员PathMatchingFilterChainResolver。如果这个成员为 Null 或没有与 url 匹配的过滤器链,那么这个方法会返回原过滤器链,即ApplicationFilterChain

PathMatchingFilterChainResolver.getChain()方法中(源码如下),遍历其成员DefaultFilterChainManager中的Map<String, NamedFilterList> filterChains的 key,与当前请求 url 进行匹配,若匹配上,则调用DefaultFilterChainManager.proxy()方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
FilterChainManager filterChainManager = getFilterChainManager();
if (!filterChainManager.hasChains()) {
return null;
}

String requestURI = getPathWithinApplication(request);

for (String pathPattern : filterChainManager.getChainNames()) {

if (pathMatches(pathPattern, requestURI)) {
if (log.isTraceEnabled()) {
log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "]. " +
"Utilizing corresponding filter chain...");
}
return filterChainManager.proxy(originalChain, pathPattern);
}
}

return null;
}

DefaultFilterChainManager.proxy()方法的作用是,从Map<String, NamedFilterList> filterChains中取出的过滤器链类型是SimpleNamedFilterList(见 ensureChain 方法),将这个过滤器链再度封装成ProxiedFilterChain类型。

所以,SpringShiroFilter 通过 PathMatchingFilterChainResolver 取出的过滤器链是ProxiedFilterChain类型,执行过滤器链时,就是调用ProxiedFilterChain.doFilter()方法,源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {
if (this.filters == null || this.filters.size() == this.index) {
//we've reached the end of the wrapped chain, so invoke the original one:
if (log.isTraceEnabled()) {
log.trace("Invoking original filter chain.");
}
this.orig.doFilter(request, response);
} else {
if (log.isTraceEnabled()) {
log.trace("Invoking wrapped filter at index [" + this.index + "]");
}
this.filters.get(this.index++).doFilter(request, response, this);
}
}

这个方法就是典型的过滤器链的实现,通过下标 index 自增来遍历 List filters 中每个元素,遍历完了就执行原过滤器链,即ApplicationFilterChain

至此,SpringShiroFilter的工作原理介绍完了。通过过滤器链,就可以进入 Shiro 的认证与授权流程了。