04-图片验证码

674 阅读2分钟

图片验证码

为了防止恶意用户无限制的重试,往往在登录页面引入验证码机制。我们在learn-03项目的基础上进行改进引入验证码。

pom.xml

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.security</groupId>
            <artifactId>spring-security-config</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
        <!--加载图片验证-->
        <dependency>
            <groupId>com.github.penggle</groupId>
            <artifactId>kaptcha</artifactId>
            <version>2.3.2</version>
        </dependency>
    </dependencies>

为了实现验证码首先需要一个获取图形验证码的API。在这里使用开源工具kaptcha

创建kaptcha的配置文件 kaptcha.properties

kaptcha.border=yes
kaptcha.border.color=105,179,90
kaptcha.textproducer.char.length=4
kaptcha.textproducer.font.names=宋体,楷体,微软雅黑
kaptcha.textproducer.font.size=40
kaptcha.textproducer.font.color=black
kaptcha.image.width=110
kaptcha.image.height=50

创建配置类 KaptchaConfig

@Configuration
@PropertySource(value = {"classpath:kaptcha/kaptcha.properties"}) // 配置文件存放的路径
public class KaptchaConfig {

    @Value("${kaptcha.border}")
    String border;
    @Value("${kaptcha.border.color}")
    String borderColor;
    @Value("${kaptcha.textproducer.char.length}")
    String charLength;
    @Value("${kaptcha.textproducer.font.names}")
    String fontName;
    @Value("${kaptcha.textproducer.font.size}")
    String fontSize;
    @Value("${kaptcha.textproducer.font.color}")
    String fontColor;
    @Value("${kaptcha.image.width}")
    String imgWidth;
    @Value("${kaptcha.image.height}")
    String imgHeight;

    @Bean
    public DefaultKaptcha getDefaultKaptcha() {
        DefaultKaptcha captchaProducer = new DefaultKaptcha();
        Properties properties = new Properties();
        properties.setProperty("kaptcha.border", border);
        properties.setProperty("kaptcha.border.color", borderColor);
        properties.setProperty("kaptcha.textproducer.font.color", fontColor);
        properties.setProperty("kaptcha.image.width", imgWidth);
        properties.setProperty("kaptcha.image.height", imgHeight);
        properties.setProperty("kaptcha.textproducer.font.size", fontSize);
        properties.setProperty("kaptcha.session.key", Constants.KAPTCHA_SESSION_KEY);
        properties.setProperty("kaptcha.textproducer.char.length", charLength);
        properties.setProperty("kaptcha.textproducer.font.names", fontName);
        Config config = new Config(properties);
        captchaProducer.setConfig(config);
        return captchaProducer;
    }
}

创建验证码验证以及过期时间配置 KaptchaImageVo

@Data
public class KaptchaImageVo {
    private String code;
    private LocalDateTime expiredTime;

    public KaptchaImageVo(String code, int expiredAfterSeconds){
        this.code = code;
        this.expiredTime = LocalDateTime.now().plusSeconds(expiredAfterSeconds);
    }

    /**
     * 判断是否过期
     */
    public boolean isExpired(){
        return expiredTime.isBefore(LocalDateTime.now());
    }

}

创建获取验证码图片的网址

@RestController
public class CodeController {
    @Resource
    private Producer captchaProducer;

    @RequestMapping("/kaptcha")
    public void getKaptchaImage(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpSession session = request.getSession();
        response.setDateHeader("Expires", 0);
        response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
        response.addHeader("Cache-Control", "post-check=0, pre-check=0");
        response.setHeader("Pragma", "no-cache");
        response.setContentType("image/jpeg");
        //生成验证码
        String capText = captchaProducer.createText();
        // 设置验证码的有效时间
        KaptchaImageVo imageVo = new KaptchaImageVo(capText, 2 * 60);
        session.setAttribute(Constants.KAPTCHA_SESSION_KEY, imageVo);
        //向客户端写出
        BufferedImage bi = captchaProducer.createImage(capText);
        ServletOutputStream out = response.getOutputStream();
        ImageIO.write(bi, "jpg", out);
        try {
            out.flush();
        } finally {
            out.close();
        }
    }
}

创建过滤器

@Component
public class CaptchaCodeFilter extends OncePerRequestFilter {
    @Resource
    private LoginFailHandler loginFailHandler;


    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response,
                                    FilterChain filterChain) throws ServletException, IOException {
        // 判断请求的url为 login和提交的方式post才能进行操作
        String uri = request.getRequestURI();
        // 去除空格和转换成小写
        String method = request.getMethod().trim().toLowerCase();
        // url为/login 而且
        String loginUrl = "/login";
        if (uri.equals(loginUrl) && "post".equals(method)) {
            // 捕获异常信息
            try {
                validate(request);
            } catch (SessionAuthenticationException e) {
                loginFailHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }
        // 继续向下执行
        filterChain.doFilter(request, response);
    }

    private void validate(HttpServletRequest request) {
        HttpSession session = request.getSession();
        // 获取前端传递回来的验证码数据
        String captcha = request.getParameter("captcha");
        // 从session中获取保存的验证码谜底信息
        KaptchaImageVo kaptchaImageVo = (KaptchaImageVo) session.getAttribute(Constants.KAPTCHA_SESSION_KEY);
        // 前端传递回来的信息为空
        if (StringUtils.isEmpty(captcha)) {
            throw new SessionAuthenticationException("没有填写验证码");
        }

        if (Objects.isNull(kaptchaImageVo)) {
            throw new SessionAuthenticationException("验证码不存在");
        }

        // 验证码过期了
        if (kaptchaImageVo.isExpired()) {
            throw new SessionAuthenticationException("验证码已经过期了");
        }

        if (!kaptchaImageVo.getCode().equals(captcha)) {
            throw new SessionAuthenticationException("验证码填写不正确");
        }
    }
}

修改失败的处理方式 加入验证码失败的提示

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException e) throws IOException, ServletException {


        String errMsg = "用户名或者密码填写错误";
        // 判断是否是验证码的错误 因为 CaptchaCodeFilter抛出的是SessionAuthenticationException异常
        if (e instanceof SessionAuthenticationException) {
            errMsg = "验证码填写错误";
        }

        response.setContentType("application/json; charset=UTF-8");
        response.getWriter().write(objectMapper.writeValueAsString(R.error().data(errMsg)));
    }

修改spring security的配置文件加如验证码的过滤器

   @Resource
    private CaptchaCodeFilter captchaCodeFilter;

@Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(captchaCodeFilter, UsernamePasswordAuthenticationFilter.class)
                .csrf().disable()
                .logout()
          ......
            .antMatchers("/login.html", "/login","/kaptcha").permitAll() // 需要放行/kaptcha用于生成验证码
    }