In the section of adding graphic verification code in Spring Security, we have implemented the account password login based on Spring Boot + Spring Security, and integrated the function of graphic verification code. At present, another very common way of website login is SMS authentication code login, but Spring Security only provides the login authentication logic of account password by default, so to achieve the login authentication function of SMS authentication code, we need to imitate the Spring Security account password login logic code to achieve a set of our own authentication logic.
SMS verification code generation
In the previous section, Spring Security added a graphic verification code to integrate the function of SMS verification code login.
Similar to the graphic verification code, we first define a SMS verification code object, SmsCode:
public class SmsCode { private String code; private LocalDateTime expireTime; public SmsCode(String code, int expireIn) { this.code = code; this.expireTime = LocalDateTime.now().plusSeconds(expireIn); } public SmsCode(String code, LocalDateTime expireTime) { this.code = code; this.expireTime = expireTime; } public boolean isExpire() { return LocalDateTime.now().isAfter(expireTime); } public String getCode() { return code; } public void setCode(String code) { this.code = code; } public LocalDateTime getExpireTime() { return expireTime; } public void setExpireTime(LocalDateTime expireTime) { this.expireTime = expireTime; } }
The SmsCode object contains two properties: the code verification code and the expireTime expiration time. The isExpire method is used to determine whether the SMS verification code has expired.
Then add the corresponding method of generating SMS verification code related request in validatecontroller:
@RestController public class ValidateController { public final static String SESSION_KEY_SMS_CODE = "SESSION_KEY_SMS_CODE"; @GetMapping("/code/sms") public void createSmsCode(HttpServletRequest request, HttpServletResponse response, String mobile) throws IOException { SmsCode smsCode = createSMSCode(); sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_SMS_CODE + mobile, smsCode); // Output verification code to console instead of SMS sending service System.out.println("Your login verification code is:" + smsCode.getCode() + ",Valid for 60 seconds"); } private SmsCode createSMSCode() { //Introducing commons Lang package String code = RandomStringUtils.randomNumeric(6); return new SmsCode(code, 60); } }
Here we use the createmscode method to generate a 6-bit pure digital random number with an effective time of 60 seconds. Then, through the setAttribute method of the SessionStrategy object, the SMS verification code is saved to the Session, and the corresponding key is Session ﹣ key ﹣ SMS ﹣ code.
At this point, the SMS verification code generation module is completed, and the next step is to transform the login page.
Transformation landing page
We add a Form related to SMS verification code authentication in the login page:
<form action="/login/mobile" method="post"> <div class="form"> <h3>SMS verification code login</h3> <input type="text" placeholder="cell-phone number" name="mobile" value="18888888888" required="required"/> <br> <span style="display: inline"> <input type="text" name="smsCode" placeholder="SMS verification code"/> <a href="/code/sms?mobile=18888888888">Send verification code</a> </span> <br> <button type="submit">Sign in</button> </div> </form>
The value of the a tag's href attribute corresponds to the request URL of our SMS verification code generation method. The action of Form corresponds to the request URL of the SMS verification code login method, which is implemented below. At the same time, we need to configure / code/sms path authentication free in Spring Security:
@Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // Add verification code verification filter .formLogin() // Form login // http.httpBasic() // HTTP Basic .loginPage("/authentication/require") // Login jump URL .loginProcessingUrl("/login") // Process form login URL .successHandler(authenticationSucessHandler) // Login processed successfully .failureHandler(authenticationFailureHandler) // Failed to process login .and() .authorizeRequests() // Authorization configuration .antMatchers("/authentication/require", "/login.html", "/code/image","/code/sms").permitAll() // Request path without authentication .anyRequest() // All requests .authenticated() // All need certification .and() .csrf().disable(); }
Restart project, access http://localhost:8080/login.html:
Click send verification code, and the console output is as follows:
Next, we start to implement the login authentication logic using SMS verification code.
Add SMS verification code authentication
In Spring Security, the process of using user name and password authentication is as follows:
Spring Security uses the UsernamePasswordAuthenticationFilter filter to intercept user name and password Authentication requests, encapsulates the user name and password into a UsernamePasswordToken object and submits it to the AuthenticationManager for processing. AuthenticationManager will select an AuthenticationProvider (here is DaoAuthenticationProvider, one of the implementation classes of AuthenticationProvider) that supports handling this type of Token for Authentication. During the Authentication process, DaoAuthenticationProvider will call the loadUserByUsername method of UserDetailService to handle the Authentication. If the Authentication is passed (that is, UsernamePasswordToken If the user name and password in n Match), a UserDetails type object will be returned and the Authentication information will be saved in the Session. After Authentication, we can obtain the Authentication information through the Authentication object.
Since Spring Security does not use the process of providing SMS verification code authentication, we need to follow the above process to achieve:
In this process, we have customized a filter named SmsAuthenticationFitler to intercept SMS Authentication code login requests and encapsulate the mobile phone number into an object called SmsAuthenticationToken. In Spring Security, Authentication processing needs to be represented by Authentication manager, so we still leave SmsAuthenticationToken to Authentication manager. Then we need to define a SmsAuthenticationProvider that supports processing SmsAuthenticationToken object. SmsAuthenticationProvider calls the loadUserByUsername method of UserDetailService to process Authentication. Different from the user name and password Authentication, this is to query whether there is a corresponding user in the database through the mobile number in the SmsAuthenticationToken. If there is one, the user information will be encapsulated in the UserDetails object to return and the authenticated information will be saved in the Authentication object.
To implement this process, we need to define SmsAuthenticationFitler, SmsAuthenticationToken and SmsAuthenticationProvider, and add these components to Spring Security. Let's implement this process step by step.
Define SmsAuthenticationToken
Check the source code of UsernamePasswordAuthenticationToken, copy it and rename it to SmsAuthenticationToken, and modify it slightly. The modified code is as follows:
org.springframework.security.authentication.UsernamePasswordAuthenticationToken
public class SmsAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID; /** * cell-phone number */ private final Object principal; /** * SmsCodeAuthenticationFilter Unauthenticated Authentication built in * @param mobile */ public SmsAuthenticationToken(String mobile) { super(null); this.principal = mobile; setAuthenticated(false); } /** * SmsCodeAuthenticationProvider Build authenticated Authentication in * @param principal * @param authorities */ public SmsAuthenticationToken(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(); } }
SmsAuthenticationToken contains a principle attribute. From its two constructors, we can see that the principle stores the mobile number before authentication and the user information after authentication. UsernamePasswordAuthenticationToken originally contained a credentials attribute for storing passwords, which is not needed here.
Define SmsAuthenticationFilter
org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter
After defining the SmsAuthenticationToken, we then define the filter SmsAuthenticationFilter, which is used to process the SMS authentication code login request. Similarly, copy the UsernamePasswordAuthenticationFilter source code and modify it slightly:
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter { /** * request Must have mobile parameter in */ public static final String MOBILE_KEY = "mobile"; private String mobileParameter = MOBILE_KEY; /** * post request */ private boolean postOnly = true; /** * Mobile phone verification code login request processing url */ public SmsAuthenticationFilter() { super(new AntPathRequestMatcher("/login/mobile", "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { //Determine if it is a post request if (postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException( "Authentication method not supported: " + request.getMethod()); } //Get phone number from request String mobile = obtainMobile(request); if (mobile == null) { mobile = ""; } mobile = mobile.trim(); //Create smscodeauthenticationtoken (not authenticated) SmsAuthenticationToken authRequest = new SmsAuthenticationToken(mobile); //Set user information setDetails(request, authRequest); //Return Authentication instance return this.getAuthenticationManager().authenticate(authRequest); } /** * Get mobile number */ protected String obtainMobile(HttpServletRequest request) { return request.getParameter(mobileParameter); } protected void setDetails(HttpServletRequest request, SmsAuthenticationToken authRequest) { authRequest.setDetails(authenticationDetailsSource.buildDetails(request)); } 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; } public final String getMobileParameter() { return mobileParameter; } }
The constructor specifies that the filter takes effect when the request is / login/mobile and the request method is POST. The value of the mobileParameter property is mobile, which corresponds to the name property of the mobile number input box on the login page. The attemptAuthentication method obtains the mobile parameter value from the request, and calls the SmsAuthenticationToken(String mobile) construction method of SmsAuthenticationToken to create a SmsAuthenticationToken. The next step, as shown in the flowchart, is for the SmsAuthenticationFilter to hand over the SmsAuthenticationToken to the authentication manager for processing.
Define SmsAuthenticationProvider
After creating the SmsAuthenticationFilter, we need to create a class that supports handling the Token of this type, namely SmsAuthenticationProvider, which needs to implement two abstract methods of AuthenticationProvider:
public class SmsAuthenticationProvider implements AuthenticationProvider { private UserDetailService userDetailService; @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsAuthenticationToken authenticationToken = (SmsAuthenticationToken) authentication; //Call custom userDetailsService authentication UserDetails userDetails = userDetailService.loadUserByUsername((String) authenticationToken.getPrincipal()); if (userDetails == null) { throw new InternalAuthenticationServiceException("The user corresponding to the mobile number was not found"); } //If user is not empty, rebuild SmsCodeAuthenticationToken (authenticated) SmsAuthenticationToken authenticationResult = new SmsAuthenticationToken(userDetails, userDetails.getAuthorities()); authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult; } /** * Only Authentication for SmsCodeAuthenticationToken uses this Provider Authentication * @param authentication * @return */ @Override public boolean supports(Class<?> authentication) { return SmsAuthenticationToken.class.isAssignableFrom(authentication); } public UserDetailService getUserDetailService() { return userDetailService; } public void setUserDetailService(UserDetailService userDetailService) { this.userDetailService = userDetailService; } }
The supports method specifies that the Token type to support processing is SmsAuthenticationToken, and the authenticate method is used to write specific authentication logic. In the authentication method, we take the phone number information from SmsAuthenticationToken and call the loadUserByUsername method of UserDetailService. In the authentication of user name and password type, the main logic of this method is to query the user information through the user name. If the user exists and the password is consistent, the authentication succeeds. In the authentication process of SMS verification code, this method needs to query the user through the mobile phone number, and if the user exists, the authentication passes. After passing the authentication, the SmsAuthenticationToken (object principal, collection <? Extends grantedauthority > authorities) constructor of SmsAuthenticationToken is called to construct an authenticated Token, which contains user information and user permissions.
You may ask, why is there no verification of SMS verification code in this step? In fact, the verification of SMS verification code is completed before SmsAuthenticationFilter, that is, only when the SMS verification code is correct can the authentication process be started. So next we need to set a filter to verify the correctness of SMS verification code.
Define SmsCodeFilter
@Component public class SmsCodeFilter extends OncePerRequestFilter { @Autowired private AuthenticationFailureHandler authenticationFailureHandler; private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { if (StringUtils.equalsIgnoreCase("/login/mobile", httpServletRequest.getRequestURI()) && StringUtils.equalsIgnoreCase(httpServletRequest.getMethod(), "post")) { try { validateSmsCode(new ServletWebRequest(httpServletRequest)); } catch (ValidateCodeException e) { authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e); return; } } filterChain.doFilter(httpServletRequest, httpServletResponse); } private void validateSmsCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException { String smsCodeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "smsCode"); String mobile = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "mobile"); SmsCode codeInSession = (SmsCode) sessionStrategy.getAttribute(servletWebRequest, ValidateController.SESSION_KEY_SMS_CODE + mobile); if (StringUtils.isBlank(smsCodeInRequest)) { throw new ValidateCodeException("Verification code cannot be empty!"); } if (codeInSession == null) { throw new ValidateCodeException("Verification code does not exist, please resend!"); } if (codeInSession.isExpire()) { sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_SMS_CODE + mobile); throw new ValidateCodeException("The verification code has expired, please resend!"); } if (!StringUtils.equalsIgnoreCase(codeInSession.getCode(), smsCodeInRequest)) { throw new ValidateCodeException("The verification code is incorrect!"); } sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_SMS_CODE + mobile); } }
Configuration takes effect
After defining the required components, we need to make some configuration, and combine these components to form a process corresponding to the above flowchart. Create a configuration class SmsAuthenticationConfig:
@Component public class SmsAuthenticationConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired private AuthenticationSuccessHandler authenticationSuccessHandler; @Autowired private AuthenticationFailureHandler authenticationFailureHandler; @Autowired private UserDetailService userDetailService; @Override public void configure(HttpSecurity http) throws Exception { SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter(); smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); smsAuthenticationFilter.setAuthenticationSuccessHandler(authenticationSuccessHandler); smsAuthenticationFilter.setAuthenticationFailureHandler(authenticationFailureHandler); SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider(); smsAuthenticationProvider.setUserDetailService(userDetailService); http.authenticationProvider(smsAuthenticationProvider) .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); } }
In the first step of the process, you need to configure SmsAuthenticationFilter, and set the AuthenticationManager, AuthenticationSuccessHandler, and AuthenticationFailureHandler properties respectively. These properties are from the AbstractAuthenticationProcessingFilter class inherited by SmsAuthenticationFilter.
The second step is to configure SmsAuthenticationProvider. In this step, we only need to inject our own UserDetailService.
Finally, HttpSecurity's authenticationprovider method is called to specify that the authenticationprovider is SmsAuthenticationProvider, and SmsAuthenticationFilter filter is added after UsernamePasswordAuthenticationFilter.
Now we have combined the components of SMS verification code authentication. The last step is to configure SMS verification code verification filter and add SMS verification code authentication process to Spring Security. Add the following configuration in the configure method of BrowserSecurityConfig:
@Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Autowired private MyAuthenticationSucessHandler authenticationSucessHandler; @Autowired private MyAuthenticationFailureHandler authenticationFailureHandler; @Autowired private ValidateCodeFilter validateCodeFilter; @Autowired private UserDetailService userDetailService; @Autowired private DataSource dataSource; @Autowired private SmsCodeFilter smsCodeFilter; @Autowired private SmsAuthenticationConfig smsAuthenticationConfig; @Override protected void configure(HttpSecurity http) throws Exception { http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // Add verification code verification filter .addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class) // Add SMS verification code verification filter .formLogin() // Form login .loginPage("/authentication/require") // Login jump URL .loginProcessingUrl("/login") // Process form login URL .successHandler(authenticationSucessHandler)//Login processed successfully .failureHandler(authenticationFailureHandler)//Failed to process login .and() .rememberMe() .tokenRepository(persistentTokenRepository()) // Configure token persistence warehouse .tokenValiditySeconds(3600) // remember expiration time, in seconds .userDetailsService(userDetailService) // Handling automatic login logic .and() .authorizeRequests() // Authorization configuration .antMatchers("/authentication/require", "/login.html", "/code/image","/code/sms").permitAll() // No authentication required for login jump URL .anyRequest() // All requests .authenticated() // All need certification .and().csrf().disable() .apply(smsAuthenticationConfig);// Add SMS verification code authentication configuration to Spring Security } /** * token Persistent objects */ @Bean public PersistentTokenRepository persistentTokenRepository() { JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); jdbcTokenRepository.setCreateTableOnStartup(false); return jdbcTokenRepository; } }
test
Restart the project, and the browser opens two windows to access http://localhost:8080/login.html,
Click the first window to send the verification code, and the console output is as follows:
Enter the verification code in the second window to log in:
Certification success
Source: https://gitee.com/hekang_admin/security-demo4.git