一文搞懂 Spring Security 是如何工作的?
Spring Security 是 Spring 大家族的一员,与 Spring Boot 应用集成起来应该更“丝滑”。今天,我将带大家一块体验下如何使用 Spring Security,并对比一下它与 Shiro 有哪些不同。
01-基于 Spring Security 的 HelloWorld 程序
要启用 Spring Security,只需要在 pom.xml 文件中增加对应的依赖即可。
<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-security</artifactId> </dependency>
然后,编写一个 HelloWorldController。
@Controller public class HelloWorldController { @GetMapping(path = {"/index", "/"}) public String index() { return "index"; } }
然后,在 /resources/templates/ 中增加一个 index.html 页面。
<html xmlns:th="https://www.thymeleaf.org"> <head> <title>Hello Security!</title> </head> <body> <h1>Hello Security</h1> <a th:href="@{/logout}">Log Out</a> </body> </html>
最后,启动应用,试着访问下就能看到 Spring Security 的登录界面了。
如果你什么都不配置,在运行日志中会有一个随机生成 UUID 作为 user 用户的登录密码。如果你想配置自己指定的用户及密码,可以在 application.yml 中通过以下属性指定:
spring: security: user: name: samson password: samson123
02-Spring Security 工作流程分析
Spring Security 与 Shiro 一样,都是基于 Servlet 中的 Filter 机制实现的,可以参考下 Spring 官网提供的架构图来理解。
对于 Spring Web 应用来说,图中的 Servlet 是 DispatcherServlet。所有的请求,需要流经一个 ApplicationFilterChain(由多个 Filter 组成,其中一个是 Spring Security 实现的 Filter),然后到达 DispatcherServlet。
接下来,通过调试来验证下我们的想法。
可以看到,在 filters 数组中有一个名为 springSecurityFilterChain,类型为 org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean$1 的 Filter。这个就是前面架构图中 DelegatingFilterProxy。
注:看到这里,你可能比较好奇,为什么架构图里的是 DelegatingFilterProxy,而调试截图里却是 DelegatingFilterProxyRegistrationBean$1 类型呢?先别着急,慢慢往下看。
接下来,我会带着大家分析它是如何注入的,以及它是如何工作的。
02.1-DelegatingFilterProxy 是如何被注入到 ServletContext 中的?
首先,我们来看一下 DelegatingFilterProxyRegistrationBean 这个类。在 autoconfigure 包中,它被实例化,代码如下:
@Bean @ConditionalOnBean(name = "springSecurityFilterChain") // 容器中有名为 springSecurityFilterChain 的 Bean 时满足条件 public DelegatingFilterProxyRegistrationBean securityFilterChainRegistration( SecurityProperties securityProperties) { DelegatingFilterProxyRegistrationBean registration = new DelegatingFilterProxyRegistrationBean( "springSecurityFilterChain"); registration.setOrder(securityProperties.getFilter().getOrder()); registration.setDispatcherTypes(getDispatcherTypes(securityProperties)); return registration; }
它的职责是向 ServletContext 中注册我们在上节中看到的那个 Filter。它是怎么注册的呢?要回答这个问题,需要先了解 DelegatingFilterProxyRegistrationBean 继承关系:
这里面比较关键的接口是 org.springframework.boot.web.servlet.ServletContextInitializer
,它有一个“孪生兄弟”接口,org.springframework.web.WebApplicationInitializer
。我先来解释下这两个接口的设计目的,以及它们在什么时候会被调用。
两者异同点:
- 共同点,这两个接口都是用来对 ServletContext 进行程序化配置的,对等于基于 web.xml 这种配置方式
- 不同点,WebApplicationInitializer 会在 Servlet 容器(这里说的是 3.0+ 版本)启动时自动被调用,是 Servlet 规范定义的动作,Spring 只不过是遵循了这种规范进行的实现。 ServletContextInitializer 接口不会在 Servlet 容器启动时被调用,它是 Spring 自己的实现,在 Spring 容器启动时会被调用。 总结来说,两个接口,一个能够感知 Servlet 容器的生命周期事件,另一个能够感知 ApplicationContext 容器的生命周期事件。后面,我会详细分析这两个过程。
WebApplicationInitializer接口的调用过程
WebApplicationInitializer 是 Spring 遵循 Servlet 规范实现的 ServletContext 程序化配置接口。Servlet 中的实现基于 SPI 机制,服务接口为 javax.servlet.ServletContainerInitializer
,以及注解 @HandlesTypes
用来指定 ServletContainerInitializer 感兴趣或要处理的类型。Spring 中对该接口的实现类是 org.springframework.web.SpringServletContainerInitializer
,其上标注了 @HandlesTypes(WebApplicationInitializer.class)
,说明该实现对 WebApplicationInitializer 类感兴趣。
在 Servlet 3.0+ 版本的容器启动时,会通过 SPI 机制查找并实例化所有实现了 ServletContainerInitializer 接口的类。
ServiceLoader.load(javax.servlet.ServletContainerInitializer.class)
然后,调用它们的 onStartup 方法。下面的代码来自于 apache-tomcat-10.1.5-src/java/org/apache/catalina/core/StandardContext.java
// Call ServletContainerInitializers for (Map.Entry<ServletContainerInitializer, Set<Class<?>>> entry : initializers.entrySet()) { // 值在 java/org/apache/catalina/startup/ContextConfig#processServletContainerInitializers 填充 try { entry.getKey().onStartup(entry.getValue(), getServletContext()); } catch (ServletException e) { log.error(sm.getString("standardContext.sciFail"), e); ok = false; break; } }
SpringServletContainerInitializer#onStartup 的处理逻辑是,对 webAppInitializerClasses 中非接口、非抽象类,创建它们的实例,排序、并调用它们的 onStartup 方法。
for (Class<?> waiClass : webAppInitializerClasses) { // 省略非关键代码... if (!waiClass.isInterface() && !Modifier.isAbstract(waiClass.getModifiers()) && WebApplicationInitializer.class.isAssignableFrom(waiClass)) { Object obj = (WebApplicationInitializer)ReflectionUtils.accessibleConstructor(waiClass).newInstance(); initializers.add(obj); } // 省略非关键代码... AnnotationAwareOrderComparator.sort(initializers); for (WebApplicationInitializer initializer : initializers) { initializer.onStartup(servletContext); } // 省略非关键代码... }
注:这里要注意区分 javax.servlet.ServletContainerInitializer 和 org.springframework.boot.web.servlet.ServletContextInitializer 两个类的类名, 极为相似,Servlet 中的是 Container,Spring 中的是 Context。 我最开始看时就没注意区分,把两个类混淆了,看代码的过程中看得云里雾里,希望不要走我的冤枉路。
ServletContextInitializer接口的调用过程
当使用嵌入式 Servlet 容器,例如我们上面用的嵌入式 Tomcat,Spring Boot 提供了另外一个 ServletContainerInitializer 实现 class TomcatStarter implements ServletContainerInitializer
。在容器启动时,它会调用 ServletContextInitializer 类的 onStartup 方法。
for (ServletContextInitializer initializer : this.initializers) { initializer.onStartup(servletContext); }
这里的 this.initializers
值是在 TomcatStarter 创建时传入的,最终来源于 org.springframework.boot.web.servlet.ServletContextInitializerBeans
。ServletContextInitializerBeans 是 Spring 实现的一个集合类,它会从 BeanFactory 中查找所有实现了 ServletContextInitializer 接口的 Bean,并分类保存:
private void addServletContextInitializerBean(String beanName, ServletContextInitializer initializer, ListableBeanFactory beanFactory) { if (initializer instanceof ServletRegistrationBean) { Servlet source = ((ServletRegistrationBean<?>) initializer).getServlet(); addServletContextInitializerBean(Servlet.class, beanName, initializer, beanFactory, source); } else if (initializer instanceof FilterRegistrationBean) { Filter source = ((FilterRegistrationBean<?>) initializer).getFilter(); addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source); } else if (initializer instanceof DelegatingFilterProxyRegistrationBean) { String source = ((DelegatingFilterProxyRegistrationBean) initializer).getTargetBeanName(); addServletContextInitializerBean(Filter.class, beanName, initializer, beanFactory, source); } else if (initializer instanceof ServletListenerRegistrationBean) { EventListener source = ((ServletListenerRegistrationBean<?>) initializer).getListener(); addServletContextInitializerBean(EventListener.class, beanName, initializer, beanFactory, source); } else { addServletContextInitializerBean(ServletContextInitializer.class, beanName, initializer, beanFactory, initializer); } }
有了上面的分析过程,我们再来看下 DelegatingFilterProxyRegistrationBean,它的父类实现了 ServletContextInitializer。我们通过断点来分析下它的调用过程:
当 TomcatStarter 的 onStartup 执行后,会调用 DelegatingFilterProxyRegistrationBean#onStartup 方法。那它的 onStartup 方法做了什么事呢?
// org.springframework.boot.web.servlet.RegistrationBean.onStartup @Override public final void onStartup(ServletContext servletContext) throws ServletException { String description = getDescription(); register(description, servletContext); } // org.springframework.boot.web.servlet.DynamicRegistrationBean.register @Override protected final void register(String description, ServletContext servletContext) { D registration = addRegistration(description, servletContext); configure(registration); } // org.springframework.boot.web.servlet.AbstractFilterRegistrationBean.addRegistration @Override protected Dynamic addRegistration(String description, ServletContext servletContext) { Filter filter = getFilter(); return servletContext.addFilter(getOrDeduceName(filter), filter); } // org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean.getFilter @Override public DelegatingFilterProxy getFilter() { return new DelegatingFilterProxy(this.targetBeanName, getWebApplicationContext()) { }; }
通过上面的源码可以看到,当调用到 会调用 DelegatingFilterProxyRegistrationBean#onStartup 方法时,会向 ServletContext 中注册一个内部匿名类,它继承自 DelegatingFilterProxy。这也就是为什么我们在之前的截图中看到的 springSecurityFilterChain 的类型为 DelegatingFilterProxyRegistrationBean$1。
02.2-为什么是 DelegatingFilterProxy?
DelegatingFilterProxy 是一个代理类,它内部包含了一个 WebApplicationContext 和 Filter:
@Nullable private WebApplicationContext webApplicationContext; @Nullable private volatile Filter delegate;
这里的 delegate 就是前面架构图中的 Bean Filter0。那 Spring 为什么要设计这样一层代理呢?
从前面的分析过程中我们知道,向 ServletContext 中注册 Filter 的时间发生的其实非常早,甚至这个时候 ApplicationContext 都还没有实例化完毕,更别说其中的 Filter Bean了。所以,Spring 这里向 ServletContext 注入了一个代理类,并且这个代理类持有一个 ApplicationContext 的引用。当请求到来的时候,可以再通过 ApplicationContext 获取到真正地 Filter Bean。这其实也是一种 Lazy 策略。
03-Shiro 中的 Filter 是如何注入到 ServletContext 的?
Shiro 与 Spring Security 一样,都是基于 Servlet 的 Filter 机制。这一节我将带着大家一块看下 Shiro 是如何把它的安全相关 Filter 注入到 ServletContext 中的。
Shiro 中通过 FilterRegistrationBean 将 AbstractShiroFilter 注入到 ServletContext 中,且名为 shiroFilter。
@Bean(name = "shiroFilter") @ConditionalOnMissingBean(name = "filterShiroFilterRegistrationBean") protected FilterRegistrationBean<AbstractShiroFilter> filterShiroFilterRegistrationBean() throws Exception { FilterRegistrationBean<AbstractShiroFilter> filterRegistrationBean = new FilterRegistrationBean<>(); filterRegistrationBean.setDispatcherTypes(DispatcherType.REQUEST, DispatcherType.FORWARD, DispatcherType.INCLUDE, DispatcherType.ERROR); filterRegistrationBean.setFilter((AbstractShiroFilter) shiroFilterFactoryBean().getObject()); // 要注册的 Filter filterRegistrationBean.setName(FILTER_NAME); filterRegistrationBean.setOrder(1); return filterRegistrationBean; }
FilterRegistrationBean 与 DelegatingFilterProxyRegistrationBean 的关系如下图所示:
它们都派生自 org.springframework.boot.web.servlet.AbstractFilterRegistrationBean
。因此,它的注册过程与 Spring Security 基本无差别。只是 getFilter 返回的是 setFilter 设置的对象,即 AbstractShiroFilter 对象。
04-总结
综上,我介绍了 Spring Security 在 Web 应用中的工作机制,即基于 Servlet Filter 机制实现。接着,我重点分析了 Spring Boot 是如何将 Security 相关的 Filter 添加到 ServletContext 中,并对比了与 Shiro 添加 Filter 过程的异同。