Spring security < < V > authentication – account / email / mobile number + password
Basic environment of local project
environment | edition |
---|---|
jdk | 1.8.0_201 |
maven | 3.6.0 |
Spring-boot | 2.3.3.RELEASE |
1. Introduction
spring security is a framework providing authentication, authorization and preventing common attacks. It has first-class support for both imperative and reactive applications. It is a de facto standard for protecting spring based applications.
For details, see spring security official website
2. Authentication (login)
Through the introduction of the previous two articles, we should also be clear about the basic concepts and the core concepts in the security framework. Starting from this article, we will mainly focus on the development at the code level; In the permission framework, authentication is the most difficult part. The next few articles mainly talk about authentication.
Introduction to spring security
Detailed explanation of spring security < II > configuration items
Spring security authentication case code
Source code analysis of spring security authentication
3. Certification process
In the process of certification, we put the previous article Source code analysis of spring security authentication Take the last diagram of, and the whole process will write code in this direction;
In the whole login process, the code in practical application will be written in detail through common login methods. In the code case, for simplicity, database query operation and simulation operation will be used;
In the temporarily sorted cases, the following two login methods will be demonstrated through two blog posts:
1. Account / email / mobile phone number + password login;

2. Mobile number + SMS verification code login
Currently, this blog post is mainly about the first one, account / email / mobile phone number + password
Other forms of login, such as QQ login, wechat login, microblog login... These are based on OAuth2 protocol. When I have time later, I will explain this protocol in detail;
3. A project that builds the underlying code
Copy item Spring security authentication case code Modify the project name to badger-spring-security-4;
4. Account / email / mobile number + password
4.1 code design analysis
From Baidu's login, you can see that there is only one login button, so the entrance is one, one entrance. You should take into account different ID s to log in;
From the certification flow chart, our steps are as follows:
1. AuthenticationFilter can only be one business processing interceptor (of course, you can also have multiple interceptors, and then distribute them down the interceptor chain. This is not recommended. The business logic is complicated);
2. Generally, the default authentication manager is used. When initializing, the default manager has been injected into the container, which is a single instance;
3. AuthenticationProvider: according to the above requirements, there can be one or three authentication providers, each with its own advantages;
**How to authenticate a provider: * * in fact, in the provider's authentication method, when executing UserDetailsService to query user details, OR operation is used. The sql is similar to the following
select * from user where username='xxx' or email='xxx@qq.com' or phone='186xxx13511'
In this way, we can find out the specific users; The advantages can also be seen. Only query the database once;
**Practices of multiple authentication providers: * * one AuthenticationProvider corresponds to one method for querying users. Among the above, we have three, that is, three; The corresponding sql statements are as follows:
select * from user where username ='xxx'; select * from user where email='xxx@qq.com'; select * from user where phone='186xxx13511';
When AuthenticationManager authenticates a provider, it can also be seen from the source code analysis in the previous article that it is traversed and executed. If one authentication is successful, it means that the authentication is passed;
4.2 coding
In the whole authentication process, there are not many places to write code;
- Interceptor: according to the actual needs, write it if necessary, and use the official one if not necessary;
- AuthenticationManager: no need to write;
- AuthenticationProvider: it needs to be written. The rules of each authentication are different
- UserDetailsService: it needs to be written. Although the query result is to find the user, the query conditions are different;
- AbstractAuthenticationToken: token type. It is written according to the actual needs. If necessary, it is official;
- AuthenticationSuccessHandler: the processor is successfully written once globally;
- AuthenticationFailureHandler: the processor is successfully written once globally;
To sum up, we need the following codes: interceptor, AuthenticationProvider, UserDetailsService, AbstractAuthenticationToken;
1. Preparation of interceptor
Log in to the interceptor in the form of account + password. In spring security, there is a default org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter processor. In the previous article, we also saw the source code analysis. The intercepted POST request has a request parameter in the form of @ RequestParam, Parameter field names are username and password; We also continue to use this interceptor;
If there are other requirements, such as adding a new picture verification code, there are also two processing schemes:
- Before the UsernamePasswordAuthenticationFilter interceptor, a new interceptor of image verification code is added for independent verification;
- Copy the UsernamePasswordAuthenticationFilter code and add an authentication parameter imageCode to verify the image authentication code
Here, I won't demonstrate this, which is very easy to implement;
2. AuthenticationProvider coding
package com.badger.spring.boot.security.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.BadCredentialsException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.password.PasswordEncoder; import lombok.Getter; import lombok.Setter; import lombok.extern.slf4j.Slf4j; /** * Account password login verifier * @author liqi */ @Slf4j public class UsernamePasswordAuthenticationProvider implements AuthenticationProvider { @Getter @Setter private UserDetailsService userDetailsService; @Autowired private PasswordEncoder passwordEncoder; public UsernamePasswordAuthenticationProvider(UserDetailsService userDetailsService) { super(); this.userDetailsService = userDetailsService; } @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { // Get form user name String username = (String) authentication.getPrincipal(); // Get the password filled in by the form user String password = (String) authentication.getCredentials(); UserDetails userDetails; try { userDetails = userDetailsService.loadUserByUsername(username); } catch (Exception e) { // When the exception is not an authentication exception, the exception cannot be thrown upward, and the exception handling controller cannot return the default exception. You need to handle the exception as an authentication exception throw new AuthenticationServiceException(e.getMessage()); } if (!passwordEncoder.matches(password, userDetails.getPassword())) { log.info("Current login:{},Current login password:{}", username, password); throw new BadCredentialsException("The user password is incorrect"); } return new UsernamePasswordAuthenticationToken(username, null, userDetails.getAuthorities()); } @Override public boolean supports(Class<?> authentication) { // Verifier for account password login return UsernamePasswordAuthenticationToken.class.isAssignableFrom(authentication); } }
The code is also relatively simple. Follow the authentication process in the figure above

-
According to our requirements, if the user enters the user name in the form, then the user name is the user name and enter the mobile phone number, and the user name is the mobile phone number. Is this easy to understand?
// Get form user name String username = (String) authentication.getPrincipal(); // Get the password filled in by the form user String password = (String) authentication.getCredentials();
-
Authentication authenticate(Authentication authentication): in the process of verifying the password, you need to use the implementation of UserDetailsService to query the specific user; In my construction method, I receive interface type parameters (policy mode);
-
Boolean supports (class <? > authentication): verify the specific type; In the verification process, the parameters we use are username and password. We don't need to implement one of the two parameters;
Then, let's start writing the implementation code of the UserDetailsService interface
3. Code UserDetailsService to query user details
3.1. Write the entity class of the database, using only the fields currently required
/** * User entity * @author liqi */ @Data public class UserEntity { /**Primary key*/ private Integer id; /**User name*/ private String name; /**User account*/ private String username; /**cell-phone number*/ private String password; /**mailbox*/ private String email; /**cell-phone number*/ private String phone; }
3.2. The UserDetailsService interface code is as follows:
public interface UserDetailsService { UserDetails loadUserByUsername(String username) throws UsernameNotFoundException; }
You can see that according to a parameter, a UserDetails object is returned, and the UserDetails object is also an interface;
3.3. Implement UserDetails interface
/** * Domain model of user system * @author liqi */ public class SystemUserDetails implements UserDetails { private static final long serialVersionUID = -7127141675788677116L; /**user name.*/ @Setter private String username; /**password.*/ @Setter private String password; /**User role information*/ @Setter private Collection<? extends GrantedAuthority> authorities; /**Current user information*/ @Getter @Setter private UserEntity user; public SystemUserDetails() { super(); } public SystemUserDetails(String username, String password, UserEntity user, Collection<? extends GrantedAuthority> authorities) { this.username = username; this.password = password; this.authorities = authorities; this.user = user; } @Override public Collection<? extends GrantedAuthority> getAuthorities() { return authorities; } @Override public String getPassword() { return this.password; } @Override public String getUsername() { return this.username; } @Override public boolean isAccountNonExpired() { // Whether the account has expired return true; } @Override public boolean isAccountNonLocked() { // The account is not locked return true; } @Override public boolean isCredentialsNonExpired() { // Credentials have not expired return true; } @Override public boolean isEnabled() { // Account deactivated return true; } }
See the comments for the method name and specific function in the interface;
3.4. Implement account + password UserDetailsService interface
It is implemented by anonymous classes. A list is used to store some default users. In the actual project, the database can be used for direct query
package com.badger.spring.boot.security.config; import java.util.ArrayList; import java.util.List; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.core.userdetails.UsernameNotFoundException; import com.badger.spring.boot.security.entity.SystemUserDetails; import com.badger.spring.boot.security.entity.UserEntity; @Configuration public class SecurityConfig { static final List<UserEntity> USER_LIST = new ArrayList<>(); static { for (int i = 1; i < 6; i++) { UserEntity userEntity = new UserEntity(); userEntity.setId(i); userEntity.setName("Tester" + i); userEntity.setUsername("ceshi_" + i); // The password is the result of encrypting 123456 with the PasswordEncoder class userEntity.setPassword("$2a$10$D1q09WtH./yTfFTh35n0k.o6yZIXwxIW1/ex6/EjYTF7EiNxXyF7m"); userEntity.setEmail("100" + i + "@qq.com"); userEntity.setPhone("186xxxx351" + i); USER_LIST.add(userEntity); } } /************************Account password login**********************/ @Bean public UserDetailsService usernamePasswordUserDetails() { return new UserDetailsService() { @Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { UserEntity user = null; for (UserEntity userEntity : USER_LIST) { if (username.equals(userEntity.getUsername()) || username.equals(userEntity.getPhone()) || username.equals(userEntity.getEmail())) { user = userEntity; } } if (user != null) { return new SystemUserDetails(user.getUsername(), user.getPassword(), user, null); } throw new UsernameNotFoundException("User is not registered, please register first"); } }; } @Bean public AuthenticationProvider usernamePasswordAuthenticationProvider() { return new UsernamePasswordAuthenticationProvider(usernamePasswordUserDetails()); } }
According to the above authentication flowchart, an AuthenticationProvider matches a UserDetailsService;
4. Authentication process serial code
Modify the WebSecurityConfig class in the project, and the results are as follows
package com.badger.spring.boot.security.config; import java.util.List; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.configurers.FormLoginConfigurer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; @EnableWebSecurity public class WebSecurityConfig extends WebSecurityConfigurerAdapter { private static final String[] EXCLUDE_URLS = { "/**/*.js", "/**/*.css", "/**/*.jpg", "/**/*.png", "/**/*.gif", "/v2/**", "/errors", "/error", "/favicon.ico", "/swagger-ui.html/**", "/swagger-ui/**", "/webjars/**", "/swagger-resources/**", "/auth/login" }; @Autowired private AuthenticationSuccessHandler successHandler; @Autowired private AuthenticationFailureHandler failureHandler; @Autowired AccessDeniedHandler deniedHandler; @Autowired AuthenticationEntryPoint entryPoint; @Autowired private List<AuthenticationProvider> authenticationProviderList; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Override protected void configure(HttpSecurity http) throws Exception { // Login processor for (AuthenticationProvider authenticationProvider : authenticationProviderList) { http.authenticationProvider(authenticationProvider); } // Global exception configuration http.exceptionHandling().accessDeniedHandler(deniedHandler).authenticationEntryPoint(entryPoint); http.authorizeRequests().antMatchers(EXCLUDE_URLS).permitAll(); // Form operation FormLoginConfigurer<HttpSecurity> formLogin = http.formLogin(); // Form request success processor and failure processor; Conflicts with loginPage. loginPage does not take effect after configuration formLogin.successHandler(successHandler).failureHandler(failureHandler); // post request address of form submission, user parameter name formLogin.loginProcessingUrl("/auth/login"); http.csrf().disable(); } }
Compared with the previous code:
1. Remove the encrypted default user in memory;
2. Take out all the authentication providers injected into the spring container and put them into the authentication manager for management;
@Autowired private List<AuthenticationProvider> authenticationProviderList; // Login processor for (AuthenticationProvider authenticationProvider : authenticationProviderList) { http.authenticationProvider(authenticationProvider); }
5. Test demonstration
After the project is started, the implementation results are obtained
curl -X POST "http://localhost:8080/auth/login?username=ceshi_1&password=123456" -H "accept: */*" curl -X POST "http://localhost:8080/auth/login?username=1001@qq.com&password=123456" -H "accept: */*" curl -X POST "http://localhost:8080/auth/login?username=186xxxx3511&password=123456" -H "accept: */*"
{ "code": 200, "message": "ceshi_1" } { "code": 200, "message": "1001@qq.com" } { "code": 200, "message": "186xxxx3511" }
Detailed code, you can view Code cloud