考虑到有多种验证机制(例如:常见的图片验证码,手机短信验证码,邮箱验证码)
所以在项目中对验证码进行代码重构,使之更加的具有可扩展性,低耦合性,此项目基于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; } }
特别说明一下
@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); } }
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; } }
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); } }
感谢阅读