Adding graphic verification code to Spring Security in Spring Boot

Adding the verification code can be roughly divided into three steps: generating the verification code picture according to the random number; displaying the verification code picture on the login page; adding the verification code verification in the authentication process. The authentication verification of Spring Security is completed by the UsernamePasswordAuthenticationFilter filter, so our verification code verification logic should precede this filter. Now let's learn how to do it in the previous section Spring Security custom user authentication The verification function of verification code is added.

Generate graphic verification code

The function of verification code requires spring social config dependency:

 <dependency>
    <groupId>org.springframework.social</groupId>
    <artifactId>spring-social-config</artifactId>
</dependency>

First, define a verification code object ImageCode:

public class ImageCode {

    private BufferedImage image;

    private String code;

    private LocalDateTime expireTime;

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

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

    public boolean isExpire() {
        return LocalDateTime.now().isAfter(expireTime);
    }
    // get,set omitted
}

The image code object contains three properties: image picture, code verification code and expireTime expiration time. The isExpire method is used to determine whether the verification code has expired.

Then define a ValidateController to process the request to generate the verification code:

@RestController
public class ValidateController {

    public final static String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE";

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ImageCode imageCode = createImageCode();
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode);
        ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());
    }
}

The createImageCode method is used to generate verification code objects. The org.springframework.social.connect.web.HttpSessionSessionStrategy object encapsulates some methods for processing sessions, including setAttribute, getAttribute and removeaattribute methods. For details, you can see the source code of this class. Use Session strategy to store the generated captcha object in Session, and output the generated picture to login page through IO flow.

The createImageCode method code is as follows:

private ImageCode createImageCode() {

    int width = 100; // Verification code picture width
    int height = 36; // Verification code picture length
    int length = 4; // Number of verification codes
    int expireIn = 60; // Validity time of verification code 60s

    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);
    }

    StringBuilder sRand = new StringBuilder();
    for (int i = 0; i < length; i++) {
        String rand = String.valueOf(random.nextInt(10));
        sRand.append(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(image, sRand.toString(), expireIn);
}

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 new Color(r, g, b);
}

After the verification code generation method is written, the next step is to transform the login page.

 

Transformation landing page

Add the following code to the login page:

<span style="display: inline">
    <input type="text" name="imageCode" placeholder="Verification Code" style="width: 50%;"/>
    <img src="/code/image"/>
</span>

The src attribute of the < img > tag corresponds to the createImageCode method of ValidateController.

For the request to generate the verification code not to be blocked, you need to configure the non blocking in the configure method of BrowserSecurityConfig:

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.formLogin() // Form login
            // http.httpBasic() // HTTP Basic
            .loginPage("/authentication/require") // Login jump URL
            .loginProcessingUrl("/login") // Process form login URL
            .successHandler(authenticationSucessHandler) // Login processed successfully
            .failureHandler(authenticationFailureHandler) // Failed to process login
            .and()
            .authorizeRequests() // Authorization configuration
            .antMatchers("/authentication/require",
                    "/login.html",
                    "/code/image").permitAll() // Request path without authentication
            .anyRequest()  // All requests
            .authenticated() // All need certification
            .and().csrf().disable();
}

Change login.html

<!DOCTYPE html>
<html>
<head>
    <meta charset="UTF-8">
    <title>Sign in</title>
</head>
<body>
<form style="width: 50%" action="/login" method="post">
    <div class="form">
        <h3>Account login</h3>
        <input type="text" placeholder="User name" name="username" required="required"/>
        <br>
        <input type="password" placeholder="Password" name="password" required="required"/>
        <br>
        <span style="display: inline">
            <input type="text" name="imageCode" placeholder="Verification Code" style="height: 100%;"/>
            <img src="/code/image"/>
        </span>
        <br>
        <button type="submit">Sign in</button>
    </div>
</form>
</body>
</html>

Restart project, access http://localhost:8080/login.html , the effect is as follows:

Verification code verification added to authentication process

During the verification of verification code, exceptions of various verification code types may be thrown, such as "verification code error", "verification code expired", etc., so we define an exception class of verification code type:

import org.springframework.security.core.AuthenticationException;
public class ValidateCodeException extends AuthenticationException {
    private static final long serialVersionUID = 5022575393500654458L;

    public ValidateCodeException(String message) {
        super(message);
    }
}

Note that AuthenticationException is inherited instead of Exception.

As we all know, Spring Security is actually a filter chain composed of many filters. The filter handling user login logic is UsernamePasswordAuthenticationFilter, and the verification process of verification code should be before this filter, that is, only after the verification code passes the verification, the user name and password will be verified. Since Spring Security does not directly provide filter interfaces related to verification code verification, we need to define a filter ValidateCodeFilter for verification code verification:

public class ValidateCodeFilter extends OncePerRequestFilter {

    @Autowired
    private AuthenticationFailureHandler authenticationFailureHandler;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse,
                                    FilterChain filterChain) throws ServletException, IOException {

        if (StringUtils.startsWithIgnoreCase("/login", httpServletRequest.getRequestURI())
                && StringUtils.startsWithIgnoreCase(httpServletRequest.getMethod(), "post")) {
            try {
                validateCode(new ServletWebRequest(httpServletRequest));
            } catch (ValidateCodeException e) {
                authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
                return;
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

    private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {
        //
    }

}

ValidateCode filter inherits org.springframework.web.filter.OncePerRequestFilter, which is only executed once.

In the doFilterInternal method, we determine whether the request URL is / login. This path corresponds to the action path of the login form, and whether the request method is POST. If yes, carry out the verification code verification logic. Otherwise, directly execute filterChain.doFilter to let the code go down. When an exception is caught during verification code verification, Spring Security's verification failure processor, AuthenticationFailureHandler, is called for processing.

The validation logic of validateCode is as follows:

private void validateCode(ServletWebRequest servletWebRequest) throws ServletRequestBindingException {
        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE);
        String codeInRequest = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(), "imageCode");

        if (StringUtils.isEmpty(codeInRequest)) {
            throw new ValidateCodeException("Verification code cannot be empty!");
        }
        if (codeInSession == null) {
            throw new ValidateCodeException("Verification code does not exist!");
        }
        if (codeInSession.isExpire()) {
            sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE);
            throw new ValidateCodeException("Verification code expired!");
        }
        if (!StringUtils.startsWithIgnoreCase(codeInSession.getCode(), codeInRequest)) {
            throw new ValidateCodeException("The verification code is incorrect!");
        }
        sessionStrategy.removeAttribute(servletWebRequest, ValidateController.SESSION_KEY_IMAGE_CODE);

    }

We obtain the Imagecode object and request parameter Imagecode (corresponding to the authentication code < input > box name attribute of the login page) from the Session, and then make various judgments and throw corresponding exceptions. When the verification code expires or passes the verification, we can delete the Imagecode attribute in the Session.

After the verification code verification filter is defined, how can it be added in front of the UsernamePasswordAuthenticationFilter? Simply add some configuration to the configure method of BrowserSecurityConfig:

@Autowired
private ValidateCodeFilter validateCodeFilter;

@Override
protected void configure(HttpSecurity http) throws Exception {
    http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) // Add verification code verification filter
            .formLogin() // Form login
            // http.httpBasic() // HTTP Basic
            .loginPage("/authentication/require") // Login jump URL
            .loginProcessingUrl("/login") // Process form login URL
            .successHandler(authenticationSucessHandler) // Login processed successfully
            .failureHandler(authenticationFailureHandler) // Failed to process login
            .and()
            .authorizeRequests() // Authorization configuration
            .antMatchers("/authentication/require",
                    "/login.html",
                    "/code/image").permitAll() // Request path without authentication
            .anyRequest()  // All requests
            .authenticated() // All need certification
            .and().csrf().disable();
}

In the above code, we injected the ValidateCode filter, and then added the ValidateCode filter verification filter in front of the UsernamePasswordAuthenticationFilter through the addFilterBefore method.

Success, restart project, visit http://localhost:8080/login.html When entering the verification code casually, click log in and the page will display as follows:

When the page is loaded for 60 seconds, input the verification code and click log in. The page is as follows

When the verification code passes and the user name and password are correct, the page will display as follows:

Source: https://gitee.com/hekang'admin/security-demo2.git

Keywords: Programming Spring Session Attribute git

Added by Baumusu on Mon, 23 Dec 2019 11:54:25 +0200