Spring Security Resolution (4) - Development of Short Message Logon
When learning Spring Cloud, when encountering the related content of authorized service oauth, he always knows half of it, so he decided to study and collate the content, principle and design of Spring Security, Spring Security Oauth2 and other permissions, authentication-related content. This series of articles is written to enhance the impression and understanding in the process of learning. If there is any infringement, please let me know.
Project environment:
- JDK1.8
- Spring boot 2.x
- Spring Security 5.x
First, how to realize the function of SMS login on the basis of Security?
_Looking back on the process of form login implemented by Security:
_From the process, we find that it has special processing or other sister implementation subclasses in the login process.
:
- Authentication Filter: Used to intercept login requests;
- Authentication object is not authenticated, which is used as the reference of authentication method.
- Authentication Provider performs authentication processing.
So we can intercept it completely by customizing a Sms Authentication Filter, a Sms Authentication Token to transmit authentication data, and a Sms Authentication Provider to process authentication business. Because we know that the doFilter of UsernamePassword Authentication Filter is implemented by AbstractAuthentication Processing Filter, while the UsernamePassword Authentication Filter itself only implements the attemptAuthentication() method. According to this design, our SmsAuthentication Filter only implements the attemptAuthentication() method, so how to verify the authentication code? At this point, we need to call a validation filter: ValidateCodeFilter, which implements the validation code, before the Sms Authentication Filter. The process after the implementation is sorted out as follows:
2. Development of Short Message Logon Authentication
(1) Implementation of Sms Authentication Filter
_. Simulate Username Password Authentication Filter to implement Sms Authentication Filter. The code is as follows:
@EqualsAndHashCode(callSuper = true) @Data public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { // Get the parameter name of the cell phone number passed in the request private String mobileParameter = SecurityConstants.DEFAULT_PARAMETER_NAME_MOBILE; private boolean postOnly = true; // Constructor, which configures the request address url to be intercepted by its interceptor public SmsCodeAuthenticationFilter() { super(new AntPathRequestMatcher(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE, "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException { // Determine whether the request is POST mode if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } // Call the obtainMobile method to get the phone number from the request String mobile = obtainMobile(request); if (mobile == null) { mobile = ""; } mobile = mobile.trim(); // Create an unauthenticated SmCodeAuthenticationToken object SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile); setDetails(request, authRequest); // Call Authentication Method return this.getAuthenticationManager().authenticate(authRequest); } /** * Get the cell phone number */ protected String obtainMobile(HttpServletRequest request) { return request.getParameter(mobileParameter); } /** * Implementing the Username Password Authentication Filter unchanged (note here is SmsCode Authentication Token) */ protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } /** * set Method of Open Setting RemmemberMeServices */ @Override public void setRememberMeServices(RememberMeServices rememberMeServices) { super.setRememberMeServices(rememberMeServices); } }
There are several points for attention in its internal implementation:
- Setting parameter properties for transmitting mobile phone number
- The constructor calls the parametric constructor of the parent class, which is mainly used to set the url it is intercepting
- The implementation of attempt Authentication () copying UsernamePassword Authentication Filter needs to be reformed in two aspects: 1. Obtain Mobile obtains mobile phone number information 2. Create SmsCode Authentication Token object
- In order to realize SMS login and have the function of remembering me, here we open the setRememberMeServices() method to set rememberMeServices.
(2) Implementation of Sms Authentication Token
Similarly, we simulate Username Password Authentication Token to implement Sms Authentication Token:
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; private final Object principal; /** * When not authenticated, the content is mobile phone number * @param mobile */ public SmsCodeAuthenticationToken(String mobile) { super(null); this.principal = mobile; setAuthenticated(false); } /** * * When the authentication is successful, the user information is in it. * * @param principal * @param authorities */ public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; 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(); } }
_Compared with Username Password Authentication Token, we reduce credentials (which can be understood as passwords), and the rest are basically intact.
(3) Implementation of Sms Authentication Provider
Since SmsCode Authentication Provider is a completely new and different authentication delegation implementation, we do not need to refer to Dao Authentication Provider to write this according to our own assumptions. Look at the code we implemented ourselves:
@Data public class SmsCodeAuthenticationProvider implements AuthenticationProvider { private UserDetailsService userDetailsService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal()); if (user == null) { throw new InternalAuthenticationServiceException("Unable to access user information"); } SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities()); authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult; } @Override public boolean supports(Class<?> authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } }
_realizes its interface methods authenticate() and supports() by directly inheriting Authentication Provider. Support () We refer directly to other Providers to determine whether the Authentication currently being processed is SmsCodeAuthentication Token or its subclasses. Auhenticate () We call the loadUserByUsername() method of userDetails Service directly. Because the validation code has passed the validation of ValidateCodeFilter, we can only query the user information through the mobile phone number to directly determine the success of the current user authentication and generate the authenticated SM. SCodeAuthentication Token returns.
(4) Implementation of ValidateCodeFilter
_As we described earlier, ValidateCodeFilter only validates the validation code. Here, we set up a validation code generated by redis to compare the user input validation code:
@Component public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean { /** * Verification Code Check Failure Processor */ @Autowired private AuthenticationFailureHandler authenticationFailureHandler; /** * System configuration information */ @Autowired private SecurityProperties securityProperties; @Resource private StringRedisTemplate stringRedisTemplate; /** * Store all URLs that require validation codes */ private Map<String, String> urlMap = new HashMap<>(); /** * Tool class to verify whether the request url matches the configured url */ private AntPathMatcher pathMatcher = new AntPathMatcher(); /** * Initialize the url configuration information to intercept */ @Override public void afterPropertiesSet() throws ServletException { super.afterPropertiesSet(); urlMap.put(SecurityConstants.DEFAULT_LOGIN_PROCESSING_URL_MOBILE, SecurityConstants.DEFAULT_PARAMETER_NAME_CODE_SMS); addUrlToMap(securityProperties.getSms().getSendSmsUrl(), SecurityConstants.DEFAULT_PARAMETER_NAME_CODE_SMS); } /** * The URL s that need to verify the validation codes in the system are put into the map according to the type of validation. * * @param urlString * @param smsParam */ protected void addUrlToMap(String urlString, String smsParam) { if (StringUtils.isNotBlank(urlString)) { String[] urls = StringUtils.splitByWholeSeparatorPreserveAllTokens(urlString, ","); for (String url : urls) { urlMap.put(url, smsParam); } } } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { String code = request.getParameter(getValidateCode(request)); if (code != null) { try { String oldCode = stringRedisTemplate.opsForValue().get(request.getParameter(SecurityConstants.DEFAULT_PARAMETER_NAME_MOBILE)); if (StringUtils.equalsIgnoreCase(oldCode,code)) { logger.info("Verification Code Pass"); } else { throw new ValidateCodeException("Verification code failure or error!"); } } catch (AuthenticationException e) { authenticationFailureHandler.onAuthenticationFailure(request, response, e); return; } } chain.doFilter(request, response); } /** * Acquisition of check codes * * @param request * @return */ private String getValidateCode(HttpServletRequest request) { String result = null; if (!StringUtils.equalsIgnoreCase(request.getMethod(), "get")) { Set<String> urls = urlMap.keySet(); for (String url : urls) { if (pathMatcher.match(url, request.getRequestURI())) { result = urlMap.get(url); } } } return result; } }
Here we mainly look at doFilter International to implement verification code verification logic.
Third, how to add the Filter setting SMS to the FilterChain to take effect?
Here we need to introduce a new configuration class, SmsCodeAuthentication Security Config. In fact, the modern code is as follows:
@Component public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired private AuthenticationSuccessHandler authenticationSuccessHandler ; @Autowired private AuthenticationFailureHandler authenticationFailureHandler; @Resource private UserDetailsService userDetailsService; @Override public void configure(HttpSecurity http) throws Exception { SmsCodeAuthenticationFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationFilter(); // Setting up Authentication Manager smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); // Setting up successful and failed processors, respectively smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler); smsCodeAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler); // Setting up RememberMeServices smsCodeAuthenticationFilter.setRememberMeServices(http .getSharedObject(RememberMeServices.class)); // Create SmsCode Authentication Provider and set userDetails Service SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(); smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService); // Add Provider to it http.authenticationProvider(smsCodeAuthenticationProvider) // Add the filter after UsernamePassword Authentication Filter .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }
Finally, we need to refer to SmsCodeAuthentication Security Config in the Spring Security Config configuration class:
http.addFilterBefore(validateCodeFilter, AbstractPreAuthenticatedProcessingFilter.class) .apply(smsCodeAuthenticationSecurityConfig) . ...
IV. Adding Authentication Code Interface and Authentication Code Logon Form
_New send authentication code interface (mainly set as unauthorized access):
@GetMapping("/send/sms/{mobile}") public void sendSms(@PathVariable String mobile) { // Random Generation of 6-bit Digital Strings String code = RandomStringUtils.randomNumeric(6); // Cache to redis through stringRedisTemplate stringRedisTemplate.opsForValue().set(mobile, code, 60 * 5, TimeUnit.SECONDS); // Simulated Sending Short Message Verification Code log.info("To mobile phone: " + mobile + " Sending a short message verification code is: " + code); }
_New authentication code login form:
// Note that the request interface here is consistent with the constructor settings of SmsAuthentication Filter <form action="/loginByMobile" method="post"> <table> <tr> <td>Cell-phone number:</td> <td><input type="text" name="mobile" value="15680659123"></td> </tr> <tr> <td>Short Message Verification Code:</td> <td> <input type="text" name="smsCode"> <a href="/send/sms/15680659123">Send Verification Code</a> </td> </tr> <tr> <td colspan='2'><input name="remember-me" type="checkbox" value="true"/>Remember me</td> </tr> <tr> <td colspan="2"> <button type="submit">Sign in</button> </td> </tr> </table> </form>
V. Personal Summary
In fact, another way of login is realized. The key points are filter, Authentication Token and Authentication Provider. It is organized as follows: intercepting by customizing a Sms Authentication Filter, transmitting authentication data by an Authentication Token, and processing authentication business by an Authentication Provider. Because we know that the doFilter of UsernamePassword Authentication Filter is implemented by AbstractAuthentication Processing Filter, while the UsernamePassword Authentication Filter itself only implements the attemptAuthentication() method. According to this design, our Authentication Filter only implements the attemptAuthentication() method, but at the same time we need to call an implementation validation filter: ValidatFilter before Authentication Filter. As in the following flow chart, you can add any login mode in this way:
_This paper introduces the code of SMS login development, which can access the security module in the code warehouse, the github address of the project: https://github.com/BUG9/sprin...
If you are interested in these, you are welcome to support star, follow, collect and forward!