鉴权模块-shiro过滤器与案例分析

ShiroFilter的实现原理:

Shiro对servlet容器的FilterChain进行了代理,在执行Servlet容器的Filter链之前,通过ProxiedFilterChain对Servlet容器的Filter链进行了代理,先执行Shiro自己的Filter体系,然后才会委托给Servlet容器的Filter链进行Servlet容器级别的过滤。

在应用启动阶段,shiro会初始化一些对象:

创建一个PathMatchingFilterChainResolver对象用来解析请求的url匹配到哪个Filter,方式是将url逐一的与Filter的url模式进行匹配,直到匹配到正确的filter,就生成ProxiedFilterChain

1
2
3
4
public PathMatchingFilterChainResolver() {
this.pathMatcher = new AntPathMatcher();
this.filterChainManager = new DefaultFilterChainManager();
}

创建PathMatchingFilterChainResolver时会创建一个DefaultFilterChainManager对象,这个对象将管理所有的shiro默认Filter以及用户自定义的Filter。

1
2
3
4
5
public DefaultFilterChainManager() {
this.filters = new LinkedHashMap<String, Filter>();
this.filterChains = new LinkedHashMap<String, NamedFilterList>();
addDefaultFilters(false); // 添加defaultFilter,
}
1
2
3
4
5
6
7
8
9
10
11
12
13
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);
}

那么用户自定义的Filter在哪里添加进去。

接下来我们看Shiro与Spring的集成,首先看一下这个类:

1
2
3
4
5
6
public class ShiroFilterFactoryBean implements FactoryBean, BeanPostProcessor {
private Map<String, Filter> filters;
private Map<String, String> filterChainDefinitionMap;
private AbstractShiroFilter instance;
...
}

ShiroFilterFactoryBean实现了FactoryBean接口,表明这是一个工厂bean,在spring中通过getBean()方法获取该bean时将调用它的getObject()方法返回实例对象:

1
2
3
4
5
6
public Object getObject() throws Exception {
if (instance == null) {
instance = createInstance();
}
return instance;
}

instance就是这个FactoryBean要返回的具体的bean对象。

继续跟:

1
2
3
4
5
6
7
8
9
10
protected AbstractShiroFilter createInstance() throws Exception {
...
SecurityManager securityManager = getSecurityManager();
...
// 主要看这边 >>
FilterChainManager manager = createFilterChainManager();
PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
chainResolver.setFilterChainManager(manager);
return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);
}

可以看到,这里实例化了上文提到的两个关键类,在创建DefaultFilterChainManager时,将用户自定义的filter也放了进去:

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
protected FilterChainManager createFilterChainManager() {
// 实例化 ,默认的filter已经放进去了
DefaultFilterChainManager manager = new DefaultFilterChainManager();
Map<String, Filter> defaultFilters = manager.getFilters();
//apply global settings if necessary:
for (Filter filter : defaultFilters.values()) {
applyGlobalPropertiesIfNecessary(filter);
}
//getFilters获取的是用户自定义的filter,在配置中设置的
//在shiro配置类中我们会这样配置 filterMap.put("tokenLogin", new AccessTokenLoginFilter());
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逐个放入`DefaultFilterChainManager`中
manager.addFilter(name, filter, false);
}
}
// getFilterChainDefinitionMap 获取的是用户配置的所以url模式
// 在配置类中我么会这样配置 filterRuleMap.put("/**","tokenLogin");
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();
// 根据每条配置,创建Chain >>
manager.createChain(url, chainDefinition);
}
}
return manager;
}

进入createChain方法:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
// chainName 就是url模式
public void createChain(String chainName, String chainDefinition) {
...//一些检查和日志操作

// 根据配置解析chainDefinition
// 比如一条过滤定义是:"authc, roles[admin,user], perms[file:edit]"
// 解析出来就是这样{ "authc", "roles[admin,user]", "perms[file:edit]" }
String[] filterTokens = splitChainDefinition(chainDefinition);

for (String token : filterTokens) {
String[] nameConfigPair = toNameConfigPair(token);
// 按照解析出来的规则,逐个添加过滤器
addToChain(chainName, nameConfigPair[0], nameConfigPair[1]);
}
}

读到这里我们可以知道,对于一条请求我们是可以添加多个过滤器的,之前开发中遇到的及安全问题也就迎刃而解了。

注意:一个请求只可以匹配到一个url模式,但是url模式对应的过滤规则可以添加多个过滤器,所以一个请求是可以执行多个shiroFilter的

每个url模式执行createChain方法都会生成一个SimpleNamedFilterList,里面存放了该模式对应的filter。

应用启动阶段的处理到此结束,以下是请求到达后才会执行的部分

请求处理阶段

请求到达之后,会通过PathMatchingFilterChainResolver匹配到请求uri对应到的url模式,那么就能获取到对应模式的SimpleNamedFilterList,然后调用ProxiedFilterChain方法生成ProxiedFilterChain

PathMatchingFilterChainResolver中的方法,获取ProxiedFilterChain

1
2
3
4
5
6
7
8
9
10
11
12
13
14
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)) {
...//日志操作
return filterChainManager.proxy(originalChain, pathPattern);
}
}
return null;
}

代理chain执行时,将先执行Shiro的Filter,再执行Servlet的Filter,贴代码:

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
public class ProxiedFilterChain implements FilterChain {
private static final Logger log = LoggerFactory.getLogger(ProxiedFilterChain.class);

private FilterChain orig; // Servlet的filter
private List<Filter> filters; // shiro的filter
private int index = 0;

public ProxiedFilterChain(FilterChain orig, List<Filter> filters) {
if (orig == null) {
throw new NullPointerException("original FilterChain cannot be null.");
}
this.orig = orig;
this.filters = filters;
this.index = 0;
}

public void doFilter(ServletRequest request, ServletResponse response)
throws IOException, ServletException {
if (this.filters == null || this.filters.size() == this.index) {
...
this.orig.doFilter(request, response);
} else {
...
this.filters.get(this.index++).doFilter(request, response, this);
}
}
}

执行逻辑清晰,不多说。

shiro过滤器

默认过滤器分类

(1)认证过滤器:anon、authcBasic、auchc、user、logout

(2)授权过滤器:perms、roles、ssl、rest、port

过滤器名称 对应类型
anom org.apache.shior.web.filter.authc.AnonymousFilter
authc org.apache.shior.web.filter.authc.FormAuthenticationFilter
authcBasic org.apache.shior.web.filter.authc.BasicHttpAuthenticationFilter
user org.apache.shior.web.filter.authc.UserFilter
logout org.apache.shior.web.filter.authc.LogoutFilter
perms org.apache.shior.web.filter.authz.PermissionsAuthorizationFilter
roles org.apache.shior.web.filter.authz.RolesAuthorizationFilter
ssl org.apache.shior.web.filter.authz.SslFilter
rest org.apache.shior.web.filter.authz.HttpMethodPermissionFilter
port org.apache.shior.web.filter.authz.PortFilter
  • anon:匿名过滤器,表示通过了url配置的资源都可以访问,例:“/statics/**=anon”表示statics目录下所有资源都能访问

  • authc:基于表单的过滤器,表示通过了url配置的资源需要登录验证,否则跳转到登录,例:“/unauthor.jsp=authc”如果用户没有登录访问unauthor.jsp则直接跳转到登录

  • authcBasic:Basic的身份验证过滤器,表示通过了url配置的资源会提示身份验证,例:“/welcom.jsp=authcBasic”访问welcom.jsp时会弹出身份验证框

  • user:用户过滤器,表示可以使用登录验证/记住我的方式访问通过了url配置的资源,例:“/welcom.jsp=user”表示访问welcom.jsp页面可以通过登录验证或使用记住我后访问,否则直接跳转到登录

  • logout:退出拦截器,表示执行logout方法后,跳转到通过了url配置的资源,例:“/logout.jsp=logout”表示执行了logout方法后直接跳转到logout.jsp页面

  • perms:权限过滤器,表示访问通过了url配置的资源会检查相应权限,例:“/statics/*=perms[“user:add:,user:modify:*”]“表示访问statics目录下的资源时只有新增和修改的权限

  • port:端口过滤器,表示会验证通过了url配置的资源的请求的端口号,例:“/port.jsp=port[8088]”访问port.jsp时端口号不是8088会提示错误

  • rest:restful类型过滤器,表示会对通过了url配置的资源进行restful风格检查,例:“/welcom=rest[user:create]”表示通过restful访问welcom资源时只有新增权限

  • roles:角色过滤器,表示访问通过了url配置的资源会检查是否拥有该角色,例:“/welcom.jsp=roles[admin]”表示访问welcom.jsp页面时会检查是否拥有admin角色

  • ssl:ssl过滤器,表示通过了url配置的资源只能通过https协议访问,例:“/welcom.jsp=ssl”表示访问welcom.jsp页面如果请求协议不是https会提示错误

自定义Filter

通过自定义自己的拦截器可以扩展一些功能,比如

动态 url -角色/权限访问控制的实现、

根据 Subject 身份信息获取用户信息绑定到 Request(即设置通用数据)、

验证码验证、

在线用户信息的保存等等