This is the second day of my participation in the First Challenge 2022

This article is based on your understanding of the Spring Security authentication process. If you don’t, read this article: The Spring Security Authentication Process.

To analyze problems

Here’s a flow chart of user name/password authentication built into Spring Security to start with:

According to the above figure, we can follow suit and customize an authentication process, such as mobile phone SMS code authentication. In the figure, I have marked the main steps involved in the process in different colors. The blue part is the user name/password authentication part, and the green part is the logic independent of the specific authentication method.

Therefore, we can develop our own custom logic according to the blue class, which mainly includes the following:

  • A customAuthenticationImplementation classes, andUsernamePasswordAuthenticationTokenSimilar to saving authentication information.
  • A custom filter, withUsernamePasswordAuthenticationFilterSimilarly, authentication information is encapsulated and authentication logic is invoked for a specific request.
  • aAuthenticationProviderImplementation class, provides authentication logic, andDaoAuthenticationProviderSimilar.

Next, take mobile phone verification code authentication as an example to complete one by one.

The custom Authentication

First give the code, then explain:

public class SmsCodeAuthenticationToken extends AbstractAuthenticationToken {

    private final Object principal;

    private Object credentials;

    public SmsCodeAuthenticationToken(Object principal, Object credentials) {
        super(null);
        this.principal = principal;
        this.credentials = credentials;
        setAuthenticated(false);
    }
    public SmsCodeAuthenticationToken(Object principal, Object credentials, Collection
        authorities) {
        super(authorities);
        this.principal = principal;
        this.credentials = credentials;
        super.setAuthenticated(true);
    }

    @Override
    public Object getCredentials(a) {
        return this.credentials;
    }

    @Override
    public Object getPrincipal(a) {
        return this.principal;
    }

    @Override
    public void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException { Assert.isTrue(! isAuthenticated,"Cannot set this token to trusted - use constructor which takes a GrantedAuthority list instead");
        super.setAuthenticated(false);
    }

    @Override
    public void eraseCredentials(a) {
        super.eraseCredentials();
        this.credentials = null; }}Copy the code

Like UsernamePasswordAuthenticationToken, inheritance AbstractAuthenticationToken abstract class, it is necessary to realize getPrincipal and getCredentials two methods. In username/password authentication, the principal represents the user name and the credentials represent the password. In this case, we can make them refer to the mobile phone number and the verification code, so we add these two attributes and then implement the method.

In addition, we need to write two constructors to create unauthenticated and successfully authenticated authentication information.

A custom Filter

This part, you can refer to UsernamePasswordAuthenticationFilter to write. Or online code:

public class SmsCodeAuthenticationProcessingFilter extends AbstractAuthenticationProcessingFilter {

    public static final String FORM_MOBILE_KEY = "mobile";
    public static final String FORM_SMS_CODE_KEY = "smsCode";

    private static final AntPathRequestMatcher DEFAULT_ANT_PATH_REQUEST_MATCHER = new AntPathRequestMatcher("/sms/login"."POST");

    private boolean postOnly = true;

    protected SmsCodeAuthenticationProcessingFilter(a) {
        super(DEFAULT_ANT_PATH_REQUEST_MATCHER);
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        if (this.postOnly && ! request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: "+ request.getMethod()); } String mobile = obtainMobile(request); mobile = (mobile ! =null)? mobile :""; mobile = mobile.trim(); String smsCode = obtainSmsCode(request); smsCode = (smsCode ! =null)? smsCode :"";
        SmsCodeAuthenticationToken authRequest = new SmsCodeAuthenticationToken(mobile, smsCode);
        // Allow subclasses to set the "details" property
        setDetails(request, authRequest);
        return this.getAuthenticationManager().authenticate(authRequest);
    }

    private String obtainMobile(HttpServletRequest request) {
        return request.getParameter(FORM_MOBILE_KEY);
    }

    private String obtainSmsCode(HttpServletRequest request) {
        return request.getParameter(FORM_SMS_CODE_KEY);
    }

    protected void setDetails(HttpServletRequest request, SmsCodeAuthenticationToken authRequest) {
        authRequest.setDetails(this.authenticationDetailsSource.buildDetails(request)); }}Copy the code

This part is easy, but the key points are as follows:

  • First, the default constructor has a filter that matches those requests, in this case/sms/loginPOST request to the.
  • inattemptAuthenticationIn the method, first fromrequestTo obtain the mobile phone number and verification code entered in the form and create the unauthenticated Token information.
  • Give the Token informationthis.getAuthenticationManager().authenticate(authRequest)Methods.

A custom Provider

Here is the main logic to complete the authentication, the code here only has the most basic verification logic, there is no more rigorous verification, such as checking whether the user is disabled, because this part is tedious but simple.

public class SmsCodeAuthenticationProvider implements AuthenticationProvider {

    public static final String SESSION_MOBILE_KEY = "mobile";
    public static final String SESSION_SMS_CODE_KEY = "smsCode";
    public static final String FORM_MOBILE_KEY = "mobile";
    public static final String FORM_SMS_CODE_KEY = "smsCode";

    private UserDetailsService userDetailsService;

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        authenticationChecks(authentication);
        String mobile = authentication.getName();
        UserDetails userDetails = userDetailsService.loadUserByUsername(mobile);
        SmsCodeAuthenticationToken authResult = new SmsCodeAuthenticationToken(userDetails, userDetails.getAuthorities());
        return authResult;
    }

    /** * Verify authentication information *@param authentication
     */
    private void authenticationChecks(Authentication authentication) {
        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();
        // The mobile phone number and verification code for the form submission
        String formMobile = request.getParameter(FORM_MOBILE_KEY);
        String formSmsCode = request.getParameter(FORM_SMS_CODE_KEY);
        // The mobile phone number and verification code saved in the session
        String sessionMobile = (String) request.getSession().getAttribute(SESSION_MOBILE_KEY);
        String sessionSmsCode = (String) request.getSession().getAttribute(SESSION_SMS_CODE_KEY);

        if (StringUtils.isEmpty(sessionMobile) || StringUtils.isEmpty(sessionSmsCode)) {
            throw new BadCredentialsException("To send a cell phone verification code.");
        }

        if(! formMobile.equals(sessionMobile)) {throw new BadCredentialsException("Inconsistent cell phone numbers.");
        }

        if(! formSmsCode.equals(sessionSmsCode)) {throw new BadCredentialsException("Inconsistent captcha"); }}@Override
    public boolean supports(Class
        authentication) {
        return (SmsCodeAuthenticationToken.class.isAssignableFrom(authentication));
    }

    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService; }}Copy the code

The main points of this code are as follows:

  • supportsThe AuthenticationToken method is used to determine the type of AuthenticationToken supported by this Provider, which corresponds to the one we created earlierSmsCodeAuthenticationToken.
  • inauthenticateIn the method, we compare the mobile phone number and verification code in the Token with the mobile phone number and verification code saved in the Session. (The part that saves the mobile phone number and verification code in the Session is implemented as follows.) After the comparison is correct, the user is obtained from the UserDetailsService, and the authenticated Token is created based on the comparison. The Token is returned and finally reaches the Filter.

User-defined Handler after authentication succeeds or fails

Previously, we learned from analyzing the source code that the doFilter method in Filter is actually in its parent class

The AbstractAuthenticationProcessingFilter, attemptAuthentication method is invoked in the doFilter.

AttemptAuthentication when we complete the previous custom logic, the attemptAuthentication method returns a successful authentication or throws an exception indicating an authentication failure, regardless of whether the authentication was successful. The doFilter method invokes different processing logic according to the result of authentication (success/failure), and these two processing logic can also be customized.

I’ll just post the code below:

public class SmsCodeAuthenticationSuccessHandler implements AuthenticationSuccessHandler {
    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
        response.setContentType("text/plain; charset=UTF-8"); response.getWriter().write(authentication.getName()); }}Copy the code
public class SmsCodeAuthenticationFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
        response.setContentType("text/plain; charset=UTF-8");
        response.getWriter().write("Authentication failed"); }}Copy the code

The above is the processing logic after success and failure, which needs to implement corresponding interfaces and methods respectively. Note that this is just for testing purposes, and I have written the simplest logic so that I can distinguish between the two cases when TESTING. In a real project, you need to perform logic based on specific services, such as saving information about the current login user.

Configure the logic of user-defined authentication

In order for our custom authentication to take effect, we need to add Filter and Provider to the Spring Security configuration. We can put this part of the configuration into a separate configuration class first:

@Component
@RequiredArgsConstructor
public class SmsCodeAuthenticationSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity> {
    private final UserDetailsService userDetailsService;

    @Override
    public void configure(HttpSecurity http) {

        SmsCodeAuthenticationProcessingFilter smsCodeAuthenticationFilter = new SmsCodeAuthenticationProcessingFilter();
        smsCodeAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsCodeAuthenticationFilter.setAuthenticationSuccessHandler(new SmsCodeAuthenticationSuccessHandler());
        smsCodeAuthenticationFilter.setAuthenticationFailureHandler(new SmsCodeAuthenticationFailureHandler());

        SmsCodeAuthenticationProvider smsCodeAuthenticationProvider = new SmsCodeAuthenticationProvider();
        smsCodeAuthenticationProvider.setUserDetailsService(userDetailsService);

        http.authenticationProvider(smsCodeAuthenticationProvider)
                .addFilterAfter(smsCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
    }
}
Copy the code

Among them, the following points need to be noted:

  • Remember to provide the AuthenticationManager to the Filter. Review the authentication logic mentioned earlier. Without this step, you would not be able to find the corresponding Provider after the authentication information is encapsulated in the Filter.
  • Feed two classes of the success/failure processing logic to Filter, otherwise you will not enter either logic, but the default processing logic.
  • The Provider uses the UserDetailsService, so provide it as well.
  • Finally, add both to the HttpSecurity object.

Next, you need to add the following to the main configuration of Spring Security.

  • First, injectionSmsCodeAuthenticationSecurityConfigConfiguration.
  • Then, in theconfigure(HttpSecurity http)Method to introduce configuration:http.apply`` ( ``smsCodeAuthenticationSecurityConfig`` ) ``;.
  • Finally, since the verification code needs to be requested and verified before authentication, the/sms/**The path is cleared.

test

Now that we’re done, let’s test. First we need to provide an interface to send the captcha. Since this is a test, we’ll return the captcha directly. The interface code is as follows:

@GetMapping("/getCode")
public String getCode(@RequestParam("mobile") String mobile,
                      HttpSession session) {
    String code = "123456";
    session.setAttribute("mobile", mobile);
    session.setAttribute("smsCode", code);
    return code;
}
Copy the code

In order to get the user, if you haven’t implemented your own UserDetailsService, write a simple logic to complete the test, in which the loadUserByUsername method is as follows:

@Override public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException { // TODO: Temporary logic, after docking User management related services return new User (username, "123456", AuthorityUtils. CreateAuthorityList (" admin ")); }Copy the code

OK, here are the test results: