[In-depth explanation of Spring Security (3)] Implementation principle of default login authentication

1. The default configuration login authentication process

Please add a picture description

2. Process analysis

Taking the default SecurityFilterChain as an example (that is, form login), the process analysis of Spring Security requesting /hello resource to the server is as follows:
insert image description here

  1. To request the /hello interface, after introducing Spring Security, it will first pass through a series of filters (one request is the /test interface);
  2. When the request arrived at FilterSecurityInterceptor, it was found that the request was not authenticated. The request is intercepted and AccessDeniedExceptionan exception is thrown;
  3. The exception thrown AccessDeniedExceptionwill be ExceptionTranslationFiltercaught, and the Filter will call the LoginUrlAuthenticationEntryPoint#commence method to return to the client 302(暂时重定向), requiring the client to redirect to the /login page.
  4. The client sends a /login request;
  5. DefaultLoginPageGeneratingFilterThe /login request will return the login page when it encounters the filter again .

The origin of the login page

The following is DefaultLoginPageGeneratingFilterthe rewriting doFiltermethod, which can also explain why the login page is returned under the default configuration, and the login page is implemented by the following filter.

// DefaultLoginPageGeneratingFilter

	@Override
	public void doFilter(ServletRequest request,
	 ServletResponse response, 
	FilterChain chain)
			throws IOException, ServletException {
    
    
		doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
	}

    private void doFilter(HttpServletRequest request, 
    HttpServletResponse response, 
    FilterChain chain) throws IOException, ServletException {
    
    
        boolean loginError = this.isErrorPage(request);
        boolean logoutSuccess = this.isLogoutSuccess(request);
        // 判断是否是登录请求、登录错误和注销确认
        // 不是的话给用户返回登录界面
        if (!this.isLoginUrlRequest(request) && !loginError && !logoutSuccess) {
    
    
            chain.doFilter(request, response);
        } else {
    
    
        // generateLoginPageHtml方法中有对页面登录代码进行了字符串拼接
        // 太长了,这里就不给出来了
            String loginPageHtml = this.generateLoginPageHtml(request, loginError, logoutSuccess);
            response.setContentType("text/html;charset=UTF-8");
            response.setContentLength(loginPageHtml.getBytes(StandardCharsets.UTF_8).length);
            response.getWriter().write(loginPageHtml);
        }
    }

Form login authentication process (source code analysis)

After being redirected to the login page, there will be a question, how is it verified, and how to authenticate the user name and password?

First of all, we know that form authentication is enabled in the default loading. In [Introduction to Spring Security (2)] In the implementation principle of Spring Security, the editor pointed out that there is one of the default loaded filters UsernamePasswordAuthenticationFilter, which is used to process form requests. In fact, it is the filter configured by calling the method HttpSecurityin .formLogin

Next, analyze what a UsernamePasswordAuthenticationFilter does (it is not a native filter, it is filtered by attemptAuthentication, not doFilter, and the parameters are one chain less than the native filter):

	@Override
	public Authentication attemptAuthentication(HttpServletRequest request, 
	HttpServletResponse response)
			throws AuthenticationException {
    
    
			// 首先是判断是否是POST请求
		if (this.postOnly && !request.getMethod().equals("POST")) {
    
    
			throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
		}
		// 获取用户名和密码
		// 这是通过获取表单输入框名为username的数据
		String username = obtainUsername(request);
		username = (username != null) ? username.trim() : "";
		// 这是获取表单输入框名为password的数据
		String password = obtainPassword(request);
		password = (password != null) ? password : "";
		UsernamePasswordAuthenticationToken authRequest = UsernamePasswordAuthenticationToken.unauthenticated(username,
				password);
		// Allow subclasses to set the "details" property
		setDetails(request, authRequest);
		// 在一中小编也说了,这是Security中的认证
		// 通过调用AuthenticationManager中的authenticate方法
		// 需要传递的参数的Authentication对象,当时是这样解释的
		return this.getAuthenticationManager()
		.authenticate(authRequest);
	}

insert image description here

After debugging, enter authenticatethe method to observe how to authenticate. The following is the authentication process of debugging:

  1. After entering the authenticate method, ProviderManagerthe authenticate method under will be called, which rewrites the AuthenticationManager. For the first time, there is only the AnoymousAuthenticationProvider object in the providers, which is used for anonymous authentication. In the end, it will judge whether this authentication is supported, and the Provider is not supported;
    insert image description hereinsert image description here

  2. At this time, the anonymous authentication cannot be matched, and the next step is executed. Since parentthe attribute is not empty, the authenticate of the parent will be called for authentication. (Its parent is also a ProviderManager object, but there is an DaoAuthenticationProviderauthentication object in its providers collection).
    insert image description hereFrom this, it can be indirectly deduced UsernamePasswordAuthenticationFilterthat the AuthenticationManager object in is obtained through the following construction method.
    insert image description here

  3. Now that provider.supportsthe method matches successfully, let the provider verify, and then return the verified result set.
    insert image description hereThe authenticate method in AuthenticationProvider is not overridden in DaoAuthenticationProvider, it AbstractUserDetailsAuthenticationProvideris implemented by its abstract parent class. The core method retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);obtains UserDetailsthe object, and then combines some other parameters to create an Authentication object and return it.

AbstractUserDetailsAuthenticationProvider下的authenticate方法

	@Override
	public Authentication authenticate(Authentication authentication) 
	throws AuthenticationException {
    
    
// 断言 authentication 是否是UsernamePasswordAuthenticationToken对象
	Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication,
				() -> this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports",
						"Only UsernamePasswordAuthenticationToken is supported"));
		// 获取一下用户名
		String username = determineUsername(authentication);
		boolean cacheWasUsed = true;
		// 从缓存中拿UserDetails 对象,显然没有,咱刚调试呢,哪来的缓存
		UserDetails user = this.userCache.getUserFromCache(username);
		if (user == null) {
    
    
		// 既然为空呢,就说明这不是从缓存中拿的,调为false
			cacheWasUsed = false;
			try {
    
    
			// 核心代码,获取UserDetails对象去
				user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			}
			catch (UsernameNotFoundException ex) {
    
    
				this.logger.debug("Failed to find user '" + username + "'");
				if (!this.hideUserNotFoundExceptions) {
    
    
					throw ex;
				}
				throw new BadCredentialsException(this.messages
						.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
			}
			Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
		}
		try {
    
    
			this.preAuthenticationChecks.check(user);
			// 这里是验证密码的,通过子类DaoAuthenticationProvider的这个方法对密码去进行验证
			// 传过去的参数是user(UserDetails对象)和authentication对象
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		catch (AuthenticationException ex) {
    
    
			if (!cacheWasUsed) {
    
    
				throw ex;
			}
			// There was a problem, so try again after checking
			// we're using latest data (i.e. not from the cache)
			cacheWasUsed = false;
			user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
			this.preAuthenticationChecks.check(user);
			additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken) authentication);
		}
		this.postAuthenticationChecks.check(user);
		if (!cacheWasUsed) {
    
    
			this.userCache.putUserInCache(user);
		}
		Object principalToReturn = user;
		if (this.forcePrincipalAsString) {
    
    
			principalToReturn = user.getUsername();
		}
		return createSuccessAuthentication(principalToReturn, authentication, user);
	}
  1. retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication)The following is an overview of the core method DaoAuthenticationProvider, which is a method under , which is used to return the UserDetails object, that is, the user's detailed information, which is convenient and packaged into the authentication information Authentication and then returns the result to determine whether the authentication is successful.
// 一共两个参数,一个是用户名,一个是传过来的认证信息
	@Override
	protected final UserDetails retrieveUser(String username, 
	UsernamePasswordAuthenticationToken authentication)
			throws AuthenticationException {
    
    
		prepareTimingAttackProtection();
		try {
    
    
		// 核心方法就是这个,通过UserDetatilsService中的loadUserByUsername方法去获取UserDetails对象
			UserDetails loadedUser = this.getUserDetailsService().loadUserByUsername(username);
			if (loadedUser == null) {
    
    
				throw new InternalAuthenticationServiceException(
						"UserDetailsService returned null, which is an interface contract violation");
			}
			return loadedUser;
		}
		catch (UsernameNotFoundException ex) {
    
    
			mitigateAgainstTimingAttack(authentication);
			throw ex;
		}
		catch (InternalAuthenticationServiceException ex) {
    
    
			throw ex;
		}
		catch (Exception ex) {
    
    
			throw new InternalAuthenticationServiceException(ex.getMessage(), ex);
		}
	}

We can see that it is an InMemoryUserDetailsManager object in the default configuration, which is a memory-based operation object about UserDetails. Just look at the loadUserByUsername method in it, the writing is also very simple,Usernames are not case sensitive
insert image description here

  1. Let's talk about password verification. Password verification 3is pointed out in the source code. After obtaining the UserDetails object user, additionalAuthenticationChecksthe method of the subclass will be called to perform password verification. The main thing is to compare the password entered in the output box with the password in the UserDetails object. The UserDetails password can be understood as an PasswordEncoderencoded password (ciphertext), while the input in the input box can be understood as plaintext. It can be as simple as this. understand. Then PasswordEncodergo through to see if there is a match. Default is DelegatingPasswordEncoderpassword-encoder;
    insert image description here

三、UserDetailsService

Implementation of UserDetailsService in Spring Security

insert image description here

  • UserDetailsManagerOn the basis of UserDetailsService, continue to define 5 methods of adding user, updating user, deleting user, changing password and judging whether the user exists.
  • JdbcDaoImplOn the basis of UserDetailsService, the method of querying users from the database is implemented through spring-jdbc.
  • InMemoryUserDetailsManagerImplemented the method of adding, deleting, modifying and querying users in UserDetailsManager, but they are all memory-based operations, and the data is not persisted.
  • JdbcUserDetailsManagerInherited from JdbcDaoImpl and implements UserDetailsManager interface at the same time, so you can add, delete, modify and query users through JdbcUserDetailsManager, and these operations will be persisted in the database. However, JdbcUserDetailsManager has a limitation, that is, the SQL for operating users in the database is written in advance, which is not flexible enough, so JdbcUserDetailsManager is not used much in actual development.
  • CachingUserDetailsServiceThe characteristic is that the UserDetailsService will be cached.
  • UserDetailsServiceDelegatorIt provides the lazy loading function of UserDetailsService.
  • ReactiveUserDetailsServiceAdapteris an implementation of the UserDetailsService defined by the webflux-web-security module.

Default UserDetailsService configuration (source code analysis)

The default configuration about UserDetailsService is in UserDetailsServiceAutoConfigurationthe auto-configuration class. (Because the code is very long, only the core part is extracted here)

@AutoConfiguration
@ConditionalOnClass(AuthenticationManager.class)
@ConditionalOnBean(ObjectPostProcessor.class)
@ConditionalOnMissingBean(
		value = {
    
     AuthenticationManager.class, AuthenticationProvider.class, UserDetailsService.class,
				AuthenticationManagerResolver.class },
		type = {
    
     "org.springframework.security.oauth2.jwt.JwtDecoder",
				"org.springframework.security.oauth2.server.resource.introspection.OpaqueTokenIntrospector",
				"org.springframework.security.oauth2.client.registration.ClientRegistrationRepository",
				"org.springframework.security.saml2.provider.service.registration.RelyingPartyRegistrationRepository" })
public class UserDetailsServiceAutoConfiguration {
    
    

	@Bean
	@Lazy
	public InMemoryUserDetailsManager inMemoryUserDetailsManager(SecurityProperties properties,
			ObjectProvider<PasswordEncoder> passwordEncoder) {
    
    
			// 这里是从SecurityProperties中获取User对象(这里的User对象是SecurityProperties的静态内部类)
		SecurityProperties.User user = properties.getUser();
		List<String> roles = user.getRoles();
		// 然后创建InMemoryUserDetailsManager对象返回
		// 交给Spring容器管理
		return new InMemoryUserDetailsManager(User.withUsername(user.getName())
			.password(getOrDeducePassword(user, passwordEncoder.getIfAvailable()))
			.roles(StringUtils.toStringArray(roles))
			.build());
	}
	}

Observe the annotations on UserDetailsServiceAutoConfiguration @ConditionalOnMissingBean, what do you think of? Automatic configuration SecurityFilterChain encountered.
The meaning of the above configuration, if you want to use the default configuration, you must first satisfy the container does not containAuthenticationManager、AuthenticationProvider、UserDetailsService、AuthenticationManagerResolverExample of this condition.

Default username and password

From the above automatic configuration of UserDetailsService, we also found that the User object used is obtained from SecurityPropertiesit , so let's take a look at what kind of User object it is.

First of all, it is obtained by calling getUser, and this user is always a new User object, which is a static inner class instance.

Looking at the static internal class User attribute below, you can see that the username is "user", the password is a UUID string, and roles is a list collection, which can be specified multiple times.
Note: The getter and setter methods below are not intercepted.

Can you configure your own username and password?
Of course it can be dropped.
insert image description hereAs you can see, it is modified SecurityPropertiesby @ConfigurationPropertiesannotations (here you need to know that SecurityProperties is an object managed by the Spring container).

The @ConfigurationProperties annotation maps the value configured in the configuration file to the object modified by the annotation through setter injection.

So we can make our own configuration in the configuration file, and we can configure our own user name and password.

For example, I configure it like this:

# application.yml
spring:
  security:
    user:
      name: xxx
      password: 123

The user name and password will be changed.

Please add a picture description

Four. Summary

  • AuthenticationManager、ProviderManager、AuthenticationProvider关系。
    insert image description here
  • You have to know DaoAuthenticationProviderthe retrieveUser method and the additionalAuthenticationChecks method (These two methods apply the UserDetailsService and PasswordEncoder objects respectively). UsernamePasswordAuthenticationFilterIn the end, it is also to pass ProviderManagerthe authentication in , and finally transfer to AbstractUserDetailsAuthenticationProviderthe authentication of the parent class of DaoAuthenticationProvider to authenticate. We have to be clear about this process and these classes and methods.Convenient for later needs and available for debugging
  • We can implement UserDetailsServicethe interface (custom UserDetailsService), and then hand over the implementation class instance to the Spring container management, so that we will not use the default implementation, but our custom implementation.
  • UserDetails is the user details object, which encapsulates information such as user name, password, permissions, etc. It is also the return value of UserDetailsService, which can be customized.

Guess you like

Origin blog.csdn.net/qq_63691275/article/details/130925783