• SpringSecurity自定义多Provider时提示No AuthenticationProvider found for问题的解决方案与原理(三)


    上一篇文章已经发现了全局与局部ProviderManager的问题,接下来就是我们要看一看这个全局的ProviderManager是怎么出来的。
    注意,ProviderManagerAuthenticationManager的默认实现,所以在文中两者会交叉出现,本质是一个东西,不要被绕晕。
    深吸一口气清醒一下,开始翻源码吧。

    AuthenticationConfiguration

    AuthenticationConfiguration这个类中方法非常多,我们只看需要的,首先发现其中一个标记了@Bean的方法,他会直接向容器中放入对象,而这个对象正是我们需要的AuthenticationManagerBuilder

    	@Bean
    	public AuthenticationManagerBuilder authenticationManagerBuilder(ObjectPostProcessor<Object> objectPostProcessor,
    			ApplicationContext context) {
    		LazyPasswordEncoder defaultPasswordEncoder = new LazyPasswordEncoder(context);
    		AuthenticationEventPublisher authenticationEventPublisher = getBeanOrNull(context,
    				AuthenticationEventPublisher.class);
    		DefaultPasswordEncoderAuthenticationManagerBuilder result = new DefaultPasswordEncoderAuthenticationManagerBuilder(
    				objectPostProcessor, defaultPasswordEncoder);
    		if (authenticationEventPublisher != null) {
    			result.authenticationEventPublisher(authenticationEventPublisher);
    		}
    		return result;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13

    这里面干了三件事:

    1. 获取一个密码编码器PasswordEncoder,还是懒加载的,没什么用;
    2. 获取了一个认证事件发布器AuthenticationEventPublisher,他是一个接口,其中的非空实现是DefaultAuthenticationEventPublisher,点进去会发现有很多认证消息相关的初始化操作,但我们并不需要;
    3. 获取了一个使用密码认证的AuthenticationManager对象DefaultPasswordEncoderAuthenticationManagerBuilder,隐约感觉这个Builder会跟那个DaoAuthenticationProvider会有什么关系,因为他也是默认的基于JDBC的验证处理器实现,也没什么帮助。

    看下来他只是构造了一个Builder对象然后放在了容器里,继续往下看会发现一个get方法,正是用来获取AuthenticationManager对象的,相关说明我会标记在注释里面。

    	public AuthenticationManager getAuthenticationManager() throws Exception {
    	    // 根据初始化标记位判断是不是需要new
    		if (this.authenticationManagerInitialized) {
    			return this.authenticationManager;
    		}
    
            // 从容器中获取上文注册进去的AuthenticationManagerBuilder对象
    		AuthenticationManagerBuilder authBuilder = this.applicationContext.getBean(AuthenticationManagerBuilder.class);
    		if (this.buildingAuthenticationManager.getAndSet(true)) {
    			return new AuthenticationManagerDelegator(authBuilder);
    		}
    
            // 这里默认是个Collections.emptyList()
    		for (GlobalAuthenticationConfigurerAdapter config : this.globalAuthConfigurers) {
    			authBuilder.apply(config);
    		}
    
            // 执行构建
    		this.authenticationManager = authBuilder.build();
    		
    		// 再次尝试获取AuthenticationManager实例
    		if (this.authenticationManager == null) {
    			this.authenticationManager = getAuthenticationManagerBean();
    		}
    
            // 修改初始化的标记位
    		this.authenticationManagerInitialized = true;
    		return this.authenticationManager;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29

    调用此方法会返回一个默认的DefaultPasswordEncoderAuthenticationManagerBuilder所得到的AuthenticationManager
    这里暂告一段落,但是还记得我们的目的是什么吗?我们要找到这个父AuthenticationManager是怎么出来的,这里只是发现了实例的构造过程,那他是怎么成为“父”的呢?


    HttpSecurityConfiguration

    我们点击AuthenticationConfiguration.getAuthenticationManager()方法,在他的所有调用中一下子就看到了一个熟悉的类HttpSecurityConfiguration,他和下面的WebSecurityConfiguration都出现在@EnableWebSecurity这个注解的Import中,而且,我们使用的Security构造方法也是针对于HttpSecurity的,那我们重点来看看他。
    在这里插入图片描述
    在这里插入图片描述

    /*
     * Copyright 2002-2020 the original author or authors.
     *
     * Licensed under the Apache License, Version 2.0 (the "License");
     * you may not use this file except in compliance with the License.
     * You may obtain a copy of the License at
     *
     *      https://www.apache.org/licenses/LICENSE-2.0
     *
     * Unless required by applicable law or agreed to in writing, software
     * distributed under the License is distributed on an "AS IS" BASIS,
     * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
     * See the License for the specific language governing permissions and
     * limitations under the License.
     */
    
    package org.springframework.security.config.annotation.web.configuration;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.ApplicationContext;
    import org.springframework.context.annotation.Bean;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.context.annotation.Scope;
    import org.springframework.security.authentication.AuthenticationManager;
    import org.springframework.security.config.annotation.ObjectPostProcessor;
    import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
    import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration;
    import org.springframework.security.config.annotation.web.builders.HttpSecurity;
    import org.springframework.security.config.annotation.web.configurers.DefaultLoginPageConfigurer;
    import org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter;
    
    import static org.springframework.security.config.Customizer.withDefaults;
    
    /**
     * {@link Configuration} that exposes the {@link HttpSecurity} bean.
     *
     * @author Eleftheria Stein
     * @since 5.4
     */
    @Configuration(proxyBeanMethods = false)
    class HttpSecurityConfiguration {
    
    	private static final String BEAN_NAME_PREFIX = "org.springframework.security.config.annotation.web.configuration.HttpSecurityConfiguration.";
    
    	private static final String HTTPSECURITY_BEAN_NAME = BEAN_NAME_PREFIX + "httpSecurity";
    
    	private ObjectPostProcessor<Object> objectPostProcessor;
    
    	private AuthenticationManager authenticationManager;
    
    	private AuthenticationConfiguration authenticationConfiguration;
    
    	private ApplicationContext context;
    
    	@Autowired
    	void setObjectPostProcessor(ObjectPostProcessor<Object> objectPostProcessor) {
    		this.objectPostProcessor = objectPostProcessor;
    	}
    
    	void setAuthenticationManager(AuthenticationManager authenticationManager) {
    		this.authenticationManager = authenticationManager;
    	}
    
    	@Autowired
    	void setAuthenticationConfiguration(AuthenticationConfiguration authenticationConfiguration) {
    		this.authenticationConfiguration = authenticationConfiguration;
    	}
    
    	@Autowired
    	void setApplicationContext(ApplicationContext context) {
    		this.context = context;
    	}
    
    	@Bean(HTTPSECURITY_BEAN_NAME)
    	@Scope("prototype")
    	HttpSecurity httpSecurity() throws Exception {
    		WebSecurityConfigurerAdapter.LazyPasswordEncoder passwordEncoder = new WebSecurityConfigurerAdapter.LazyPasswordEncoder(
    				this.context);
    		AuthenticationManagerBuilder authenticationBuilder = new WebSecurityConfigurerAdapter.DefaultPasswordEncoderAuthenticationManagerBuilder(
    				this.objectPostProcessor, passwordEncoder);
    		authenticationBuilder.parentAuthenticationManager(authenticationManager());
    		HttpSecurity http = new HttpSecurity(this.objectPostProcessor, authenticationBuilder, createSharedObjects());
    		// @formatter:off
    		http
    			.csrf(withDefaults())
    			.addFilter(new WebAsyncManagerIntegrationFilter())
    			.exceptionHandling(withDefaults())
    			.headers(withDefaults())
    			.sessionManagement(withDefaults())
    			.securityContext(withDefaults())
    			.requestCache(withDefaults())
    			.anonymous(withDefaults())
    			.servletApi(withDefaults())
    			.apply(new DefaultLoginPageConfigurer<>());
    		http.logout(withDefaults());
    		// @formatter:on
    		return http;
    	}
    
    	private AuthenticationManager authenticationManager() throws Exception {
    		return (this.authenticationManager != null) ? this.authenticationManager
    				: this.authenticationConfiguration.getAuthenticationManager();
    	}
    
    	private Map<Class<?>, Object> createSharedObjects() {
    		Map<Class<?>, Object> sharedObjects = new HashMap<>();
    		sharedObjects.put(ApplicationContext.class, this.context);
    		return sharedObjects;
    	}
    
    }
    
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35
    • 36
    • 37
    • 38
    • 39
    • 40
    • 41
    • 42
    • 43
    • 44
    • 45
    • 46
    • 47
    • 48
    • 49
    • 50
    • 51
    • 52
    • 53
    • 54
    • 55
    • 56
    • 57
    • 58
    • 59
    • 60
    • 61
    • 62
    • 63
    • 64
    • 65
    • 66
    • 67
    • 68
    • 69
    • 70
    • 71
    • 72
    • 73
    • 74
    • 75
    • 76
    • 77
    • 78
    • 79
    • 80
    • 81
    • 82
    • 83
    • 84
    • 85
    • 86
    • 87
    • 88
    • 89
    • 90
    • 91
    • 92
    • 93
    • 94
    • 95
    • 96
    • 97
    • 98
    • 99
    • 100
    • 101
    • 102
    • 103
    • 104
    • 105
    • 106
    • 107
    • 108
    • 109
    • 110
    • 111
    • 112
    • 113
    • 114
    • 115

    点进来,代码不长关键点可不少,从上面看有AuthenticationManager,还有AuthenticationConfiguration,还有最重要的HttpSecurity的Bean,还是个原型类型说明他并不是单例的,这也是SpringSecurity可以配置多套认证体系的关键,那自然,这也是初始化默认体系的地方了。分解看一下:

    1. new WebSecurityConfigurerAdapter.LazyPasswordEncoder(this.context);创建了一个密码编码器;
    2. new WebSecurityConfigurerAdapter.DefaultPasswordEncoderAuthenticationManagerBuilder(this.objectPostProcessor, passwordEncoder);创建了一个DefaultPasswordEncoderAuthenticationManagerBuilder实例对象
    3. 关键的一句authenticationBuilder.parentAuthenticationManager(authenticationManager());,这里给刚才的authenticationBuilder设置了父对象。父对象哪里来?先从当前的成员变量中获取,如果没有则调用getAuthenticationManager()方法进行获取。这个方法是哪里的?是刚才的AuthenticationConfiguration中的。这个方法干了什么?是返回了一个由DefaultPasswordEncoderAuthenticationManagerBuilder所创建的AuthenticationManager对象。

    现在我们弄清了这个全局的AuthenticationManager是怎么出来的了,但是还有一个疑问没有解答,还记得上篇文章中的debug时看到的吗?默认的DaoAuthenticationProvider又是怎么被加到这个全局AuthenticationManager中的呢?
    在这里插入图片描述


    HttpSecurity

    在刚才的httpSecurity()方法中继续往下走,我们点开new HttpSecurity这一行看看,看看它的构造方法里都有什么。
    在这里插入图片描述

    1. 上来就是一个super(),点进去之后,只是一些赋值操作,没什么含义;
    2. setSharedObject这是一个很有意思的地方,SpringSecurity自己维护了一个缓存map,可以把一些常用的对象放在里面,用的时候直接通过HttpSecurity实例进行获取;
    3. 接下来拿到了一个RequestMatcherConfigurer实例,提供给url配置用的,但好像也对我们没什么帮助。

    到这里我陷入了一个困境,只是知道了有全局配置这回事,但是对于怎么获取到局部配置仍然不清楚。继续向下翻看,发现了在配置时用到的authenticationProvider()方法。

    	@Override
    	public HttpSecurity authenticationProvider(AuthenticationProvider authenticationProvider) {
    		getAuthenticationRegistry().authenticationProvider(authenticationProvider);
    		return this;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5

    getAuthenticationRegistry()是一个私有方法,点进去继续看,是从SharedObject中获取到了AuthenticationManagerBuilder对象。

    	private AuthenticationManagerBuilder getAuthenticationRegistry() {
    		return getSharedObject(AuthenticationManagerBuilder.class);
    	}
    
    • 1
    • 2
    • 3

    到这里,可以确定确定HttpSecurityDefaultPasswordEncoderAuthenticationManagerBuilder以及父AuthenticationManager它们三者之间的关系了,但是仍然没有发现默认的provider是怎么加进去的,以及如何获取local AuthenticationManager。找到这已经花了我一天半的时间了。


    回到SecurityFilterChain

    其实在上面的搜索过程中我发现了另一种方法,还记得之前看到的如何定义全局的ProviderManager吗?事实上我也尝试过完全手动将ProviderManager构造出来然后注册为Bean,这方法是可行的。

    但是这样做有一个极大的问题:这样会直接修改全局AuthenticationManager实例,Local AuthenticationManager其实还是原来那样,这样做能成功的原因仅仅是当local验证失败后自动转向父级验证而已。虽然问题能解决,但实在算不上好方法,没有解决根本问题。
    在这里插入图片描述

    再打开Spring Security without the WebSecurityConfigurerAdapter这篇文章看看,突然发现,在我之前看到Global AuthenticationManager这一节下方,就介绍了Accessing the local AuthenticationManager,这不正是我想要的吗?
    在这里插入图片描述

    原来Spring早就给我们指了一条明路出来,只可惜当时太心急完全没有注意这里。但当我看完之后心又凉了半截,他说可以自定义一个DSL,可这DSL是个啥啊,跟随文档指引点开了下面的连接:
    Custom DSLs
    在这里插入图片描述

    写的非常简练,就是你可以balabala这样定义一个DSL,然后balabala这样使用它,然后还告诉你,HttpSecurity.authorizeRequests()就是这样实现的。感觉,什么也没说啊?我的AuthenticationManager呢?

    来都来了,这里面不是继承了一个AbstractHttpConfigurer吗,管他干啥的先点开看看。一个抽象类,看一下实现吧,这一看了不得,里面有很多类非常熟悉啊。
    在这里插入图片描述

    只看名字,大概就能认出来这是SpringSecurity各种登录方式的默认实现,比如FormLogin,比如HttpBasic,比如OAuth等等,这和我现在要做的验证码、短信登录岂不正好是一样的,那就找一个最简单最贴切的表单登录FormLoginConfigurer来看看吧。


    FormLoginConfigurer

    先看注释
    在这里插入图片描述

    第一行就说明了这是框架默认的登录页,这个地方在哪用的呢?回头看Security的配置类。
    在这里插入图片描述

    第三行,由于我们要使用自定义的登陆接口,所以上来就把自带的FormLogin关掉了,点进去看看formLogin()这个方法。
    在这里插入图片描述

    怎么样,正好是我们正在看的FormLoginConfigurer这个类,说明我们没找错,那就一点一点来啃这个配置类吧。全代码太长,主要是注释占了很大一部分,就不贴完整代码了,捡重要的说。
    首先这是FormLoginConfigurer的无参构造器,后两行是在设置用户名和密码,没什么好说的,这个super(new UsernamePasswordAuthenticationFilter(), null)点开看看,先看里面的UsernamePasswordAuthenticationFilter

    	/**
    	 * Creates a new instance
    	 * @see HttpSecurity#formLogin()
    	 */
    	public FormLoginConfigurer() {
    		super(new UsernamePasswordAuthenticationFilter(), null);
    		usernameParameter("username");
    		passwordParameter("password");
    	}
    		
    	/**
    	 * The HTTP parameter to look for the username when performing authentication. Default
    	 * is "username".
    	 * @param usernameParameter the HTTP parameter to look for the username when
    	 * performing authentication
    	 * @return the {@link FormLoginConfigurer} for additional customization
    	 */
    	public FormLoginConfigurer<H> usernameParameter(String usernameParameter) {
    		getAuthenticationFilter().setUsernameParameter(usernameParameter);
    		return this;
    	}
    
    	/**
    	 * The HTTP parameter to look for the password when performing authentication. Default
    	 * is "password".
    	 * @param passwordParameter the HTTP parameter to look for the password when
    	 * performing authentication
    	 * @return the {@link FormLoginConfigurer} for additional customization
    	 */
    	public FormLoginConfigurer<H> passwordParameter(String passwordParameter) {
    		getAuthenticationFilter().setPasswordParameter(passwordParameter);
    		return this;
    	}
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33

    点开super后注意我们来到了他的父类UsernamePasswordAuthenticationFilter中,DEFAULT_ANT_PATH_REQUEST_MATCHER是个常量,里面定义了一个POST /login接口,这也就是SpringSecurity默认登录地址的来源。里面还有个super,继续看。
    在这里插入图片描述

    现在我们又到了新的父类AbstractAuthenticationProcessingFilter中,乍一看这个构造方法是指做了个校验然后赋值而已,但是这个抽象类的说明非常值得我们一看。
    在这里插入图片描述
    在这里插入图片描述

    看完这个说明其实我是有一点激动的,感觉已经很接近真相了。因为他明确提到了需要AuthenticationManager来处理验证请求,并且由attemptAuthentication()方法执行验证。还告诉我们,验证成功的话由AuthenticationSuccessHandler进行处理,验证失败由AuthenticationFailureHandler进行处理。

    继续向下看,陆续会发现setAuthenticationManager(AuthenticationManager authenticationManager)setAuthenticationSuccessHandler(AuthenticationSuccessHandler successHandler)setAuthenticationFailureHandler(AuthenticationFailureHandler failureHandler)等认证相关的方法,最关键的attemptAuthentication(HttpServletRequest request, HttpServletResponse response)方法是一个抽象方法,需要子类去实现。而authenticationManager恰好也是它的一个成员变量。
    在这里插入图片描述

    向上回到UsernamePasswordAuthenticationFilter中,果然发现了重写的attemptAuthentication登录逻辑,熟悉的AuthenticationToken也出现了。感觉这里才是应该做认证的地方,莫非我之前所有的写法都是错误的?
    在这里插入图片描述

    线索逐渐清晰,但还有一个关键的点,即authenticationManager这个成员变量是哪里来的。虽然有一个以authenticationManager为参数的构造方法,但并没有地方调用,显然不是通过new的方式构造出来的。那只剩下一种可能了:它是通过父类AbstractAuthenticationProcessingFiltersetAuthenticationManager方法注入进去的,那我们就以这个为关键点继续查找。

    在这里插入图片描述


    AbstractAuthenticationFilterConfigurer

    查找setAuthenticationManager方法,发现在外部只有一处调用,点开看看。
    在这里插入图片描述

    来到了第三个没见过的抽象类AbstractAuthenticationFilterConfigurer中。这里一目了然,认证需要的三个实体都有了:AuthenticationManager、SuccessHandler、FailHandler。而这个类,又是我们一开始看到的FormLoginConfigurer的父类。
    在这里插入图片描述

    是不是已经晕了?没关系,我看到这里的时候也是晕晕乎乎的,接下来就不放图了,实在是太多了,简单文字整理一下:

    1. UsernamePasswordAuthenticationFilter这是用户名密码模式的登录拦截器,他继承自AbstractAuthenticationProcessingFilter这个抽象类,且实现了里面的attemptAuthentication()认证方法;
    2. FormLoginConfigurer创建了一个UsernamePasswordAuthenticationFilter实例,并且继承自AbstractAuthenticationFilterConfigurer,后者继续继承自AbstractHttpConfigurer,又继承了SecurityConfigurerAdapter,里面包含了一些初始化的抽象方法,重点来了! 其中有一个configure(B builder)可以获得HttpSecurity对象。

    看看这个关系图,是不是觉得自己不配写Java。。。。。。
    在这里插入图片描述

    到这里,我们可以大胆地猜测一下,SpringSecurity的设计思路也许并不是 登陆接口放白名单 -> 请求 -> token拦截器 -> 接口 -> 不同登录方式走不同业务 -> 返回 这样,而是每一种登录模式走一套自己的拦截器链,而在这套拦截器链中,SpringSecurity为我们提供了Local AuthenticationManager,这一点可以从AbstractAuthenticationFilterConfigurer的子类中发现端倪。
    在这里插入图片描述

    为什么会这么说,当你多看几个AbstractAuthenticationFilterConfigurer的子类时,这种感觉会越来越强烈,比如下面的HttpBasicConfigurer
    在这里插入图片描述

    同时,在常规的Filter、Token、Provider之外,还应该有一个Configurer类,专门用于创建Filter实例以及注入Local AuthenticationManager
    至此,大体思路已然清晰,接下来就上手实践。

  • 相关阅读:
    C语言结构体(struct)一些常见问题 ⎛⎝≥⏝⏝≤⎛⎝
    归并排序,求逆序对
    Leetcode151. 反转字符串中的单词
    element ui中el-form-item的属性rules的用法
    4 个 Linux 技巧让工作效率翻倍
    【JavaWeb】ServletContext配置信息
    如何解决虚拟仿真教学中的设备限制和卡顿问题?|点量云流技术解决方案
    iptables 源地址、目标地址转换
    pcan二次开发文档 | PEAK-System Documentation
    掌握雅思写作:任务 2(在 7 小时内达到 7+ 级)
  • 原文地址:https://blog.csdn.net/u012760435/article/details/126580985