[Spring Security] 图片验证码的实现与校验

本文介绍如何使用Spring Security实现图片验证码的生成与验证过程,包括定义ImageCode实体类、生成验证码图片、设置验证码过期时间及实现自定义过滤器进行验证。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

图片验证码是当今使用最广泛的用户验证方式之一,在之前实现了简单的表单登录的基础上,今天用spring security来实现一下图片验证码的实现过程。

1.demo结构

2.实现过程

首先创建ImageCode实体类,定义图片验证码各参数。

ImageCode.java

package com.security.validate.code;

import lombok.Data;

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

/**
 * @author ShotMoon
 */
@Data
public class ImageCode {
    //验证码图片
    private BufferedImage image;
    //验证码随机数字
    private String code;
    //验证码失效时间
    private LocalDateTime expireTime;
    //验证码过期与否
    public boolean isExpired(){
        return LocalDateTime.now().isAfter(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;
    }
}

登录页面添加图片验证码显示

default-loginPage.html:

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>security login</title>
</head>
<body>
    <h3>登录</h3>
    <form action="/authentication/form" method="post">
        <table>
            <tr>
                <td>用户名:</td>
                <td><input type="text" name="username"></td>
            </tr>
            <tr>
                <td>密码:</td>
                <td><input type="password" name="password"></td>
            </tr>
            <tr>
                <td>图形验证码:</td>
                <td>
                    <input type="text" name="imageCode">
                    <img src="/code/image">
                </td>
            </tr>
            <tr>
                <td colspan="2"><button type="submit">登录</button></td>
            </tr>
        </table>
    </form>
</body>
</html>

登录时页面会向"/code/image"发送一个GET请求来获取图片验证码,我们来写一下图片验证码的生成逻辑。

ValidateController.java:

package com.security.validate.code;

import com.security.properties.SecurityProperties;
import org.apache.catalina.servlet4preview.http.HttpServletRequest;
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.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

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

/**
 * @author ShotMoon
 */
@RestController
public class ValidateController {

    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";//图片验证码存入session时,作为Key

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {

        ImageCode imageCode = createImageCode(request);

        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);//将验证码存入session,留验证之用
        ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());//将验证码图片以jpeg格式写入输出流,以在页面显示
    }

    private ImageCode createImageCode(HttpServletRequest request) {
        int width = 65; //验证码图片长度
        int height = 25;//宽

        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 < securityProperties.getImageCodeProperties().getLength(); 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(image, sRand, securityProperties.getImageCodeProperties().getExpireIn());
    }

    /**
     * 生成随机背景条纹
     *
     */
    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);
    }
}

经上述步骤,我们的验证码已经可以在页面显示了,效果如下图,

接下来是校验的逻辑,还记得我们之前说过,spring security的本质是一系列的过滤器组成的过滤器链吗,我们只需要自定义一个过滤器并将其插入到spring security的过滤器链上,就可以执行我们自定义的认证逻辑了。


ValidateCodeFilter.java:

package com.security.validate.code;

import lombok.Data;
import org.apache.commons.lang.StringUtils;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * @author ShotMoon
 */
@Data
public class ValidateCodeFilter extends OncePerRequestFilter {  //保证过滤器每次请求只会被调用一次

    private AuthenticationFailureHandler authenticationFailureHandler;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        //当请求路径为/authentication/form,且请求为POST请求时,才执行验证。(对应登录页面发送的请求)
        if (StringUtils.equals("/authentication/form", request.getRequestURI())
                && StringUtils.equals(request.getMethod(), "POST")) {

            try {
                validate(new ServletWebRequest((request)));
            } catch (ValidateCodeException e) {
                authenticationFailureHandler.onAuthenticationFailure(request, response, e);
            }
        }

        filterChain.doFilter(request, response);
    }

    private void validate(ServletWebRequest request) throws ServletRequestBindingException, ValidateCodeException {
        //从请求中取出之前存入session的验证码
        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request,
                ValidateController.SESSION_KEY);
        //获取form表单中用户输入的验证码
        String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");  //对应form表单中图片name

        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException("验证码不能为空");
        }
        if (codeInSession == null) {
            throw new ValidateCodeException("验证码不存在");
        }
        if (codeInSession.isExpired()) {
            sessionStrategy.removeAttribute(request, ValidateController.SESSION_KEY);
            throw new ValidateCodeException("验证码已过期");
        }
        if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
            throw new ValidateCodeException("验证码不匹配");
        }

        sessionStrategy.removeAttribute(request, ValidateController.SESSION_KEY);
    }
}

ValidateCodeException.java:

package com.security.validate.code;


import org.springframework.security.core.AuthenticationException;

/**
 * @author ShotMoon
 */
public class ValidateCodeException extends AuthenticationException {

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

然后,我们将定义好的过滤器插入到UsernamePasswordAuthenticationFilter前面,来验证图片验证码

SecurityConfig.java:

package com.security.config;

import com.security.authentication.MyAuthenticationFailureHandler;
import com.security.authentication.MyAuthenticationSuccessHandler;
import com.security.validate.code.ValidateCodeFilter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * @author ShotMoon
 */
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

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

    @Autowired
    private MyAuthenticationSuccessHandler myAuthenticationSuccessHandler;

    @Autowired
    private MyAuthenticationFailureHandler myAuthenticationFailureHandler;

    /**
     * @description :
     * @param : [http]
     * @return : void
     * @date : 2018/5/13 13:41
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {

        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(myAuthenticationFailureHandler);

        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class) //插入目标过滤器之前
                .formLogin()
                .loginPage("/authentication/require")
                .loginProcessingUrl("/authentication/form")
                .successHandler(myAuthenticationSuccessHandler)
                .failureHandler(myAuthenticationFailureHandler)
                .and()
                .authorizeRequests()
                .antMatchers(    //不需验证的请求路径,防止出现redirect too many times死循环
                        "/default-loginPage.html",
                        "/customized-loginPage.html",
                        "/authentication/require",
                        "/code/image"

                ).permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .csrf().disable();
    }
}

为了增强程序的可定义性,我们同样增加了配置验证码相关选项的功能:

ImageCodeProperties.java:

package com.security.properties;

import lombok.Data;

/**
 * @author ShotMoon
 */
@Data
public class ImageCodeProperties {
    //若application.properties配置了相关参数,则默认数值被覆盖不生效
    private int width = 65;

    private int height = 25;

    private int length = 4;

    private int expireIn = 60;
}

application.properties:

spring.datasource.driver-class-name = com.mysql.jdbc.Driver
spring.datasource.url=jdbc:mysql://127.0.0.1:3306/security?useUnicode=yes&characterEncoding=UTF-8
spring.datasource.username=root
spring.datasource.password=qwe123

security.basic.enabled=false

spring.session.store-type=none

#shotmoon.security.LoginProperties.loginPage = /customized-loginPage.html

//配置验证码过期时间为120秒
shotmoon.security.ImageCodeProperties.expireIn  = 120

好了,大功告成,经过以上代码,我们已经可以生成校验图片验证码了!(其他代码在上次的博文中,不重复列举了)


测试下


验证码正确:


验证码错误:(输出exception)



评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值