本篇是 Shiro 系列第三篇, Shiro 中的过滤器初始化流程和实现原理. Shiro 基于 URL 的权限控制是通过 Filter 实现的, 本篇从我们注入的 ShiroFilterFactoryBean 开始入手, 翻看源码追寻 Shiro 中的过滤器的实现原理.
初始化流程
ShiroFilterFactoryBean 实现了 FactoryBean 接口, 那么 Spring 在初始化的时候必然会调用 ShiroFilterFactoryBean 的 getObject()获取实例, 而 ShiroFilterFactoryBean 也在此时做了一系列初始化操作.
关于 FactoryBean 的介绍和实现方式另外也记了一篇: https://www.guitu18.com/post/2019/04/28/33.html
在 getObject()中会调用 createInstance(), 初始化相关的东西都在这里了, 代码贴过来去掉了注释和校验相关的代码.
- protected AbstractShiroFilter createInstance() throws Exception {
- SecurityManager securityManager = getSecurityManager();
- FilterChainManager manager = createFilterChainManager();
- PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();
- chainResolver.setFilterChainManager(manager);
- return new SpringShiroFilter((webSecurityManager) securityManager, chainResolver);
- }
这里面首先获取了我们在 ShiroConfig 中注入好参数的 SecurityManager, 再次强调, 这位是 Shiro 中的核心组件. 然后创建了一个 FilterChainManager, 这个类看名字就知道是用来管理和操作过滤器执行链的, 我们来看它的创建方法 createFilterChainManager().
- protected FilterChainManager createFilterChainManager() {
- DefaultFilterChainManager manager = new DefaultFilterChainManager();
- Map<String, Filter> defaultFilters = manager.getFilters();
- for (Filter filter : defaultFilters.values()) {
- applyGlobalPropertiesIfNecessary(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);
- }
- manager.addFilter(name, filter, false);
- }
- }
- 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;
- }
第一步 new 了一个 DefaultFilterChainManager, 在它的构造方法中将 filters 和 filterChains 两个成员变量都初始化为一个能保持插入顺序的 LinkedHashMap 了, 之后再调用 addDefaultFilters()添加 Shiro 内置的一些过滤器.
- public DefaultFilterChainManager() {
- this.filters = new LinkedHashMap<String, Filter>();
- this.filterChains = new LinkedHashMap<String, NamedFilterList>();
- addDefaultFilters(false);
- }
- protected void addDefaultFilters(boolean init) {
- for (DefaultFilter defaultFilter : DefaultFilter.values()) {
- addFilter(defaultFilter.name(), defaultFilter.newInstance(), init, false);
- }
- }
这里用枚举列出了所有 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);
- }
以上代码有省略, 上面列出的枚举类型正好对应 Shiro 第一篇提到的 Shiro 内置的一些过滤器, 这些过滤器正好是在这里初始化并添加到过滤器执行链中的, 每个过滤器都有不同的功能, 我们常用的其实只有前面两个.
回到上上一步中, DefaultFilterChainManager 初始化完成后, 遍历了每一个默认的过滤器并调用了 applyGlobalPropertiesIfNecessary()设置一些必要的全局属性.
- private void applyGlobalPropertiesIfNecessary(Filter filter) {
- applyLoginUrlIfNecessary(filter);
- applySuccessUrlIfNecessary(filter);
- applyUnauthorizedUrlIfNecessary(filter);
- }
在这个方法中调用了三个方法, 三个方法逻辑是一样的, 分别是设置 loginUrl,successUrl 和 unauthorizedUrl, 我们就看第一个 applyLoginUrlIfNecessary().
- private void applyLoginUrlIfNecessary(Filter filter) {
- String loginUrl = getLoginUrl();
- if (StringUtils.hasText(loginUrl) && (filter instanceof AccessControlFilter)) {
- AccessControlFilter acFilter = (AccessControlFilter) filter;
- String existingLoginUrl = acFilter.getLoginUrl();
- if (AccessControlFilter.DEFAULT_LOGIN_URL.equals(existingLoginUrl)) {
- acFilter.setLoginUrl(loginUrl);
- }
- }
- }
看方法名就知道是要设置 loginUrl, 如果我们配置了 loginUrl, 那么会将 AccessControlFilter 中默认的 loginUrl 替换为我们设置的值, 默认的 loginUrl 为 / login.jsp. 后面两个方法道理一样, 都是将我们设置的参数替换进去, 只不过第三个认证失败跳转 URL 的默认值为 null.
继续回到上一步, Map<String, Filter> filters = getFilters(); 这里是获取我们自定义的过滤器, 默认是为空的, 如果我们配置了自定义的过滤器, 那么会将其添加到 filters 中. 至此 filters 中包含着 Shiro 内置的过滤器和我们配置的所有过滤器.
下一步, 遍历 filterChainDefinitionMap, 这个 filterChainDefinitionMap 就是我们在 ShiroConfig 中注入进去的拦截规则配置. 这里是根据我们配置的过滤器规则创建创建过滤器执行链.
- public void createChain(String chainName, String chainDefinition) {
- String[] filterTokens = splitChainDefinition(chainDefinition);
- for (String token : filterTokens) {
- String[] nameConfigPair = toNameConfigPair(token);
- addToChain(chainName, nameConfigPair[0], nameConfigPair[1]);
- }
- }
chainName 是我们配置的过滤路径, chainDefinition 是该路径对应的过滤器, 通常我们都是一对一的配置, 比如: filterMap.put("/login", "anon");, 但看到这个方法我们知道了一个过滤路径其实是可以通过传入 ["filter1","filter2"...] 配置多个过滤器的. 在这里会根据我们配置的过滤路径和过滤器映射关系一步步配置过滤器执行链.
- public void addToChain(String chainName, String filterName, String chainSpecificFilterConfig) {
- Filter filter = getFilter(filterName);
- applyChainConfig(chainName, filter, chainSpecificFilterConfig);
- NamedFilterList chain = ensureChain(chainName);
- chain.add(filter);
- }
先从 filters 中根据 filterName 获取对应过滤器, 然后 ensureChain()会先从 filterChains 根据 chainName 获取 NamedFilterList, 获取不到就创建一个并添加到 filterChains 然后返回.
- protected NamedFilterList ensureChain(String chainName) {
- NamedFilterList chain = getChain(chainName);
- if (chain == null) {
- chain = new SimpleNamedFilterList(chainName);
- this.filterChains.put(chainName, chain);
- }
- return chain;
- }
因为过滤路径和过滤器是一对多的关系, 所以 ensureChain()返回的 NamedFilterList 其实就是一个有着 name 称属性的 List<Filter>, 这个 name 保存的就是过滤路径, List 保存着我们配置的过滤器. 获取到 NamedFilterList 后在将过滤器加入其中, 这样过滤路径和过滤器映射关系就初始化好了.
至此, createInstance()中的 createFilterChainManager()才算执行完成, 它返回了一个 FilterChainManager 实例. 之后再将这个 FilterChainManager 注入 PathMatchingFilterChainResolver 中, 它是一个过滤器执行链解析器.
PathMatchingFilterChainResolver 中的方法不多, 最为重要的是这个 getChain()方法.
- 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;
- }
看到形参中 ServletRequest 和 ServletResponse 这两个参数是不是感觉特别亲切, 终于看到了点熟悉的东西了, 一看就知道肯定跟请求有关. 是的, 我们每次请求服务器都会调用这个方法, 根据请求的 URL 去匹配过滤器执行链中的过滤路径, 匹配上了就返回其对应的过滤器进行过滤.
这个方法中的 filterChainManager.getChainNames()返回的是根据我们的配置配置生成的执行链的过滤路径集合, 执行链生成的顺序跟我们的配置的顺序相同. 从前文中我们也可以看到, 在 DefaultFilterChainManager 的构造方法中将 filterChains 初始化为一个 LinkedHashMap. 所以在我的 Shiro 笔记第一篇中提到要将范围大的过滤器放在后面就是这个道理, 如果第一个匹配的过滤路径就是 /** 那后面的过滤器永远也匹配不上.
过滤实现原理
那么这个 getChain()是如何被调用的呢? 既然是 HTTP 请求那肯定是从 Tomcat 过来的, 当一个请求到达 Tomcat 时, Tomcat 以责任链的形式调用了一系列 Filter,OncePerRequestFilter 就是众多 Filter 中的一个. 它所实现的 doFilter()方法调用了自身的抽象方法 doFilterInternal(), 这个方法在它的子类 AbstractShiroFilter 中被实现了.
PathMatchingFilterChainResolver.getChain()就是被在 doFilterInternal()中被一步步调用的调用的.
- protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse,
- final FilterChain chain) throws ServletException, IOException {
- final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);
- final ServletResponse response = prepareServletResponse(request, servletResponse, chain);
- final Subject subject = createSubject(request, response);
- subject.execute(new Callable() {
- public Object call() throws Exception {
- updateSessionLastAccessTime(request, response);
- executeChain(request, response, chain);
- return null;
- }
- });
- }
这里先获获取滤器, 然后执行.
- protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)
- throws IOException, ServletException {
- FilterChain chain = getExecutionChain(request, response, origChain);
- chain.doFilter(request, response);
- }
获取过滤器方法如下.
- protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {
- FilterChain chain = origChain;
- FilterChainResolver resolver = getFilterChainResolver();
- if (resolver == null) {
- return origChain;
- }
- FilterChain resolved = resolver.getChain(request, response, origChain);
- if (resolved != null) {
- chain = resolved;
- } else {
- }
- return chain;
- }
通过 getFilterChainResolver()就拿到了上面提到的过滤器执行链解析器 PathMatchingFilterChainResolver, 然后再调用它的 getChain()匹配获取过滤器, 最终过滤器在 executeChain()中被执行.
首发地址: https://www.guitu18.com/post/2019/08/01/45.html
总结
Shiro 框架在我们配置的 ShiroFilterFactoryBean 进行初始化的时候就做了很多初始化操作, 将我们配置的过滤器规则一步步添加对应的过滤器到过滤器执行链中, 这个执行链最终被放入执行链解析器. 当有请求到达 Tomcat 时, 通过 Tomcat 中的 Filter 责任链执行流程, 最终 Shiro 所定义的 AbstractShiroFilter.doFilter()被执行, 那么它会去获取执行链解析器, 通过解析器拿到执行链中的过滤器并执行, 这样就实现了基于 URL 的权限过滤.
本文结束, 这篇也算是浅入了 Shiro 源码了解了一下 Shiro 过滤器的初始化以及执行过程, 相比 Spring 的源码 Shiro 的源码要简单易懂的很多很多, 它没有 Spring 那么绕. 每次看源码的时候, 我都有下面这种感觉, 特别是看 Spring 源码的时候这种感觉尤为强烈:
对于框架我们所配置和调用的, 永远是浮在水面上的那一点点, 不点进去, 你永远不知道下边是一个怎样的庞然大物. 封装的越好的框架, 浮现出来的越少, 隐藏的部分就越多.
比如 SpringBoot, 为什么能通过一个 main 方法就启动一个项目, Web.xml 呢, application.properties 呢; SpringMVC 为什么就需要一个 @RequestMapping 就能实现从 URL 到方法的调用; Shiro 为什么仅需要 @RequiresPermissions 就能实现方法级别的权限控制.
学到的越多就越是感觉自己知道的越少, 这是一个很矛盾却又真实存在的感觉. 不说了该学习了, 上面的最后一条方法级别权限控制下一篇写, 时间待定, 因为最近在项目中刚好遇到数据库优化相关问题, 想先去看看 MySQL.
来源: https://www.cnblogs.com/guitu18/p/11315195.html