一、引入验证码模块:

通过以下maven依赖将验证码模块引入项目中

  <!-- 验证码 -->
        <dependency>
            <groupId>com.github.whvcse</groupId>
            <artifactId>easy-captcha</artifactId>
            <version>1.6.2</version>
        </dependency>

二、验证码接口:

创建一个Controller,在里面写上如下代码: 生成验证码和一个随机数,并将随机数和验证码作为键和值存入redis缓存中,同时返回随机数和验证码图片的base64编码。

    @GetMapping("/code")
    public ResponseEntity captcha() {
        SpecCaptcha specCaptcha = new SpecCaptcha(130, 50, 5);
        String verCode = specCaptcha.text().toLowerCase();
        String key = UUID.randomUUID().toString();
        log.info("code: {}, key: {}", verCode, key);
        // 存入redis并设置过期时间为5分钟
        redisService.setCacheObject("code::" + key, verCode, 5, TimeUnit.MINUTES);
        // 将key和base64返回给前端
        Map<String, Object> map = new HashMap<>();
        map.put("key", key);
        map.put("image", specCaptcha.toBase64());
        return Result.success(map);
    }

三、编写过滤器拦:

创建一个类继承GenericFilterBean类,重写doFilter方法。对请求的参数中验证码部分进行拦截校验。

@Component
public class VerifyCodeFilter extends GenericFilterBean {

    @Autowired
    private RedisService redisService;

    private String defaultFilterProcessUrl = "/login";

    @Override
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest) req;
        HttpServletResponse response = (HttpServletResponse) res;
        if ("POST".equalsIgnoreCase(request.getMethod()) && defaultFilterProcessUrl.equals(request.getServletPath())) {
            // 验证码验证
            String requestCaptcha = request.getParameter("code");
            String key = request.getParameter("key");
            if (StringUtils.isEmpty(requestCaptcha)) {
                ResponseUtil.outMessage(response, ResponseUtil.resultMap(false, 400, "登录失败,验证码不能为空!"));
                throw new CustomException("登录失败!验证码不能为空!");
            }
            if (StringUtils.isEmpty(key)) {
                ResponseUtil.outMessage(response, ResponseUtil.resultMap(false, 400, "登录失败,参数不足!"));
                throw new CustomException("参数不足!登录失败");
            }
            String originCode = redisService.getCacheObject("code::" + key.trim().toLowerCase(Locale.ROOT));
            redisService.deleteObject("code::" + key);
            if (StringUtils.isNull(originCode)) {
                ResponseUtil.outMessage(response, ResponseUtil.resultMap(false, 400, "登录失败,验证码已过期!"));
                throw new CustomException("登录失败,验证码已过期!");
            }
            if (!originCode.equalsIgnoreCase(requestCaptcha)) {
                ResponseUtil.outMessage(response, ResponseUtil.resultMap(false, 400, "登录失败,验证码错误"));
                throw new CustomException("验证码错误!");
            }
            chain.doFilter(request, response);
        }
    }
}

ResponseUtil工具类的outMessage方法如下 :

public static void outMessage(HttpServletResponse response, Map<String, Object> resultMap) {
        ServletOutputStream outputStream = null;
        try {
            outputStream = response.getOutputStream();
            outputStream.write(JSON.toJSONString(resultMap).getBytes(StandardCharsets.UTF_8));
            outputStream.flush();
        } catch (IOException e) {
        } finally {
            if (outputStream != null) {
                try {
                    outputStream.close();
                } catch (IOException e) {
                }
            }
        }
    }

四、将过滤器加入到Spring Security的过滤器链中,且在账号密码校验之前。

@Configuration
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    VerifyCodeFilter verifyCodeFilter;
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(verifyCodeFilter,UsernamePasswordAuthenticationFilter.class)
        //其余配置
    }
}

总结:

获取验证码 >> 将验证码存入缓存同时响应到前端 >> 登录时带上验证码和key >> 拦截器拦截登录请求 >> 根据key从缓存获取验证码进行校验

最重要的就是将写好的过滤器放到正确的位置,如这里将自定义验证码校验过滤器verifyCodeFilter放到账号密码认证过滤器UsernamePasswordAuthenticationFilter之前:

http.addFilterBefore(verifyCodeFilter,UsernamePasswordAuthenticationFilter.class)