Shiro过滤器基类介绍

前言

Shiro 已经实现的过滤器有以下几种:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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);
// ...
}

这里的枚举名如”anon”是这个过滤器的名字,配置过滤器链时会用到它。上面标注了“认证”的过滤器继承自AuthenticationFilter,“授权”过滤器继承自AuthorizationFilter。这两个过滤器都继承AccessControlFilter,AccessControlFilter 之上又有一系列继承(从父类到子类):
Filter(javax.servlet接口) -> AbstractFilter -> NameableFilter -> OncePerRequestFilter -> AdviceFilter -> PathMatchingFilter -> AccessControlFilter。

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

AbstractFilter

抽象类,实现了 Filter 接口的 init 方法。扩展点包括添加FilterConfig为成员(这个类里包含 filterName 等属性)。部分源码如下:

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
public abstract class AbstractFilter extends ServletContextSupport implements Filter {

protected FilterConfig filterConfig;

public FilterConfig getFilterConfig() {
return filterConfig;
}

public void setFilterConfig(FilterConfig filterConfig) {
this.filterConfig = filterConfig;
setServletContext(filterConfig.getServletContext());
}

// Filter接口的init方法
public final void init(FilterConfig filterConfig) throws ServletException {
setFilterConfig(filterConfig); // 设置成员
try {
onFilterConfigSet(); // 后续设置方法,钩子,可由子类扩展
} catch (Exception e) {
if (e instanceof ServletException) {
throw (ServletException) e;
} else {
if (log.isErrorEnabled()) {
log.error("Unable to start Filter: [" + e.getMessage() + "].", e);
}
throw new ServletException(e);
}
}
}

protected void onFilterConfigSet() throws Exception {
}

// Filter接口的destroy方法
public void destroy() {
}

// ...
}

NameableFilter

抽象类,未实现 Filter 接口的任何方法,扩展了一个属性name,用于表示过滤器的名字,这个名字可以外部调用 set 方法设置,如果没有设置,则来源是FilterConfig。部分源码如下:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
public abstract class NameableFilter extends AbstractFilter implements Nameable {

private String name;

protected String getName() {
if (this.name == null) {
FilterConfig config = getFilterConfig();
if (config != null) {
this.name = config.getFilterName();
}
}
return this.name;
}

public void setName(String name) {
this.name = name;
}

// ...
}

OncePerRequestFilter

抽象类,实现了 Filter 接口的 doFilter 方法,方法内有控制同一个过滤器只执行一次,且提供了过滤器的开关(默认开启)。在这个方法内调用doFilterInternal方法(抽象方法,由子类实现)真正执行过滤器的逻辑。

AdviceFilter

抽象类,实现了doFilterInternal方法。实现方式是,在调用filterChain.doFilter()之前调用preHandle方法,之后调用postHandler,执行完整条过滤器后调用afterComplete方法。preHandle、postHandler、afterComplete 是它提供给子类的钩子,可由子类实现。部分源码如下:

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
public abstract class AdviceFilter extends OncePerRequestFilter {

protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
return true;
}

@SuppressWarnings({"UnusedDeclaration"})
protected void postHandle(ServletRequest request, ServletResponse response) throws Exception {
}

@SuppressWarnings({"UnusedDeclaration"})
public void afterCompletion(ServletRequest request, ServletResponse response, Exception exception) throws Exception {
}

public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)
throws ServletException, IOException {
Exception exception = null;
try {
boolean continueChain = preHandle(request, response);
if (log.isTraceEnabled()) {
log.trace("Invoked preHandle method. Continuing chain?: [" + continueChain + "]");
}

if (continueChain) { // preHandle返回true才执行后面过滤器, false直接中断
executeChain(request, response, chain); // 调用chain.doFilter()
}

postHandle(request, response);
if (log.isTraceEnabled()) {
log.trace("Successfully invoked postHandle method");
}
} catch (Exception e) {
exception = e;
} finally {
// 执行完所有过滤器(或中断)后调用, 里面有afterComplete方法
cleanup(request, response, exception);
}
}

// ...
}

PathMatchingFilter

抽象类,实现了直接父类 AdviceFilter 定义的三个钩子方法中的preHandle方法。在这个方法里先匹配 requestURI,匹配方法是把 requestURI 和成员Map<String, Object> appliedPaths中保存的URI进行 match。如果 match 失败,preHandle 就直接返回 true,表示去执行下一个过滤器,当前这个过滤器除了匹配URI之外什么都没做。如果 match 成功,再调用onPreHandle方法,此时 onPreHandle 的返回值就是 preHandle 的返回值,可以决定是继续执行后续的过滤器还是中断。onPreHandle 也是一个钩子方法,默认实现直接返回 true,可由子类重写。

很容易想到,在 onPreHandle 内定义的就是当前过滤器对自己匹配的URI的处理工作,如判断用户是否有权访问匹配的URI,有权访问则返回 true,可继续执行后面的过滤器,无权访问则中断。

下面介绍成员Map<String, Object> appliedPaths的含义。map key 为当前过滤器适用的URI,value 为URI对应的“配置”。比如按下面过滤器链的配置:

1
2
3
4
5
6
7
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/logout", "logout");
filterChainDefinitionMap.put("/login", "anon");
filterChainDefinitionMap.put("/login/submit", "anon");
filterChainDefinitionMap.put("/*.ico", "anon");
filterChainDefinitionMap.put("/**", "authc");
factory.setFilterChainDefinitionMap(filterChainDefinitionMap);

这将会创建3个 Filter 对象,分别叫 logout、anon、authc(具体的实现类见前言中的DefaultFilter)。每个过滤器的成员 appliedPaths 中的 map key 就是和它们匹配的URI,value 为 null。如”anon”过滤器的 appliedPaths 含3个元素:<”/login”, null>, <”/login/submit”, null>, <”/*.ico”, null>

如果在过滤器名字之后还加了“配置”,像这样:

1
filterChainDefinitionMap.put("/**", "roles[admin, user]");

那么”roles”过滤器的 appliedPaths 包含:<”/**”, [“admin”,”user”]>

appliedPaths 是如何添加的?见博客《SpringShiroFilter原理》中的“创建原理”一节。

AccessControlFilter

抽象类,实现了直接父类 PathMatchingFilter 的onPreHandle方法,实现代码是:

1
2
3
public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {
return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);
}

AccessControlFilter 新定义了两个方法:isAccessAllowedonAccessDenied,但未实现。

onPreHandle 方法的含义是,先执行isAccessAllowed方法判断是否可继续(可访问),若可继续(isAccessAllowed=true),则 onPreHandle=true。若不可继续,则调用onAccessDenied处理不可访问的情况,处理完可返回 true or false。onAccessDenied=true 说明可执行后面的过滤器,onAccessDenied=false 则中断过滤器的执行直接返回响应(如 AdviceFilter 中所述)。

除此以外,AccessControlFilter 还增加了对 loginUrl 的识别和处理,以及获取Subject对象的方法,定义它的子类时可使用这些方法,例如:

1
2
3
4
5
6
7
8
9
// 获取当前请求线程中的 subject
protected Subject getSubject(ServletRequest request, ServletResponse response) {
return SecurityUtils.getSubject();
}

// 判断是否是登录请求
protected boolean isLoginRequest(ServletRequest request, ServletResponse response) {
return pathsMatch(getLoginUrl(), request);
}

AuthenticationFilter 和 AuthenticatingFilter

2个都属于认证过滤器,都是抽象类。继承链(从子类到父类)是:AuthenticatingFilter -> AuthenticationFilter -> AccessControlFilter。这两个类都只实现了 isAccessAllowed 方法(如下源码),未实现 onAccessDenied 方法。

1
2
3
4
5
6
7
8
9
10
11
// AuthenticationFilter
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
Subject subject = getSubject(request, response);
return subject.isAuthenticated();
}

// AuthenticatingFilter
protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
return super.isAccessAllowed(request, response, mappedValue) ||
(!isLoginRequest(request, response) && isPermissive(mappedValue));
}

考虑到这是一个用于认证的过滤器,就很好理解上面 AuthenticatingFilter.isAccessAllowed() 方法的含义:若用户已登录,或 requestURI 不是 loginUrl 且 requestURI 带有”permissive”配置,则当前过滤器结束,继续执行后续过滤器。否则进入 onAccessDenied 方法内执行认证。

AuthenticatingFilter 与父类相比,扩展了更多和登录有关的方法。

自定义的认证过滤器一般继承于它们,需要实现 onAccessDenied 方法,方法内实现用户登录校验等逻辑。

AuthorizationFilter

授权过滤器,抽象类,继承自 AccessControlFilter,实现了 onAccessDenied 方法(如下源码),代表不可访问后的处理,未实现 isAccessAllowed 方法。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
 protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {

Subject subject = getSubject(request, response);
// 若用户未登录,重定向到loginUrl
if (subject.getPrincipal() == null) {
saveRequestAndRedirectToLogin(request, response);
} else {
// 401页面URL
String unauthorizedUrl = getUnauthorizedUrl();
// 若有401页面就重定向到此页,若无则只返回HTTP状态码401
if (StringUtils.hasText(unauthorizedUrl)) {
WebUtils.issueRedirect(request, response, unauthorizedUrl);
} else {
WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);
}
}
return false;
}

自定义的授权过滤器一般继承于它,需要实现 isAccessAllowed 方法,方法内判断 requestURI 是否可访问。

LogoutFilter

继承自 AdviceFilter,只实现了 preHandle 方法。如下:

1
2
3
4
5
6
7
8
9
10
11
12
@Override
protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {
Subject subject = getSubject(request, response);
String redirectUrl = getRedirectUrl(request, response, subject); // 退出登录后跳转的页面
try {
subject.logout(); // 执行用户退出登录的操作
} catch (SessionException ise) {
log.debug("Encountered session exception during logout. This can generally safely be ignored.", ise);
}
issueRedirect(request, response, redirectUrl); // 跳转页面
return false; // 返回false说明中断Shiro过滤器链
}

给 URL=”/logout” 配置这个 Filter,就能在用户点击“退出登录”时进入这个 Filter,完成退出以及退出后跳转页面的操作,不需要自己写用户退出时的代码。

另外,退出后跳转页面默认URL=”/“,如果要自定义,则需要这样配置:

1
2
3
4
5
6
7
8
9
10
11
LogoutFilter logoutFilter = new LogoutFilter();
logoutFilter.setRedirectUrl("/login"); // 自定义跳转页面

Map<String, Filter> filtersMap = new LinkedHashMap<String, Filter>();
filtersMap.put("logout", logoutFilter); // 用自己的实例替换默认的LogoutFilter实例
factory.setFilters(filtersMap); // 设置到 ShiroFilterFactoryBean

// 配置过滤器链
Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
filterChainDefinitionMap.put("/logout", "logout");
// 以下略...