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 过程的异同。