考虑到有多种验证机制(例如:常见的图片验证码,手机短信验证码,邮箱验证码)
所以在项目中对验证码进行代码重构,使之更加的具有可扩展性,低耦合性,此项目基于springboot
1.首先Controller层
@RestControllerpublic 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.验证码管家
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
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 */@Componentpublic class ValidateCodeProcessorHolder { @Autowired private MapvalidateCodeProcessors; 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; }}
特别说明一下
@Autowiredprivate MapvalidateCodeProcessors;
采用这样的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.将验证码处理器的生成、保存、验证、验证抽象出来,单独将发送写成一个抽象方法,用具体的验证码处理来实现此方法
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
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 AbstractValidateCodeProcessorimplements ValidateCodeProcessor { @Autowired private Map 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); }}
5.具体的验证码生成器只有发送方法,这里只贴了图片验证码处理器
@Component("imageValidateCodeProcessor")public class ImageCodeProcessor extends AbstractValidateCodeProcessor{ @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.具体的验证码生成器
![](https://images.cnblogs.com/OutliningIndicators/ContractedBlock.gif)
![](https://images.cnblogs.com/OutliningIndicators/ExpandedBlockStart.gif)
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
8.如果想更换系统中默认的验证码处理器可以采用一种可扩展的bean注入方式
@Configurationpublic 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{ @Autowired private Filter vaildateCodeFilter; @Override public void configure(HttpSecurity http) throws Exception { http.addFilterBefore(vaildateCodeFilter,AbstractPreAuthenticatedProcessingFilter.class); }}
感谢阅读