springboot统一验证码组件设计(一)

简介: springboot统一验证码组件设计

前言:平时咱们在使用各类平台或系统的时候,都会弹出验证码,类似这种:

image.png

亦或是这种

image.png

还有就是这种:

image.png

好吧,这种也算:

image.png

所有的验证码,无论是图片验证码,还是滑块验证码,亦或是短信验证码、语音验证码,它们的作用都是为了防止应用接口被恶意的非人为操作不断调用。

以第一张图或第二张图为例,不针对这个发短信的接口做一个图片验证码的话,那么就很可能被恶意程序调用,导致后台程序不断地发送短信验证码给指定手机号码的人,这样不仅会造成公司的损失,也会给接收短信的人造成不必要的困扰。有了图片验证码后,调用接口的时候需要带上被识别的验证码,恶意程序就相对有难度才能调用你的这个被保护的接口了,大大降低了这方面的困扰。

注意点: 很多同学在这个验证码的时候,仅仅是简单地通过前端调用获取验证码的接口,然后再把用户提交的验证码交给后台验证,验证通过后再发起业务请求。这种做法只是做到了表面上有验证码的验证过程,实际上还是没有做到对业务接口的保护。交互过程如下图:

image.png

这样的交互逻辑是有很明显的漏洞的,它把验证的权限交给了客户端,前端说通过就通过,那么对于任何一个了解并且会使用一定手段或工具的人来说,这样的验证码就是形同虚设。使用api工具就可以直接跳到第三步直接调用业务接口。

真正的验证码应该做到和业务接口绑定,如下图的交互逻辑:

image.png

按照以上交互逻辑,无论如何,客户端必须带上验证码才能真正地调用后台服务处理业务请求,否则就无法达到目的。

后面就以java web为例来实现上述的交互逻辑:

为了可以将验证码逻辑和具体业务逻辑解藕,利用了servlet的Filter作为过滤器来判断当前请求的接口是否需要通过验证码验证后才能被调用

import com.zx.silverfox.common.exception.GlobalException;
import com.zx.silverfox.common.validate.code.AbstractValidateCodeProcessor;
import com.zx.silverfox.common.vo.CommonResponse;
import lombok.SneakyThrows;
import org.apache.commons.lang3.ArrayUtils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.web.servlet.filter.OrderedFilter;
import org.springframework.http.HttpMethod;
import org.springframework.http.MediaType;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.context.request.ServletWebRequest;
import javax.servlet.*;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.List;
import java.util.Objects;
/** @author zouwei */
public class ValidateCodeFilter implements OrderedFilter {
  //利用spring特性获取所有的验证码处理器
    @Autowired private List<AbstractValidateCodeProcessor> validateCodeProcessorList;
    @Override
    public void init(FilterConfig filterConfig) {}
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
            throws IOException, ServletException {
        HttpServletRequest req = (HttpServletRequest) request;
        HttpServletResponse res = (HttpServletResponse) response;
        AntPathMatcher matcher = new AntPathMatcher();
      // 判断当前这个请求是否需要验证,并且验证请求中携带的验证码
        if (!validateCode(req, res, matcher)) {
            return;
        }
      // 生成验证码
        if (generatorCode(req, res, matcher)) {
            return;
        }
        chain.doFilter(request, response);
    }
    @Override
    public void destroy() {}
    /**
     * 验证操作
     * @param request
     * @param response
     * @param matcher
     * @return
     */
    private boolean validateCode(
            HttpServletRequest request, HttpServletResponse response, AntPathMatcher matcher) {
        String url = request.getRequestURI();
      //循环调用验证码处理器进行验证
        for (AbstractValidateCodeProcessor processor : validateCodeProcessorList) {
            String[] filterUrls = processor.filterUrls();
            if (ArrayUtils.isEmpty(filterUrls)) {
                continue;
            }
            for (String filterUrl : filterUrls) {
              // 先判断当前接口是否需要拦截,如果匹配成功,就开始进行验证
                if (matcher.match(filterUrl, url)) {
                    return validate(request, response, processor);
                }
            }
        }
        return true;
    }
    @SneakyThrows
    private boolean validate(
            HttpServletRequest request,
            HttpServletResponse response,
            AbstractValidateCodeProcessor processor) {
        if (Objects.isNull(processor)) {
            return false;
        }
        try {
          // 执行验证
            processor.validate(new ServletWebRequest(request, response));
        } catch (GlobalException e) {
          // 验证失败的话,捕获异常,并处理响应
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.getOutputStream()
                    .write(CommonResponse.exceptionInstance(e).toJson().getBytes());
            return false;
        }
        return true;
    }
    /**
     * 生成验证码
     * @param request
     * @param response
     * @param matcher
     * @return
     */
    @SneakyThrows
    private boolean generatorCode(
            HttpServletRequest request, HttpServletResponse response, AntPathMatcher matcher) {
      // 获取验证码只能通过GET请求
        if (!StringUtils.equalsIgnoreCase(request.getMethod(), HttpMethod.GET.name())) {
            return false;
        }
        String url = request.getRequestURI();
      // 依然还是通过验证码处理器去做生成验证码的操作
        for (AbstractValidateCodeProcessor processor : validateCodeProcessorList) {
          // 检查当前请求是要生成哪种类型的验证码
            if (matcher.match(processor.generatorUrl(), url)) {
                try {
                  // 生成验证码
                    processor.create(new ServletWebRequest(request, response));
                } catch (GlobalException e) {
                    //失败后捕获异常,并处理响应
                  response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                    response.getOutputStream()
                            .write(CommonResponse.exceptionInstance(e).toJson().getBytes());
                }
                return true;
            }
        }
        return false;
    }
// 设置当前过滤器的优先级
    @Override
    public int getOrder() {
        return REQUEST_WRAPPER_FILTER_MAX_ORDER - 104;
    }
}
复制代码

上述ValidateCodeFilter是一个验证码逻辑入口类,也是整个逻辑的黏合剂,真正实现还是要靠AbstractValidateCodeProcessor这个处理器

import com.zx.silverfox.common.exception.GlobalException;
import com.zx.silverfox.common.properties.ValidateCodeProperties;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.CollectionUtils;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.List;
import java.util.Objects;
/** @author zouwei */
public abstract class AbstractValidateCodeProcessor<C extends ValidateCode>
        implements ValidateCodeProcessor, ApplicationContextAware {
    /** 标记验证码的唯一key */
    protected static final String CODE_KEY = "code_key";
    /** 发送验证码前需要调用的操作 */
    @Autowired(required = false)
    private List<ValidateCodeHandler> handlerList;
    /** 实现ApplicationContextAware,获取ApplicationContext */
    private static ApplicationContext APPLICATION_CONTEXT;
    /** 用作生成验证码 */
    private ValidateCodeGenerator validateCodeGenerator;
    /** 用作存取验证码 */
    private ValidateCodeRepository validateCodeRepository;
    /** 用作获取验证码相关系统配置 */
    private ValidateCodeProperties.CodeProperties codeProperties;
  /** 构造函数 */
    public AbstractValidateCodeProcessor(
            ValidateCodeGenerator validateCodeGenerator,
            ValidateCodeRepository validateCodeRepository,
            ValidateCodeProperties.CodeProperties codeProperties) {
        this.validateCodeGenerator = validateCodeGenerator;
        this.validateCodeRepository = validateCodeRepository;
        this.codeProperties = codeProperties;
    }
    protected static ApplicationContext getApplicationContext() {
        return APPLICATION_CONTEXT;
    }
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        APPLICATION_CONTEXT = applicationContext;
    }
    /** 生成验证码逻辑 */
    @Override
    public void create(ServletWebRequest request) throws GlobalException {
        // 生成指定验证码
        C validateCode = generate(request);
        ValidateCodeType codeType = getValidateCodeType();
       // 检查是否需要在发送该验证码之前执行一些指定的操作;比如注册的时候验证一下手机号码是否已经被注册;
        if (!CollectionUtils.isEmpty(handlerList)) {
            for (ValidateCodeHandler handler : handlerList) {
                if (handler.support(request, codeType)) {
                    handler.beforeSend(request, codeType, validateCode);
                }
            }
        }
        HttpServletResponse response = request.getResponse();
        // 用作保存验证码的key,方便后续的验证操作
        String codeKeyValue = request.getSessionId();
        response.setHeader(CODE_KEY, codeKeyValue);
        // 保存验证码数据
        save(request, validateCode, codeKeyValue);
        // 发送验证码
        send(request, validateCode);
    }
    /**
     * 保存验证码
     *
     * @param request
     * @param validateCode
     */
    private void save(ServletWebRequest request, C validateCode, String codeKeyValue) {
        validateCodeRepository.save(request, validateCode, getValidateCodeType(), codeKeyValue);
    }
    /**
     * 获取ValidateCodeType
     *
     * @return
     */
    protected abstract ValidateCodeType getValidateCodeType();
    /**
     * 验证码发送
     *
     * @param request
     * @param validateCode
     * @throws Exception
     */
    protected abstract void send(ServletWebRequest request, C validateCode) throws GlobalException;
    /**
     * 创建验证码
     *
     * @param request
     * @return
     */
    private C generate(ServletWebRequest request) {
        return (C) validateCodeGenerator.createValidateCode(request);
    }
    private String getCodeKeyValue(ServletWebRequest servletWebRequest)
            throws ServletRequestBindingException {
        HttpServletRequest request = servletWebRequest.getRequest();
        // 从请求头或者参数中获取用户输入的验证码
        String codeKeyValue = request.getHeader(CODE_KEY);
        codeKeyValue =
                StringUtils.isBlank(codeKeyValue)
                        ? ServletRequestUtils.getStringParameter(request, CODE_KEY)
                        : codeKeyValue;
        return codeKeyValue;
    }
    /**
     * 校验验证码
     *
     * @param servletWebRequest
     * @return
     * @throws GlobalException
     */
    @Override
    public boolean validate(ServletWebRequest servletWebRequest) throws GlobalException {
        // 获取验证码类型
        ValidateCodeType codeType = getValidateCodeType();
        C codeInSession;
        String codeKeyValue;
        String codeInRequest;
        try {
            codeKeyValue = getCodeKeyValue(servletWebRequest);
            // 使用codeKeyValue取出保存在后台验证码数据
            codeInSession =
                    (C) validateCodeRepository.get(servletWebRequest, codeType, codeKeyValue);
            // 获取请求中用户输入的验证码
            codeInRequest =
                    ServletRequestUtils.getStringParameter(
                            servletWebRequest.getRequest(), codeType.getParamNameOnValidate());
        } catch (Exception e) {
            throw GlobalException.newInstance(
                    "VALIDATE_CODE_OBTAIN_ERROR", "获取验证码失败,应该是前端请求中没有提交验证码");
        }
        if (StringUtils.isBlank(codeInRequest)) {
            throw GlobalException.newInstance("VALIDATE_CODE_EMPTY_ERROR", "验证码为空,用户没有填写验证码");
        }
        if (Objects.isNull(codeInSession) || Objects.isNull(codeInSession.getCode())) {
            throw GlobalException.newInstance(
                    "VALIDATE_CODE_VALIDATE_ERROR", "存储的验证码没有找到,应该是验证码失效了");
        }
        if (codeInSession.isExpired()) {
            validateCodeRepository.remove(servletWebRequest, codeType, codeKeyValue);
            throw GlobalException.newInstance("VALIDATE_CODE_VALIDATE_ERROR", "验证码已过期,请重新获取");
        }
        if (!validate(codeInRequest, codeInSession)) {
            throw GlobalException.newInstance("VALIDATE_CODE_VALIDATE_ERROR", "验证码匹配错误");
        }
        // 验证成功后移除保存的数据
        validateCodeRepository.remove(servletWebRequest, codeType, codeKeyValue);
        return true;
    }
    /**
     * 验证
     *
     * @param code
     * @return
     */
    protected abstract boolean validate(String code, C validateCode);
    /**
     * 生成验证码的url
     *
     * @return
     */
    public String generatorUrl() {
        return this.codeProperties.getGeneratorUrl();
    }
    /**
     * 需要拦截的url
     *
     * @return
     */
    public String[] filterUrls() {
        return this.codeProperties.getFilterUrls();
    }
}



相关文章
|
3月前
|
人工智能 自然语言处理 前端开发
SpringBoot + 通义千问 + 自定义React组件:支持EventStream数据解析的技术实践
【10月更文挑战第7天】在现代Web开发中,集成多种技术栈以实现复杂的功能需求已成为常态。本文将详细介绍如何使用SpringBoot作为后端框架,结合阿里巴巴的通义千问(一个强大的自然语言处理服务),并通过自定义React组件来支持服务器发送事件(SSE, Server-Sent Events)的EventStream数据解析。这一组合不仅能够实现高效的实时通信,还能利用AI技术提升用户体验。
257 2
|
5月前
|
SQL 前端开发 NoSQL
SpringBoot+Vue 实现图片验证码功能需求
这篇文章介绍了如何在SpringBoot+Vue项目中实现图片验证码功能,包括后端生成与校验验证码的方法以及前端展示验证码的实现步骤。
SpringBoot+Vue 实现图片验证码功能需求
|
5月前
|
SQL JavaScript 前端开发
vue中使用分页组件、将从数据库中查询出来的数据分页展示(前后端分离SpringBoot+Vue)
这篇文章详细介绍了如何在Vue.js中使用分页组件展示从数据库查询出来的数据,包括前端Vue页面的表格和分页组件代码,以及后端SpringBoot的控制层和SQL查询语句。
vue中使用分页组件、将从数据库中查询出来的数据分页展示(前后端分离SpringBoot+Vue)
|
4月前
|
缓存 监控 Java
造轮子能力大提升:基于SpringBoot打造高性能缓存组件
在快节奏的软件开发领域,"不重复造轮子" 常常被视为提高效率的金科玉律。然而,在某些特定场景下,定制化的高性能缓存组件却是提升系统性能、优化用户体验的关键。今天,我们将深入探讨如何利用SpringBoot框架,从零开始打造一款符合项目需求的高性能缓存组件,分享我在这一过程中的技术心得与学习体会。
78 6
|
5月前
|
NoSQL JavaScript Java
SpringBoot+Vue+Redis实现验证码功能、一个小时只允许发三次验证码。一次验证码有效期二分钟。SpringBoot整合Redis
这篇文章介绍了如何使用SpringBoot结合Vue和Redis实现验证码功能,包括验证码的有效期控制和一小时内发送次数的限制。
|
7月前
|
缓存 NoSQL Java
案例 采用Springboot默认的缓存方案Simple在三层架构中完成一个手机验证码生成校验的程序
案例 采用Springboot默认的缓存方案Simple在三层架构中完成一个手机验证码生成校验的程序
126 5
|
7月前
|
NoSQL 前端开发 Java
技术笔记:springboot分布式锁组件spring
技术笔记:springboot分布式锁组件spring
65 1
|
7月前
|
Java API 数据安全/隐私保护
在Spring Boot中,过滤器(Filter)是一种非常有用的组件
在Spring Boot中,过滤器(Filter)是一种非常有用的组件
94 6
|
7月前
|
Java
springboot用户登录使用验证码
springboot用户登录使用验证码
|
7月前
|
开发框架 安全 Java
信息打点-语言框架&开发组件&FastJson&Shiro&Log4j&SpringBoot等
信息打点-语言框架&开发组件&FastJson&Shiro&Log4j&SpringBoot等