讲的很好
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
全部评论

相关推荐

02-14 07:38
已编辑
门头沟学院 Java
2.4 一面2.6 二面2.9 三面(hr面)2.13 oc1.15号收到面试电话那会就开始准备,因为一开始没底所以选择推迟一段时间面试,之后开始准备八股,准备实习可能会问的东西,这期间hot100过了有六七遍,真的是做吐了快,八股也是背了忘,忘了背,面经也看了很多,虽然最后用上的只有几道题,可是谁知道会问什么呢自从大二上开始学java以来,一路走来真的太痛了,一开始做外卖,点评,学微服务,大二下五六月时,开始投简历,哎,投了一千份了无音讯,开始怀疑自己(虽然能力确实很一般),后来去到一家小小厂,但是并不能学到什么东西,而且很多东西都很不规范,没待多久便离开,大二暑假基本上摆烂很怀疑自己,大三上因为某些原因开始继续学,期间也受到一俩个中小厂的offer,不过学校不知道为啥又不允许中小厂实习只允许大厂加上待遇不太好所以也没去,感觉自己后端能力很一般,于是便打算转战测开,学习了一些比较简单的测试理论(没有很深入的学),然后十二月又开始继续投,java和测开都投,不过好像并没有几个面试,有点打击不过并没有放弃心里还是想争一口气,一月初因为学校事比较多加上考试便有几天没有继续投,10号放假后便继续,想着放假应该很多人辞职可能机会大一点,直到接到字节的面试,心里挺激动的,总算有大厂面试了,虽然很开心,但同时压力也很大,心里真的很想很想很想进,一面前几天晚上都睡不好觉,基本上都是二三点睡六七点醒了,好在幸运终于眷顾我一次了(可能是之前太痛了),一面三十几分钟结束,问的都不太难,而且面试官人挺好但是有些问题问的很刁钻问到了测试的一些思想并不是理论,我不太了解这方面,但是也会给我讲一讲他的理解,但是面完很伤心觉得自己要挂了。但是幸运的是一面过了(感谢面试官),两天后二面,问的同样不算难,手撕也比较简单,但也有一两个没答出来,面试官人很好并没有追问,因为是周五进行的二面,没有立即出结果,等到周一才通知到过了,很煎熬的两天,根本睡不好,好在下周一终于通知二面过了(感谢面试官),然后约第二天三面,听别的字节同学说hr面基本上是谈薪资了,但是我的并不是,hr还问了业务相关的问题,不过问的比较浅,hr还问我好像比较紧张,而且hr明确说了还要比较一下,我说我有几家的面试都拒了就在等字节的面试(当然紧张,紧张到爆了要),三面完后就开始等结果,这几天干啥都没什么劲,等的好煎熬,终于13号下午接到了电话通知oc了,正式邮件也同时发了,接到以后真的不敢信,很激动但更重要的是可以松一口气了,可以安心的休息一下了终于可以带着个好消息过年了,找实习也可以稍微告一段落了,虽然本人很菜,但是感谢字节收留,成为忠诚的节孝子了因为问的比较简单,面经就挑几个记得的写一下一面:1.实习项目的难点说一下2.针对抖音评论设计一下测试用例3.手撕:合并两个有序数组二面:1.为什么转测开2.线程进程区别,什么场景适合用哪个3.发送一个朋友圈,从发出到别人看到,从数据流转的角度说一下会经历哪些过程4.针对抖音刷到广告视频设计测试用例5.手撕:无重复字符的最长字串
牛客85811352...:测开问这么简单?
查看8道真题和解析
点赞 评论 收藏
分享
评论
点赞
收藏
分享

创作者周榜

更多
牛客网
牛客网在线编程
牛客网题解
牛客企业服务