spring security——集成验证码(三 )

版权声明:本文为博主原创文章,遵循 CC 4.0 BY-SA 版权协议,转载请附上原文出处链接和本声明。
本文链接: https://blog.csdn.net/qq_22172133/article/details/100037388

一、图片验证码

        我们知道在登录界面往往要添加图形验证码来进行人机验证,这里我们就来在之前的认证流程中加入图形验证码校验功能。

1、开发生成图型验证码接口

        流程其实很简单,首先根据随机数生成图片,然后将随机数存到session中,最后将生成的图片写入响应中,没了。这里生成图片什么的就不说了。网上一大堆,记录下这里的一些代码思路。首先是图片验证码信息类

/**
 * 图形验证码
 */
public class ImageCode {
    private BufferedImage image;
    private String code;
    private LocalDateTime expireTime; // 过期时间
​
    /**
     * @param image
     * @param code
     * @param expireIn 过期时间,单位秒
     */
    public ImageCode(BufferedImage image, String code, int expireIn) {
        this.image = image;
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    }
    // 是否过期
    public boolean isExpried() {
      return this.expireTime.isBefore(LocalDateTime.now());
  }

然后验证码服务

@RestController
public class ValidateCodeController {
    private static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
    // 这里又使用了spring的工具类来操作session
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
​
    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ImageCode imageCode = createImageCode(request);
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
        response.setContentType("image/jpeg");
        //禁止图像缓存。
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
    }
​
    private ImageCode createImageCode(HttpServletRequest request) throws IOException {
        String code = RandomStringUtils.randomAlphanumeric(4);
        BufferedImage image = createImageCode(80, 40, code);
        return new ImageCode(image, code, 60);
    }

当然,不要忘了该服务需要配置放行。

2、在认证流程中加入图像验证码校验

        之前已经看过Spring Security源码了,我们知道如果想在Security中加入功能,只要把过滤器添加到spring Security现有的过滤器链上就可以了。现在开始编写验证码过滤器

/**
 * 图片验证码验证过滤器
 * OncePerRequestFilter spring提供的,保证在一个请求中只会被调用一次
 */
public class ValidateCodeFilter extends OncePerRequestFilter {
    // 在初始化本类的地方进行注入
    // 一般在配置security http的地方进行添加过滤器
    private AuthenticationFailureHandler failureHandler;
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
​
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 为登录请求,并且为post请求【即登录请求时,先验证图片验证码】
        if (StringUtils.equals("/authentication/form", request.getRequestURI())
                && StringUtils.equalsAnyIgnoreCase(request.getMethod(), "post")) {
            try {
                validate(request);//验证
            } catch (ValidateCodeException e) {
                failureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }
        filterChain.doFilter(request, response);
    }
​
    private void validate(HttpServletRequest request) throws ServletRequestBindingException {
        // 拿到之前存储的imageCode信息
        ServletWebRequest swr = new ServletWebRequest(request);
        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(swr, ValidateCodeController.SESSION_KEY);
        // 又是一个spring中的工具类,
        // 试问一下,如果不看源码怎么可能知道有这些工具类可用?
        String codeInRequest = ServletRequestUtils.getStringParameter(request, "imageCode");
​
        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException("验证码的值不能为空");
        }
        if (codeInSession == null) {
            throw new ValidateCodeException("验证码不存在");
        }
        if (codeInSession.isExpried()) {
            sessionStrategy.removeAttribute(swr, ValidateCodeController.SESSION_KEY);
            throw new ValidateCodeException("验证码已过期");
        }
        if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
            throw new ValidateCodeException("验证码不匹配");
        }
        //移除验证码
        sessionStrategy.removeAttribute(swr, ValidateCodeController.SESSION_KEY);
    }

    public AuthenticationFailureHandler getFailureHandler() {
        return failureHandler;
    }
    public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
        this.failureHandler = failureHandler;
    }
}

把过滤器添加到现有认证流程中,我们要放在UsernamePasswordAuthenticationFilter过滤器之前

ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setFailureHandler(myAuthenticationFailureHandler);
http
        // 由源码得知,在最前面的是UsernamePasswordAuthenticationFilter
        .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
        // 定义表单登录 - 身份认证的方式
        .formLogin()
        .loginPage("/authentication/require")
        .loginProcessingUrl("/authentication/form")

需要注意的一个地方就是myAuthenticationFailureHandler,因为失败会调用这个处理器。

3、图片验证码重构

        我们的目的是写一个可重用的安全模块,但是上面~~~很乱,不过代码还不多,现在重新梳理一下。那我们就从项目开始,首先我们创建如下几个项目【实际项目使用时,按需依赖下面具体的项目模块即可】:

自此项目搭建完成。现在开始进行代码重构。

1.配置信息抽取

        我们如果想写一个可重用的项目模块,就需要把一些配置(比如自定义登录请求等)让使用方来进行配置。思路是使用配置文件,比如在application.yml中:

tin:
  security:
    browser:
      loginPage: /tin-signIn.html

由于这些配置类是 token 和 browser 项目公用的,写在core里面

@ConfigurationProperties(prefix = "tin.security")
@Data
public class SecurityProperties {
    //浏览器端配置, tin.security.browser 路径下的配置会被映射到该配置类中
    private BrowserProperties browser = new BrowserProperties();
    //验证码配置
    private ValidateCodeProperties code = new ValidateCodeProperties();
    //当请求需要身份认证时,默认跳转的url
    private String authenticationUrl = SecurityConstant.DEFAULT_UNAUTHENTICATION_URL;
    //默认的用户请求登录处理url
    private String loginProcessingUrl = SecurityConstant.DEFAULT_LOGIN_PROCESSING_URL_FORM;
}

然后我们在看看 BrowserProperties 的内部实现

@Data
public class BrowserProperties {
    //session相关配置,类推
    private SessionProperties session = new SessionProperties();
    //默认的注册页面
    private String signUpUrl = "/tin-signUp.html";
    //默认的登录页面
    private String loginPage = "/tin-signIn.html";
    //默认的退出登录时跳转的url。如果配置了,则跳到指定的url,如果没配置,则返回json数据。
    private String signOutUrl;
    //默认的登录响应方式
    private LoginResponseType loginType = LoginResponseType.JSON;
}

为了职责分离,单独写一个入口被扫描开启的配置类(也在core中)

@Configuration
@EnableConfigurationProperties(SecurityProperties.class)
public class SecurityCoreConfig {
}

然后我们就可以使用配置的方式,对这些信息进行配置了。之前写死 登录页面 相关代码处都需要更改从配置类中获取(browser),使用 Autowired 注解可以获得SecurityProperties配置类实例

@Autowired
private SecurityProperties securityProperties;
​
// 等等。。。
.antMatchers(//"/authentication/require",
            securityProperties.getBrowser().getLoginPage()).permitAll()

​2.图片验证码重构

        其他部分的代码重构后面再说,现在先进行图片验证码的代码重构。我们重构的目的是:

  • 验证码基本参数可配置
  • 验证码校验拦截的接口可配置
  • 验证码的生成逻辑可配

1.验证码基本参数配置

        在参数配置中上面的会覆盖下级的配置,请求级配置(配置值在调用接口的时候传递)会覆盖应用级配置(配置写在portal项目中);应用级配置会覆盖默认配置(配置值写在core项目中)。首先创建图形验证码配置类

@Data
public class ImageCodeProperties {
    //图片验证码长度
    private int length = 4;
    //图片验证码过期时间
    private int expireIn = 60;
    //图片验证码宽
    private int width = 67;
    //图片验证码高
    private int height = 23;
}

然后用验证码配置类封装图片验证码配置类

@Data
public class ValidateCodeProperties {
    //图片验证码配置
    private ImageCodeProperties image = new ImageCodeProperties();
    //后面还会新增短信验证码等的配置
}

最后加入总配置类中

@ConfigurationProperties(prefix = "tin.security")
public class SecurityProperties {
    private BrowserProperties browser = new BrowserProperties();
    private ValidateCodeProperties code = new ValidateCodeProperties();

然后修改处理逻辑处中的代码即可,将之前写死的改成配置项。

2.验证码校验拦截的接口可配置

        我们提供url拦截地址配置属性,然后在过滤器中获取配置的属性,并且循环匹配。首先增加url配置属性

@Data
public class ImageCodeProperties {
    //图片验证码长度
    private int length = 4;
    //图片验证码过期时间
    private int expireIn = 60;
    //需要拦截的路径
    private String[] interceptUrls = {};
    //图片验证码宽
    private int width = 67;
    //图片验证码高
    private int height = 23;
}

然后在过滤器中对目标url进行匹配逻辑

@Slf4j
@Component("validateCodeFilter")
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
    @Autowired
    private AuthenticationFailureHandler failureHandler;
    @Autowired
    private SecurityProperties securityProperties;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
​
    // 存储所有需要拦截的url
    private Set<String> urls;
    ////验证请求url与配置的url是否匹配的工具类
    private AntPathMatcher pathMatcher = new AntPathMatcher();
​
    /**
     * org.springframework.beans.factory.InitializingBean 保证在其他属性都设置完成后,有beanFactory调用
     * 但是在这里目前还是需要初始化处调用该方法
     */
    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        String[] configUrl = securityProperties.getCode().getImage().getInterceptUrls();
        urls = Stream.of(configUrl).collect(Collectors.toSet());
        urls.add("/authentication/form"); // 登录请求
    }
​
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 为登录请求,并且为post请求
        boolean action = false;
        for (String url : urls) {
            // org.springframework.util.AntPathMatcher 能匹配spring中的url模式
            // 支持通配符路径那种
            if (pathMatcher.match(url, request.getRequestURI())) {
                action = true;
            }
        }
        if (action) {
            try {
                validate(request);
            } catch (ValidateCodeException e) {
                failureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }
        filterChain.doFilter(request, response);
    }

然后在原配置中心进行属性注入

// 有三个configure的方法,这里使用http参数的
@Override
protected void configure(HttpSecurity http) throws Exception {
    // 最简单的修改默认配置的方法
    // 在v5+中,该配置(表单登录)应该是默认配置了
    // basic登录(也就是弹框登录的)应该是v5-的版本默认
​
    ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
    validateCodeFilter.setFailureHandler(myAuthenticationFailureHandler);
    validateCodeFilter.setSecurityProperties(securityProperties);  // 注入配置属性类
    validateCodeFilter.afterPropertiesSet(); // 初始化url配置

测试:tin-portal/application.yml

tin:
  security:
    browser:
      loginType: JSON
    code:
      image:
        width: 100
        height: 50
        interceptUrls[0]: /order
        interceptUrls[1]: /user/*

3.验证码的生成逻辑可配

        逻辑可配,就是抽象成接口,然后由客户端提供。这里提供一个生成图片信息的接口

public interface ValidateCodeGenerate {
    ImageCode generate(HttpServletRequest request) throws IOException;
}

然后实现默认图片生成接口类

public class ImageCodeGenerate implements ValidateCodeGenerate {
    private SecurityProperties securityProperties;
​
    //就是返回生成的图片验证码对象即可
    @Override
    public ImageCode generate(HttpServletRequest request) throws IOException {
        return createImageCode(request);
    }
​
    public ImageCode createImageCode(HttpServletRequest request) throws IOException {
        ImageCodeProperties imageProperties = securityProperties.getCode().getImage()
        int width = ServletRequestUtils.getIntParameter(request, "width", imageProperties.getWidth());
        int height = ServletRequestUtils.getIntParameter(request, "height", imageProperties.getHeight());
        int length = ServletRequestUtils.getIntParameter(request, "length", imageProperties.getLength());
        int expireIn = ServletRequestUtils.getIntParameter(request, "expireIn", imageProperties.getExpireIn());
        String code = RandomStringUtils.randomNumeric(length);
        BufferedImage image = createImageCode(width, height, code);
        return new ImageCode(image, code, expireIn);
    }
    public SecurityProperties getSecurityProperties() {
        return securityProperties;
    }
​
    public void setSecurityProperties(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }
    //...后面的就是具体的工具类代码 不贴了

增加配置类,初始化图片生成器实例。这个是重点!!!

@Configuration
public class ValidateCodeBeanConfig {
    @Autowired
    private SecurityProperties securityProperties;
​
    @Bean
    // 条件注解,spring 容器中如果存在imageCodeGenerate的bean就不会再初始化该bean了
    @ConditionalOnMissingBean(name = "imageValidateCodeGenerator")
    public ValidateCodeGenerate imageValidateCodeGenerator() {
        ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
        codeGenerator.setSecurityProperties(securityProperties);
        return codeGenerator;
    }    
}

之前调用处修改成调用接口【1-1】

public class ValidateCodeController {
    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
​
    @Autowired  // 使用生成接口处理信息
    private ValidateCodeGenerate validateCodeGenerate;

二、短信验证码

        短信验证码登录是目前惯用的登录方式之一,这里我们就先来实现短信验证码的功能。这里的套路与之前图形验证码的套路类似(后面会实现短信验证码登录)。

1、开发短信验证码接口

        我们在之前的验证码API上加上短信验证码的服务。

@RestController
public class ValidateCodeController {
    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    @Autowired
    private SecurityProperties securityProperties;
    @Autowired
    private ValidateCodeGenerate smsCodeGenerate;
    @Autowired
    private SmsCodeSender smsCodeSender;
    @GetMapping("/code/sms")
    public void createSmsCode(HttpServletRequest request, HttpServletResponse response) throws IOException, ServletRequestBindingException {
        ValidateCode validateCode = smsCodeGenerate.generate(request);
        String mobile = ServletRequestUtils.getRequiredStringParameter(request, "mobile");
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, validateCode.getCode());
        smsCodeSender.send(mobile, validateCode.getCode());
    }

然后配置类上

@Configuration
public class ValidateCodeBeanConfig{
    @Autowired
    private SecurityProperties securityProperties;
​
    @Bean
    @ConditionalOnMissingBean(name = "imageValidateCodeGenerator")
    public ValidateCodeGenerate imageValidateCodeGenerator() {
        //...
    }
​
    // 这里由于产生了多个ValidateCodeGenerate的实现类,所以需要使用name来区分
    @Bean
    @ConditionalOnMissingBean(name = "smsValidateCodeGenerator")
    public ValidateCodeGenerator smsValidateCodeGenerator() {
        SmsCodeGenerator codeGenerator = new SmsCodeGenerator();
        codeGenerator.setSecurityProperties(securityProperties);
        return codeGenerator;
    }
}

短信验证码生成类

public class SmsCodeGenerate implements ValidateCodeGenerate {
    private SmsCodeProperties smsCodeProperties;
​
    public SmsCodeGenerate(SmsCodeProperties smsCodeProperties) {
        this.smsCodeProperties = smsCodeProperties;
    }
​
    @Override
    public ValidateCode generate(HttpServletRequest request) {
        int count = smsCodeProperties.getLength();
        int expireIn = smsCodeProperties.getExpireIn();
        String smsCode = RandomStringUtils.randomNumeric(count);
        //ValidateCode——验证码,把之前的ImageCode给抽取了一下,ImageCode继承ValidateCode【code,expireTime;】
        return new ValidateCode(smsCode, expireIn);
    }

这里目前没有什么特别的,都是伪代码,提供一种思路。 还有就是贴出来的代码与之前图形验证码的部分代码重合了,就重构了。

2、重构验证码逻辑

        由于发现有好多逻辑都是重复的,所以在这里我们进行深度重构抽象。验证码处理器结构如下:

上图逻辑清晰,看着么多类,实际上是把变化的部分抽象成接口了,公共的逻辑使用模版方法模式封装起来了。以后可以应对不同的变化,比如:

  • 图形验证码或则短信验证码的 生成逻辑变了,提供ValidateCodeGenerator实现类即可
  • 图形或则短信验证码的响应/发送逻辑变了,提供AbstractValidateCodeProcessor的子类实现abstract void send发送方法

经过上面的思路分析,就是把变化的流程单独拿出来了。下面先把主要的重构代码粘一下吧,看一下大体流程。首先我们来看一下ValidateCodeController实现

@RestController
@RequestMapping(SecurityConstant.DEFAULT_VALIDATE_CODE_URL_PREFIX)
public class ValidateCodeController {
    @Autowired
    private ValidateCodeProcessorHolder validateCodeProcessorHolder;
​
    /*
     * 功能描述:根据不同类型的参数获取验证码
     * 创建时间:2018/11/29 11:39
     * 入参:[request, response, type 验证码类型【image:图片验证码;sms:短信验证码;email:邮箱验证码】]
     * 返回值:void
     */
    @GetMapping("/{type}")
    public void createCode(HttpServletRequest request, HttpServletResponse response, @PathVariable String type)
            throws Exception {
        validateCodeProcessorHolder.findValidateCodeProcessor(type).create(new ServletWebRequest(request, response));
    }
}

很简单,不过这里把创建手机、图片验证码等通过url传参合并了,我们跟进一下findValidateCodeProcessor

@Autowired
private Map<String, ValidateCodeProcessor> validateCodeProcessors;//这里我们后面会讲
​
public ValidateCodeProcessor findValidateCodeProcessor(String type) {
    String name = type.toLowerCase() + ValidateCodeProcessor.class.getSimpleName();
    ValidateCodeProcessor processor = validateCodeProcessors.get(name);
    if (processor == null) {
        throw new ValidateCodeException("验证码处理器" + name + "不存在");
    }
    return processor;
}

这里主要干的事就是查找是否存在我们的验证码校验器(根据type),如果不存在则抛异常。如果存在,我们回去看上一块内容,即调用ValidateCodeProcessor.create方法。其实ValidateCodeProcessor接口中内容很简单,创建和校验

public interface ValidateCodeProcessor {
    //验证码放入session时的前缀
    String SESSION_KEY_PREFIX = "SESSION_KEY_FOR_CODE_";
​
    //创建校验码
    void create(ServletWebRequest request) throws Exception;
​
    //校验验证码
    void validate(ServletWebRequest request);
}

AbstractValidateCodeProcessor的create实现如下:

public void create(ServletWebRequest request) throws Exception {
    C validateCode = generate(request);//调用验证码生成器,生成验证码
    save(request, validateCode);//保存验证码到Session
    send(request, validateCode);//发送验证码
}

其中send方法为抽象方法,需要子类来实现。好了,抽取完成!还有一个遗留问题,

@Autowired
private Map<String, ValidateCodeGenerate> validateCodeGenerates;

此功能为 收集系统中所有 {@link ValidateCodeGenerate} 接口的实现,spring会查找所有ValidateCodeGenerate的实现,beanName做为key,实现作为value注入这里。在 ValidateCodeProcessorHolder、AbstractValidateCodeProcessor 中声明的该参数,再来看下在其他地方是怎么初始化的

@Bean
@ConditionalOnMissingBean(name = "imageValidateCodeGenerator")
public ValidateCodeGenerator imageValidateCodeGenerator() {
    ImageCodeGenerator codeGenerator = new ImageCodeGenerator();
    codeGenerator.setSecurityProperties(securityProperties);
    return codeGenerator;
}
​
@Bean
@ConditionalOnMissingBean(name = "smsValidateCodeGenerator")
public ValidateCodeGenerator smsValidateCodeGenerator() {
    SmsCodeGenerator codeGenerator = new SmsCodeGenerator();
    codeGenerator.setSecurityProperties(securityProperties);
    return codeGenerator;
}

上面使用了 条件排除,可以看出来这里有一个限制,就是短信和图形验证码的生成接口都使用的同一个。那么这里在排除的时候就只能写上beanName来限制了,对于上面的依赖查找技巧不会产生任何问题,但是对于使用处想替换该实现的时候。对于bean的name只能是覆盖这里的同名name,否则就会出现配置不成功的问题。

三、邮箱验证码

        邮箱验证码的实现就很简单了,和上面没什么区别。我们在上面的实现流程上补上邮箱验证

1、开发邮箱验证码接口

        上面的代码开发完成后,邮箱验证码接口就很简单了,基本上和短信验证码一致,这里就不详说了(EmailCodeProcessor,EmailCodeGenerator,EmailCodeSender)。我们就在上面的基础上再改写一下这几个验证码对拦截路径的处理。首先,邮箱验证码同样有配置信息

@Data
public class EmailCodeProperties {
    //邮箱验证码长度
    private int length = 6;
    //邮箱验证码过期时间
    private int expireIn = 600;
    //需要拦截的路径
    private String[] interceptUrls = {};
}

把其加入到验证码配置中

@Data
public class ValidateCodeProperties {
    //图片验证码配置
    private ImageCodeProperties image = new ImageCodeProperties();
    //手机验证码配置
    private SmsCodeProperties sms = new SmsCodeProperties();
    //邮箱验证码配置
    private EmailCodeProperties email = new EmailCodeProperties();
}

然后我们再回到图片验证码验证过滤器ValidateCodeFilter中,还记得之前的代码吗?好吧,这里重粘一下

public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
    // 存储所有需要拦截的url
    private Set<String> urls;
    ////验证请求url与配置的url是否匹配的工具类
    private AntPathMatcher pathMatcher = new AntPathMatcher();
​
    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        String[] configUrl = securityProperties.getCode().getImage().getInterceptUrls();
        urls = Stream.of(configUrl).collect(Collectors.toSet());
    }
​
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        for (String url : urls) {
            if (pathMatcher.match(url, request.getRequestURI())) {
                validate(request);
            }
        }
        filterChain.doFilter(request, response);
    }
​
    private void validate(HttpServletRequest request) throws ServletRequestBindingException {
        // 拿到之前存储的imageCode信息
        ServletWebRequest swr = new ServletWebRequest(request);
        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(swr, ValidateCodeController.SESSION_KEY);
        String codeInRequest = ServletRequestUtils.getStringParameter(request, "imageCode");
​
        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException("验证码的值不能为空");
        }
        //。。。各种条件判断:验证码不存在,验证码已过期,验证码不匹配
    }

}

首先通过afterPropertiesSet方法获取所有的图片验证码需要拦截的路径集合(Set),然后在做url过滤时循环匹配,如果匹配成功调用validate方法进行验证码校验,当然这里只支持图片验证码。如果我们想支持所有的验证码验证类型,这里就不能用Set了,改成Map

//存放所有需要校验验证码的url,key是url value是type
private Map<String, ValidateCodeType> urlMap = new HashMap<>();

这里用到了ValidateCodeType枚举,我把代码粘上

/*
 * 功能描述:验证码类型
 */
public enum  ValidateCodeType {
​
    /*
     * 功能描述:验证短信验证码时,http请求中默认的携带短信验证码信息的参数的名称
     */
    SMS {
        @Override
        public String getParamNameOnValidate() {
            return "smsCode";
        }
    },
​
    /*
     * 功能描述:验证图片验证码时,http请求中默认的携带图片验证码信息的参数的名称
     */
    IMAGE {
        @Override
        public String getParamNameOnValidate() {
            return "imageCode";
        }
    },
​
    /*
     * 功能描述:验证邮箱验证码时,http请求中默认的携带邮箱验证码信息的参数的名称
     */
    EMAIL{
        @Override
        public String getParamNameOnValidate() {
            return "emailCode";
        }
    };
​
​
    /*
     * 功能描述:校验时从请求中获取的参数的名字
     */
    public abstract String getParamNameOnValidate();
}

然后我们在afterPropertiesSet中分别加载

    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        //手机验证码需要拦截的路径
        for (String url : securityProperties.getCode().getSms().getInterceptUrls()) {
            addUrlToMap(url, ValidateCodeType.SMS);
        }
        //图片验证码需要拦截的路径
        for (String url : securityProperties.getCode().getImage().getInterceptUrls()){
            addUrlToMap(url, ValidateCodeType.IMAGE);
        }
        //邮箱验证码需要拦截的路径
        for (String url : securityProperties.getCode().getEmail().getInterceptUrls()) {
            addUrlToMap(url, ValidateCodeType.EMAIL);
        }
    }

在doFilterInternal方法中我们做的主要有两件事:匹配url获取要拦截的请求;验证。下面我们先做第一件事

private ValidateCodeType getValidateCodeType(HttpServletRequest request) {
    ValidateCodeType result = null;
    //这里限制不能是get请求方式
    if (!StringUtils.equalsIgnoreCase(request.getMethod(), "get")) {
        Set<String> urls = urlMap.keySet();
        for (String url : urls) {
            //支持通配符
            if (pathMatcher.match(url, request.getRequestURI())) {
                result = urlMap.get(url);
                break;
            }
        }
    }
    return result;
}

这样就可以匹配到需要验证的请求并知道他的验证类型了。然后后面再根据其验证码类型进行相应校验即可。我们上面已经封装了验证码校验器ValidateCodeProcessor,所有这里直接使用即可

protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
    throws ServletException, IOException {
    ValidateCodeType type = getValidateCodeType(request);
    if (type != null) {
        log.info("校验请求(" + request.getRequestURI() + ")中的验证码,验证码类型" + type);
        try {
            validateCodeProcessorHolder.findValidateCodeProcessor(type)
                .validate(new ServletWebRequest(request, response));
            log.info("验证码校验通过");
        } catch (ValidateCodeException exception) {
            authenticationFailureHandler.onAuthenticationFailure(request, response, exception);
            return;
        }
    }
    chain.doFilter(request, response);
}

2、默认的邮箱验证码发送实现

        我们知道邮箱发送是要根据具体业务调取相应的邮箱服务商接口的,这里肯定不适合放到core下面,但是我们为了功能的完整性可以先实现一个简单的默认实现,后面其他模块使用时直接替换掉即可(当然短信发送吗默认实现也一样)。下面是一个简单实现

public class DefaultEmailCodeSender implements EmailCodeSender{
    @Override
    public void send(String email, String code, ValidateCodeTemplate template) {
        System.out.println("向邮箱"+email+"发送验证码"+code);
    }
}

然后在ValidateCodeBeanConfig中配置条件注入即可

//短信验证码发送方式
@Bean
@ConditionalOnMissingBean(SmsCodeSender.class)
public SmsCodeSender smsCodeSender() {
    return new DefaultSmsCodeSender();
}
//邮箱验证码发送方式
@Bean
@ConditionalOnMissingBean(EmailCodeSender.class)
public EmailCodeSender emailCodeSender() {
    return new DefaultEmailCodeSender();
}

3、验证码模板

        当我们使用图片验证码时,直接生成一种验证码图片返回给页面,没有什么格式上的区别。但是对于邮箱/短信验证码是有格式区别的,比如注册和登录时给用户发送的验证码模板是不一样的。下面我们就来实现不同情况下发送不同模板的验证码,首先我们来定义一个枚举对象

/**
 * 功能描述 :验证码模板
 */
public enum  ValidateCodeTemplate {
    //默认
    NONE,
    //登录
    LONGIN,
    //注册
    REGISTER,
    //修改密码
    CHANGEPASSWORD;
​
    public static ValidateCodeTemplate get(String code) {
​
        ValidateCodeTemplate template = ValidateCodeTemplate.NONE;
        if (code.equals("0")){
            template = ValidateCodeTemplate.NONE;
        }else if (code.equals("1")){
            template = ValidateCodeTemplate.LONGIN;
        }else if (code.equals("2")){
            template = ValidateCodeTemplate.REGISTER;
        }else if (code.equals("3")){
            template = ValidateCodeTemplate.CHANGEPASSWORD;
        }
        return template;
    }
}

同时,我们在ValidateCode中也加一个模板变量

public class ValidateCode implements Serializable {
    private String code;////验证码
    private ValidateCodeTemplate template = ValidateCodeTemplate.NONE;//模板,默认没有模板
    private LocalDateTime expireTime;//保留时间

至于在发送验证码时对不同情况的区分,我们通过request中的参数来判断,下面我们改一下EmailCodeProcessor#send方法

protected void send(ServletWebRequest request, ValidateCode validateCode) throws Exception {
​
    String emailParam = SecurityConstant.DEFAULT_EMAIL_PARAMETER;
    String templateParam = SecurityConstant.DEFAULT_TEMPLATE_PARAMETER;
    String email = ServletRequestUtils.getRequiredStringParameter(request.getRequest(), emailParam);
    String template = "0";
    try {//可以为空
        template = ServletRequestUtils.getRequiredStringParameter(request.getRequest(), templateParam);
    }catch (Exception e){
    }
    emailCodeSender.send(email, validateCode.getCode(), ValidateCodeTemplate.get(template));
}

猜你喜欢

转载自blog.csdn.net/qq_22172133/article/details/100037388
今日推荐