統(tǒng)一驗(yàn)證碼組件設(shè)計(jì)

前言:平時咱們在使用各類平臺或系統(tǒng)的時候殊轴,都會彈出驗(yàn)證碼嚎杨,類似這種:
image-20200713200103827.png

亦或是這種


image-20200713200346675.png

還有就是這種:
image-20200713200700689.png

好吧蓄喇,這種也算:


A3C58CC936B3A96C22D263AE89752599.jpg

所有的驗(yàn)證碼发侵,無論是圖片驗(yàn)證碼,還是滑塊驗(yàn)證碼妆偏,亦或是短信驗(yàn)證碼刃鳄、語音驗(yàn)證碼,它們的作用都是為了防止應(yīng)用接口被惡意的非人為操作不斷調(diào)用钱骂。

以第一張圖或第二張圖為例叔锐,不針對這個發(fā)短信的接口做一個圖片驗(yàn)證碼的話,那么就很可能被惡意程序調(diào)用见秽,導(dǎo)致后臺程序不斷地發(fā)送短信驗(yàn)證碼給指定手機(jī)號碼的人愉烙,這樣不僅會造成公司的損失,也會給接收短信的人造成不必要的困擾解取。有了圖片驗(yàn)證碼后步责,調(diào)用接口的時候需要帶上被識別的驗(yàn)證碼,惡意程序就相對有難度才能調(diào)用你的這個被保護(hù)的接口了,大大降低了這方面的困擾蔓肯。

注意點(diǎn):很多同學(xué)在這個驗(yàn)證碼的時候遂鹊,僅僅是簡單地通過前端調(diào)用獲取驗(yàn)證碼的接口,然后再把用戶提交的驗(yàn)證碼交給后臺驗(yàn)證省核,驗(yàn)證通過后再發(fā)起業(yè)務(wù)請求稿辙。這種做法只是做到了表面上有驗(yàn)證碼的驗(yàn)證過程,實(shí)際上還是沒有做到對業(yè)務(wù)接口的保護(hù)气忠。交互過程如下圖:

image-20200713213339552.png

這樣的交互邏輯是有很明顯的漏洞的,它把驗(yàn)證的權(quán)限交給了客戶端赋咽,前端說通過就通過旧噪,那么對于任何一個了解并且會使用一定手段或工具的人來說,這樣的驗(yàn)證碼就是形同虛設(shè)脓匿。使用api工具就可以直接跳到第三步直接調(diào)用業(yè)務(wù)接口淘钟。

真正的驗(yàn)證碼應(yīng)該做到和業(yè)務(wù)接口綁定,如下圖的交互邏輯:
image-20200713214331770.png

按照以上交互邏輯陪毡,無論如何米母,客戶端必須帶上驗(yàn)證碼才能真正地調(diào)用后臺服務(wù)處理業(yè)務(wù)請求,否則就無法達(dá)到目的毡琉。

后面就以java web為例來實(shí)現(xiàn)上述的交互邏輯:

為了可以將驗(yàn)證碼邏輯和具體業(yè)務(wù)邏輯解藕铁瞒,利用了servlet的Filter作為過濾器來判斷當(dāng)前請求的接口是否需要通過驗(yàn)證碼驗(yàn)證后才能被調(diào)用


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特性獲取所有的驗(yàn)證碼處理器
    @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();
      // 判斷當(dāng)前這個請求是否需要驗(yàn)證,并且驗(yàn)證請求中攜帶的驗(yàn)證碼
        if (!validateCode(req, res, matcher)) {
            return;
        }
      // 生成驗(yàn)證碼
        if (generatorCode(req, res, matcher)) {
            return;
        }
        chain.doFilter(request, response);
    }

    @Override
    public void destroy() {}
    /**
     * 驗(yàn)證操作
     * @param request
     * @param response
     * @param matcher
     * @return
     */
    private boolean validateCode(
            HttpServletRequest request, HttpServletResponse response, AntPathMatcher matcher) {
        String url = request.getRequestURI();
      //循環(huán)調(diào)用驗(yàn)證碼處理器進(jìn)行驗(yàn)證
        for (AbstractValidateCodeProcessor processor : validateCodeProcessorList) {
            String[] filterUrls = processor.filterUrls();
            if (ArrayUtils.isEmpty(filterUrls)) {
                continue;
            }
            for (String filterUrl : filterUrls) {
              // 先判斷當(dāng)前接口是否需要攔截桅滋,如果匹配成功慧耍,就開始進(jìn)行驗(yàn)證
                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 {
          // 執(zhí)行驗(yàn)證
            processor.validate(new ServletWebRequest(request, response));
        } catch (GlobalException e) {
          // 驗(yàn)證失敗的話,捕獲異常丐谋,并處理響應(yīng)
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.getOutputStream()
                    .write(CommonResponse.exceptionInstance(e).toJson().getBytes());
            return false;
        }
        return true;
    }

    /**
     * 生成驗(yàn)證碼
     * @param request
     * @param response
     * @param matcher
     * @return
     */
    @SneakyThrows
    private boolean generatorCode(
            HttpServletRequest request, HttpServletResponse response, AntPathMatcher matcher) {
      // 獲取驗(yàn)證碼只能通過GET請求
        if (!StringUtils.equalsIgnoreCase(request.getMethod(), HttpMethod.GET.name())) {
            return false;
        }
        String url = request.getRequestURI();
      // 依然還是通過驗(yàn)證碼處理器去做生成驗(yàn)證碼的操作
        for (AbstractValidateCodeProcessor processor : validateCodeProcessorList) {
          // 檢查當(dāng)前請求是要生成哪種類型的驗(yàn)證碼
            if (matcher.match(processor.generatorUrl(), url)) {
                try {
                  // 生成驗(yàn)證碼
                    processor.create(new ServletWebRequest(request, response));
                } catch (GlobalException e) {
                    //失敗后捕獲異常芍碧,并處理響應(yīng)
                  response.setContentType(MediaType.APPLICATION_JSON_VALUE);
                    response.getOutputStream()
                            .write(CommonResponse.exceptionInstance(e).toJson().getBytes());
                }
                return true;
            }
        }
        return false;
    }
// 設(shè)置當(dāng)前過濾器的優(yōu)先級
    @Override
    public int getOrder() {
        return REQUEST_WRAPPER_FILTER_MAX_ORDER - 104;
    }
}

上述ValidateCodeFilter是一個驗(yàn)證碼邏輯入口類,也是整個邏輯的黏合劑号俐,真正實(shí)現(xiàn)還是要靠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 {

    /** 標(biāo)記驗(yàn)證碼的唯一key */
    protected static final String CODE_KEY = "code_key";
    /** 發(fā)送驗(yàn)證碼前需要調(diào)用的操作 */
    @Autowired(required = false)
    private List<ValidateCodeHandler> handlerList;
    /** 實(shí)現(xiàn)ApplicationContextAware泌豆,獲取ApplicationContext */
    private static ApplicationContext APPLICATION_CONTEXT;
    /** 用作生成驗(yàn)證碼 */
    private ValidateCodeGenerator validateCodeGenerator;
        /** 用作存取驗(yàn)證碼 */
    private ValidateCodeRepository validateCodeRepository;
        /** 用作獲取驗(yàn)證碼相關(guān)系統(tǒng)配置 */
    private ValidateCodeProperties.CodeProperties codeProperties;

  /** 構(gòu)造函數(shù) */
    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;
    }
        /** 生成驗(yàn)證碼邏輯 */
    @Override
    public void create(ServletWebRequest request) throws GlobalException {
        // 生成指定驗(yàn)證碼
        C validateCode = generate(request);
        ValidateCodeType codeType = getValidateCodeType();
       // 檢查是否需要在發(fā)送該驗(yàn)證碼之前執(zhí)行一些指定的操作;比如注冊的時候驗(yàn)證一下手機(jī)號碼是否已經(jīng)被注冊吏饿;
        if (!CollectionUtils.isEmpty(handlerList)) {
            for (ValidateCodeHandler handler : handlerList) {
                if (handler.support(request, codeType)) {
                    handler.beforeSend(request, codeType, validateCode);
                }
            }
        }
        HttpServletResponse response = request.getResponse();
        // 用作保存驗(yàn)證碼的key踪危,方便后續(xù)的驗(yàn)證操作
        String codeKeyValue = request.getSessionId();
        response.setHeader(CODE_KEY, codeKeyValue);
                // 保存驗(yàn)證碼數(shù)據(jù)
        save(request, validateCode, codeKeyValue);
        // 發(fā)送驗(yàn)證碼
        send(request, validateCode);
    }

    /**
     * 保存驗(yàn)證碼
     *
     * @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();
    /**
     * 驗(yàn)證碼發(fā)送
     *
     * @param request
     * @param validateCode
     * @throws Exception
     */
    protected abstract void send(ServletWebRequest request, C validateCode) throws GlobalException;

    /**
     * 創(chuàng)建驗(yàn)證碼
     *
     * @param request
     * @return
     */
    private C generate(ServletWebRequest request) {
        return (C) validateCodeGenerator.createValidateCode(request);
    }

    private String getCodeKeyValue(ServletWebRequest servletWebRequest)
            throws ServletRequestBindingException {
        HttpServletRequest request = servletWebRequest.getRequest();
        // 從請求頭或者參數(shù)中獲取用戶輸入的驗(yàn)證碼
        String codeKeyValue = request.getHeader(CODE_KEY);
        codeKeyValue =
                StringUtils.isBlank(codeKeyValue)
                        ? ServletRequestUtils.getStringParameter(request, CODE_KEY)
                        : codeKeyValue;
        return codeKeyValue;
    }
    /**
     * 校驗(yàn)驗(yàn)證碼
     *
     * @param servletWebRequest
     * @return
     * @throws GlobalException
     */
    @Override
    public boolean validate(ServletWebRequest servletWebRequest) throws GlobalException {
        // 獲取驗(yàn)證碼類型
        ValidateCodeType codeType = getValidateCodeType();
        C codeInSession;
        String codeKeyValue;
        String codeInRequest;
        try {
            codeKeyValue = getCodeKeyValue(servletWebRequest);
            // 使用codeKeyValue取出保存在后臺驗(yàn)證碼數(shù)據(jù)
            codeInSession =
                    (C) validateCodeRepository.get(servletWebRequest, codeType, codeKeyValue);
            // 獲取請求中用戶輸入的驗(yàn)證碼
            codeInRequest =
                    ServletRequestUtils.getStringParameter(
                            servletWebRequest.getRequest(), codeType.getParamNameOnValidate());
        } catch (Exception e) {
            throw GlobalException.newInstance(
                    "VALIDATE_CODE_OBTAIN_ERROR", "獲取驗(yàn)證碼失敗,應(yīng)該是前端請求中沒有提交驗(yàn)證碼");
        }
        if (StringUtils.isBlank(codeInRequest)) {
            throw GlobalException.newInstance("VALIDATE_CODE_EMPTY_ERROR", "驗(yàn)證碼為空找岖,用戶沒有填寫驗(yàn)證碼");
        }
        if (Objects.isNull(codeInSession) || Objects.isNull(codeInSession.getCode())) {
            throw GlobalException.newInstance(
                    "VALIDATE_CODE_VALIDATE_ERROR", "存儲的驗(yàn)證碼沒有找到陨倡,應(yīng)該是驗(yàn)證碼失效了");
        }
        if (codeInSession.isExpired()) {
            validateCodeRepository.remove(servletWebRequest, codeType, codeKeyValue);
            throw GlobalException.newInstance("VALIDATE_CODE_VALIDATE_ERROR", "驗(yàn)證碼已過期,請重新獲取");
        }
        if (!validate(codeInRequest, codeInSession)) {
            throw GlobalException.newInstance("VALIDATE_CODE_VALIDATE_ERROR", "驗(yàn)證碼匹配錯誤");
        }
        // 驗(yàn)證成功后移除保存的數(shù)據(jù)
        validateCodeRepository.remove(servletWebRequest, codeType, codeKeyValue);
        return true;
    }

    /**
     * 驗(yàn)證
     *
     * @param code
     * @return
     */
    protected abstract boolean validate(String code, C validateCode);
    /**
     * 生成驗(yàn)證碼的url
     *
     * @return
     */
    public String generatorUrl() {
        return this.codeProperties.getGeneratorUrl();
    }

    /**
     * 需要攔截的url
     *
     * @return
     */
    public String[] filterUrls() {
        return this.codeProperties.getFilterUrls();
    }
}

所有的驗(yàn)證碼處理器必須實(shí)現(xiàn)的接口,創(chuàng)建和驗(yàn)證

import com.zx.silverfox.common.exception.GlobalException;
import org.springframework.web.context.request.ServletWebRequest;

/**
 * @author zouwei
 */
public interface ValidateCodeProcessor {

    /**
     * 創(chuàng)建驗(yàn)證碼
     *
     * @param request
     * @throws Exception
     */
    void create(ServletWebRequest request) throws GlobalException;

    /**
     * 校驗(yàn)驗(yàn)證碼
     *
     * @param servletWebRequest
     */
    boolean validate(ServletWebRequest servletWebRequest) throws GlobalException;
}

創(chuàng)建驗(yàn)證碼接口:

import org.springframework.web.context.request.ServletWebRequest;

public interface ValidateCodeGenerator {

    /**
     * 生成驗(yàn)證碼
     *
     * @param request
     * @return
     */
    ValidateCode createValidateCode(ServletWebRequest request);
}

存取驗(yàn)證碼接口:

import org.springframework.web.context.request.ServletWebRequest;

/** @author zouwei */
public interface ValidateCodeRepository {
    /**
     * 保存驗(yàn)證碼
     *
     * @param request
     * @param code
     * @param validateCodeType
     */
    void save(
            ServletWebRequest request,
            ValidateCode code,
            ValidateCodeType validateCodeType,
            String codeKeyValue);
    /**
     * 獲取驗(yàn)證碼
     *
     * @param request
     * @param validateCodeType
     * @return
     */
    ValidateCode get(
            ServletWebRequest request, ValidateCodeType validateCodeType, String codeKeyValue);
    /**
     * 移除驗(yàn)證碼
     *
     * @param request
     * @param codeType
     */
    void remove(ServletWebRequest request, ValidateCodeType codeType, String codeKeyValue);
}

存取驗(yàn)證碼的具體實(shí)現(xiàn)许布,我就使用了redis來做兴革,其他的小伙伴也可以使用其他存儲方案來做:

import com.zx.silverfox.common.validate.code.ValidateCode;
import com.zx.silverfox.common.validate.code.ValidateCodeRepository;
import com.zx.silverfox.common.validate.code.ValidateCodeType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.context.request.ServletWebRequest;

import java.util.concurrent.TimeUnit;

/** @author zouwei */
public class RedisValidateCodeRepository implements ValidateCodeRepository {

    @Autowired private RedisTemplate<Object, Object> redisTemplate;

    @Override
    public void save(
            ServletWebRequest request,
            ValidateCode code,
            ValidateCodeType type,
            String codeKeyValue) {
        redisTemplate.opsForValue().set(buildKey(type, codeKeyValue), code, 30, TimeUnit.MINUTES);
    }

    @Override
    public ValidateCode get(ServletWebRequest request, ValidateCodeType type, String codeKeyValue) {
        Object value = redisTemplate.opsForValue().get(buildKey(type, codeKeyValue));
        if (value == null) {
            return null;
        }
        return (ValidateCode) value;
    }

    @Override
    public void remove(ServletWebRequest request, ValidateCodeType type, String codeKeyValue) {
        redisTemplate.delete(buildKey(type, codeKeyValue));
    }

    /**
     * @param type
     * @param key
     * @return
     */
    private String buildKey(ValidateCodeType type, String key) {
        return "code:" + type.toString().toLowerCase() + ":" + key;
    }
}

發(fā)送驗(yàn)證碼前需要處理的接口:

import com.zx.silverfox.common.exception.GlobalException;
import org.springframework.web.context.request.ServletWebRequest;

/**
 * @author zouwei
 */
public interface ValidateCodeHandler<C extends ValidateCode> {
    /**
     * 是否匹配成功
     * @param request
     * @param validateCodeType
     * @return
     */
    boolean support(ServletWebRequest request, ValidateCodeType validateCodeType);

    /**
     * 開始處理發(fā)送驗(yàn)證碼前的邏輯
     * @param request
     * @param validateCodeType
     * @param validateCode
     * @throws GlobalException
     */
    void beforeSend(ServletWebRequest request, ValidateCodeType validateCodeType, C validateCode) throws GlobalException;
}

驗(yàn)證碼實(shí)體類:

import lombok.Data;

import java.io.Serializable;
import java.time.LocalDateTime;

/** @author zouwei */
@Data
public class ValidateCode implements Serializable {
    private static final long serialVersionUID = -7827043337909063779L;

    private String code;

    private long expireInSeconds;

    private LocalDateTime expireTime;

    public ValidateCode(String code, LocalDateTime expireTime) {
        this.code = code;
        this.expireTime = expireTime;
    }

    public ValidateCode(String code, long expireInSeconds) {
        this.code = code;
        this.expireInSeconds = expireInSeconds;
        this.expireTime = LocalDateTime.now().plusSeconds(expireInSeconds);
    }

    /**
     * 判斷是否過期
     *
     * @return
     */
    public boolean isExpired() {
        return LocalDateTime.now().isAfter(expireTime);
    }

    /**
     * 轉(zhuǎn)換成分鐘
     *
     * @return
     */
    public long minute() {
        return this.expireInSeconds / 60;
    }
}

各種驗(yàn)證碼類型,可無限擴(kuò)展:


public enum ValidateCodeType {
    /** 短信驗(yàn)證碼 */
    SMS {
        @Override
        public String getParamNameOnValidate() {
            return "smsCode";
        }
    },

    /** 圖片驗(yàn)證碼 */
    IMAGE {
        @Override
        public String getParamNameOnValidate() {
            return "imageCode";
        }
    },
    /** 滑動圖片驗(yàn)證碼 */
    SLIDE {
        @Override
        public String getParamNameOnValidate() {
            return "slideCode";
        }
    };

    public abstract String getParamNameOnValidate();
}

還有相關(guān)配置類:

import lombok.Data;
import lombok.EqualsAndHashCode;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Configuration;

/** @author zouwei */
@Data
@Configuration
@ConfigurationProperties(prefix = "validate.code")
public class ValidateCodeProperties {
    /** 圖像驗(yàn)證碼 */
    private ImageProperties image = new ImageProperties();
    /** 短信驗(yàn)證碼 */
    private SmsProperties sms = new SmsProperties();
    /** 滑動驗(yàn)證碼 */
    private SlideImageProperties slide = new SlideImageProperties();

    @Data
    @EqualsAndHashCode(callSuper = true)
    public static class SlideImageProperties extends CodeProperties {

        private String generatorUrl = "/code/slide";
    }

    @Data
    @EqualsAndHashCode(callSuper = true)
    public static class ImageProperties extends CodeProperties {

        private int length = 6;

        private int height = 23;

        private int width = 67;

        private String generatorUrl = "/code/image";
    }

    @Data
    @EqualsAndHashCode(callSuper = true)
    public static class SmsProperties extends CodeProperties {

        private int length = 6;

        private String generatorUrl = "/code/sms";
    }

    @Data
    public abstract static class CodeProperties {

        private long expiredInSecond = 300;

        private String[] filterUrls;

        private String generatorUrl;
    }
}

為了開發(fā)者使用方便,我也模仿spring boot的方式使用注解自動化配置:

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/** @author zouwei */
@Target({ElementType.TYPE, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Import(ValidateCodeConfigSelector.class)
public @interface EnableValidateCode {
    /**
     * 驗(yàn)證碼實(shí)現(xiàn)類
     *
     * @return
     */
    Class<? extends AbstractValidateCodeProcessor>[] value() default {
        ImageValidateCodeProcessor.class
    };

    /**
     * 驗(yàn)證碼存儲方式
     *
     * @return
     */
    Class<? extends ValidateCodeRepository> repository() default RedisValidateCodeRepository.class;
}

import com.zx.silverfox.common.filter.ValidateCodeFilter;
import com.zx.silverfox.common.validate.code.AbstractValidateCodeProcessor;
import com.zx.silverfox.common.validate.code.ValidateCodeRepository;
import org.apache.commons.lang3.ArrayUtils;
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
import org.springframework.beans.factory.support.RootBeanDefinition;
import org.springframework.context.annotation.ImportBeanDefinitionRegistrar;
import org.springframework.core.annotation.AnnotationAttributes;
import org.springframework.core.type.AnnotationMetadata;

import java.util.Map;

public class ValidateCodeConfigSelector implements ImportBeanDefinitionRegistrar {
    @Override
    public void registerBeanDefinitions(
            AnnotationMetadata importingClassMetadata, BeanDefinitionRegistry registry) {
        Map<String, Object> attributeMap =
                importingClassMetadata.getAnnotationAttributes(
                        EnableValidateCode.class.getName());
        AnnotationAttributes attributes = AnnotationAttributes.fromMap(attributeMap);
        Class<? extends ValidateCodeRepository> repositoryClass = attributes.getClass("repository");

        Class<? extends AbstractValidateCodeProcessor>[] imageProcessorClass =
                (Class<? extends AbstractValidateCodeProcessor>[])
                        attributes.getClassArray("value");

        if (!registry.containsBeanDefinition("validateCodeRepository")) {
            registry.registerBeanDefinition(
                    "validateCodeRepository", new RootBeanDefinition(repositoryClass));
        }
        if (ArrayUtils.isNotEmpty(imageProcessorClass)) {
            for (Class<? extends AbstractValidateCodeProcessor> clazz : imageProcessorClass) {
                registry.registerBeanDefinition(
                        clazz.getSimpleName(), new RootBeanDefinition(clazz));
            }
        }
        if (!registry.containsBeanDefinition("validateCodeFilter")) {
            registry.registerBeanDefinition(
                    "validateCodeFilter", new RootBeanDefinition(ValidateCodeFilter.class));
        }
    }
}

上述代碼基本框架已經(jīng)完成杂曲,后續(xù)代碼就是真正地實(shí)現(xiàn)圖片驗(yàn)證碼及短信驗(yàn)證碼:

簡單圖片驗(yàn)證碼:

import com.zx.silverfox.common.validate.code.ValidateCode;
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.awt.image.BufferedImage;

/** @author zouwei */
@Data
@EqualsAndHashCode(callSuper = true)
public class ImageValidateCode extends ValidateCode {

    private transient BufferedImage image;

    public ImageValidateCode(BufferedImage image, String code, long expireInSeconds) {
        super(code, expireInSeconds);
        this.image = image;
    }
}
import com.zx.silverfox.common.properties.ValidateCodeProperties;
import com.zx.silverfox.common.validate.code.ValidateCode;
import com.zx.silverfox.common.validate.code.ValidateCodeGenerator;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;

import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;

/** @author zouwei */
public class ImageValidateCodeGenerator implements ValidateCodeGenerator {

    private ValidateCodeProperties.ImageProperties imageProperties;

    public ImageValidateCodeGenerator(ValidateCodeProperties.ImageProperties imageProperties) {
        this.imageProperties = imageProperties;
    }

    @Override
    public ValidateCode createValidateCode(ServletWebRequest request) {
        int height =
                ServletRequestUtils.getIntParameter(
                        request.getRequest(), "height", imageProperties.getHeight());
        int width =
                ServletRequestUtils.getIntParameter(
                        request.getRequest(), "width", imageProperties.getWidth());
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        Graphics g = image.getGraphics();

        Random random = new Random();

        g.setColor(getRandColor(200, 250));
        g.fillRect(0, 0, width, height);
        g.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        g.setColor(getRandColor(160, 200));
        for (int i = 0; i < 155; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            g.drawLine(x, y, x + xl, y + yl);
        }

        String sRand = "";
        for (int i = 0; i < imageProperties.getLength(); i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            g.setColor(
                    new Color(
                            20 + random.nextInt(110),
                            20 + random.nextInt(110),
                            20 + random.nextInt(110)));
            g.drawString(rand, 13 * i + 6, 16);
        }

        g.dispose();

        return new ImageValidateCode(image, sRand, imageProperties.getExpiredInSecond());
    }

    /**
     * 生成隨機(jī)背景條紋
     *
     * @param fc
     * @param bc
     * @return
     */
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc);
        int g = fc + random.nextInt(bc - fc);
        int b = fc + random.nextInt(bc - fc);
        return new Color(r, g, b);
    }
}
import com.zx.silverfox.common.exception.GlobalException;
import com.zx.silverfox.common.properties.ValidateCodeProperties;
import com.zx.silverfox.common.validate.code.AbstractValidateCodeProcessor;
import com.zx.silverfox.common.validate.code.ValidateCodeRepository;
import com.zx.silverfox.common.validate.code.ValidateCodeType;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.context.request.ServletWebRequest;

import javax.imageio.ImageIO;
import java.io.IOException;

/** @author zouwei */
public class ImageValidateCodeProcessor extends AbstractValidateCodeProcessor<ImageValidateCode> {
    /** 生成的圖片的格式 */
    private static final String JPEG_IMAGE_TYPE = "JPEG";

    public ImageValidateCodeProcessor(
            @Autowired ValidateCodeProperties validateCodeProperties,
            @Autowired ValidateCodeRepository repository) {
        super(
                new ImageValidateCodeGenerator(validateCodeProperties.getImage()),
                repository,
                validateCodeProperties.getImage());
    }

    @Override
    protected ValidateCodeType getValidateCodeType() {
        return ValidateCodeType.IMAGE;
    }

    @Override
    protected void send(ServletWebRequest request, ImageValidateCode validateCode)
            throws GlobalException {
        try {
            ImageIO.write(
                    validateCode.getImage(),
                    JPEG_IMAGE_TYPE,
                    request.getResponse().getOutputStream());
        } catch (IOException e) {
            throw GlobalException.newInstance("IMAGE_CODE_CREATE_FAIL", "圖片驗(yàn)證碼生成失敗");
        }
    }

    @Override
    protected boolean validate(String code, ImageValidateCode validateCode) {
        return StringUtils.equalsIgnoreCase(code, validateCode.getCode());
    }
}

滑塊驗(yàn)證碼:

import com.zx.silverfox.common.validate.code.ValidateCode;
import lombok.Data;
import lombok.EqualsAndHashCode;

/** @author zouwei */
@Data
@EqualsAndHashCode(callSuper = true)
public class SlideImageCode extends ValidateCode {

    private double heightYPercentage;

    private transient String srcImg;

    private transient String markImg;

    public SlideImageCode(
            double heightYPercentage,
            String srcImg,
            String markImg,
            String code,
            long expireInSeconds) {
        super(code, expireInSeconds);
        this.heightYPercentage = heightYPercentage;
        this.srcImg = srcImg;
        this.markImg = markImg;
    }
}
import com.zx.silverfox.common.properties.ValidateCodeProperties;
import com.zx.silverfox.common.validate.code.ValidateCode;
import com.zx.silverfox.common.validate.code.ValidateCodeGenerator;
import lombok.extern.slf4j.Slf4j;
import org.springframework.core.io.ClassPathResource;
import org.springframework.web.context.request.ServletWebRequest;

import javax.imageio.ImageIO;
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Random;

@Slf4j
public class SlideImageCodeGenerator implements ValidateCodeGenerator {

    private ValidateCodeProperties.SlideImageProperties slideImageProperties;

    public SlideImageCodeGenerator(
            ValidateCodeProperties.SlideImageProperties slideImageProperties) {
        this.slideImageProperties = slideImageProperties;
    }

    @Override
    public ValidateCode createValidateCode(ServletWebRequest request) {
        try (InputStream in = getOriginImage()) {
            SlideImageUtil.SlideImage slideImage = SlideImageUtil.getVerifyImage(ImageIO.read(in));
            int width = slideImage.getWidth();
            int x = slideImage.getX();
            int height = slideImage.getHeight();
            int y = slideImage.getY();
            double widthXPercentage = width / (x * 1.0);
            double heightYPercentage = height / (y * 1.0);
            String code = widthXPercentage + ":" + heightYPercentage;
            return new SlideImageCode(
                    heightYPercentage,
                    slideImage.getSrcImg(),
                    slideImage.getMarkImg(),
                    code,
                    slideImageProperties.getExpiredInSecond());
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    private InputStream getOriginImage() throws IOException {
            // 從resources下的slideimg文件夾中隨機(jī)獲取一張圖片進(jìn)行處理
        ClassPathResource classPathResource = new ClassPathResource("slideimg");
        File dirFile = classPathResource.getFile();
        File[] listFiles = dirFile.listFiles();
        int index = new Random().nextInt(listFiles.length);
        return new FileInputStream(listFiles[index]);
    }
}

import com.zx.silverfox.common.exception.GlobalException;
import com.zx.silverfox.common.properties.ValidateCodeProperties;
import com.zx.silverfox.common.util.CastUtil;
import com.zx.silverfox.common.validate.code.AbstractValidateCodeProcessor;
import com.zx.silverfox.common.validate.code.ValidateCodeRepository;
import com.zx.silverfox.common.validate.code.ValidateCodeType;
import com.zx.silverfox.common.vo.CommonResponse;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.context.request.ServletWebRequest;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 滑動驗(yàn)證碼
 *
 * @author zouwei
 */
public class SlideImageCodeProcessor extends AbstractValidateCodeProcessor<SlideImageCode> {

    public SlideImageCodeProcessor(
            @Autowired ValidateCodeProperties validateCodeProperties,
            @Autowired ValidateCodeRepository validateCodeRepository) {
        super(
                new SlideImageCodeGenerator(validateCodeProperties.getSlide()),
                validateCodeRepository,
                validateCodeProperties.getSlide());
    }

    @Override
    protected ValidateCodeType getValidateCodeType() {
        return ValidateCodeType.SLIDE;
    }

    @Override
    protected void send(ServletWebRequest request, SlideImageCode validateCode)
            throws GlobalException {
        double heightY = validateCode.getHeightYPercentage();
        try {
            HttpServletResponse response = request.getResponse();
            response.setContentType(MediaType.APPLICATION_JSON_VALUE);
            response.getOutputStream()
                    .write(
                            CommonResponse.successInstance(
                                            new SlideValidateCodeImage(
                                                    heightY,
                                                    validateCode.getSrcImg(),
                                                    validateCode.getMarkImg()))
                                    .toJson()
                                    .getBytes());
        } catch (IOException e) {
            throw GlobalException.newInstance("", "圖片驗(yàn)證碼生成失敗");
        }
    }

    /**
     * 滑動驗(yàn)證碼驗(yàn)證
     *
     * @param code
     * @param validateCode
     * @return
     */
    @Override
    protected boolean validate(String code, SlideImageCode validateCode) {
        try {
            String[] location = StringUtils.splitByWholeSeparatorPreserveAllTokens(code, ":");
            double x1 = CastUtil.castDouble(location[0]);
            double y1 = CastUtil.castDouble(location[1]);
            String sessionCode = validateCode.getCode();
            String[] sessionLocation =
                    StringUtils.splitByWholeSeparatorPreserveAllTokens(sessionCode, ":");
            double x2 = CastUtil.castDouble(sessionLocation[0]);
            double y2 = CastUtil.castDouble(sessionLocation[1]);
            double distance = Math.sqrt(Math.pow((x1 - x2), 2) + Math.pow((y1 - y2), 2));
            return distance < 0.06;
        } catch (Exception e) {
            return false;
        }
    }

    @Data
    @NoArgsConstructor
    @AllArgsConstructor
    private static class SlideValidateCodeImage {

        private double heightY;

        private String srcImg;

        private String markImg;
    }
}

滑塊處理工具類:

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import org.springframework.util.Base64Utils;

import javax.imageio.ImageIO;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Random;

/**
 * @author zouwei
 */
public final class SlideImageUtil {

    private static final String IMAGE_TYPE = "png";

    /** 源文件寬度 */
    private static int ORI_WIDTH = 300;
    /** 源文件高度 */
    private static int ORI_HEIGHT = 150;
    /** 模板圖寬度 */
    private static int CUT_WIDTH = 50;
    /** 模板圖高度 */
    private static int CUT_HEIGHT = 50;
    /** 摳圖凸起圓心 */
    private static int circleR = 5;
    /** 摳圖內(nèi)部矩形填充大小 */
    private static int RECTANGLE_PADDING = 8;
    /** 摳圖的邊框?qū)挾?*/
    private static int SLIDER_IMG_OUT_PADDING = 1;

    @Data
    @AllArgsConstructor
    @NoArgsConstructor
    public static class SlideImage {
        /** 底圖 */
        private String srcImg;
        /** 標(biāo)記圖片 */
        private String markImg;
        /** x軸 */
        private int x;
        /** y軸 */
        private int y;
        /** 原圖的寬度 */
        private int width;
        /** 原圖的高度 */
        private int height;
    }
    /**
     * 根據(jù)傳入的路徑生成指定驗(yàn)證碼圖片
     *
     * @param originImage
     * @return
     * @throws IOException
     */
    public static SlideImage getVerifyImage(BufferedImage originImage) throws IOException {
        int width = originImage.getWidth();
        int height = originImage.getHeight();
        int locationX = CUT_WIDTH + new Random().nextInt(width - CUT_WIDTH * 3);
        int locationY = CUT_HEIGHT + new Random().nextInt(height - CUT_HEIGHT) / 2;
        BufferedImage markImage =
                new BufferedImage(CUT_WIDTH, CUT_HEIGHT, BufferedImage.TYPE_4BYTE_ABGR);
        int[][] data = getBlockData();
        cutImgByTemplate(originImage, markImage, data, locationX, locationY);
        return new SlideImage(
                getImageBASE64(originImage),
                getImageBASE64(markImage),
                locationX,
                locationY,
                width,
                height);
    }

    /**
     * 生成隨機(jī)滑塊形狀
     *
     * <p>0 透明像素 1 滑塊像素 2 陰影像素
     *
     * @return int[][]
     */
    private static int[][] getBlockData() {
        int[][] data = new int[CUT_WIDTH][CUT_HEIGHT];
        Random random = new Random();
        // (x-a)2+(y-b)2=r2
        // x中心位置左右5像素隨機(jī)
        double x1 =
                RECTANGLE_PADDING
                        + (CUT_WIDTH - 2 * RECTANGLE_PADDING) / 2.0
                        - 5
                        + random.nextInt(10);
        // y 矩形上邊界半徑-1像素移動
        double y1_top = RECTANGLE_PADDING - random.nextInt(3);
        double y1_bottom = CUT_HEIGHT - RECTANGLE_PADDING + random.nextInt(3);
        double y1 = random.nextInt(2) == 1 ? y1_top : y1_bottom;

        double x2_right = CUT_WIDTH - RECTANGLE_PADDING - circleR + random.nextInt(2 * circleR - 4);
        double x2_left = RECTANGLE_PADDING + circleR - 2 - random.nextInt(2 * circleR - 4);
        double x2 = random.nextInt(2) == 1 ? x2_right : x2_left;
        double y2 =
                RECTANGLE_PADDING
                        + (CUT_HEIGHT - 2 * RECTANGLE_PADDING) / 2.0
                        - 4
                        + random.nextInt(10);

        double po = Math.pow(circleR, 2);
        for (int i = 0; i < CUT_WIDTH; i++) {
            for (int j = 0; j < CUT_HEIGHT; j++) {
                // 矩形區(qū)域
                boolean fill;
                if ((i >= RECTANGLE_PADDING && i < CUT_WIDTH - RECTANGLE_PADDING)
                        && (j >= RECTANGLE_PADDING && j < CUT_HEIGHT - RECTANGLE_PADDING)) {
                    data[i][j] = 1;
                    fill = true;
                } else {
                    data[i][j] = 0;
                    fill = false;
                }
                // 凸出區(qū)域
                double d3 = Math.pow(i - x1, 2) + Math.pow(j - y1, 2);
                if (d3 < po) {
                    data[i][j] = 1;
                } else {
                    if (!fill) {
                        data[i][j] = 0;
                    }
                }
                // 凹進(jìn)區(qū)域
                double d4 = Math.pow(i - x2, 2) + Math.pow(j - y2, 2);
                if (d4 < po) {
                    data[i][j] = 0;
                }
            }
        }
        // 邊界陰影
        for (int i = 0; i < CUT_WIDTH; i++) {
            for (int j = 0; j < CUT_HEIGHT; j++) {
                // 四個正方形邊角處理
                for (int k = 1; k <= SLIDER_IMG_OUT_PADDING; k++) {
                    // 左上庶艾、右上
                    if (i >= RECTANGLE_PADDING - k
                            && i < RECTANGLE_PADDING
                            && ((j >= RECTANGLE_PADDING - k && j < RECTANGLE_PADDING)
                                    || (j >= CUT_HEIGHT - RECTANGLE_PADDING - k
                                            && j < CUT_HEIGHT - RECTANGLE_PADDING + 1))) {
                        data[i][j] = 2;
                    }

                    // 左下、右下
                    if (i >= CUT_WIDTH - RECTANGLE_PADDING + k - 1
                            && i < CUT_WIDTH - RECTANGLE_PADDING + 1) {
                        for (int n = 1; n <= SLIDER_IMG_OUT_PADDING; n++) {
                            if (((j >= RECTANGLE_PADDING - n && j < RECTANGLE_PADDING)
                                    || (j >= CUT_HEIGHT - RECTANGLE_PADDING - n
                                            && j <= CUT_HEIGHT - RECTANGLE_PADDING))) {
                                data[i][j] = 2;
                            }
                        }
                    }
                }

                if (data[i][j] == 1
                        && j - SLIDER_IMG_OUT_PADDING > 0
                        && data[i][j - SLIDER_IMG_OUT_PADDING] == 0) {
                    data[i][j - SLIDER_IMG_OUT_PADDING] = 2;
                }
                if (data[i][j] == 1
                        && j + SLIDER_IMG_OUT_PADDING > 0
                        && j + SLIDER_IMG_OUT_PADDING < CUT_HEIGHT
                        && data[i][j + SLIDER_IMG_OUT_PADDING] == 0) {
                    data[i][j + SLIDER_IMG_OUT_PADDING] = 2;
                }
                if (data[i][j] == 1
                        && i - SLIDER_IMG_OUT_PADDING > 0
                        && data[i - SLIDER_IMG_OUT_PADDING][j] == 0) {
                    data[i - SLIDER_IMG_OUT_PADDING][j] = 2;
                }
                if (data[i][j] == 1
                        && i + SLIDER_IMG_OUT_PADDING > 0
                        && i + SLIDER_IMG_OUT_PADDING < CUT_WIDTH
                        && data[i + SLIDER_IMG_OUT_PADDING][j] == 0) {
                    data[i + SLIDER_IMG_OUT_PADDING][j] = 2;
                }
            }
        }
        return data;
    }

    /**
     * 裁剪區(qū)塊 根據(jù)生成的滑塊形狀擎勘,對原圖和裁剪塊進(jìn)行變色處理
     *
     * @param oriImage 原圖
     * @param targetImage 裁剪圖
     * @param blockImage 滑塊
     * @param x 裁剪點(diǎn)x
     * @param y 裁剪點(diǎn)y
     */
    private static void cutImgByTemplate(
            BufferedImage oriImage, BufferedImage targetImage, int[][] blockImage, int x, int y) {
        for (int i = 0; i < CUT_WIDTH; i++) {
            for (int j = 0; j < CUT_HEIGHT; j++) {
                int _x = x + i;
                int _y = y + j;
                int rgbFlg = blockImage[i][j];
                int rgb_ori = oriImage.getRGB(_x, _y);
                // 原圖中對應(yīng)位置變色處理
                if (rgbFlg == 1) {
                    // 摳圖上復(fù)制對應(yīng)顏色值
                    targetImage.setRGB(i, j, rgb_ori);
                    // 原圖對應(yīng)位置顏色變化
                    oriImage.setRGB(_x, _y, Color.LIGHT_GRAY.getRGB());
                } else if (rgbFlg == 2) {
                    targetImage.setRGB(i, j, Color.WHITE.getRGB());
                    oriImage.setRGB(_x, _y, Color.GRAY.getRGB());
                } else if (rgbFlg == 0) {
                    // int alpha = 0;
                    targetImage.setRGB(i, j, rgb_ori & 0x00ffffff);
                }
            }
        }
    }

    /**
     * 隨機(jī)獲取一張圖片對象
     *
     * @param path
     * @return
     * @throws IOException
     */
    public static BufferedImage getRandomImage(String path) throws IOException {
        File files = new File(path);
        File[] fileList = files.listFiles();
        List<String> fileNameList = new ArrayList<>();
        if (fileList != null && fileList.length != 0) {
            for (File tempFile : fileList) {
                if (tempFile.isFile() && tempFile.getName().endsWith(".jpg")) {
                    fileNameList.add(tempFile.getAbsolutePath().trim());
                }
            }
        }
        Random random = new Random();
        File imageFile = new File(fileNameList.get(random.nextInt(fileNameList.size())));
        return ImageIO.read(imageFile);
    }

    /**
     * 將IMG輸出為文件
     *
     * @param image
     * @param file
     * @throws Exception
     */
    public static void writeImg(BufferedImage image, String file) throws Exception {
        try (ByteArrayOutputStream bao = new ByteArrayOutputStream()) {
            ImageIO.write(image, IMAGE_TYPE, bao);
            FileOutputStream out = new FileOutputStream(new File(file));
            out.write(bao.toByteArray());
        }
    }

    /**
     * 將圖片轉(zhuǎn)換為BASE64
     *
     * @param image
     * @return
     * @throws IOException
     */
    public static String getImageBASE64(BufferedImage image) throws IOException {
        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {
            ImageIO.write(image, IMAGE_TYPE, out);
            // 生成BASE64編碼
            return Base64Utils.encodeToString(out.toByteArray());
        }
    }

    /**
     * 將BASE64字符串轉(zhuǎn)換為圖片
     *
     * @param base64String
     * @return
     */
    public static BufferedImage base64StringToImage(String base64String) throws IOException {
        try (ByteArrayInputStream bais =
                new ByteArrayInputStream(Base64Utils.decodeFromString(base64String))) {
            return ImageIO.read(bais);
        }
    }
}

短信驗(yàn)證碼:

import com.zx.silverfox.common.validate.code.ValidateCode;

public class SmsValidateCode extends ValidateCode {

    public SmsValidateCode(String code, long expireInSeconds) {
        super(code, expireInSeconds);
    }
}

import com.zx.silverfox.common.properties.ValidateCodeProperties;
import com.zx.silverfox.common.validate.code.ValidateCodeGenerator;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.web.context.request.ServletWebRequest;

/** @author zouwei */
public class SmsValidateCodeGenerator implements ValidateCodeGenerator {

    private ValidateCodeProperties.SmsProperties smsProperties;

    public SmsValidateCodeGenerator(ValidateCodeProperties.SmsProperties smsProperties) {
        this.smsProperties = smsProperties;
    }

    @Override
    public SmsValidateCode createValidateCode(ServletWebRequest request) {
        String code = RandomStringUtils.randomNumeric(smsProperties.getLength());
        return new SmsValidateCode(code, smsProperties.getExpiredInSecond());
    }
}
import com.zx.silverfox.common.exception.GlobalException;
import com.zx.silverfox.common.properties.ValidateCodeProperties;
import com.zx.silverfox.common.util.CastUtil;
import com.zx.silverfox.common.util.SmsUtil;
import com.zx.silverfox.common.validate.code.AbstractValidateCodeProcessor;
import com.zx.silverfox.common.validate.code.ValidateCodeRepository;
import com.zx.silverfox.common.validate.code.ValidateCodeType;
import com.zx.silverfox.common.vo.CommonResponse;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.MediaType;
import org.springframework.web.context.request.ServletWebRequest;

import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** @author zouwei */
@Slf4j
public class SmsValidateCodeProcessor extends AbstractValidateCodeProcessor<SmsValidateCode> {
    public SmsValidateCodeProcessor(
            @Autowired ValidateCodeProperties validateCodeProperties,
            @Autowired ValidateCodeRepository validateCodeRepository) {
        super(
                new SmsValidateCodeGenerator(validateCodeProperties.getSms()),
                validateCodeRepository,
                validateCodeProperties.getSms());
    }

    @Override
    protected ValidateCodeType getValidateCodeType() {
        return ValidateCodeType.SMS;
    }

    @Override
    protected void send(ServletWebRequest request, SmsValidateCode validateCode)
            throws GlobalException {
        // 手機(jī)號碼
        String mobile = request.getParameter("mobile");
        String type = request.getParameter("type");
        if (StringUtils.isBlank(mobile) || StringUtils.isBlank(type)) {
            // 獲取驗(yàn)證碼參數(shù)沒提供
            throw GlobalException.newInstance(
                    "SMS_VALIDATE_CODE_PARAM_ERROR", "沒有給電話號碼或者指明短信類型咱揍,無法發(fā)送短信");
        }
        long minute = validateCode.minute();
        SmsUtil.send(
                SmsUtil.SmsType.format(type),
                mobile,
                validateCode.getCode(),
                CastUtil.castString(minute));
        HttpServletResponse response = request.getResponse();
        response.setContentType(MediaType.APPLICATION_JSON_VALUE);
        try {
            response.getOutputStream().write(CommonResponse.successInstance().toJson().getBytes());
        } catch (IOException e) {
            log.error("response.getOutputStream()出異常", e);
        }
    }

    @Override
    protected boolean validate(String code, SmsValidateCode validateCode) {
        return StringUtils.equalsIgnoreCase(code, validateCode.getCode());
    }
}

注意事項(xiàng):想要使用滑塊驗(yàn)證碼,需要在resources文件夾里面創(chuàng)建一個slideimg文件夾棚饵,并且把需要的圖片放進(jìn)去:
image-20200713231006902.png

GlobalException是我自己設(shè)計(jì)的異常類煤裙,建議需要的小伙伴換成自己應(yīng)用的異常類。

ok噪漾,整個驗(yàn)證碼組件設(shè)計(jì)加上具體實(shí)現(xiàn)都已經(jīng)完畢硼砰,下面就是如何使用:

首先,把自定義注解放在springboot項(xiàng)目啟動類上


image-20200713231335775.png

建議按需配置欣硼,如果不需要圖片驗(yàn)證碼或者滑塊驗(yàn)證碼题翰,可以不加載進(jìn)來

然后就是配置文件:
image-20200713231512035.png

比如你需要在發(fā)生短信驗(yàn)證碼之前先觸發(fā)滑塊驗(yàn)證碼,那么可以把"/code/sms"這個url放進(jìn)validate.slide.filter-urls配置中诈胜。

好吧豹障,怎么使用已經(jīng)講解完畢,配置文件中的其他配置參數(shù)包括圖片的大小和驗(yàn)證碼的位數(shù)等等焦匈,小伙伴可以根據(jù)自身需要去配置血公。

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市括授,隨后出現(xiàn)的幾起案子坞笙,更是在濱河造成了極大的恐慌,老刑警劉巖荚虚,帶你破解...
    沈念sama閱讀 222,627評論 6 517
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件薛夜,死亡現(xiàn)場離奇詭異,居然都是意外死亡版述,警方通過查閱死者的電腦和手機(jī)梯澜,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,180評論 3 399
  • 文/潘曉璐 我一進(jìn)店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來渴析,“玉大人晚伙,你說我怎么就攤上這事〖蠹耄” “怎么了咆疗?”我有些...
    開封第一講書人閱讀 169,346評論 0 362
  • 文/不壞的土叔 我叫張陵,是天一觀的道長母债。 經(jīng)常有香客問我午磁,道長尝抖,這世上最難降的妖魔是什么? 我笑而不...
    開封第一講書人閱讀 60,097評論 1 300
  • 正文 為了忘掉前任迅皇,我火速辦了婚禮昧辽,結(jié)果婚禮上,老公的妹妹穿的比我還像新娘登颓。我一直安慰自己搅荞,他們只是感情好,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,100評論 6 398
  • 文/花漫 我一把揭開白布框咙。 她就那樣靜靜地躺著咕痛,像睡著了一般。 火紅的嫁衣襯著肌膚如雪喇嘱。 梳的紋絲不亂的頭發(fā)上暇检,一...
    開封第一講書人閱讀 52,696評論 1 312
  • 那天,我揣著相機(jī)與錄音婉称,去河邊找鬼。 笑死构蹬,一個胖子當(dāng)著我的面吹牛王暗,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播庄敛,決...
    沈念sama閱讀 41,165評論 3 422
  • 文/蒼蘭香墨 我猛地睜開眼俗壹,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了藻烤?” 一聲冷哼從身側(cè)響起绷雏,我...
    開封第一講書人閱讀 40,108評論 0 277
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎怖亭,沒想到半個月后涎显,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 46,646評論 1 319
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡兴猩,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,709評論 3 342
  • 正文 我和宋清朗相戀三年期吓,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學(xué)時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片倾芝。...
    茶點(diǎn)故事閱讀 40,861評論 1 353
  • 序言:一個原本活蹦亂跳的男人離奇死亡讨勤,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出晨另,到底是詐尸還是另有隱情潭千,我是刑警寧澤,帶...
    沈念sama閱讀 36,527評論 5 351
  • 正文 年R本政府宣布借尿,位于F島的核電站刨晴,受9級特大地震影響,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜割捅,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,196評論 3 336
  • 文/蒙蒙 一奶躯、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧亿驾,春花似錦嘹黔、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,698評論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至疼邀,卻和暖如春喂江,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背旁振。 一陣腳步聲響...
    開封第一講書人閱讀 33,804評論 1 274
  • 我被黑心中介騙來泰國打工获询, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留,地道東北人拐袜。 一個月前我還...
    沈念sama閱讀 49,287評論 3 379
  • 正文 我出身青樓吉嚣,卻偏偏與公主長得像,于是被迫代替她去往敵國和親蹬铺。 傳聞我的和親對象是個殘疾皇子尝哆,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,860評論 2 361