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



相关文章
|
4天前
|
XML Java 数据格式
Springboot中自定义组件
Springboot中自定义组件
|
4天前
|
Java
SpringBoot集成RestTemplate组件
SpringBoot集成RestTemplate组件
46 0
|
4天前
|
Dubbo Java 应用服务中间件
微服务框架(十六)Spring Boot及Dubbo zipkin 链路追踪组件埋点
此系列文章将会描述Java框架Spring Boot、服务治理框架Dubbo、应用容器引擎Docker,及使用Spring Boot集成Dubbo、Mybatis等开源框架,其中穿插着Spring Boot中日志切面等技术的实现,然后通过gitlab-CI以持续集成为Docker镜像。 本文第一部分为调用链、OpenTracing、Zipkin和Jeager的简述;第二部分为Spring Boot及Dubbo zipkin 链路追踪组件埋点
|
4天前
|
Java 应用服务中间件 容器
SpringBoot之Web原生组件注入
SpringBoot之Web原生组件注入
|
4天前
|
前端开发 安全 Java
SpringBoot 实现登录验证码(附集成SpringSecurity)
SpringBoot 实现登录验证码(附集成SpringSecurity)
|
4天前
|
Java 数据库 数据安全/隐私保护
【SpringBoot】Validator组件+自定义约束注解实现手机号码校验和密码格式限制
【SpringBoot】Validator组件+自定义约束注解实现手机号码校验和密码格式限制
155 1
|
4天前
|
XML 缓存 算法
SpringBoot2 | SpingBoot FilterRegistrationBean 注册组件 | FilterChain 责任链源码分析(九)
SpringBoot2 | SpingBoot FilterRegistrationBean 注册组件 | FilterChain 责任链源码分析(九)
28 0
|
4天前
|
缓存 Java 开发者
10个点介绍SpringBoot3工作流程与核心组件源码解析
Spring Boot 是Java开发中100%会使用到的框架,开发者不仅要熟练使用,对其中的核心源码也要了解,正所谓知其然知其所以然,V 哥建议小伙伴们在学习的过程中,一定要去研读一下源码,这有助于你在开发中游刃有余。欢迎一起交流学习心得,一起成长。
|
4天前
|
XML Java 应用服务中间件
Springboot中tomcat配置、三大组件配置、拦截器配置
Springboot中tomcat配置、三大组件配置、拦截器配置
|
4天前
|
前端开发 JavaScript Java
springboot 集成easy-captcha实现图像验证码显示和登录
springboot 集成easy-captcha实现图像验证码显示和登录
177 0