springboot验证码重构

Posted wenfan

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了springboot验证码重构相关的知识,希望对你有一定的参考价值。

考虑到有多种验证机制(例如:常见的图片验证码,手机短信验证码,邮箱验证码)

所以在项目中对验证码进行代码重构,使之更加的具有可扩展性,低耦合性,此项目基于springboot

1.首先Controller层

@RestController
public class ValidateCodeController {

    @Autowired
    private ValidateCodeProcessorHolder holder;

    @GetMapping(SecurityConstants.DEFAULT_VALIDATE_CODE_URL_PREFIX+"/{type}")
    public void createCode(HttpServletRequest request, HttpServletResponse response, @PathVariable String type) throws Exception {
        holder.findValidateCodeProcessor(type).create(request,response);
    }
}

前端发起类似 /code/image这样的请求,将验证码类型获取到,从hold中找到哪个验证码处理器来进行处理

2.验证码管家

技术图片
package club.wenfan.youtube.validate;

import club.wenfan.youtube.validate.exception.ValidateCodeException;
import club.wenfan.youtube.validate.processor.ValidateCodeProcessor;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import java.util.Map;


/**
 * @author:wenfan
 * @description:
 * @data: 2019/1/22 9:31
 */
@Component
public class ValidateCodeProcessorHolder {

    @Autowired
    private Map<String,ValidateCodeProcessor> validateCodeProcessors;

    private Logger log = LoggerFactory.getLogger(getClass());

    public ValidateCodeProcessor findValidateCodeProcessor(ValidateCodeType type){
        return findValidateCodeProcessor(type.toString().toLowerCase());

    }

    public ValidateCodeProcessor findValidateCodeProcessor(String type){
        String name=type.toLowerCase() + ValidateCodeProcessor.class.getSimpleName();
        ValidateCodeProcessor processor=validateCodeProcessors.get(name);  //通过类型查找出用那个验证码处理器
        log.info("验证码处理器"+name);
        if(processor == null){
            throw new ValidateCodeException("验证码处理器"+name+"不存在");
        }
        return processor;
    }


}
ValidateCodeProcessorHolder类

特别说明一下

@Autowired
private Map<String,ValidateCodeProcessor> validateCodeProcessors;
采用这样的Map Bean注入方式,注入时将所有Bean的名字和类型作为Map注入进来
然后选择时通过Bean的名字来确定用哪个验证码处理器来完成。
3.ValidateCodeProcessor接口
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

/**
 * @author:wenfan
 * @description:
 * @data: 2019/1/21 12:03
 */
public interface ValidateCodeProcessor {

    /**
     *  验证码放入session时的前缀
     * @author wenfan
     * @date
     * @param
     * @return
     */
    String SESSION_KEY_PREFIX="SESSION_KEY_FOR_CODE_";

    /**
     *  创建校验码
     * @author wenfan
     */
    void create(HttpServletRequest request, HttpServletResponse response) throws Exception;

    void validate(HttpServletRequest request,HttpServletResponse response);

}

4.将验证码处理器的生成、保存、验证、验证抽象出来,单独将发送写成一个抽象方法,用具体的验证码处理来实现此方法

技术图片
import club.wenfan.youtube.validate.ValidateCodeType;
import club.wenfan.youtube.validate.code.ValidateCode;
import club.wenfan.youtube.validate.code.ValidateCodeGenerator;
import club.wenfan.youtube.validate.exception.ValidateCodeException;
import club.wenfan.youtube.validate.processor.ValidateCodeProcessor;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.util.Map;

/**
 *
 * @author wenfan
 * @date
 * @param
 * @return
 */
public abstract class AbstractValidateCodeProcessor<C extends ValidateCode> implements ValidateCodeProcessor {

    @Autowired
    private Map<String, ValidateCodeGenerator> validateCodeGenerators;

    /**
     *
     * @author wenfan
     * @date
     * @param
     * @return
     */
    @Override
    public void create(HttpServletRequest request, HttpServletResponse response) throws Exception {
        C validateCode = generate(request);
        System.out.println(request.getRequestURI());
        System.out.println(validateCode.getCode());
        save(request, validateCode);
        send(request,response, validateCode);
    }

    /**
     * 生成校验码
     *
     * @param request
     * @return
     */
    @SuppressWarnings("unchecked")
    private C generate(HttpServletRequest request)  {
        String type = getValidateCodeType(request).toString().toLowerCase();
        String generatorName = type + ValidateCodeGenerator.class.getSimpleName();
        ValidateCodeGenerator validateCodeGenerator = validateCodeGenerators.get(generatorName);
        if (validateCodeGenerator == null) {
            throw new ValidateCodeException("验证码生成器" + generatorName + "不存在");
        }
        return (C) validateCodeGenerator.CreateCode(request);
    }

    /**
     * 保存校验码
     *
     * @param request
     * @param validateCode
     */
    private void save(HttpServletRequest request, C validateCode) {
        ValidateCode code = new ValidateCode(validateCode.getCode(),validateCode.getExpireTime());
        System.out.println(getSessionKey(request));
        request.getSession(true).setAttribute(getSessionKey(request), code);
        System.out.println(request.getSession().getAttribute(getSessionKey(request)));
    }

    /**
     * 构建验证码放入session时的key
     *
     * @param request
     * @return
     */
    private String getSessionKey(HttpServletRequest request) {
        return SESSION_KEY_PREFIX + getValidateCodeType(request).toString().toUpperCase();
    }

    /**
     * 发送校验码,由子类实现
     *
     * @param request
     * @param validateCode
     * @throws Exception
     */
    protected abstract void send(HttpServletRequest request,HttpServletResponse response, C validateCode) throws Exception;

    /**
     * 根据请求的url获取校验码的类型
     *
     * @param request
     * @return
     */
    private ValidateCodeType getValidateCodeType(HttpServletRequest request) {
        String type = StringUtils.substringBefore(getClass().getSimpleName(), "CodeProcessor");
        return ValidateCodeType.valueOf(type.toUpperCase());
    }

    @SuppressWarnings("unchecked")
    @Override
    public void validate(HttpServletRequest request,HttpServletResponse response) {

        ValidateCodeType processorType = getValidateCodeType(request);
        String sessionKey = getSessionKey(request);
        System.out.println("sessionKey="+sessionKey);
        C codeInSession = (C) request.getSession(false).getAttribute(sessionKey);
        System.out.println(codeInSession==null?"codeinsession为null":"codeinsession不为null");
        String codeInRequest;
        try {
            codeInRequest = ServletRequestUtils.getStringParameter(request,
                    processorType.getParamNameOnValidate());
        } catch (ServletRequestBindingException e) {
            throw new ValidateCodeException("获取验证码的值失败");
        }

        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException(processorType + "验证码的值不能为空");
        }

        if (codeInSession == null) {
            throw new ValidateCodeException(processorType + "验证码不存在");
        }

        if (codeInSession.isExpired()) {
            request.getSession().removeAttribute(sessionKey);
            throw new ValidateCodeException(processorType + "验证码已过期");
        }

        if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
            throw new ValidateCodeException(processorType + "验证码不匹配");
        }
        request.getSession().removeAttribute(sessionKey);
    }

}
AbstractValidateCodeProcessor

5.具体的验证码生成器只有发送方法,这里只贴了图片验证码处理器

@Component("imageValidateCodeProcessor")
public class ImageCodeProcessor extends AbstractValidateCodeProcessor<ImgCode> {

    @Override
    protected void send(HttpServletRequest request, HttpServletResponse response, ImgCode imgCode) throws Exception {
        ImageIO.write(imgCode.getImage(),"JPEG",response.getOutputStream());
    }

}

6.在处理器抽象法中也运用方法Map Bean 的方式来选择验证码生成器的类型

public interface ValidateCodeGenerator {
    ValidateCode CreateCode(HttpServletRequest request);
}

7.具体的验证码生成器

技术图片
import club.wenfan.youtube.properties.SecurityProperties;
import club.wenfan.youtube.validate.code.ImgCode;
import club.wenfan.youtube.validate.code.ValidateCodeGenerator;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.bind.ServletRequestUtils;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;

/**
 * @author:wenfan
 * @description:
 * @data: 2019/1/1 18:33
 */

@Component("imageValidateCodeGenerator")
public class ImgCodeGenerator implements ValidateCodeGenerator {

    @Autowired
    private SecurityProperties securityProperties;

    private Logger log = LoggerFactory.getLogger(getClass());

    @Override
    public ImgCode CreateCode(HttpServletRequest request) {
        //可以在请求中加入 width/height get参数  当没有参数时从用户的自定义的配置文件中读取
        int width =ServletRequestUtils.getIntParameter(request,"width",securityProperties.getCode().getImg().getWidth());// 定义图片的width
        int height = ServletRequestUtils.getIntParameter(request," height",securityProperties.getCode().getImg().getHeight());// 定义图片的height
        int codeCount =securityProperties.getCode().getImg().getCodeCount();// 定义图片上显示验证码的个数
        int expiredTime = securityProperties.getCode().getImg(). getExpiredTime();
        int xx = 18;
        int fontHeight = 20;
        int codeY = 27;
        char[] codeSequence = { ‘0‘,‘1‘, ‘2‘, ‘3‘, ‘4‘,‘5‘, ‘6‘, ‘7‘, ‘8‘, ‘9‘ };

        // 定义图像buffer
        BufferedImage buffImg = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics gd = buffImg.getGraphics();
        // 创建一个随机数生成器类
        Random random = new Random();
        // 将图像填充为白色
        gd.setColor(Color.WHITE);
        gd.fillRect(0, 0, width, height);

        // 创建字体,字体的大小应该根据图片的高度来定。
        Font font = new Font("Fixedsys", Font.BOLD, fontHeight);
        // 设置字体。
        gd.setFont(font);

        // 画边框。
        gd.setColor(Color.BLACK);
        gd.drawRect(0, 0, width - 1, height - 1);

        // 随机产生40条干扰线,使图象中的认证码不易被其它程序探测到。
        gd.setColor(Color.BLACK);
        for (int i = 0; i < 30; i++) {
            int x = random.nextInt(width);
            int y = random.nextInt(height);
            int xl = random.nextInt(12);
            int yl = random.nextInt(12);
            gd.drawLine(x, y, x + xl, y + yl);
        }

        // randomCode用于保存随机产生的验证码,以便用户登录后进行验证。
        StringBuffer randomCode = new StringBuffer();
        int red = 0, green = 0, blue = 0;

        // 随机产生codeCount数字的验证码。
        for (int i = 0; i <codeCount; i++) {
            // 得到随机产生的验证码数字。
            String code = String.valueOf(codeSequence[random.nextInt(10)]);
            // 产生随机的颜色分量来构造颜色值,这样输出的每位数字的颜色值都将不同。
            red = random.nextInt(255);
            green = random.nextInt(255);
            blue = random.nextInt(255);

            // 用随机产生的颜色将验证码绘制到图像中。
            gd.setColor(new Color(red, green, blue));
            gd.drawString(code, (i + 1) * xx, codeY);

            // 将产生的四个随机数组合在一起。
            randomCode.append(code);

        }
        log.info("产生验证码"+randomCode.toString());
        return new ImgCode(buffImg,randomCode.toString(),expiredTime);
    }

    public SecurityProperties getSecurityProperties() {
        return securityProperties;
    }

    public void setSecurityProperties(SecurityProperties securityProperties) {
        this.securityProperties = securityProperties;
    }
}
View Code

8.如果想更换系统中默认的验证码处理器可以采用一种可扩展的bean注入方式

@Configuration
public class ValidateCodeBeanConfig {

    @Autowired
    private SecurityProperties securityProperties;


    /**
     *  当容器中没有imageValidateCodeGenerator 这个Bean的时候,会主动配置下面的默认Bean
     *  以增量的形式实现变化不
     *
     * @author wenfan
     * @date
     * @param
     * @return
     */
    @Bean
    @ConditionalOnMissingBean(name = "imageValidateCodeGenerator")
    public ValidateCodeGenerator imgCodeGenerator(){
        ImgCodeGenerator codeGenerator=new ImgCodeGenerator();
        codeGenerator.setSecurityProperties(securityProperties);
        return codeGenerator;
    }


    /**
     *  class形式和 name的方式相同
     * @author wenfan
     * @date
     * @param
     * @return
     */

    @Bean
    @ConditionalOnMissingBean(SmsCodeSender.class)
    public SmsCodeSender smsCodeSender(){
        return new SmsCodeSenderImpl();
    }

}

如果不想用默认验证码生成器,之需要注册bean,bean名字为imageValidateCodeGenerator

9.验证码过滤

写一个过滤器继承OncePerRequestFilter

@Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {

        response.setContentType("application/json;charset=utf-8");
        ValidateCodeType type=getValidateCodeType(request);
        if(type !=null){
            logger.info("校验请求(" + request.getRequestURI() + ")中的验证码,验证码类型" + type);
            try {
                validateCodeProcessorHolder.findValidateCodeProcessor(type).validate(request,response);
                logger.info("验证码通过");
            } catch (ValidateCodeException e) {
                authenticationFailureHandler.onAuthenticationFailure(request,response,e);
                return;
            }
        }
            filterChain.doFilter(request,response);
    }

发起请求时,验证码处理器管家来决定哪一个处理器来处理

10.将过滤器添加到配置中

@Component("validateCodeSecurityConfig")
public class ValidateCodeSecurityConfig extends SecurityConfigurerAdapter<DefaultSecurityFilterChain,HttpSecurity> {

    @Autowired
    private Filter vaildateCodeFilter;

    @Override
    public void configure(HttpSecurity http) throws Exception {
        http.addFilterBefore(vaildateCodeFilter,AbstractPreAuthenticatedProcessingFilter.class);
    }
}

 

感谢阅读

 


以上是关于springboot验证码重构的主要内容,如果未能解决你的问题,请参考以下文章

重构客户注册-基于ActiveMQ实现短信验证码生产者

图形验证码及其重构

重构Spring Security实现图形验证码的功能

如何重构这个 Java 代码片段

php生成各种验证码

JSP 设计教师与学生不同登陆界面(带验证码)