Spring Science (5) -- SMS verification code login function
Previous articles on spring Science Series
1,Spring security (1) -- authentication + authorization code implementation
2,Spring security (2) --- remember my feature implementation
3,Spring Science (3) -- function implementation of graphic verification code
4,Spring Science (4) -- implementation of SMS verification code function
1, Principle analysis of SMS login verification mechanism
Before we understand the login mechanism of SMS verification code, we first need to understand the mechanism of user account password login. Let's briefly analyze how Spring Security verifies the login method based on user name and password,
After the analysis, think about how to integrate SMS login authentication into Spring Security.
1. Account password login process
General account password login has the function of graphics verification code and remembering me, so its general process is like this.
1. After entering the user name, account number and picture verification code, the user clicks log in. For spring science, the SMS verification code Filter will be entered first, because it will be configured in the Before UsernamePasswordAuthenticationFilter, verify the information of the current verification code with the verification code of the picture verification code with session. 2. After the SMS verification code is passed, enter the UsernamePasswordAuthenticationFilter, and construct a temporary unauthenticated one according to the entered user name and password information UsernamePasswordAuthenticationToken, and submit the UsernamePasswordAuthenticationToken to the AuthenticationManager for processing. 3. The AuthenticationManager does not do the authentication processing itself. It traverses for each to find an AuthenticationProvider that conforms to the current login mode, and gives it to perform the authentication processing , for user name and password login mode, this Provider is DaoAuthenticationProvider. 4. Carry out a series of validation processing in this Provider. If the validation passes, a UsernamePasswordAuthenticationToken with added authentication will be reconstructed, and the token is passed back to UsernamePasswordAuthenticationFilter. 5. In the parent class AbstractAuthenticationProcessingFilter of the Filter, it will jump to successHandler or failureHandler according to the result of the previous step.
flow chart
2. SMS verification code login process
Because SMS login is not integrated into Spring Security, we often need to develop our own SMS login logic and integrate it into Spring Security, so here we imitate the account
Password login to achieve SMS verification code login.
1. There is a UsernamePasswordAuthenticationFilter for user name and password login. We have a SmsAuthenticationFilter and paste the code to change it. 2. UsernamePasswordAuthenticationToken is required for user name and password login. We need to create a SmsAuthenticationToken and paste the code to change it. 3. The user name and password login require DaoAuthenticationProvider. We imitate it as also implements authenticationprovider, which is called SmsAuthenticationProvider.
This picture is found on the Internet. I don't want to draw it
After we make the above three classes ourselves, the effect we want to achieve is shown in the figure above. When we log in with SMS verification code:
1. After SmsAuthenticationFilter, construct a SmsAuthenticationToken without authentication, and then submit it to the authentication manager for processing. 2. The authentication manager selects a suitable provider for processing through for each. Of course, we hope this provider is SmsAuthenticationProvider. 3. After the validation, an authenticated SmsAuthenticationToken is reconstructed and returned to SmsAuthenticationFilter. Based on the verification results of the previous step, filter jumps to the processing logic of success or failure.
2, Code implementation
1,SmsAuthenticationToken
First of all, we write SmsAuthenticationToken. Here, we directly refer to the UsernamePasswordAuthenticationToken source code, directly paste it, and change it.
explain
The principle originally represents the user name, which is reserved here, but represents the mobile number. The original code password of credentials can't be used for SMS login. Delete it directly. SmsCodeAuthenticationToken() has two construction methods: one is to construct without authentication, the other is to construct with authentication. The rest of the methods remove the useless attributes.
code
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; /** * In UsernamePasswordAuthenticationToken, this field represents the login user name, * Here is the mobile number for login */ private final Object principal; /** * Build a SmsCodeAuthenticationToken without authentication */ public SmsCodeAuthenticationToken(Object principal) { super(null); this.principal = principal; setAuthenticated(false); } /** * Build an authenticated SmsCodeAuthenticationToken */ public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; // must use super, as we override super.setAuthenticated(true); } @Override public Object getCredentials() { return null; } @Override public Object getPrincipal() { return this.principal; } @Override public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { if (isAuthenticated) { throw new IllegalArgumentException( "Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead"); } super.setAuthenticated(false); } @Override public void eraseCredentials() { super.eraseCredentials(); } }
2,SmsAuthenticationFilter
Then write SmsAuthenticationFilter, refer to the source code of UsernamePasswordAuthenticationFilter, paste it directly, and change it.
explain
The original static fields, including username and password, are removed and replaced with our mobile number field.
The intercepting Url of this filter is specified in SmsCodeAuthenticationFilter(), which I specify as / sms/login in post mode.
The rest of the way is to delete and change the invalid ones.
code
public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { /** * form Field name of mobile number in the form */ public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile"; private String mobileParameter = "mobile"; /** * POST only */ private boolean postOnly = true; public SmsCodeAuthenticationFilter() { //The address of SMS verification code is / sms/login and the request is also a post super(new AntPathRequestMatcher("/sms/login", "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } String mobile = obtainMobile(request); if (mobile == null) { mobile = ""; } mobile = mobile.trim(); SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile); // Allow subclasses to set the "details" property setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } protected String obtainMobile(HttpServletRequest request) { return request.getParameter(mobileParameter); } protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } public String getMobileParameter() { return mobileParameter; } public void setMobileParameter(String mobileParameter) { Assert.hasText(mobileParameter, "Mobile parameter must not be empty or null"); this.mobileParameter = mobileParameter; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } }
3,SmsAuthenticationProvider
This method is very important. Firstly, it can be selected by the authentication manager when using SMS authentication code to log in. Secondly, it needs to process the authentication logic in this class.
explain
Implement the AuthenticationProvider interface and the authentication () and supports() methods.
code
public class SmsCodeAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; /** * Process session utility class */ private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_SMS"; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; String mobile = (String) authenticationToken.getPrincipal(); checkSmsCode(mobile); UserDetails userDetails = userDetailsService.loadUserByUsername(mobile); // In this case, after the authentication is successful, a new authentication result with authentication should be returned SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities()); authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult; } private void checkSmsCode(String mobile) { HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest(); // Get picture verification code from session SmsCode smsCodeInSession = (SmsCode) sessionStrategy.getAttribute(new ServletWebRequest(request), SESSION_KEY_PREFIX); String inputCode = request.getParameter("smsCode"); if(smsCodeInSession == null) { throw new BadCredentialsException("Application verification code not detected"); } String mobileSsion = smsCodeInSession.getMobile(); if(!Objects.equals(mobile,mobileSsion)) { throw new BadCredentialsException("Incorrect mobile number"); } String codeSsion = smsCodeInSession.getCode(); if(!Objects.equals(codeSsion,inputCode)) { throw new BadCredentialsException("Verification code error"); } } @Override public boolean supports(Class<?> authentication) { // Determine whether authentication is a subclass or sub interface of SmsCodeAuthenticationToken return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } }
4,SmsCodeAuthenticationSecurityConfig
Now that you have customized the interceptor, you need to make changes in the configuration.
code
@Component public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired private SmsUserService smsUserService; @Autowired private AuthenctiationSuccessHandler authenctiationSuccessHandler; @Autowired private AuthenctiationFailHandler authenctiationFailHandler; @Override public void configure(HttpSecurity http) { SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter(); smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenctiationSuccessHandler); smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenctiationFailHandler); SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(); //It needs to change the interface of querying user information through user name to mobile number smsCodeAuthenticationProvider.setUserDetailsService(smsUserService); http.authenticationProvider(smsCodeAuthenticationProvider) .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
5,SmsUserService
Because the user name and password login ultimately query the user information through the user name, and the mobile phone authentication code login is through the mobile phone login, so we need to implement another SmsUserService by ourselves
@Service @Slf4j public class SmsUserService implements UserDetailsService { @Autowired private UserMapper userMapper; @Autowired private RolesUserMapper rolesUserMapper; @Autowired private RolesMapper rolesMapper; /** * Mobile number query user */ @Override public UserDetails loadUserByUsername(String mobile) throws UsernameNotFoundException { log.info("Mobile number query user, mobile number = {}",mobile); //TODO here I didn't write the sql to check the user information through the mobile number, because when I first built the user table, I didn't build the mobile field, and now I don't want to add it temporarily //TODO, so I'm going to use the user name to query the user information for the moment User user = userMapper.findOneByUsername("Small"); if (user == null) { throw new UsernameNotFoundException("No user information found"); } //Get user association role information. If it is blank, the user is not associated with a role List<RolesUser> userList = rolesUserMapper.findAllByUid(user.getId()); if (CollectionUtils.isEmpty(userList)) { return user; } //Get role ID collection List<Integer> ridList = userList.stream().map(RolesUser::getRid).collect(Collectors.toList()); List<Roles> rolesList = rolesMapper.findByIdIn(ridList); //Insert user role information user.setRoles(rolesList); return user; } }
6. Summary
The train of thought is very clear here. I'm here to summarize.
1. First, from the time of obtaining the verification, the current verification code information has been saved to the session, which includes the verification code and mobile phone number. 2. The user enters the authentication login, which is written directly in SmsAuthenticationFilter. First, verify whether the authentication code and mobile number are correct, and then query the user information. We can also open it into a user name and password to log in The filter specially verifies whether the verification code and mobile phone number are correct, and logs in the filter with the correct verification code. 3. There is also a key step in the SmsAuthenticationFilter process, that is, the user name and password login is the user-defined UserService. After the UserDetailsService is implemented, the user name information is queried through the user name, and here is Query user information by mobile number, so you need to customize SmsUserService to implement UserDetailsService.
3, Testing
1. Get verification code
The mobile number to obtain the verification code is 15612345678. Because there is no third-party sms SDK here, just output in the background.
Send the verification code: 254792 to the user whose mobile number is 15612345678
2. Landing
1) Incorrect verification code input
It is found that the login fails. Similarly, if the mobile number is not entered correctly, the login fails
2) Login succeeded
When the mobile number and SMS verification code are correct, the login is successful.
reference resources
1. Spring Security technology stack development enterprise level authentication and authorization (JoJo)
2,Spring boot integrates Spring Security (8) - SMS verification code login
I will be angry when others scold me for being fat, because I admit that I am fat in my heart. When people say I'm short, I feel funny because I know in my heart that I can't be short. That's why we get angry at other people's attacks. The one who attacks my shield is the spear of my heart (21)