brief introduction
Using SMS authentication code to log in, security does not support SMS authentication code login process by default, so we imitate password login to do a new authentication process.
Login process
- SmsCodeAuthenticationFilter SMS login request
- SmsCodeAuthenticationProvider provides an implementation class for SMS login processing
- SmsCodeAuthenticationToken stores authentication information (including parameter information transfer before authentication)
- Finally, a filter is developed to verify the SMS verification code before the SMS login request
Because this filter only cares about whether the submitted verification code is normal. Therefore, it can be applied to any business to verify the short message submitted by any business
UsernamePasswordAuthenticationToken
The default password of security is to log in. On this basis, clear the password related code
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.security.authentication; import java.util.Collection; import org.springframework.security.core.GrantedAuthority; public class UsernamePasswordAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 520L; private final Object principal; private Object credentials; public UsernamePasswordAuthenticationToken(Object principal, Object credentials) { super((Collection)null); this.principal = principal; this.credentials = credentials; this.setAuthenticated(false); } public UsernamePasswordAuthenticationToken(Object principal, Object credentials, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; this.credentials = credentials; super.setAuthenticated(true); } public Object getCredentials() { return this.credentials; } public Object getPrincipal() { return this.principal; } 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"); } else { super.setAuthenticated(false); } } public void eraseCredentials() { super.eraseCredentials(); this.credentials = null; } }
SmsCodeAuthenticationToken
The core project creates the SmsCodeAuthenticationToken class
Copy all contents of UsernamePasswordAuthenticationToken class to SmsCodeAuthenticationToken class
Delete the code related to the credentials value, and remove the credentials field because the SMS authentication has been filtered before the authorization authentication
package com.spring.security.authentication.mobile; import org.springframework.security.authentication.AbstractAuthenticationToken; import org.springframework.security.core.GrantedAuthority; import java.util.Collection; public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken { private static final long serialVersionUID = 520L; //Mobile phone number should be put before authentication, and authenticated user should be put after authentication private final Object principal; public SmsCodeAuthenticationToken(String mobile) { super((Collection)null); this.principal = mobile; //Put your phone number before you log in this.setAuthenticated(false); } public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) { super(authorities); this.principal = principal; //Log in successfully put user information super.setAuthenticated(true); } @Override public Object getPrincipal() { return this.principal; } @Override public Object getCredentials() { return null; } @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"); } else { super.setAuthenticated(false); } } @Override public void eraseCredentials() { super.eraseCredentials(); } }
UsernamePasswordAuthenticationFilter
Write SmsCodeAuthenticationFilter and agree to refer to the security default interceptor
// // Source code recreated from a .class file by IntelliJ IDEA // (powered by Fernflower decompiler) // package org.springframework.security.web.authentication; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.lang.Nullable; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.util.Assert; public class UsernamePasswordAuthenticationFilter extends AbstractAuthenticationProcessingFilter { public static final String SPRING_SECURITY_FORM_USERNAME_KEY = "username"; public static final String SPRING_SECURITY_FORM_PASSWORD_KEY = "password"; private String usernameParameter = "username"; private String passwordParameter = "password"; private boolean postOnly = true; public UsernamePasswordAuthenticationFilter() { super(new AntPathRequestMatcher("/login", "POST")); } public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { String username = this.obtainUsername(request); String password = this.obtainPassword(request); if (username == null) { username = ""; } if (password == null) { password = ""; } username = username.trim(); UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password); this.setDetails(request, authRequest); return this.getAuthenticationManager().authenticate(authRequest); } } @Nullable protected String obtainPassword(HttpServletRequest request) { return request.getParameter(this.passwordParameter); } @Nullable protected String obtainUsername(HttpServletRequest request) { return request.getParameter(this.usernameParameter); } protected void setDetails(HttpServletRequest request, UsernamePasswordAuthenticationToken authRequest) { authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); } public void setUsernameParameter(String usernameParameter) { Assert.hasText(usernameParameter, "Username parameter must not be empty or null"); this.usernameParameter = usernameParameter; } public void setPasswordParameter(String passwordParameter) { Assert.hasText(passwordParameter, "Password parameter must not be empty or null"); this.passwordParameter = passwordParameter; } public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getUsernameParameter() { return this.usernameParameter; } public final String getPasswordParameter() { return this.passwordParameter; } }
SmsCodeAuthenticationFilter
Also copy the UsernamePasswordAuthenticationFilter code
package com.spring.security.authentication.mobile; import org.springframework.lang.Nullable; import org.springframework.security.authentication.AuthenticationServiceException; import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; import org.springframework.security.core.Authentication; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter; import org.springframework.security.web.util.matcher.AntPathRequestMatcher; import org.springframework.util.Assert; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; public class SmsCodeAuthenticationFilter extends AbstractAuthenticationProcessingFilter { //Define parameter name of carrying mobile number public static final String SPRING_SECURITY_FORM_MOBILE_KEY = "mobile"; private String mobileParameter = SPRING_SECURITY_FORM_MOBILE_KEY; private boolean postOnly = true; public SmsCodeAuthenticationFilter() { //Request connections to process //Login page form form action < form action = "/ authentication / mobile" method = "post" > super(new AntPathRequestMatcher("/authentication/mobile", "POST")); } @Override public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException { if (this.postOnly && !request.getMethod().equals("POST")) { throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod()); } else { //Get mobile number String mobile = this.obtainMobile(request); if (mobile == null) { mobile = ""; } //Remove spaces mobile = mobile.trim(); SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile); // copy some information in the request to the token // Later, when the authentication succeeds, you need to copy the information to the new token this.setDetails(request, authRequest); //Authentication completed return this.getAuthenticationManager().authenticate(authRequest); } } /** * Get mobile number * * @param request * @return */ @Nullable protected String obtainMobile(HttpServletRequest request) { return request.getParameter(this.mobileParameter); } /** * Set authentication information to request header * * @param request * @param authRequest */ protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) { authRequest.setDetails(this.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 this.mobileParameter; } }
SmsCodeAuthenticationProvider
There is no imitation of this. No provider found with usernamePassword type
package com.spring.security.authentication.mobile; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationProvider; import org.springframework.security.authentication.InternalAuthenticationServiceException; 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; public class SmsCodeAuthenticationProvider implements AuthenticationProvider { @Autowired private UserDetailsService userDetailsService; /** * Authentication logic * @param authentication * @return * @throws AuthenticationException */ @Override public Authentication authenticate(Authentication authentication) throws AuthenticationException { SmsCodeAuthenticationToken authenticationToken = (SmsCodeAuthenticationToken) authentication; //Get user information according to mobile phone number UserDetails user = userDetailsService.loadUserByUsername((String) authenticationToken.getPrincipal()); if (user == null) { throw new InternalAuthenticationServiceException("Unable to get user information"); } SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user,user.getAuthorities()); //Copy unauthenticated information to authenticated authenticationResult.setDetails(authenticationToken.getDetails()); return authenticationResult; } @Override public boolean supports(Class<?> authentication) { return SmsCodeAuthenticationToken.class.isAssignableFrom(authentication); } public UserDetailsService getUserDetailsService() { return userDetailsService; } public void setUserDetailsService(UserDetailsService userDetailsService) { this.userDetailsService = userDetailsService; } }
smsCodeFilter
Directly copy the ValidateCodeFilter class and change its name to: smsCodeFilter
package com.spring.security.validate.code; import com.spring.security.properties.SecurityProperties; import com.spring.security.validate.code.image.ImageCode; import org.apache.commons.lang.StringUtils; import org.springframework.beans.factory.InitializingBean; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.social.connect.web.HttpSessionSessionStrategy; import org.springframework.social.connect.web.SessionStrategy; import org.springframework.stereotype.Component; import org.springframework.web.bind.ServletRequestBindingException; import org.springframework.web.bind.ServletRequestUtils; import org.springframework.web.context.request.ServletWebRequest; import org.springframework.web.filter.OncePerRequestFilter; import javax.servlet.FilterChain; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; import java.util.HashSet; import java.util.Set; /** * SMS verification code filter */ @Component public class SmsCodeFilter extends OncePerRequestFilter implements InitializingBean { @Autowired private AuthenticationFailureHandler authenticationFailureHandler; private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @Autowired private SecurityProperties securityProperties; private Set<String> urls = new HashSet<>(); @Override public void afterPropertiesSet() throws ServletException { super.afterPropertiesSet(); String[] configUrls = StringUtils.splitPreserveAllTokens(securityProperties.getCode().getSms().getUrl(), ","); for (String configUrl : configUrls) { urls.add(configUrl); } //Add fixed submission address urls.add("/authentication/mobile"); } /** * Inside the filter * * @param httpServletRequest http Servlet request * @param httpServletResponse * @param filterChain Filter chain * @throws ServletException Servlet abnormal * @throws IOException IOException */ @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { boolean action = false; for (String url : urls) { if (StringUtils.startsWithIgnoreCase(url, httpServletRequest.getRequestURI()) && StringUtils.startsWithIgnoreCase(httpServletRequest.getMethod(), "post")) { action = true; } } if (action) { try { validateCode(new ServletWebRequest(httpServletRequest)); } catch (ValidateCodeException e) { authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e); return; } } filterChain.doFilter(httpServletRequest, httpServletResponse); } /** * Validation code * * @param servletWebRequest servlet Web request * @throws ServletRequestBindingException Servlet Request binding exception */ private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException { ValidateCode validateSession = (ValidateCode) sessionStrategy.getAttribute(servletWebRequest, "SESSION_KEY_FOR_CODE_SMS"); String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "smsCode"); if (StringUtils.isEmpty(codeInRequest)) { throw new ValidateCodeException("Verification code cannot be empty!"); } if (validateSession == null) { throw new ValidateCodeException("Verification code does not exist!"); } if (validateSession.isExpire()) { sessionStrategy.removeAttribute(servletWebRequest, "SESSION_KEY_FOR_CODE_SMS"); throw new ValidateCodeException("Verification code expired!"); } if (!StringUtils.startsWithIgnoreCase(validateSession.getCode(), codeInRequest)) { throw new ValidateCodeException("The verification code is incorrect!"); } sessionStrategy.removeAttribute(servletWebRequest, "SESSION_KEY_FOR_CODE_SMS"); } }
SmsCodeAuthenticationSecurityConfig
A few things are ready for you. Here we need to configure to add these to the authentication process of security;
package com.spring.security.authentication.mobile; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.authentication.AuthenticationManager; import org.springframework.security.config.annotation.SecurityConfigurerAdapter; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.web.DefaultSecurityFilterChain; import org.springframework.security.web.authentication.AuthenticationFailureHandler; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.stereotype.Component; @Component public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> { @Autowired private AuthenticationFailureHandler hkAuthenticationFailureHandler; @Autowired private AuthenticationSuccessHandler hkAuthenticationSuccessHandler; @Autowired private UserDetailsService userDetailsService; @Override public void configure(HttpSecurity http) throws Exception { SmsCodeAuthenticationFilter filter = new SmsCodeAuthenticationFilter(); // Give the filter to the manager // The process in the figure, because the first SMS authentication filter (not the verification code, just the authentication) // To use the manager to get the provider, register the manager filter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class)); //Authentication failure processor filter.setAuthenticationFailureHandler(hkAuthenticationFailureHandler); //Authentication successful processor filter.setAuthenticationSuccessHandler(hkAuthenticationSuccessHandler); SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider(); smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService); //Add to security authentication process http // Register to AuthenticationManager .authenticationProvider(smsCodeAuthenticationProvider) // After adding to UsernamePasswordAuthenticationFilter // It seems that all the entries are UsernamePasswordAuthenticationFilter // Then the provider of UsernamePasswordAuthenticationFilter does not support the request for this address // So it will fall on our own certified filter. Complete the next certification .addFilterAfter(filter, UsernamePasswordAuthenticationFilter.class); } }
BrowserSecurityConfig application side configuration
package com.spring.security; import com.spring.security.authentication.HkAuthenticationFailureHandler; import com.spring.security.authentication.HkAuthenticationSuccessHandler; import com.spring.security.authentication.mobile.SmsCodeAuthenticationSecurityConfig; import com.spring.security.properties.SecurityProperties; import com.spring.security.validate.code.SmsCodeFilter; import com.spring.security.validate.code.ValidateCodeFilter; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.core.userdetails.UserDetailsService; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl; import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository; import javax.sql.DataSource; @Configuration public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter { @Autowired private SecurityProperties securityProperties; @Autowired private HkAuthenticationSuccessHandler hkAuthenticationSuccessHandler; @Autowired private HkAuthenticationFailureHandler hkAuthenticationFailureHandler; @Autowired private ValidateCodeFilter validateCodeFilter; @Autowired private SmsCodeFilter smsCodeFilter; // A data source is information that needs to be configured where it is used @Autowired private DataSource dataSource; // MyUserDetailsService previously written @Autowired private UserDetailsService userDetailsService; //SmsCodeAuthenticationSecurityConfig joins the authentication process @Autowired private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfig; @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public PersistentTokenRepository persistentTokenRepository() { // org.springframework.security.config.annotation.web.configurers.RememberMeConfigurer.tokenRepository JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl(); jdbcTokenRepository.setDataSource(dataSource); // There are statements defining the creation of tables in this object //create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null) // You can set this class to create tables // However, this function is only used once. If the database already has tables, an error will be reported //jdbcTokenRepository.setCreateTableOnStartup(true); //One of two methods return jdbcTokenRepository; } @Override protected void configure(HttpSecurity http) throws Exception { //To configure http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // Add verification code verification filter // SMS verification code verification filter .addFilterBefore(smsCodeFilter, UsernamePasswordAuthenticationFilter.class) .formLogin() .loginPage("/authentication/require")//Login page path // Process login request path .loginProcessingUrl("/authentication/form") .successHandler(hkAuthenticationSuccessHandler) // Login processed successfully .failureHandler(hkAuthenticationFailureHandler) // Failed to process login .and() // From here on, configure and remember my function .rememberMe() .tokenRepository(persistentTokenRepository()) // New expired configuration, unit: Second .tokenValiditySeconds(securityProperties.getBrowser().getRememberMeSeconds()) // userDetailsService is required. Otherwise, it's wrong .userDetailsService(userDetailsService) .and() .authorizeRequests() // Authorized configuration //Path without authentication .antMatchers("/authentication/require", "/code/*", "/signIn.html", securityProperties.getBrowser().getLoginPage(), "/failure").permitAll() .anyRequest() // All requests .authenticated() // All need certification .and().csrf().disable() //Join SMS verification code authentication process .apply(smsCodeAuthenticationSecurityConfig); } }
Testing
Launch project access: http://127.0.0.1:8080/signIn.html
Get verification code:
Enter error verification code:
Enter the correct verification code: