Spring Boot:一文搞懂 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 过程的异同。

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值