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

  1. 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.)
  2. 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

  1. Enter theUsernamePasswordAuthenticationFilterClass:attemptAuthentication()Method to pass the front end tousername,passwordWrapped inUsernamePasswordAuthenticationTokenAnd marked as unauthenticated. The last callthis.getAuthenticationManager().authenticate(authRequest)Pass to AuthenticationManager for processing.
  2. The ProviderManager handles authentication changes by identifying the appropriate AuthenticationProvider from among the numerous authenticationProviders based on the incoming token class.
  3. 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:

  1. The front-end input mobile phone number, and then obtain SMS verification code, finally with mobile phone number and verification code login
  2. The server listens to the login address to verify that the verification code is correct
  3. 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)