讲的很好
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。@Controllerpublic 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: samson12302-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(540609)@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 ServletContainerInitializersfor (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(992988)public final void onStartup(ServletContext servletContext) throws ServletException {    String description = getDescription();    register(description, servletContext); }// org.springframework.boot.web.servlet.DynamicRegistrationBean.register@Override(992988)protected final void register(String description, ServletContext servletContext) {    D registration = addRegistration(description, servletContext);    configure(registration);}// org.springframework.boot.web.servlet.AbstractFilterRegistrationBean.addRegistration@Override(992988)protected Dynamic addRegistration(String description, ServletContext servletContext) {    Filter filter = getFilter();    return servletContext.addFilter(getOrDeduceName(filter), filter);}// org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean.getFilter@Override(992988)public DelegatingFilterProxy getFilter() {    return new DelegatingFilterProxy(this.targetBeanName, getWebApplicationContext()) { };}通过上面的源码可以看到,当调用到 会调用 DelegatingFilterProxyRegistrationBean#onStartup 方法时,会向 ServletContext 中注册一个内部匿名类,它继承自 DelegatingFilterProxy。这也就是为什么我们在之前的截图中看到的 springSecurityFilterChain 的类型为 DelegatingFilterProxyRegistrationBean$1。02.2-为什么是 DelegatingFilterProxy?DelegatingFilterProxy 是一个代理类,它内部包含了一个 WebApplicationContext 和 Filter:@Nullable(187852900)private WebApplicationContext webApplicationContext;@Nullable(187852900)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 过程的异同。
点赞 0
评论 0
全部评论

相关推荐

刘湘_passion:太强了牛肉哥有被激励到
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客企业服务