前言:平时咱们在使用各类平台或系统的时候,都会弹出验证码,类似这种:
亦或是这种
还有就是这种:
好吧,这种也算:
所有的验证码,无论是图片验证码,还是滑块验证码,亦或是短信验证码、语音验证码,它们的作用都是为了防止应用接口被恶意的非人为操作不断调用。
以第一张图或第二张图为例,不针对这个发短信的接口做一个图片验证码的话,那么就很可能被恶意程序调用,导致后台程序不断地发送短信验证码给指定手机号码的人,这样不仅会造成公司的损失,也会给接收短信的人造成不必要的困扰。有了图片验证码后,调用接口的时候需要带上被识别的验证码,恶意程序就相对有难度才能调用你的这个被保护的接口了,大大降低了这方面的困扰。
注意点: 很多同学在这个验证码的时候,仅仅是简单地通过前端调用获取验证码的接口,然后再把用户提交的验证码交给后台验证,验证通过后再发起业务请求。这种做法只是做到了表面上有验证码的验证过程,实际上还是没有做到对业务接口的保护。交互过程如下图:
这样的交互逻辑是有很明显的漏洞的,它把验证的权限交给了客户端,前端说通过就通过,那么对于任何一个了解并且会使用一定手段或工具的人来说,这样的验证码就是形同虚设。使用api工具就可以直接跳到第三步直接调用业务接口。
真正的验证码应该做到和业务接口绑定,如下图的交互逻辑:
按照以上交互逻辑,无论如何,客户端必须带上验证码才能真正地调用后台服务处理业务请求,否则就无法达到目的。
后面就以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(); } }