Personal blog :www.zhenganwen.top, surprise at the end of the article! This article is a follow-up to the article “Developing Enterprise-level Authentication and Authorization in The Spring Security Technology Stack.

Use Spring Security to develop form-based authentication

Realize graphic verification code function

Function implementation

Since graphic captcha is a common feature, we write the logic in security-code

First, encapsulate the graph, the captcha in the graph, and the captcha expiration time

package top.zhenganwen.security.core.verifycode.dto;

import lombok.Data;
import java.awt.image.BufferedImage;
import java.time.LocalDateTime;

/ * * *@author zhenganwen
 * @date 2019/8/24
 * @desc ImageCode
 */
@Data
public class ImageCode {
    private String code;
    private BufferedImage image;
    // Verification code expiration time
    private LocalDateTime expireTime;

    public ImageCode(String code, BufferedImage image, LocalDateTime expireTime) {
        this.code = code;
        this.image = image;
        this.expireTime = expireTime;
    }

    public ImageCode(String code, BufferedImage image, int durationSeconds) {
        this(code, image, LocalDateTime.now().plusSeconds(durationSeconds));
    }

    public boolean isExpired(a) {
        returnLocalDateTime.now().isAfter(expireTime); }}Copy the code

It then provides an interface to generate captcha

package top.zhenganwen.security.core.verifycode;

import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import top.zhenganwen.security.core.verifycode.dto.ImageCode;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;

/ * * *@author zhenganwen
 * @date 2019/8/24
 * @desc VerifyCodeController
 */
@RestController
@RequestMapping("/verifyCode")
public class VerifyCodeController {

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

    /** * 1. Generate graphic verification code * 2. Save the verification code to Session * 3. Gives the graph response to the front end */
    @GetMapping("/image")
    public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ImageCode imageCode = generateImageCode(67.23.4);
        // Session read/write tool class, the first parameter is fixed
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode.getCode());
        ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
    }

    / * * *@paramWidth Graphic width *@paramHeight Graph height *@paramStrLength Indicates the number of verification code characters *@return* /
    private ImageCode generateImageCode(int width, int height, int strLength) {

        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        Random random = new Random();

        g.setColor(getRandColor(200.250));
        g.fillRect(0.0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160.200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        String sRand = "";
        for (int i = 0; i < strLength; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6.16);
        }

        g.dispose();

        return new ImageCode(sRand, image, 60);
    }

    /** * generates a random background stripe **@param fc
     * @param bc
     * @return* /
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return newColor(r, g, b); }}Copy the code

Release the interface for generating captcha in the security-Browser configuration class:

protected void configure(HttpSecurity http) throws Exception {
        http
                .formLogin()
                    .loginPage("/auth/require")
                    .loginProcessingUrl("/auth/login")
                    .successHandler(customAuthenticationSuccessHandler)
                    .failureHandler(customAuthenticationFailureHandler)
                .and()
                .authorizeRequests()
                    .antMatchers(
                            "/auth/require",
                            securityProperties.getBrowser().getLoginPage(),
                            "/verifyCode/image").permitAll()
                    .anyRequest().authenticated()
                .and()
                .csrf().disable();
    }
Copy the code

To test the generation of the verification code in security-demo, add the verification code input box in login. HTML:

<form action="/auth/login" method="post">User name:<input type="text" name="username">Password:<input type="password" name="password">Verification code:<input type="text" name="verifyCode"><img src="/verifyCode/image" alt="">
    <button type="submit">submit</button>
</form>
Copy the code

To access /login.html, the verification code is generated as follows:

Next we write the verification logic. Since Security does not provide a filter for verification, we need to define one and insert it before UsernamePasswordFilter:

package top.zhenganwen.security.core.verifycode;


import org.springframework.security.core.AuthenticationException;

/ * * *@author zhenganwen
 * @date 2019/8/24
 * @desc VerifyCodeException
 */
public class VerifyCodeException extends AuthenticationException {
    public VerifyCodeException(String explanation) {
        super(explanation); }}Copy the code
package top.zhenganwen.security.core.verifycode;

import org.apache.commons.lang.StringUtils;
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.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import top.zhenganwen.security.core.verifycode.dto.ImageCode;

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.Objects;

/ * * *@author zhenganwen
 * @date 2019/8/24
 * @desc VerifyCodeAuthenticationFilter
 */
@Component
// Inherits OncePerRequestFilter filters will only be executed once in a request
public class VerifyCodeAuthenticationFilter extends OncePerRequestFilter {

    @Autowired
    private AuthenticationFailureHandler customAuthenticationFailureHandler;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException,
    IOException {
        // If it is a login request
        if (Objects.equals(request.getRequestURI(), "/auth/login") && StringUtils.endsWithIgnoreCase(request.getMethod(), "POST")) {
            try {
                this.validateVerifyCode(new ServletWebRequest(request));
            } catch (VerifyCodeException e) {
                / / if an exception is the use custom authentication failure processing, otherwise no one captured (because the filter is in front of the UsernamePasswordAuthenticationFilter)
                customAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
            }
        }
        filterChain.doFilter(request, response);
    }

    // Read the verification code from Session and compare it with the verification code submitted by the user
    private void validateVerifyCode(ServletWebRequest request) {
        String verifyCode = (String) request.getParameter("verifyCode");
        if (StringUtils.isBlank(verifyCode)) {
            throw new VerifyCodeException("Verification code cannot be empty.");
        }
        ImageCode imageCode = (ImageCode) sessionStrategy.getAttribute(request, VerifyCodeController.SESSION_KEY);
        if (imageCode == null) {
            throw new VerifyCodeException("Captcha does not exist");
        }
        if (imageCode.isExpired()) {
            throw new VerifyCodeException("Verification code has expired. Please refresh the page.");
        }
        if (StringUtils.equals(verifyCode,imageCode.getCode()) == false) {
            throw new VerifyCodeException("Verification code error");
        }
        // If the login succeeds, remove the verification code saved in the SessionsessionStrategy.removeAttribute(request, VerifyCodeController.SESSION_KEY); }}Copy the code

security-browser

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http
                .addFilterBefore(verifyCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                    .loginPage("/auth/require")
                    .loginProcessingUrl("/auth/login")
                    .successHandler(customAuthenticationSuccessHandler)
                    .failureHandler(customAuthenticationFailureHandler)
                .and()
                .authorizeRequests()
                    .antMatchers(
                            "/auth/require",
                            securityProperties.getBrowser().getLoginPage(),
                            "/verifyCode/image").permitAll()
                    .anyRequest().authenticated()
                .and()
                .csrf().disable();
    }
Copy the code

Go to /login. HTML and login directly without filling in anything, and return JSON as follows

{"cause":null."stackTrace": [...]. ."localizedMessage":"Verification code cannot be empty."."message":"Verification code cannot be empty."."suppressed": []} {"cause":null."stackTrace": [...]. ."localizedMessage":"Bad papers."."message":"Bad papers."."suppressed": []}Copy the code

Find a JSON string that returns two exceptions, one before or after (the two strings are attached, without any symbols in the middle), This is because we call in VerifyCodeAuthenticationFilter customAuthenticationFailureHandler authentication failure after processing, and then execute the doFilter, Then UsernamePasswordAuthenticationFilter will intercept the login request/auth/login, in the process of checking captured BadCredentialsException, Call customAuthenticationFailureHandler return another exceptionJSON string

There are two things that need to be optimized

  • The exception message returned should not contain a stack

    Return in CustomAuthenticationFailureHandler exception information extracted from the exception, rather than direct return to the exception

    // response.getWriter().write(objectMapper.writeValueAsString(exception));
    response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponseResult(exception.getMessage())));
    Copy the code
- in ` VerifyCodeAuthenticationFilter ` found authentication failed abnormal failure after processing, and call the certification should be `returnNow, there is no need to go through the following filter Javaif (Objects.equals(request.getRequestURI(), "/auth/login") && StringUtils.endsWithIgnoreCase(request.getMethod(), "POST")) { try { this.validateVerifyCode(new ServletWebRequest(request)); } catch (VerifyCodeException e) {// If an exception is thrown, use a custom authentication failure handler. Otherwise no one captured (because the filter is in front of the UsernamePasswordAuthenticationFilter) customAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);return;
              }
          }
          filterChain.doFilter(request, response);
Copy the code

To test

{content: "Captcha cannot be empty"}Copy the code

Then test the verification code, fill in admin,123456 and graphics verification code after login, login success, Authentication success processor return Authentication

{
    authorities: [
        {
            authority: "admin"
        },
        {
            authority: "user"
        }
    ],
    details: {
        remoteAddress: "0:0:0:0:0:0:0:1",
        sessionId: "452F44596C9D9FF55DBA91A1F24E05B0"
    },
    authenticated: true,
    principal: {
        password: null,
        username: "admin",
        authorities: [
            {
                authority: "admin"
            },
            {
                authority: "user"
            }
        ],
        accountNonExpired: true,
        accountNonLocked: true,
        credentialsNonExpired: true,
        enabled: true
    },
    credentials: null,
    name: "admin"
}
Copy the code

Reconstruct the graphics captcha function

At this point, the function of the graphical verification code we have finished basic implementation, but as we should not be content with the senior engineers, in the realization of function and should think about how to refactor your code to make the reusable function, while others require a different size, the number of different character, different authentication logic, can also reuse the code

The basic parameters of the graphic verification code are configurable

For example, the length and width of the graph, the number of characters of the verification code, and the duration of the validity period of the verification code

The effective mechanism of the general system configuration is as follows. As a dependent module, we need to provide a common default configuration, and the dependent application can add configuration items to cover the default configuration. Finally, when the application is running, it can dynamically switch the configuration by attaching parameters in the request

Security-core adds a configuration class

package top.zhenganwen.security.core.properties;

import lombok.Data;

/ * * *@author zhenganwen
 * @date 2019/8/25
 * @desc ImageCodeProperties
 */
@Data
public class ImageCodeProperties {
    private int width=67;
    private int height=23;
    private int strLength=4;
    private int durationSeconds = 60;
}
Copy the code
package top.zhenganwen.security.core.properties;

import lombok.Data;

/ * * *@author zhenganwen
 * @date 2019/8/25
 * @descVerifyCodeProperties encapsulates both graphic and SMS verification codes */
@Data
public class VerifyCodeProperties {
    private ImageCodeProperties image = new ImageCodeProperties();
}
Copy the code
package top.zhenganwen.security.core.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;

/ * * *@author zhenganwen
 * @date 2019/8/23
 * @descSecurityProperties encapsulates the configuration items */ for each module of the entire project
@Data
@ConfigurationProperties(prefix = "demo.security")
public class SecurityProperties {
    private BrowserProperties browser = new BrowserProperties();
    private VerifyCodeProperties code = new VerifyCodeProperties();
}
Copy the code

In the generate validation interface, change the corresponding parameters to dynamic reading

package top.zhenganwen.security.core.verifycode;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.security.core.verifycode.dto.ImageCode;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;

/ * * *@author zhenganwen
 * @date 2019/8/24
 * @desc VerifyCodeController
 */
@RestController
@RequestMapping("/verifyCode")
public class VerifyCodeController {

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

    @Autowired
    private SecurityProperties securityProperties;

    /** * 1. Generate graphic verification code * 2. Save the verification code to session * 3. Gives the graph response to the front end */
    @GetMapping("/image")
    public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // First read width/height in the URL argument, if not in the configuration file
        int width = ServletRequestUtils.getIntParameter(request, "width", securityProperties.getCode().getImage().getWidth());
        int height = ServletRequestUtils.getIntParameter(request, "height", securityProperties.getCode().getImage().getHeight());

        ImageCode imageCode = generateImageCode(width, height, securityProperties.getCode().getImage().getStrLength());
        // Session read/write tool class, the first parameter is fixed
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
        ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
    }

    / * * *@paramWidth Graphic width *@paramHeight Graph height *@paramStrLength Indicates the number of verification code characters *@return* /
    private ImageCode generateImageCode(int width, int height, int strLength) {

        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        Random random = new Random();

        g.setColor(getRandColor(200.250));
        g.fillRect(0.0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160.200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        String sRand = "";
        for (int i = 0; i < strLength; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6.16);
        }

        g.dispose();

        return newImageCode(sRand, image, securityProperties.getCode().getImage().getDurationSeconds()); }}Copy the code

To test the application level configuration of the verification code to override the default number of characters, add the configuration item in the security-demo application.properties file

demo.security.code.image.strLength=6
Copy the code

The test request parameter-level configuration overrides the application level configuration

demo.security.code.image.width=100
Copy the code
Verification code:<input type="text" name="verifyCode"><img src="/verifyCode/image? width=200" alt="">
Copy the code

/login. HTML, the width of the graph is 200, the number of captcha characters is 6, and the test is successful

Verification code Specifies the interface intercepted by the authentication filter

Currently, our VerifyCodeFilter only intercepts login requests and performs verification. Other interfaces may also require verification (perhaps for illegal duplicate requests), so we need to support applications that can dynamically configure interfaces that require verification, for example

demo.security.code.image.url=/user,/user/*
Copy the code

Indicates that the request /user and /user/* require verification code

So we add a property that can be configured to intercept urIs

@Data
public class ImageCodeProperties {
    private int width=67;
    private int height=23;
    private int strLength=4;
    private int durationSeconds = 60;
    // A list of URIs to intercept, separated by commas
    private String uriPatterns;
}
Copy the code

Then in VerifyCodeAuthenticationFilter demo. In the configuration file is read. The security code. The image. The uriPatterns and initialize a uriPatternSet collection, In the interception logic, the collection is traversed and the intercepted URI is patternmatched with the elements of the collection. If there is a match, it means that the URI needs to check the verification code. If the verification fails, an exception is thrown and left to the authentication failure processor

@Component
public class VerifyCodeAuthenticationFilter extends OncePerRequestFilter implements InitializingBean {

    @Autowired
    private AuthenticationFailureHandler customAuthenticationFailureHandler;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    private Set<String> uriPatternSet = new HashSet<>();

    // uri matching tool class, help us to do similar /user/1 to /user/* matching
    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public void afterPropertiesSet(a) throws ServletException {
        super.afterPropertiesSet();
        String uriPatterns = securityProperties.getCode().getImage().getUriPatterns();
        if (StringUtils.isNotBlank(uriPatterns)) {
            String[] strings = StringUtils.splitByWholeSeparatorPreserveAllTokens(uriPatterns, ",");
            uriPatternSet.addAll(Arrays.asList(strings));
        }
        uriPatternSet.add("/auth/login");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException,
            IOException {
        for (String uriPattern : uriPatternSet) {
            if (antPathMatcher.match(uriPattern, request.getRequestURI())) {
                try {
                    this.validateVerifyCode(new ServletWebRequest(request));
                } catch (VerifyCodeException e) {
                    / / if an exception is thrown, the use of custom authentication failure processing, otherwise no one captured (because the filter match in front of the UsernamePasswordAuthenticationFilter) is thrown to the front
                    customAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
                    return;
                }
                break;
            }
        }
        filterChain.doFilter(request, response);
    }

    private void validateVerifyCode(ServletWebRequest request) {...}
}
Copy the code

We write the initialization logic for uriPatternSet in the afterPropertiesSet method of the InitializingBean interface, which is equivalent to configuring an init-method tag in traditional Spring.xml, This method can be in all the autowire VerifyCodeAuthenticationFilter properties are performed by the spring after the assignment

Modify the configuration item to uriPattern=/user/* If the user accesses /user and /user/1, the verification code cannot be empty. If the user logs in to /login. HTML and accesses /user after the restart, the verification code cannot be empty

Graphical captcha generation logic is configurable — incrementally adapted to change

Now our graphic captcha style is fixed, can only generate a number of captcha, others want to change a style or generate letters, man captcha seems powerless. He wondered if he could implement an interface that returned custom ImageCode to use his own captcha generation logic as he did with Spring

Spring provides the idea that you can implement an interface instead of a Spring implementation. A common design pattern is that you don’t need to change the code when you need to extend functionality, but simply add an implementation class to accommodate the change incrementally

First we abstract the logic that generates the graphic captcha into an interface

package top.zhenganwen.security.core.verifycode;

import top.zhenganwen.security.core.verifycode.dto.ImageCode;

/ * * *@author zhenganwen
 * @date 2019/8/25
 * @descImageCodeGenerator Graphic verification code generator interface */
public interface ImageCodeGenerator {

    ImageCode generateImageCode(int width, int height, int strLength, int durationSeconds);
}
Copy the code

The method of generating graphical captcha, previously written in Controller, is then used as the default implementation of this interface

package top.zhenganwen.security.core.verifycode;

import top.zhenganwen.security.core.verifycode.dto.ImageCode;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;

/ * * *@author zhenganwen
 * @date 2019/8/25
 * @desc DefaultImageCodeGenerator
 */
public class DefaultImageCodeGenerator implements ImageCodeGenerator {

    @Override
    public ImageCode generateImageCode(int width, int height, int strLength, int durationSeconds) {
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        Random random = new Random();

        g.setColor(getRandColor(200.250));
        g.fillRect(0.0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160.200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        String sRand = "";
        for (int i = 0; i < strLength; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6.16);
        }

        g.dispose();

        return new ImageCode(sRand, image, durationSeconds);
    }

    /** * generates a random background stripe **@param fc
     * @param bc
     * @return* /
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return newColor(r, g, b); }}Copy the code

ConditionOnMissingBean (@conditiononMissingBean). ConditionOnMissingBean (@conditiononMissingBean); ConditionOnMissingBean (@conditiononMissingBean); Determine if there is a bean with the ID imageCodeGenerator in the container. If not, instantiate it and use it as a bean with the ID imageCodeGenerator

package top.zhenganwen.security.core;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.security.core.verifycode.DefaultImageCodeGenerator;
import top.zhenganwen.security.core.verifycode.ImageCodeGenerator;

/ * * *@author zhenganwen
 * @date 2019/8/23
 * @desc SecurityCoreConfig
 */
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {

    @Bean
    @ConditionalOnMissingBean(name = "imageCodeGenerator")
    public ImageCodeGenerator imageCodeGenerator(a) {
        ImageCodeGenerator imageCodeGenerator = new DefaultImageCodeGenerator();
        returnimageCodeGenerator; }}Copy the code

The captcha generation interface was changed to rely on the Captcha generator interface to generate captcha (abstract oriented programming to accommodate changes) :

@RestController
@RequestMapping("/verifyCode")
public class VerifyCodeController {

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private ImageCodeGenerator imageCodeGenerator;

    /** * 1. Generate graphic verification code * 2. Save the verification code to session * 3. Gives the graph response to the front end */
    @GetMapping("/image")
    public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // First read width/height in the URL argument, if not in the configuration file
        int width = ServletRequestUtils.getIntParameter(request, "width", securityProperties.getCode().getImage().getWidth());
        int height = ServletRequestUtils.getIntParameter(request, "height", securityProperties.getCode().getImage().getHeight());

        ImageCode imageCode = imageCodeGenerator.generateImageCode(width, height,
                securityProperties.getCode().getImage().getStrLength(),
                securityProperties.getCode().getImage().getDurationSeconds());
        // Session read/write tool class, the first parameter is fixed
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
        ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream()); }}Copy the code

Restart the service and log in to ensure that the refactoring has not changed the functionality of the code

Finally, we added a custom graphic captcha generator in security-Demo to replace the default:

package top.zhenganwen.securitydemo.security;

import top.zhenganwen.security.core.verifycode.ImageCodeGenerator;
import top.zhenganwen.security.core.verifycode.dto.ImageCode;

/ * * *@author zhenganwen
 * @date 2019/8/25
 * @desc CustomImageCodeGenerator
 */
@Component("imageCodeGenerator")
public class CustomImageCodeGenerator implements ImageCodeGenerator {
    @Override
    public ImageCode generateImageCode(int width, int height, int strLength, int durationSeconds) {
        System.out.println("Call a custom code generator");
        return null; }}Copy the code

Here we simply print the log and return a NULL so that login. HTML will throw an exception if it uses our custom graphic captcha generator interface to generate a graphic captcha. Note that the value attribute of @Component must match the name attribute of @conditiononmissingBean for this substitution to work

Implement remember me function

demand

Sometimes users want to check a “Remember me” box when filling out a login form to access protected urls without logging in for a certain period of time after login

implementation

In this section, we will implement the following function:

  1. First of all, the page needs a “Remember me” box, whose name attribute should be remembered -me (configurable) and value attribute should be true

    <form action="/auth/login" method="post">User name:<input type="text" name="username">Password:<input type="password" name="password">Verification code:<input type="text" name="verifyCode"><img src="/verifyCode/image? width=200" alt="">
        <input type="checkbox" name="remember-me" value="true">Remember that I<button type="submit">submit</button>
    </form>
    Copy the code
  2. Create a table persistent_logins in the database corresponding to the data source, and the table creation statement is in the CREATE_TABLE_SQL variable of JdbcTokenRepositoryImpl

    create table persistent_logins (username varchar(64) not null, series varchar(64) primary key."+"token varchar(64) not null, last_used timestamp not null)
    Copy the code
  3. Add “rememberMe” to the seurity configuration class. Here, since cookies are restricted to the browser, we configure them in the security-browser module, below rememberMe() section

    @Autowired
        private DataSource dataSource;
    
        @Autowired
        private UserDetailsService userDetailsService;
    
        @Bean
        public PersistentTokenRepository persistentTokenRepository(a) {
            JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
            jdbcTokenRepository.setDataSource(dataSource);
            return jdbcTokenRepository;
        }
    
        @Override
        protected void configure(HttpSecurity http) throws Exception {
            http
                    .addFilterBefore(verifyCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                    .formLogin()
                        .loginPage("/auth/require")
                        .loginProcessingUrl("/auth/login")
                        .successHandler(customAuthenticationSuccessHandler)
                        .failureHandler(customAuthenticationFailureHandler)
                        .and()
                    .rememberMe()
                        .tokenRepository(persistentTokenRepository())
                        .tokenValiditySeconds(3600)
                        .userDetailsService(userDetailsService)
    // You can configure the name attribute of the page selection box
    // .rememberMeParameter()
                        .and()
                    .authorizeRequests()
                        .antMatchers(
                                "/auth/require",
                                securityProperties.getBrowser().getLoginPage(),
                                "/verifyCode/image").permitAll()
                        .anyRequest().authenticated()
                    .and()
                    .csrf().disable();
        }
    Copy the code
  4. test

    The persistent_logins database table is displayed. The persistent_logins database table is displayed. The persistent_logins database table is displayed. Closing the service simulates closing the Session (since the Session is the saving server, closing the server is a better guarantee of closing the Session than closing the browser). After the service is restarted, protected /user cannot be accessed directly

Source code analysis

Above is the “remember me”, the user first login sequence diagram, check in AbstractAuthenticationProcessingFilter user name password after successful at the end of the method will be called successfulAuthentication, Check the source code (partially omitted) :

protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult)
    throws IOException, ServletException {

    SecurityContextHolder.getContext().setAuthentication(authResult);

    rememberMeServices.loginSuccess(request, response, authResult);

    successHandler.onAuthenticationSuccess(request, response, authResult);
}
Copy the code

Found in successHandler. OnAuthenticationSuccess () call authentication processor before success, also performs the rememberMeServices. LoginSuccess, This method is used to the database insert a username – token of modules and the token Cookie, specific logic in PersistentTokenBasedRememberMeServices# onLoginSuccess ()

protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
    String username = successfulAuthentication.getName();

    PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
        username, generateSeriesData(), generateTokenData(), new Date());
    try {
        tokenRepository.createNewToken(persistentToken);
        addCookie(persistentToken, request, response);
    }catch (Exception e) {
        logger.error("Failed to save persistent token ", e); }}Copy the code

During our set tokenValiditySeconds, if the user login but not from the same browser to access the protected services, RememberMeAuthenticationFilter will intercept to the request:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
    throws IOException, ServletException {

    if (SecurityContextHolder.getContext().getAuthentication() == null) { Authentication rememberMeAuth = rememberMeServices.autoLogin(request, response); . }Copy the code

AutoLogin () is called to try to read the token from the Cookie and query the username-token from the persistence layer. If found, UserDetailsService is called to find the user based on username. Find the successful Authentication generated by the new Authentication and save it to the current thread safe:

AbstractRememberMeServices#autoLogin

public final Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
    String rememberMeCookie = extractRememberMeCookie(request);

    if (rememberMeCookie == null) {
        return null;
    }

    if (rememberMeCookie.length() == 0) {
        logger.debug("Cookie was empty");
        cancelCookie(request, response);
        return null;
    }

    UserDetails user = null;

    try {
        String[] cookieTokens = decodeCookie(rememberMeCookie);
        user = processAutoLoginCookie(cookieTokens, request, response);
        userDetailsChecker.check(user);

        returncreateSuccessfulAuthentication(request, user); }... }Copy the code

PersistentTokenBasedRememberMeServices

protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {

    final String presentedSeries = cookieTokens[0];
    final String presentedToken = cookieTokens[1];

    PersistentRememberMeToken token = tokenRepository
        .getTokenForSeries(presentedSeries);

    return getUserDetailsService().loadUserByUsername(token.getUsername());
}
Copy the code

SMS verification code login

Before, we used the traditional login method of user name and password, but with the popularity of SMS verification code login and third-party applications such as QQ login, the traditional login method can no longer meet our needs

The user name and password authentication process is already solidified in the Security framework, we can only write some implementation interface extension details, and the general process is not able to change. Therefore, to achieve SMS verification code login, we need to define a set of login procedures

Interface for sending SMS verification codes

To realize the SMS verification code function, we need to provide this interface first. The front end can call this interface to transmit the SMS verification code to the mobile phone number. As follows, the verification code is sent by clicking the event on the login page of the browser. It should have been asynchronously called the sending interface through AJAX. Here, for the convenience of demonstration, hyperlink is used for synchronous invocation, and the mobile phone number is written down instead of dynamically obtaining the mobile phone number entered by the user through JS

<form action="/auth/login" method="post">User name:<input type="text" name="username">Password:<input type="password" name="password">Verification code:<input type="text" name="verifyCode"><img src="/verifyCode/image? width=200" alt="">
    <input type="checkbox" name="remember-me" value="true">Remember that I<button type="submit">submit</button>
</form>
<hr/>
<form action="/auth/sms" method="post">Mobile phone no. :<input type="text" name="phoneNumber" value="12345678912">Verification code:<input type="text"><a href="/verifyCode/sms? phoneNumber=12345678912">Click on send</a>
    <input type="checkbox" name="remember-me" value="true">Remember that I<button type="submit">submit</button>
</form>
Copy the code

Refactoring PO

Security-core creates a new class to encapsulate the properties of the SMS verification code:

package top.zhenganwen.security.core.verifycode.dto;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.time.LocalDateTime;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class SmsCode {
    protected String code;
    protected LocalDateTime expireTime;
    public boolean isExpired(a) {
        returnLocalDateTime.now().isAfter(expireTime); }}Copy the code

Here, since the previous ImageCode also had these two properties, we rename SmsCode to VerifyCode for ImageCode to inherit to reuse code

@Data
@AllArgsConstructor
@NoArgsConstructor
public class VerifyCode {
    protected String code;
    protected LocalDateTime expireTime;
    public boolean isExpired(a) {
        returnLocalDateTime.now().isAfter(expireTime); }}Copy the code
@Data
public class ImageCode extends VerifyCode{
    private BufferedImage image;
    public ImageCode(String code, BufferedImage image, LocalDateTime expireTime) {
        super(code,expireTime);
        this.image = image;
    }
    public ImageCode(String code, BufferedImage image, int durationSeconds) {
        this(code, image, LocalDateTime.now().plusSeconds(durationSeconds)); }}Copy the code

Refactor the captcha generator

Next we need a SHORT message captcha generator, not as complex as a graphic captcha generator. ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean () ConditionOnMissingBean

package top.zhenganwen.security.core.verifycode.generator;

import top.zhenganwen.security.core.verifycode.dto.VerifyCode;

public interface VerifyCodeGenerator<T extends VerifyCode> {

    /** * Generates a verification code *@return* /
    T generateVerifyCode(a);
}

Copy the code
package top.zhenganwen.security.core.verifycode.generator;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.ServletRequestUtils;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.security.core.verifycode.dto.ImageCode;

import javax.servlet.http.HttpServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;

public class DefaultImageCodeGenerator implements VerifyCodeGenerator<ImageCode> {

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    HttpServletRequest request;

    @Override
    public ImageCode generateVerifyCode(a) {

        int width = ServletRequestUtils.getIntParameter(request, "width", securityProperties.getCode().getImage().getWidth());
        int height = ServletRequestUtils.getIntParameter(request, "height", securityProperties.getCode().getImage().getHeight());
        int strLength = securityProperties.getCode().getImage().getStrLength();
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        Random random = new Random();

        g.setColor(getRandColor(200.250));
        g.fillRect(0.0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160.200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        String sRand = "";
        for (int i = 0; i < strLength; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6.16);
        }

        g.dispose();

        return newImageCode(sRand, image, securityProperties.getCode().getImage().getDurationSeconds()); }... }Copy the code
package top.zhenganwen.securitydemo.security;

import top.zhenganwen.security.core.verifycode.generator.VerifyCodeGenerator;
import top.zhenganwen.security.core.verifycode.dto.ImageCode;

//@Component
public class CustomImageCodeGenerator implements VerifyCodeGenerator<ImageCode> {
    @Override
    public ImageCode generateVerifyCode(a) {
        System.out.println("Call a custom code generator");
        return null; }}Copy the code
package top.zhenganwen.security.core.verifycode.generator;

import org.apache.commons.lang.RandomStringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.security.core.verifycode.dto.VerifyCode;

import java.time.LocalDateTime;


@Component("smsCodeGenerator")
public class SmsCodeGenerator implements VerifyCodeGenerator<VerifyCode> {

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public VerifyCode generateVerifyCode(a) {
        // Generate a random string of pure digits strLength
        String randomCode = RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getStrLength());
        return newVerifyCode(randomCode, LocalDateTime.now().plusSeconds(securityProperties.getCode().getSms().getDurationSeconds())); }}Copy the code

SMS verification code sender

Generated after the message authentication code we need to save it in the Session and call the SMS service provider interface will be sent out, because the future rely on our application can configure different SMS service provider interface, in order to guarantee the scalability of the code we need to send a text message this behavior into abstract interface and provide a default can be covered, This allows applications that depend on us to enable their SMS logic by injecting a new implementation

package top.zhenganwen.security.core.verifycode;

public interface SmsCodeSender {
    /** * Send SMS verification code * according to mobile phone number@param smsCode
     * @param phoneNumber
     */
    void send(String smsCode, String phoneNumber);
}
Copy the code
package top.zhenganwen.security.core.verifycode;

public class DefaultSmsCodeSender implements SmsCodeSender {
    @Override
    public void send(String smsCode, String phoneNumber) {
        // This is just a simple print, actually should call the SMS service provider to send SMS verification code to the mobile phone number
        System.out.printf("Send SMS verification code %s to mobile phone number %s", phoneNumber, smsCode); }}Copy the code
package top.zhenganwen.security.core;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.security.core.verifycode.DefaultImageCodeGenerator;
import top.zhenganwen.security.core.verifycode.DefaultSmsCodeSender;
import top.zhenganwen.security.core.verifycode.ImageCodeGenerator;
import top.zhenganwen.security.core.verifycode.SmsCodeSender;

@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {

    @Bean
    @ConditionalOnMissingBean(name = "imageCodeGenerator")
    public ImageCodeGenerator imageCodeGenerator(a) {
        ImageCodeGenerator imageCodeGenerator = new DefaultImageCodeGenerator();
        return imageCodeGenerator;
    }

    @Bean
    @ConditionalOnMissingBean(name = "smsCodeSender")
    public SmsCodeSender smsCodeSender(a) {
        return newDefaultSmsCodeSender(); }}Copy the code

Refactoring configuration classes

package top.zhenganwen.security.core.properties;

import lombok.Data;

@Data
public class SmsCodeProperties {
    // Number of SMS verification codes. The default value is 4
    private int strLength = 4;
    // Valid time, 60 seconds by default
    private int durationSeconds = 60;
}

Copy the code
package top.zhenganwen.security.core.properties;

import lombok.Data;

@Data
public class ImageCodeProperties extends SmsCodeProperties{
    private int width=67;
    private int height=23;
    private String uriPatterns;

    public ImageCodeProperties(a) {
        // The graphic verification code displays 6 characters by default
        this.setStrLength(6);
        // The graphical verification code expires in 3 minutes by default
        this.setDurationSeconds(180); }}Copy the code
package top.zhenganwen.security.core.properties;

import lombok.Data;

@Data
public class VerifyCodeProperties {
    private ImageCodeProperties image = new ImageCodeProperties();
    private SmsCodeProperties sms = new SmsCodeProperties();
}
Copy the code

Interface for sending SMS verification codes

@RestController
@RequestMapping("/verifyCode")
public class VerifyCodeController {

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    public static final String IMAGE_CODE_SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

    public static final String SMS_CODE_SESSION_KEY = "SESSION_KEY_SMS_CODE";

    @Autowired
    private VerifyCodeGenerator<ImageCode> imageCodeGenerator;

    @Autowired
    private VerifyCodeGenerator<VerifyCode> smsCodeGenerator;

    @Autowired
    private SmsCodeSender smsCodeSender;

    /** * 1. Generate graphic verification code * 2. Save the verification code to session * 3. Gives the graph response to the front end */
    @GetMapping("/image")
    public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException {

        ImageCode imageCode = imageCodeGenerator.generateVerifyCode();
        sessionStrategy.setAttribute(new ServletWebRequest(request), IMAGE_CODE_SESSION_KEY, imageCode);
        ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
    }

    /** * 1. Generate the SMS verification code * 2. Save the verification code to the session * 3. Invokes the SMS verification code sender to send SMS */
    @GetMapping("/sms")
    public void smsCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException {
        long phoneNumber = ServletRequestUtils.getRequiredLongParameter(request, "phoneNumber");
        VerifyCode verifyCode = smsCodeGenerator.generateVerifyCode();
        sessionStrategy.setAttribute(newServletWebRequest(request), SMS_CODE_SESSION_KEY, verifyCode); smsCodeSender.send(verifyCode.getCode(), String.valueOf(phoneNumber)); }}Copy the code

test

In security-Browser, we will allow access to the new interface /verifyCode/ SMS:

	.authorizeRequests()
                    .antMatchers(
                            "/auth/require",
                            securityProperties.getBrowser().getLoginPage(),
                            "/verifyCode/**").permitAll()
                    .anyRequest().authenticated()
Copy the code

Go to /login.html and click send hyperlink. The background output is as follows:

Send SMS verification code 1220 to the mobile phone number 12345678912Copy the code

Refactoring – Template methods & dependency lookup

Now our two methods in VerifyCodeController have the same main flow for imageCode and smsCode:

  1. Generate captcha
  2. Save the verification code, for example, toSessionIn the,redisIn, etc.
  3. Send the verification code to the user

In this case, we can apply the template approach to the design pattern (see my other article, Graphic Design Patterns), and the reconstructed class diagram looks like this:

Constant class

public class VerifyCodeConstant {
    public static final String IMAGE_CODE_SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

    public static final String SMS_CODE_SESSION_KEY = "SESSION_KEY_SMS_CODE";

    public static final String VERIFY_CODE_PROCESSOR_IMPL_SUFFIX = "CodeProcessorImpl";

    public static final String VERIFY_CODE_Generator_IMPL_SUFFIX = "CodeGenerator";

    public static final String PHONE_NUMBER_PARAMETER_NAME = "phoneNumber";
}
Copy the code
public enum VerifyCodeTypeEnum {

    IMAGE("image"),SMS("sms");

    private String type;

    public String getType(a) {
        return type;
    }

    VerifyCodeTypeEnum(String type) {
        this.type = type; }}Copy the code

Captcha sending handler – template method & Interface isolation & dependency lookup

public interface VerifyCodeProcessor {
    /** * Send verification code logic * 1. Generate verification code * 2. Save verification code * 3. Send the verification code *@paramRequest is a utility class that encapsulates request and Response so that we don't have to pass {@linkJavax.mail. Servlet. HTTP. It} and {@linkJavax.mail. Servlet. HTTP. HttpServletResponse} * /
    void sendVerifyCode(ServletWebRequest request);
}
Copy the code
public abstract class AbstractVerifyCodeProcessor<T extends VerifyCode> implements VerifyCodeProcessor {

    @Override
    public void sendVerifyCode(ServletWebRequest request) {
        T verifyCode = generateVerifyCode(request);
        save(request, verifyCode);
        send(request, verifyCode);
    }

    /** * Generates a verification code **@param request
     * @return* /
    public abstract T generateVerifyCode(ServletWebRequest request);

    /** * Save the verification code **@param request
     * @param verifyCode
     */
    public abstract void save(ServletWebRequest request, T verifyCode);

    /** ** Send verification code **@param request
     * @param verifyCode
     */
    public abstract void send(ServletWebRequest request, T verifyCode);
}
Copy the code
@Component
public class ImageCodeProcessorImpl extends AbstractVerifyCodeProcessor<ImageCode> {

    private Logger logger = LoggerFactory.getLogger(getClass());

    /** * Spring will look for all {@linkAn instance of VerifyCodeGenerator} is injected into the map as key=beanId,value=bean */
    @Autowired
    private Map<String, VerifyCodeGenerator> verifyCodeGeneratorMap = new HashMap<>();

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    public ImageCode generateVerifyCode(ServletWebRequest request) {
        VerifyCodeGenerator<ImageCode> verifyCodeGenerator = verifyCodeGeneratorMap.get(IMAGE.getType() + VERIFY_CODE_Generator_IMPL_SUFFIX);
        return verifyCodeGenerator.generateVerifyCode();
    }

    @Override
    public void save(ServletWebRequest request, ImageCode imageCode) {
        sessionStrategy.setAttribute(request,IMAGE_CODE_SESSION_KEY, imageCode);
    }

    @Override
    public void send(ServletWebRequest request, ImageCode imageCode) {
        HttpServletResponse response = request.getResponse();
        try {
            ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
        } catch (IOException e) {
            logger.error("Output graphic verification code :{}", e.getMessage()); }}}Copy the code
@Component
public class SmsCodeProcessorImpl extends AbstractVerifyCodeProcessor<VerifyCode> {

    private Logger logger = LoggerFactory.getLogger(getClass());

    @Autowired
    private Map<String, VerifyCodeGenerator> verifyCodeGeneratorMap = new HashMap<>();

    @Autowired
    private SmsCodeSender smsCodeSender;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    public VerifyCode generateVerifyCode(ServletWebRequest request) {
        VerifyCodeGenerator verifyCodeGenerator = verifyCodeGeneratorMap.get(SMS.getType() + VERIFY_CODE_Generator_IMPL_SUFFIX);
        return verifyCodeGenerator.generateVerifyCode();
    }

    @Override
    public void save(ServletWebRequest request, VerifyCode verifyCode) {
        sessionStrategy.setAttribute(request, SMS_CODE_SESSION_KEY, verifyCode);
    }

    @Override
    public void send(ServletWebRequest request, VerifyCode verifyCode) {
        try {
            long phoneNumber = ServletRequestUtils.getRequiredLongParameter(request.getRequest(),PHONE_NUMBER_PARAMETER_NAME);
            smsCodeSender.send(verifyCode.getCode(),String.valueOf(phoneNumber));
        } catch (ServletRequestBindingException e) {
            throw new RuntimeException("Cell phone number cannot be empty."); }}}Copy the code

Captcha generator

public interface VerifyCodeGenerator<T extends VerifyCode> {

    /** * Generates a verification code *@return* /
    T generateVerifyCode(a);
}
Copy the code
public class DefaultImageCodeGenerator implements VerifyCodeGenerator<ImageCode> {

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    HttpServletRequest request;

    @Override
    public ImageCode generateVerifyCode(a) {

        int width = ServletRequestUtils.getIntParameter(request, "width", securityProperties.getCode().getImage().getWidth());
        int height = ServletRequestUtils.getIntParameter(request, "height", securityProperties.getCode().getImage().getHeight());
        int strLength = securityProperties.getCode().getImage().getStrLength();
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics g = image.getGraphics();
        Random random = new Random();

        g.setColor(getRandColor(200.250));
        g.fillRect(0.0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160.200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        String sRand = "";
        for (int i = 0; i < strLength; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            g.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6.16);
        }

        g.dispose();

        return new ImageCode(sRand, image, securityProperties.getCode().getImage().getDurationSeconds());
    }

    /** * generates a random background stripe **@param fc
     * @param bc
     * @return* /
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return newColor(r, g, b); }}Copy the code
@Component("smsCodeGenerator")
public class SmsCodeGenerator implements VerifyCodeGenerator<VerifyCode> {

    @Autowired
    private SecurityProperties securityProperties;

    @Override
    public VerifyCode generateVerifyCode(a) {
        // Generate a random string of pure digits strLength
        String randomCode = RandomStringUtils.randomNumeric(securityProperties.getCode().getSms().getStrLength());
        return newVerifyCode(randomCode, LocalDateTime.now().plusSeconds(securityProperties.getCode().getSms().getDurationSeconds())); }}Copy the code

Verification code sender

public interface SmsCodeSender {
    /** * Send SMS verification code * according to mobile phone number@param smsCode
     * @param phoneNumber
     */
    void send(String smsCode, String phoneNumber);
}
Copy the code
public class DefaultSmsCodeSender implements SmsCodeSender {
    @Override
    public void send(String smsCode, String phoneNumber) {
        System.out.printf("Send SMS verification code %s to mobile phone number %s", phoneNumber, smsCode); }}Copy the code

Verification code sending interface

@RestController
@RequestMapping("/verifyCode")
public class VerifyCodeController {

/* private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @Autowired private VerifyCodeGenerator
      
        imageCodeGenerator; @Autowired private VerifyCodeGenerator
       
         smsCodeGenerator; @Autowired private SmsCodeSender smsCodeSender; * /
       
      /** * 1. Generate graphic verification code * 2. Save the verification code to session * 3. Gives the graph response to the front end *//* @GetMapping("/image") public void imageCode(HttpServletRequest request, HttpServletResponse response) throws IOException { ImageCode imageCode = imageCodeGenerator.generateVerifyCode(); sessionStrategy.setAttribute(new ServletWebRequest(request), IMAGE_CODE_SESSION_KEY, imageCode); ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream()); } * //** * 1. Generate the SMS verification code * 2. Save the verification code to the session * 3. Invokes the SMS verification code sender to send SMS *//* @GetMapping("/sms") public void smsCode(HttpServletRequest request, HttpServletResponse response) throws ServletRequestBindingException { long phoneNumber = ServletRequestUtils.getRequiredLongParameter(request, "phoneNumber"); VerifyCode verifyCode = smsCodeGenerator.generateVerifyCode(); sessionStrategy.setAttribute(new ServletWebRequest(request), SMS_CODE_SESSION_KEY, verifyCode); smsCodeSender.send(verifyCode.getCode(), String.valueOf(phoneNumber)); } * /

    @Autowired
    private Map<String, VerifyCodeProcessor> verifyCodeProcessorMap = new HashMap<>();

    @GetMapping("/{type}")
    public void sendVerifyCode(@PathVariable String type, HttpServletRequest request, HttpServletResponse response) {
        if (Objects.equals(type, IMAGE.getType()) == false && Objects.equals(type, SMS.getType()) == false) {
            throw new IllegalArgumentException("Unsupported captcha types");
        }
        VerifyCodeProcessor verifyCodeProcessor = verifyCodeProcessorMap.get(type + VERIFY_CODE_PROCESSOR_IMPL_SUFFIX);
        verifyCodeProcessor.sendVerifyCode(newServletWebRequest(request, response)); }}Copy the code

The configuration class

package top.zhenganwen.security.core;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.security.core.verifycode.generator.DefaultImageCodeGenerator;
import top.zhenganwen.security.core.verifycode.sender.DefaultSmsCodeSender;
import top.zhenganwen.security.core.verifycode.generator.VerifyCodeGenerator;
import top.zhenganwen.security.core.verifycode.sender.SmsCodeSender;

/ * * *@author zhenganwen
 * @date 2019/8/23
 * @desc SecurityCoreConfig
 */
@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {

    @Bean
    @ConditionalOnMissingBean(name = "imageCodeGenerator")
    public VerifyCodeGenerator imageCodeGenerator(a) {
        VerifyCodeGenerator imageCodeGenerator = new DefaultImageCodeGenerator();
        return imageCodeGenerator;
    }

    @Bean
    @ConditionalOnMissingBean(name = "smsCodeSender")
    public SmsCodeSender smsCodeSender(a) {
        return newDefaultSmsCodeSender(); }}Copy the code

test

Keep in mind that refactoring only improves the quality and readability of your code, so after each small refactoring, always test to see if the original functionality has been affected

  • Access /login. HTML to login with the user name and password, and then access the protected service /user

  • Visit /login. HTML and click Send to check whether the console prints send logs

  • Modify /login. HTML to set the width of the graphic verification code to 600

    Verification code:<input type="text" name="verifyCode"><img src="/verifyCode/image? width=600" alt="">
    Copy the code

Test passed, refactoring successful!

SMS verification code login

To realize the SMS verification code login process, we can learn from the existing user name and password login process and analyze which components need to be realized by ourselves:

First we need an SmsAuthenticationFilter to intercept the SMS login request for Authentication, during which it will encapsulate the login information into an Authentication request AuthenticationManager for Authentication

AuthenticationManager will go through all authenticationProviders to find out which AuthenticationProvider supports Authentication and call Authenticate for the actual Authentication. So we need to implement your own Authentication (SmsAuthenticationToken) and the certification of the Authentication AuthenticationProvider (SmsAuthenticationProvider), And add SmsAuthenticationProvider SpringSecurty AuthenticationProvider collection, In order to make the AuthenticationManager traverse the collection can find our custom SmsAuthenticationProvider

When SmsAuthenticationProvider authentication, you need to call UserDetailsService according to cell number of storing user information (loadUserByUsername), So we also need a custom SmsUserDetailsService

Let’s implement it one by one (actually, COPY the code of the component corresponding to the login process of the user name and password to change it).

SmsAuthenticationToken

package top.zhenganwen.security.core.verifycode.sms;

import org.springframework.security.authentication.AbstractAuthenticationToken;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.SpringSecurityCoreVersion;

import java.util.Collection;

/ * * *@author zhenganwen
 * @date 2019/8/30
 * @desc SmsAuthenticationToken
 */
public class SmsAuthenticationToken extends AbstractAuthenticationToken {

    private static final long serialVersionUID = SpringSecurityCoreVersion.SERIAL_VERSION_UID;

    // ~ Instance fields
    / / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =
    // Before authentication, the mobile phone number entered by the user is saved. After authentication, the user details stored in the backend are saved
    private final Object principal;

    // ~ Constructors
    / / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

    /** * Call this method before authentication to encapsulate request parameters into an unauthenticated token => authRequest **@paramPhoneNumber mobile phoneNumber */
    public SmsAuthenticationToken(Object phoneNumber) {
        super(null);
        this.principal = phoneNumber;
        setAuthenticated(false);
    }

    SuccessToken => successToken **@paramPrincipal User details *@paramAuthorities */
    public SmsAuthenticationToken(Object principal, Object credentials, Collection
        authorities) {
        super(authorities);
        this.principal = principal;
        super.setAuthenticated(true); // must use super, as we override
    }

    // ~ Methods
    / / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

    // User name Password The login credential is the password, but the verification code does not transmit the password
    @Override
    public Object getCredentials(a) {
        return null;
    }

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

SmsAuthenticationFilter

package top.zhenganwen.security.core.verifycode.sms;

import org.springframework.security.authentication.AuthenticationServiceException;
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;

/ * * *@author zhenganwen
 * @date 2019/8/30
 * @desc SmsAuthenticationFilter
 */
public class SmsAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    // ~ Static fields/initializers
    / / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

    public static final String SPRING_SECURITY_FORM_PHONE_NUMBER_KEY = "phoneNumber";

    private String phoneNumberParameter = SPRING_SECURITY_FORM_PHONE_NUMBER_KEY;
    private boolean postOnly = true;

    // ~ Constructors
    / / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

    public SmsAuthenticationFilter(a) {
        super(new AntPathRequestMatcher("/auth/sms"."POST"));
    }

    // ~ Methods
    / / = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = = =

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if(postOnly && ! request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException(
                    "Authentication method not supported: " + request.getMethod());
        }

        String phoneNumber = obtainPhoneNumber(request);

        if (phoneNumber == null) {
            phoneNumber = "";
        }

        phoneNumber = phoneNumber.trim();

        SmsAuthenticationToken authRequest = new SmsAuthenticationToken(phoneNumber);

        return this.getAuthenticationManager().authenticate(authRequest);
    }

    /**
     * Enables subclasses to override the composition of the phoneNumber, such as by
     * including additional values and a separator.
     *
     * @param request so that request attributes can be retrieved
     *
     * @return the phoneNumber that will be presented in the <code>Authentication</code>
     * request token to the <code>AuthenticationManager</code>
     */
    protected String obtainPhoneNumber(HttpServletRequest request) {
        return request.getParameter(phoneNumberParameter);
    }

    /**
     * Sets the parameter name which will be used to obtain the phoneNumber from the login
     * request.
     *
     * @param phoneNumberParameter the parameter name. Defaults to "phoneNumber".
     */
    public void setPhoneNumberParameter(String phoneNumberParameter) {
        Assert.hasText(phoneNumberParameter, "phoneNumber parameter must not be empty or null");
        this.phoneNumberParameter = phoneNumberParameter;
    }

    /** * Defines whether only HTTP POST requests will be allowed by this filter. If set to * true, and an authentication request is received which is not a POST request, an * exception will be raised immediately and authentication will not be attempted. The * unsuccessfulAuthentication() method will be called as if handling a failed * authentication. * 

* Defaults to true but may be overridden by subclasses. */

public void setPostOnly(boolean postOnly) { this.postOnly = postOnly; } public final String getPhoneNumberParameter(a) { returnphoneNumberParameter; }}Copy the code

SmsAuthenticationProvider

package top.zhenganwen.security.core.verifycode.sms;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;

/ * * *@author zhenganwen
 * @date 2019/8/30
 * @desc SmsAuthenticationProvider
 */
public class SmsAuthenticationProvider implements AuthenticationProvider {

    private UserDetailsService userDetailsService;

    public SmsAuthenticationProvider(a) {}/** * This method is called by AuthenticationManager to verify authentication and returns an authenticated {@link Authentication}
     * @param authentication
     * @return* /
    @Override
    public Authentication authenticate(Authentication authentication){
        // User name password Login mode Check whether the password imported from the front end is consistent with that stored in the back end
        // However, if the verification of the SMS verification code is stored here, it cannot be reused. For example, users may need to send the SMS verification code to access the "My Wallet" service after logging in
        // Therefore, the verification logic of the SMS verification code is extracted separately into a filter (reserved for later implementation), which directly returns a successful authentication
        if (authentication instanceof SmsAuthenticationToken == false) {
            throw new IllegalArgumentException("Only SmsAuthenticationToken authentication is supported");
        }

        SmsAuthenticationToken authRequest = (SmsAuthenticationToken) authentication;
        UserDetails userDetails = getUserDetailsService().loadUserByUsername((String) authentication.getPrincipal());
        SmsAuthenticationToken successfulAuthentication = new SmsAuthenticationToken(userDetails, userDetails.getPassword(), userDetails.getAuthorities());
        return successfulAuthentication;
    }

    /** * The authenticate method calls this method when traversing all AuthenticationProviders to determine whether the current AuthenticationProvider is true Validation for a specific Authentication * * override this method to support validation for {@linkSmsAuthenticationToken} authentication verification *@paramToken types supported by Clazz *@return* /
    @Override
    public boolean supports(Class
        clazz) {
        // If the class passed is SmsAuthenticationToken or a subclass of it
        return SmsAuthenticationToken.class.isAssignableFrom(clazz);
    }

    public UserDetailsService getUserDetailsService(a) {
        return userDetailsService;
    }

    /** * Provides dynamic injection of UserDetailsService *@return* /
    public void setUserDetailsService(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService; }}Copy the code

SmsDetailsService

package top.zhenganwen.security.core.verifycode.sms;

import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.stereotype.Service;

import java.util.Objects;

/ * * *@author zhenganwen
 * @date 2019/8/30
 * @desc SmsUserDetailsService
 */
@Service
public class SmsUserDetailsService implements UserDetailsService {

    /** * Query the user based on the login name, which is the mobile phone number **@param phoneNumber
     * @return
     * @throws PhoneNumberNotFoundException
     */
    @Override
    public UserDetails loadUserByUsername(String phoneNumber) throws PhoneNumberNotFoundException {
        // DAO should actually be called to query the user based on the phone number
        if (Objects.equals(phoneNumber, "12345678912") = =false) {
            / / not to
            throw new PhoneNumberNotFoundException();
        }
        The / / check
        // Use the implementation of UserDetails provided by Security to simulate the detected User. In your project, you can implement the UserDetails interface using the User entity class, which returns the detected User entity object directly
        return new User("anwen"."123456", AuthorityUtils.createAuthorityList("admin"."super_admin")); }}Copy the code

Note that when this class is added, there are two UserDetails in the container. The @autowire UserDetails should be replaced with @Autowire customDetailsService, otherwise an error will be reported

SmsLoginConfig

We have implemented the components for each step, and now we need to write a configuration class to string them together and tell Security that these custom components exist. Since SMS login can be used on both PC and mobile terminals, it is defined in security-core

package top.zhenganwen.security.core.verifycode.sms;

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;

/ * * *@author zhenganwen
     * @date 2019/8/30
     * @desc SmsSecurityConfig
     */
@Component
public class SmsLoginConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {

    @Autowired
    AuthenticationSuccessHandler customAuthenticationSuccessHandler;

    @Autowired
    AuthenticationFailureHandler customAuthenticationFailureHandler;

    @Autowired
    UserDetailsService smsUserDetailsService;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
        // The authentication filter will ask AuthenticationManager to authenticate authRequest, so we need to inject AuthenticatonManager into it, but the instance is managed by Security and we need to get it via getSharedObject
        smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        // Authentication succeeds/fails with the same handler as before
        smsAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
        smsAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);

        SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
        / / will be injected into SmsAuthenticationProvider SmsUserDetailsService
        smsAuthenticationProvider.setUserDetailsService(smsUserDetailsService);

        / / will SmsAuthenticationProvider added to the Security management of AuthenticationProvider collection
        http.authenticationProvider(smsAuthenticationProvider)
            / / note to add to UsernamePasswordAuthenticationFilter, custom authentication filter should be added to the later, the custom authentication code of the filter should be added to it before.addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }}Copy the code

test

Login to /login. HTML and click send to view the SMS verification code output by the console. Then login to /login. HTML.

However, the user name and password login failed! The Bad Credentials are incorrect, so I debug the breakpoint at the password verification site:

DaoAuthenticationProvider#additionalAuthenticationChecks

protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication)
    throws AuthenticationException {
    Object salt = null;

    if (this.saltSource ! =null) {
        salt = this.saltSource.getSalt(userDetails);
    }

    if (authentication.getCredentials() == null) {
        logger.debug("Authentication failed: no credentials provided");

        throw new BadCredentialsException(messages.getMessage(
            "AbstractUserDetailsAuthenticationProvider.badCredentials"."Bad credentials"));
    }

    String presentedPassword = authentication.getCredentials().toString();

    if(! passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) { logger.debug("Authentication failed: password does not match stored value");

        throw new BadCredentialsException(messages.getMessage(
            "AbstractUserDetailsAuthenticationProvider.badCredentials"."Bad credentials")); }}Copy the code

PasswordEncoder is PlaintextPasswordEncoder instead of BCryptPasswordEncoder. Why?

We need to go back to the source to see when the passwordEncoder was assigned, Alt + F7 in this file to see when the setPasswordEncoder(Object passwordEncoder) method of that class was called, It’s going to be initialized to PlaintextPasswordEncoder in the constructor; But that’s not what we want. We want to see why the BCryptPasswordEncoder that was injected before adding the SMS verification code login function works. Ctrl + Alt + F7 searches the entire project and library for setPasswordEncoder(Object passwordEncoder) calls and finds the following clues:

InitializeUserDetailsManagerConfigurer

@Override
public void configure(AuthenticationManagerBuilder auth) throws Exception {
    if (auth.isConfigured()) {
        return;
    }
    UserDetailsService userDetailsService = getBeanOrNull(
        UserDetailsService.class);
    if (userDetailsService == null) {
        return;
    }

    PasswordEncoder passwordEncoder = getBeanOrNull(PasswordEncoder.class);

    DaoAuthenticationProvider provider = new DaoAuthenticationProvider();
    provider.setUserDetailsService(userDetailsService);
    if(passwordEncoder ! =null) {
        provider.setPasswordEncoder(passwordEncoder);
    }

    auth.authenticationProvider(provider);
}

/ * * *@return* /
private <T> T getBeanOrNull(Class<T> type) {
    String[] userDetailsBeanNames = InitializeUserDetailsBeanManagerConfigurer.this.context
        .getBeanNamesForType(type);
    if(userDetailsBeanNames.length ! =1) {
        return null;
    }

    return InitializeUserDetailsBeanManagerConfigurer.this.context
        .getBean(userDetailsBeanNames[0], type);
}
Copy the code

Originally, in and tried to find if we inject other PasswordEncoder instance DaoAuthenticationProvider injection before we configure BCryptPasswordEncoder, will from the container get populated UserDetails instance, If there are no instances in the container or the number of instances is greater than 1, then it returns.

Turns out, when we implemented SMS verification code login, The @Component of the SmsUserDetailsService annotation results in the existence of two UserDetailsService instances in the container, SmsUserDetailsService and the previous customUserDetailsService. So the above code code are not executed after 12, that is to say we have no injection CustomUserDetailsService and BCryptPasswordEncoder to DaoAuthenticationProvider.

As for why, before verifying the password, DaoAuthenticationProvider of enclosing getUserDetailsService (.) loadUserByUsername (username) can still call CustomUserDetailsService Cu is and why Rather than SmsUserDetialsService stomUserDetailsService injected by DaoAuthenticationProvider, remains to be analyzed

Now that the problem has been identified (there are two UserDetailsService instances in the container), the simple solution is to remove the @Component of SmsUserDetailsService and simply create a new one when configuring the SMS login tandem Component

//@Component
public class SmsUserDetailsService implements UserDetailsService {
Copy the code
@Component
public class SmsLoginConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {

    @Autowired
    AuthenticationSuccessHandler customAuthenticationSuccessHandler;

    @Autowired
    AuthenticationFailureHandler customAuthenticationFailureHandler;

    //	@Autowired
    //	SmsUserDetailsService smsUserDetailsService;

    @Override
    public void configure(HttpSecurity http) throws Exception {

        SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
        smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
        smsAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);

        SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
        // Do it yourself
        SmsUserDetailsService smsUserDetailsService = newSmsUserDetailsService(); smsAuthenticationProvider.setUserDetailsService(smsUserDetailsService); http.authenticationProvider(smsAuthenticationProvider) .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }}Copy the code

Retest both login methods, both can pass!

SMS verification code filter

As mentioned in the previous section, for reuse, we should put the verification logic of the SMS verification code into a separate filter. Here we can refer to the graphic verification code filter written before, and copy it for modification

package top.zhenganwen.security.core.verifycode.filter;

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.util.AntPathMatcher;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;
import top.zhenganwen.security.core.properties.SecurityProperties;
import top.zhenganwen.security.core.verifycode.exception.VerifyCodeException;
import top.zhenganwen.security.core.verifycode.po.VerifyCode;

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.Arrays;
import java.util.HashSet;
import java.util.Set;

import static top.zhenganwen.security.core.verifycode.constont.VerifyCodeConstant.SMS_CODE_SESSION_KEY;

/ * * *@author zhenganwen
 * @date 2019/8/24
 * @desc VerifyCodeAuthenticationFilter
 */
@Component
public class SmsCodeAuthenticationFilter extends OncePerRequestFilter implements InitializingBean {

    @Autowired
    private AuthenticationFailureHandler customAuthenticationFailureHandler;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    private Set<String> uriPatternSet = new HashSet<>();

    // uri matching tool class, help us to do similar /user/1 to /user/* matching
    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Override
    public void afterPropertiesSet(a) throws ServletException {
        super.afterPropertiesSet();
        String uriPatterns = securityProperties.getCode().getSms().getUriPatterns();
        if (StringUtils.isNotBlank(uriPatterns)) {
            String[] strings = StringUtils.splitByWholeSeparatorPreserveAllTokens(uriPatterns, ",");
            uriPatternSet.addAll(Arrays.asList(strings));
        }
        uriPatternSet.add("/auth/sms");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException,
    IOException {
        for (String uriPattern : uriPatternSet) {
            // If there is a match, the verification code needs to be intercepted
            if (antPathMatcher.match(uriPattern, request.getRequestURI())) {
                try {
                    this.validateVerifyCode(new ServletWebRequest(request));
                } catch (VerifyCodeException e) {
                    customAuthenticationFailureHandler.onAuthenticationFailure(request, response, e);
                    return;
                }
                break;
            }
        }
        filterChain.doFilter(request, response);
    }

    // Intercepts the user login request and reads the saved SMS verification code from the Session for comparison with the verification code submitted by the user
    private void validateVerifyCode(ServletWebRequest request){
        String smsCode = (String) request.getParameter("smsCode");
        if (StringUtils.isBlank(smsCode)) {
            throw new VerifyCodeException("Verification code cannot be empty.");
        }
        VerifyCode verifyCode = (VerifyCode) sessionStrategy.getAttribute(request, SMS_CODE_SESSION_KEY);
        if (verifyCode == null) {
            throw new VerifyCodeException("Captcha does not exist");
        }
        if (verifyCode.isExpired()) {
            throw new VerifyCodeException("Verification code has expired. Please refresh the page.");
        }
        if (StringUtils.equals(smsCode,verifyCode.getCode()) == false) {
            throw new VerifyCodeException("Verification code error"); } sessionStrategy.removeAttribute(request, SMS_CODE_SESSION_KEY); }}Copy the code

Then remember to add it to the security filter chain, and only before all authentication filters:

SecurityBrowserConfig

@Override
protected void configure(HttpSecurity http) throws Exception {

    http
        .addFilterBefore(smsCodeAuthenticationFilter,UsernamePasswordAuthenticationFilter.class)
        .addFilterBefore(verifyCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
        .formLogin()
        .loginPage("/auth/require")
        .loginProcessingUrl("/auth/login")
        .successHandler(customAuthenticationSuccessHandler)
        .failureHandler(customAuthenticationFailureHandler)
        .and()
        .rememberMe()
        .tokenRepository(persistentTokenRepository())
        .tokenValiditySeconds(3600)
        .userDetailsService(customUserDetailsService)
        // You can configure the name attribute of the page selection box
        // .rememberMeParameter()
        .and()
        .authorizeRequests()
        .antMatchers(
        "/auth/require",
        securityProperties.getBrowser().getLoginPage(),
        "/verifyCode/**").permitAll()
        .anyRequest().authenticated()
        .and()
        .csrf().disable()
        .apply(smsLoginConfig);
}
Copy the code

Finally, modify login URL/ Auth/SMS and smsCode in login. HTML:

<form action="/auth/login" method="post">User name:<input type="text" name="username" value="admin">Password:<input type="password" name="password" value="123">Verification code:<input type="text" name="verifyCode"><img src="/verifyCode/image? width=600" alt="">
    <input type="checkbox" name="remember-me" value="true">Remember that I<button type="submit">submit</button>
</form>
<hr/>
<form action="/auth/sms" method="post">Mobile phone no. :<input type="text" name="phoneNumber" value="12345678912">Verification code:<input type="text" name="smsCode"><a href="/verifyCode/sms? phoneNumber=12345678912">Click on send</a>
    <input type="checkbox" name="remember-me" value="true">Remember that I<button type="submit">submit</button>
</form>
Copy the code

Refactoring – Eliminates duplicate code

Before, we copied the code of graphic verification code filter and changed it into SMS verification code filter. The main process of these two classes is the same, but the specific implementation is slightly different (reading and writing the verification code object corresponding to different keys from the Session), which can be extracted by using template method

There are many literal mana values in our code, and we should eliminate them as much as possible, extract them as constants or configuration properties, and reference them where they are needed, so that we don’t have to forget the mana value and cause exceptions when we need to change it later. For example, if you simply change.loginPage(“/auth/require”) to.loginPage(“/authentication/require”), Rather than by changing the BrowserSecurityController @ RequestMapping (“/auth/require “), will cause the program function problems

We can package the codes related to system configuration into modules and put them into the corresponding configuration classes in security-core. Security-browser and security-app only leave their own specific configuration (e.g., remember-me mode of writing token to cookie should be put in security-Browser, Security-app corresponds to the configuration mode of mobile terminal remember-me). Finally, security-browser and security-app can reference the general configuration of security-core through HTTP. Apply to realize code reuse

As soon as there are two or more identical pieces of code in your project, your nose should be sharp enough to spot the least noticeable and most noticeable bad code smells, and you should be looking for ways to refactor them in a timely manner, rather than waiting for the system to become too big to change

Mana reconstruction

package top.zhenganwen.security.core.verifycode.filter;

public enum VerifyCodeType {

    SMS{
        @Override
        public String getVerifyCodeParameterName(a) {
            return SecurityConstants.DEFAULT_SMS_CODE_PARAMETER_NAME;
        }
    },

    IMAGE{
        @Override
        public String getVerifyCodeParameterName(a) {
            returnSecurityConstants.DEFAULT_IMAGE_CODE_PARAMETER_NAME; }};public abstract String getVerifyCodeParameterName(a);
}
Copy the code
package top.zhenganwen.security.core;

public interface SecurityConstants {

    /** * Form password login URL */
    String DEFAULT_FORM_LOGIN_URL = "/auth/login";

    /** * SMS login URL */
    String DEFAULT_SMS_LOGIN_URL = "/auth/sms";

    /** * Front-end graphics verification code parameter name */
    String DEFAULT_IMAGE_CODE_PARAMETER_NAME = "imageCode";

    /** * Front-end SMS verification code parameter name */
    String DEFAULT_SMS_CODE_PARAMETER_NAME = "smsCode";

    /** * Graphic captcha cached in Session key */
    String IMAGE_CODE_SESSION_KEY = "IMAGE_CODE_SESSION_KEY";

    /** * THE SMS verification code is cached in key */ of the Session
    String SMS_CODE_SESSION_KEY = "SMS_CODE_SESSION_KEY";

    /** * Validator bean name suffix */
    String VERIFY_CODE_VALIDATOR_NAME_SUFFIX = "CodeValidator";

    /** * If you do not log in to the protected URL, jump to this */
    String FORWARD_TO_LOGIN_PAGE_URL = "/auth/require";

    /** * The user clicks to send the verification code to invoke the service */
    String VERIFY_CODE_SEND_URL = "/verifyCode/**";
}
Copy the code

The verification code filter was reconstructed. Procedure

  • VerifyCodeValidatorFilterIs responsible for intercepting requests that require captcha verification
  • VerifyCodeValidator, using template method, abstract verification logic of the verification code
  • VerifyCodeValidatorHolderUsing Spring’s dependency lookup, aggregate all of theVerifyCodeValidatorImplementation class (specific verification logic of various verification codes) to provide external verification based on the verification code typebeanThe method of

Login. HTML, where the graphic captcha parameter is changed to imageCode

<form action="/auth/login" method="post">User name:<input type="text" name="username" value="admin">Password:<input type="password" name="password" value="123">Verification code:<input type="text" name="imageCode"><img src="/verifyCode/image? width=600" alt="">
    <input type="checkbox" name="remember-me" value="true">Remember that I<button type="submit">submit</button>
</form>
<hr/>
<form action="/auth/sms" method="post">Mobile phone no. :<input type="text" name="phoneNumber" value="12345678912">Verification code:<input type="text" name="smsCode"><a href="/verifyCode/sms? phoneNumber=12345678912">Click on send</a>
    <input type="checkbox" name="remember-me" value="true">Remember that I<button type="submit">submit</button>
</form>
Copy the code

VerifyCodeValidateFilter:

package top.zhenganwen.security.core.verifycode.filter;

import static top.zhenganwen.security.core.SecurityConstants.DEFAULT_SMS_LOGIN_URL;

@Component
public class VerifyCodeValidateFilter extends OncePerRequestFilter implements InitializingBean {

    // Failed to authenticate the processor
    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    // session reads and writes tools
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    // Map the URI and type of the verification code, for example, /auth/login -> Graphic verification code /auth/ SMS -> SMS verification code
    private Map<String, VerifyCodeType> uriMap = new HashMap<>();

    @Autowired
    private SecurityProperties securityProperties;

    private AntPathMatcher antPathMatcher = new AntPathMatcher();

    @Autowired
    private VerifyCodeValidatorHolder verifyCodeValidatorHolder;

    @Override
    public void afterPropertiesSet(a) throws ServletException {
        super.afterPropertiesSet();

        uriMap.put(SecurityConstants.DEFAULT_FORM_LOGIN_URL, VerifyCodeType.IMAGE);
        putUriPatterns(uriMap, securityProperties.getCode().getImage().getUriPatterns(), VerifyCodeType.IMAGE);

        uriMap.put(SecurityConstants.DEFAULT_SMS_LOGIN_URL, VerifyCodeType.SMS);
        putUriPatterns(uriMap, securityProperties.getCode().getSms().getUriPatterns(), VerifyCodeType.SMS);
    }

    private void putUriPatterns(Map<String, VerifyCodeType> urlMap, String uriPatterns, VerifyCodeType verifyCodeType) {
        if (StringUtils.isNotBlank(uriPatterns)) {
            String[] strings = StringUtils.splitByWholeSeparatorPreserveAllTokens(uriPatterns, ",");
            for(String string : strings) { urlMap.put(string, verifyCodeType); }}}@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException
            , IOException {
        try {
            checkVerifyCodeIfNeed(request, uriMap);
        } catch (VerifyCodeException e) {
            authenticationFailureHandler.onAuthenticationFailure(request, response, e);
            return;
        }
        filterChain.doFilter(request, response);
    }

    private void checkVerifyCodeIfNeed(HttpServletRequest request, Map<String, VerifyCodeType> uriMap) {
        String requestUri = request.getRequestURI();
        Set<String> uriPatterns = uriMap.keySet();
        for (String uriPattern : uriPatterns) {
            if (antPathMatcher.match(uriPattern, requestUri)) {
                VerifyCodeType verifyCodeType = uriMap.get(uriPattern);
                verifyCodeValidatorHolder.getVerifyCodeValidator(verifyCodeType).validateVerifyCode(new ServletWebRequest(request), verifyCodeType);
                break; }}}}Copy the code

VerifyCodeValidator

package top.zhenganwen.security.core.verifycode.filter;

import java.util.Objects;

public abstract class VerifyCodeValidator {

    protected SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private VerifyCodeValidatorHolder verifyCodeValidatorHolder;

    /** * Verification code * 1. Obtain the verification code * 2 from the request. Obtain the verification code from the server * 3. Verify the verification code * 4. Remove the verification code from the server successfully@param request
     * @param verifyCodeType
     * @throws VerifyCodeException
     */
    public void validateVerifyCode(ServletWebRequest request, VerifyCodeType verifyCodeType) throws VerifyCodeException {
        String requestCode = getVerifyCodeFromRequest(request, verifyCodeType);

        VerifyCodeValidator codeValidator = verifyCodeValidatorHolder.getVerifyCodeValidator(verifyCodeType);
        if (Objects.isNull(codeValidator)) {
            throw new VerifyCodeException("Unsupported verification type:" + verifyCodeType);
        }
        VerifyCode storedVerifyCode = codeValidator.getStoredVerifyCode(request);

        codeValidator.validate(requestCode, storedVerifyCode);

        codeValidator.removeStoredVerifyCode(request);
    }

    /** * To verify whether the verification code is expired, simple text comparison is performed by default. Subclasses can be overridden to verify the incoming plaintext verification code and the stored ciphertext verification code **@param requestCode
     * @param storedVerifyCode
     */
    private void validate(String requestCode, VerifyCode storedVerifyCode) {
        if (Objects.isNull(storedVerifyCode) || storedVerifyCode.isExpired()) {
            throw new VerifyCodeException("Verification code is invalid. Please regenerate it.");
        }
        if (StringUtils.isBlank(requestCode)) {
            throw new VerifyCodeException("Verification code cannot be empty.");
        }
        if (StringUtils.equalsIgnoreCase(requestCode, storedVerifyCode.getCode()) == false) {
            throw new VerifyCodeException("Verification code error"); }}/** * It is up to subclasses to remove captcha from Session or from other caching methods **@param request
     */
    protected abstract void removeStoredVerifyCode(ServletWebRequest request);

    It is up to subclasses to read captcha from Session or from other caches **@param request
     * @return* /
    protected abstract VerifyCode getStoredVerifyCode(ServletWebRequest request);


    /** * Takes the captcha argument from the request by default and can be overridden by subclasses **@param request
     * @param verifyCodeType
     * @return* /
    private String getVerifyCodeFromRequest(ServletWebRequest request, VerifyCodeType verifyCodeType) {
        try {
            return ServletRequestUtils.getStringParameter(request.getRequest(), verifyCodeType.getVerifyCodeParameterName());
        } catch (ServletRequestBindingException e) {
            throw new VerifyCodeException("Illegal request, please attach captcha parameter"); }}}Copy the code

ImageCodeValidator

package top.zhenganwen.security.core.verifycode.filter;

@Component
public class ImageCodeValidator extends VerifyCodeValidator {

    @Override
    protected void removeStoredVerifyCode(ServletWebRequest request) {
        sessionStrategy.removeAttribute(request, SecurityConstants.IMAGE_CODE_SESSION_KEY);
    }

    @Override
    protected VerifyCode getStoredVerifyCode(ServletWebRequest request) {
        return(VerifyCode) sessionStrategy.getAttribute(request, SecurityConstants.IMAGE_CODE_SESSION_KEY); }}Copy the code

SmsCodeValidator

package top.zhenganwen.security.core.verifycode.filter;

@Component
public class SmsCodeValidator extends VerifyCodeValidator {

    @Override
    protected void removeStoredVerifyCode(ServletWebRequest request) {
        sessionStrategy.removeAttribute(request, SecurityConstants.SMS_CODE_SESSION_KEY);
    }

    @Override
    protected VerifyCode getStoredVerifyCode(ServletWebRequest request) {
        return(VerifyCode) sessionStrategy.getAttribute(request,SecurityConstants.SMS_CODE_SESSION_KEY); }}Copy the code

VerifyCodeValidatorHolder

package top.zhenganwen.security.core.verifycode.filter;

@Component
public class VerifyCodeValidatorHolder {

    @Autowired
    private Map<String, VerifyCodeValidator> verifyCodeValidatorMap = new HashMap<>();

    public VerifyCodeValidator getVerifyCodeValidator(VerifyCodeType verifyCodeType) {
        VerifyCodeValidator verifyCodeValidator =
                verifyCodeValidatorMap.get(verifyCodeType.toString().toLowerCase() + SecurityConstants.VERIFY_CODE_VALIDATOR_NAME_SUFFIX);
        if (Objects.isNull(verifyCodeType)) {
            throw new VerifyCodeException("Unsupported captchas :" + verifyCodeType);
        }
        returnverifyCodeValidator; }}Copy the code

SecurityBrowserConfig

@Autowire
VerifyCodeValidatorFilter verifyCodeValidatorFilter;

http
// .addFilterBefore(smsCodeAuthenticationFilter,UsernamePasswordAuthenticationFilter.class)
// .addFilterBefore(verifyCodeAuthenticationFilter, UsernamePasswordAuthenticationFilter.class)
                .addFilterBefore(verifyCodeValidateFilter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()
                    .loginPage(SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL)
                    .loginProcessingUrl(SecurityConstants.DEFAULT_FORM_LOGIN_URL)
                    .successHandler(customAuthenticationSuccessHandler)
                    .failureHandler(customAuthenticationFailureHandler)
                    .and()
                .rememberMe()
                    .tokenRepository(persistentTokenRepository())
                    .tokenValiditySeconds(3600)
                    .userDetailsService(customUserDetailsService)
                    .and()
                .authorizeRequests()
                    .antMatchers(
                            SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL,
                            securityProperties.getBrowser().getLoginPage(),
                            SecurityConstants.VERIFY_CODE_SEND_URL).permitAll()
                    .anyRequest().authenticated()
                .and()
                .csrf().disable()
                .apply(smsLoginConfig);
Copy the code

System Configuration Reconstruction

security-core

package top.zhenganwen.security.core.config;

@Component
public class SmsLoginConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {

    @Autowired
    AuthenticationSuccessHandler customAuthenticationSuccessHandler;

    @Autowired
    AuthenticationFailureHandler customAuthenticationFailureHandler;

    @Override
    public void configure(HttpSecurity http) throws Exception {

        SmsAuthenticationFilter smsAuthenticationFilter = new SmsAuthenticationFilter();
        smsAuthenticationFilter.setAuthenticationManager(http.getSharedObject(AuthenticationManager.class));
        smsAuthenticationFilter.setAuthenticationSuccessHandler(customAuthenticationSuccessHandler);
        smsAuthenticationFilter.setAuthenticationFailureHandler(customAuthenticationFailureHandler);

        SmsAuthenticationProvider smsAuthenticationProvider = new SmsAuthenticationProvider();
        SmsUserDetailsService smsUserDetailsService = newSmsUserDetailsService(); smsAuthenticationProvider.setUserDetailsService(smsUserDetailsService); http.authenticationProvider(smsAuthenticationProvider) .addFilterAfter(smsAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); }}Copy the code
package top.zhenganwen.security.core.config;

@Component
public class VerifyCodeValidatorConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain.HttpSecurity> {

    @Autowired
    private VerifyCodeValidateFilter verifyCodeValidateFilter;

    @Override
    public void configure(HttpSecurity builder) throws Exception { builder.addFilterBefore(verifyCodeValidateFilter, UsernamePasswordAuthenticationFilter.class); }}Copy the code

security-browser

package top.zhenganwen.securitydemo.browser;

@Configuration
public class SecurityBrowserConfig extends WebSecurityConfigurerAdapter {

    @Bean
    public BCryptPasswordEncoder passwordEncoder(a) {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private AuthenticationSuccessHandler customAuthenticationSuccessHandler;

    @Autowired
    private AuthenticationFailureHandler customAuthenticationFailureHandler;

    @Autowired
    private DataSource dataSource;

    @Autowired
    private UserDetailsService customUserDetailsService;

    @Bean
    public PersistentTokenRepository persistentTokenRepository(a) {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        return jdbcTokenRepository;
    }

    @Autowired
    SmsLoginConfig smsLoginConfig;

    @Autowired
    private VerifyCodeValidatorConfig verifyCodeValidatorConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception {

        // Enable the verification filter
        http.apply(verifyCodeValidatorConfig);
        // Enable the SMS login filter
        http.apply(smsLoginConfig);
        
        http
                // Enable the form password login filter
                .formLogin()
                    .loginPage(SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL)
                    .loginProcessingUrl(SecurityConstants.DEFAULT_FORM_LOGIN_URL)
                    .successHandler(customAuthenticationSuccessHandler)
                    .failureHandler(customAuthenticationFailureHandler)
                    .and()
                // The browser application-specific configuration saves the token generated after login in a cookie
                .rememberMe()
                    .tokenRepository(persistentTokenRepository())
                    .tokenValiditySeconds(3600)
                    .userDetailsService(customUserDetailsService)
                    .and()
                // Browser application-specific configuration.authorizeRequests() .antMatchers( SecurityConstants.FORWARD_TO_LOGIN_PAGE_URL, securityProperties.getBrowser().getLoginPage(), SecurityConstants.VERIFY_CODE_SEND_URL).permitAll() .anyRequest().authenticated().and() .csrf().disable(); }}Copy the code

Use Spring Social to develop third-party logins

Introduction to the OAuth protocol

background

Sometimes applications cooperate with each other to achieve win-win results. For example, the popular micro channel public number, micro channel small program. On the one hand, developers of public accounts and small programs can attract wechat users with rich content to improve the user retention rate of wechat. On the other hand, public accounts and small programs can use wechat’s strong user base for their own services

When problems come, if you use the traditional way, the small program to achieve the user information to the user application to claim your account password (such as skin care small programs need to read the user’s WeChat photo albums to beautify), not to mention the user give not to, even if the user to, so will still exist the following questions (facial small programs, for example)

  • Access permissions

    It is impossible to control the access of the small program, saying that it only reads wechat photo albums. Who knows if he will check his wechat friends and use wechat wallet after taking the account password

  • Authorized limitation

    Once the mini program obtains the user’s account password, the user cannot control the authorization. The mini program will not use the account password to log in illegally in the future. The user can only change the password after each authorization

  • reliability

    If a user authorizes multiple applets in this way, once the applets disclose the user password, the user faces the risk of number theft

OAuth solution

When a user agrees to authorize a third party application (such as wechat applet relative to wechat user), the third party application will only be given a token (through which the third party application can access the user’s specific data resources). This token is created to solve the above problem:

  • The token is time-limited and only valid for a specified period of time, which solves the problem of authorization limitation
  • Tokens can only access specific resources granted by the user, which solves the problem of access rights
  • A token is a random string that is valid for a short time and has no meaning when expired, which solves the reliability problem

OAuth protocol running process

Let’s start with a few of the roles and responsibilities involved:

  • ProviderService providers such as wechat and QQ have huge amounts of user data
    • Authorization Server, the authentication server is generated by the authentication server after the user agrees to authorizetokenTo a third party application
    • Resource ServerTo store resources required by third-party applicationstokenIf correct, the corresponding resources are open to third-party applications
  • Resource Owner, the resource owner. For example, the wechat user is the resource owner of the wechat album. The photos are taken by the wechat user but stored on the wechat server
  • Client, third-party applications that rely on service providers with a strong user base for traffic diversion

The second step above also involves several authorization modes:

  • Authorization Code Mode (authorization code)
  • Password mode (resource owner password credentials)
  • Customer Card Mode (client credentials)
  • Simplified mode (implicit)

This chapter and the next chapter (APP) will respectively introduce the first two modes in detail. At present, almost most social platforms on the Internet, such as QQ, Weibo, Taobao and other service providers, adopt the authorization code mode

Authorization code mode Authorization process

For example, when we visit a social networking site, we do not want to register the users of the site but directly use QQ to log in. The figure is the approximate sequence diagram of QQ joint login developed by the social networking site as a third-party application using OAuth protocol

The authorization code pattern is widely used for the following two reasons:

  • The behavior of user consent authorization is confirmed on the authentication server, which is more transparent than the confirmation on the third-party application client in the other three modes (the client can forge user consent authorization)
  • The authentication server does not return the token directly, but the authorization code first. Static sites like some may be usedimplicitThe pattern lets the authentication server return the token directly and then make an AJAX call to the resource server interface on the page. The authentication server connects to a third-party application servertokenIt is used to call back the third-party application interface that has been agreed with the third-party applicationtokenTherefore, alltokenAre stored on the server); The latter is the client that the authentication server connects to third-party applications such as browsers,tokenSecurity risks exist if the client is directly sent to the client

This is why the mainstream service providers are adopting the authorization code model, because the authorization process is more complete and secure.

The fundamentals of Spring Social

Spring Social encapsulates the authorization process described in the sequence diagram into specific classes and interfaces. OAuth protocol has two versions, foreign very early use so popular OAuth1, and domestic use is relatively late so basically OAuth2, this chapter is also based on OAuth2 to integrate QQ, wechat login function.

The main components of Spring Social are shown below:

  • OAuth2OperationsThe encapsulation goes back to us from requesting user authorization to the authentication servicetokenThe whole process.OAuth2TemplateIs the default implementation provided for us, and the process is basically fixed without our involvement
  • Api, package gettokenThen we call the resource server interface to get user information, which needs to be defined by ourselves, after all, the framework does not know which open platform we want to access, but it also provides us with an abstractionAbstractOAuth2ApiBinding
  • AbstractOAuth2ServiceProviderThe integrationOAuth2OperationandApi, string fetchtokenAnd taketokenTwo processes for accessing user resources
  • ConnectionSince the data structure of user information returned by different service providers is inconsistent, we need to use the adapterApiAdapterTo unify itConnectionThis data structure can be viewed as the user’s entity in the service provider
  • OAuth2ConnectionFactoryThe integrationAbstractOAuth2ServiceProviderandApiAdapterTo complete the process of user authorization and obtaining user information entities
  • UsersConnectionRepository, our system generally has its own user table, how to access the system of user entitiesConnectionAnd our own user entitiesUserThat’s how it works, how it works for ususerIdtoConnectionThe mapping of

Develop QQ login function

To be continued…

The resources

Video tutorial links: pan.baidu.com/s/1wQWD4wE0… Extraction code: Z6ZI