Customized authentication based on Spring Security – Take SMS login as an example
“Spring Security Actual Combat”, mooC “Spring Security Technology Stack Development enterprise-level authentication and Authorization” notes
There are two ways to implement Spring Security-based authentication
- Add a Filter that inherits OncePerRequestFilter and places this Filter in the appropriate location for HttpSecurity. (The purpose of inheriting OncePerRequestFilter is to ensure that a request only passes through the filter once.)
- Custom authentication based on Spring Security
The filter method of Method 1 should be familiar, so I won’t expand the record. Let’s go into more details about custom authentication based on Spring Security.
Take the UsernamePassword authentication as an example to outline the authentication process.
The basic concept
-
Authentication: Encapsulation class for Spring Security Authentication, including permissions, credentials to determine identity, identity details, and whether it is authenticated. Common RememberMeAuthenticationToken, UsernamePasswordAuthenticationToken implementation class
-
AuthenticationProvider: A validation process for Spring Security. A complete authentication can contain multiple AuthenticationProviders, typically managed by the ProviderManager. Authentication flows through the AuthenticationProvider. In plain English, different Authenticationproviders provide different tokens.
-
AuthenticationManager: Processes Authentication requests. There is only one AuthenticationManager in the entire system. ProviderManager is the implementation class of AuthenticationManager
UsernamePassword Authentication process
- Enter the
UsernamePasswordAuthenticationFilter
Class:attemptAuthentication()
Method to pass the front end tousername
,password
Wrapped inUsernamePasswordAuthenticationToken
And marked as unauthenticated. The last callthis.getAuthenticationManager().authenticate(authRequest)
Pass to AuthenticationManager for processing. - The ProviderManager handles authentication changes by identifying the appropriate AuthenticationProvider from among the numerous authenticationProviders based on the incoming token class.
- In specific XXXAuthenticationProvider authenticated user returns a certification through the token, and detailed information.
So we based on Spring Security custom a certification to create a new XXXAuthenticationToken and XXXAuthenticationProvider, finally will own logic to join HttpSecurity in the filter chain.
Below take SMS login as an example to practice the above knowledge points.
SMS login logic:
- The front-end input mobile phone number, and then obtain SMS verification code, finally with mobile phone number and verification code login
- The server listens to the login address to verify that the verification code is correct
- Properly use Spring Security custom authentication to issue a token to the front end.
The following code is posted:
SmsValidateCodeFilter
SMS verification code filter to verify the SMS login verification code
package com.zchi.customizeAuthentication.security.smsCode;
import lombok.Setter;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.util.AntPathMatcher;
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;
/ * * *@DescriptionSMS verification code filter to verify whether the SMS login verification code is correct *@AuthorZhang *@Datee 2021/7/4
* @Version1.0 * * /
@Setter
public class SmsValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
private AntPathMatcher pathMatcher = new AntPathMatcher();
private AuthenticationFailureHandler authenticationFailureHandler;
@Override
protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
boolean action = false;
String url = "/authentication/smsLogin";
if (pathMatcher.match(url, httpServletRequest.getRequestURI())) {
action = true;
}
if (action) {
try {
validate(new ServletWebRequest(httpServletRequest));
} catch (ValidateCodeException e) {
authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
return;
}
}
filterChain.doFilter(httpServletRequest, httpServletResponse);
}
private void validate(ServletWebRequest request) throws ServletRequestBindingException {
// Todo obtains the captcha from session or Redis
String codeInSession = "f123";
String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "smsCode");
if (StringUtils.isBlank(codeInRequest)) {
throw new ValidateCodeException("The value of the captcha cannot be null.");
}
if(codeInSession == null) {throw new ValidateCodeException("Captcha does not exist");
}
// Whether the toDO verification code has expired
if(! StringUtils.equals(codeInSession, codeInRequest)) {throw new ValidateCodeException("Verification code does not match");
}
// Todo removes the captcha after using it
}
public void setFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
this.authenticationFailureHandler = authenticationFailureHandler; }}Copy the code
SmsCodeAuthenticationToken
According to UsernamePasswordAuthenticationToken write line directly
package com.zchi.customizeAuthentication.security.smsCode;
import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import javax.security.auth.Subject;
import java.util.Collection;
/ * * *@DescriptionDirectly according to UsernamePasswordAuthenticationToken write line *@AuthorZhang *@Datee 2021/7/3
* @Version1.0 * * /
public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {
private final Object principal;
public SmsCodeAuthenticationToken(String mobile) {
super(null);
this.principal = mobile;
setAuthenticated(false);
}
public SmsCodeAuthenticationToken(Object principal, Collection<? extends GrantedAuthority> authorities) {
super(authorities);
this.principal = principal;
setAuthenticated(true);
}
@Override
public Object getCredentials(a) {
return this.principal;
}
@Override
public Object getPrincipal(a) {
return null;
}
@Override
public boolean implies(Subject subject) {
return false; }}Copy the code
SmsCodeAuthenticationProvider
The Authentication provider, according to the previous SmsCodeAuthenticationFilter deposited in the information in the token for the current user information. Declare supported token types
package com.zchi.customizeAuthentication.security.smsCode;
import lombok.Data;
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;
/ * * *@DescriptionThe Authentication provider, according to the previous SmsCodeAuthenticationFilter deposited in the information in the token for the current user information. Declare the supported token type *@AuthorZhang *@Datee 2021/7/3
* @Version 1.0
* @see SmsCodeAuthenticationFilter,SmsCodeAuthenticationToken
**/
@Data
public class SmsCodeAuthenticationProvider implements AuthenticationProvider {
private UserDetailsService userDetailsService;
@Override
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
SmsCodeAuthenticationToken token = (SmsCodeAuthenticationToken) authentication;
UserDetails user = userDetailsService.loadUserByUsername((String) token.getPrincipal());
if(user == null) {throw new InternalAuthenticationServiceException("Unable to obtain user information");
}
SmsCodeAuthenticationToken authenticationResult = new SmsCodeAuthenticationToken(user, user.getAuthorities());
// Some unauthenticated information needs to be copied to the authenticated token
authenticationResult.setDetails(token);
return authenticationResult;
}
// Tokens supported by the provider
@Override
public boolean supports(Class
authentication) {
returnSmsCodeAuthenticationToken.class.isAssignableFrom(authentication); }}Copy the code
SmsCodeAuthenticationSecurityConfig
Configure the classes you wrote to the filter chain
package com.zchi.customizeAuthentication.security.smsCode;
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.UsernamePasswordAuthenticationFilter;
import org.springframework.stereotype.Component;
/ * * *@DescriptionConfigure the classes you wrote to the filter chain *@AuthorZhang *@Datee 2021/7/3
* @Version1.0 * * /
@Component
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {
@Autowired
private SmsCodeAuthenticationSuccessHandler authenticationSuccessHandler;
@Autowired
private SmsCodeAuthenctiationFailureHandler authenticationFailureHandler;
@Autowired
private UserDetailsService userDetailsService;
@Override
public void configure(HttpSecurity httpSecurity) {
SmsCodeAuthenticationFilter filter = new SmsCodeAuthenticationFilter();
filter.setAuthenticationManager(httpSecurity.getSharedObject(AuthenticationManager.class));
filter.setAuthenticationFailureHandler(authenticationFailureHandler);
filter.setAuthenticationSuccessHandler(authenticationSuccessHandler);
SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);
SmsValidateCodeFilter smsValidateCodeFilter = newSmsValidateCodeFilter(); smsValidateCodeFilter.setAuthenticationFailureHandler(authenticationFailureHandler); httpSecurity.addFilterBefore(smsValidateCodeFilter, UsernamePasswordAuthenticationFilter.class); httpSecurity.authenticationProvider(smsCodeAuthenticationProvider) .addFilterAt(filter, UsernamePasswordAuthenticationFilter.class); }}Copy the code
SecurityConfig
Security configuration class for the project
package com.zchi.customizeAuthentication.security;
import com.zchi.customizeAuthentication.security.smsCode.SmsCodeAuthenticationSecurityConfig;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;
import java.util.Collection;
/ * * *@DescriptionSecurity configuration for the project *@AuthorZhang *@Datee 2021/7/3
* @Version1.0 * * /
@Component
public class SecurityConfig extends WebSecurityConfigurerAdapter {
@Autowired
AuthenticationFailureHandler authenticationFailureHandler;
@Autowired
private SmsCodeAuthenticationSecurityConfig smsCodeAuthenticationSecurityConfigs;
@Override
protected void configure(HttpSecurity http) throws Exception {
http
.authorizeRequests()
.antMatchers("/authentication/require"."/login"."/code/*"."/error"."/authentication/smsLogin"
)
.permitAll()
.anyRequest()
.authenticated()
.and()
.csrf().disable()
// Configure our own SMS login to the project's filter chain
.apply(smsCodeAuthenticationSecurityConfigs);
}
@Bean
public UserDetailsService userDetailsService(a) {
// Obtain the login user information
return username -> {
return new UserDetails() {
@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return null;
}
@Override
public String getPassword(a) {
return null;
}
@Override
public String getUsername(a) {
return null;
}
@Override
public boolean isAccountNonExpired(a) {
return false;
}
@Override
public boolean isAccountNonLocked(a) {
return false;
}
@Override
public boolean isCredentialsNonExpired(a) {
return false;
}
@Override
public boolean isEnabled(a) {
return false; }}; }; }}Copy the code
Reference:
Spring Security Combat
Spring Security Technology Stack Development Enterprise Level Authentication and Authorization
SecurityDemo: “Implementing custom Authentication based on Spring Security – using SMS Login as an example” code (gitee.com)