图形验证码接口及其重构思想

mac2024-07-08  54

文章目录

开发生成图形验证码接口图片实体 ImageCode图片接口 ValidateCodeController 在认证流程中加入图形验证码校验登录页面安全认证配置不拦截图片路径测试 图片路径无法访问404及解决访问自定义过滤器 ValidateCodeFilter 校验登录验证码自定义异常 ValidateCodeException配置指定位置中加入此拦截器测试ok 重构图形验证码接口验证码基本参数可配置默认配置应用级配置请求级配置拦截器引用配置验证码拦截的接口可配置验证码的生成逻辑可配置创建验证码生成器接口及实现类ValidateCodeController中调用验证码生成器接口imageCodeGenerator 图片生成器接口实现类的初始化和可配置测试demo模块中覆盖验证码生成器

开发生成图形验证码接口

图片实体 ImageCode

public class ImageCode { /** * 一个图片验证码包含三个信息 */ private BufferedImage image; //图片 private String code;//code是一个随机数,图片根据这个随机数生成,这个随机数是要存入到session中的 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 ImageCode(BufferedImage image, String code, LocalDateTime expireTime) { this.image = image; this.code = code; this.expireTime = expireTime; } public boolean isExpried() { return LocalDateTime.now().isAfter(expireTime); }

图片接口 ValidateCodeController

根据随机数生成图片 将随机数存到session中 将生成的图片写到接口的响应中

@RestController public class ValidateCodeController implements Serializable { private static final String SESSION_KEY ="SESSION_KEY_IMAGE_CODE";//key //spring 操作session的工具类 private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @RequestMapping("/code/image") private void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException { //1根据请求中的随机数生成图片 ImageCode imageCode = createImageCode(request); //2将随机数放到session中 sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode); //3将生成的图片写到接口的响应中 ImageIO.write(imageCode.getImage(),"jpeg",response.getOutputStream()); } private ImageCode createImageCode(HttpServletRequest request) { //生成一个图片对象 int width = 67; int height =23; 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 <4; 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 ImageCode(image, sRand, 60); }

在认证流程中加入图形验证码校验

登录页面

<table> <tr> <td>用户名:</td> <td><input type="text" name="username"></td> </tr> <tr> <td>密码:</td> <td><input type="password" name="password"></td> </tr> <tr> <td>验证码:</td> <td> <input type="text" name="imageCode"> <img src="/code/image"> </td> </tr> <tr> <td colspan="2"><button type="submit">登录</button></td> </tr> </table>

安全认证配置不拦截图片路径

测试 图片路径无法访问404及解决

图片路径无法访问404,发现对应的model在mave中为灰色 解决

https://blog.csdn.net/L359389556/article/details/82852244

访问

自定义过滤器 ValidateCodeFilter 校验登录验证码

/** * 继承spring中的OncePerRequestFilter,确保每次请求调用一次过滤器 * implements InitializingBean的作用:其他参数都初始化完毕后,我们去初始化urls */ public class ValidateCodeFilter extends OncePerRequestFilter { private AuthenticationFailureHandler authenticationFailureHandler; public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) { this.authenticationFailureHandler = authenticationFailureHandler; } private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { if(StringUtils.equals("/authentication/form",httpServletRequest.getRequestURI()) && StringUtils.equalsAnyIgnoreCase(httpServletRequest.getMethod(),"post")){ try { validate(new ServletWebRequest(httpServletRequest)); }catch (ValidateCodeException e){ authenticationFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e); return;//失败后直接返回,不再走下面的过滤器 } } //如果不是登录请求,直接放行 filterChain.doFilter(httpServletRequest,httpServletResponse); } private void validate(ServletWebRequest request) throws ServletRequestBindingException { ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY); String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode"); if(StringUtils.isBlank(codeInRequest)){ throw new ValidateCodeException("验证码的值不能为空"); } if(codeInSession == null){ throw new ValidateCodeException("验证码不存在"); } if(codeInSession.isExpried()){ sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY); throw new ValidateCodeException("验证码已过期"); } if(! StringUtils.equals(codeInSession.getCode(),codeInRequest)){ throw new ValidateCodeException("验证码不匹配"); } sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY); } }

自定义异常 ValidateCodeException

/** * AuthenticationException 是 springframework.security提供的,登录过程中所有异常的基类 * */ public class ValidateCodeException extends AuthenticationException { private static final long serialVersionUID = -7285211528095468156L; public ValidateCodeException(String msg) { super(msg); } }

配置指定位置中加入此拦截器

protected void configure(HttpSecurity http) throws Exception { //http.formLogin() //指定身份认证的方式为表单登录 //http.httpBasic() ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter(); validateCodeFilter.setAuthenticationFailureHandler(whaleAuthenctiationFailureHandler);//设置错误过滤器 http.addFilterBefore(validateCodeFilter,UsernamePasswordAuthenticationFilter.class) .formLogin() // .loginPage("/signIn.html") //指定登录页面的url // .loginPage("/anthentication/require") //指定登录页面的url .loginPage(securityProperties.getBrowser().getLoginPage()) //指定登录页面的url .loginProcessingUrl("/authentication/form") .successHandler(whaleAuthenticationSuccessHandler) .failureHandler(whaleAuthenctiationFailureHandler) .permitAll() .and() .authorizeRequests() //对请求授权 // .antMatchers("/signIn.html","/code/image").permitAll() //加一个匹配器 对匹配的路径不进行身份认证 .antMatchers(securityProperties.getBrowser().getLoginPage(),"/code/image").permitAll() //加一个匹配器 对匹配的路径不进行身份认证 .anyRequest() //任何请求 .authenticated() //安全认证 .and() .cors().disable().csrf().disable();// 禁用跨站攻击 // 默认都会产生一个hiden标签 里面有安全相关的验证 防止请求伪造 这边我们暂时不需要 可禁用掉 //任何请求都必须经过表单验证才能进行访问 /* http.csrf().disable().cors().disable().headers().disable() .authorizeRequests() .antMatchers("/signIn.html").permitAll() // 配置不需要身份认证的请求地址 .anyRequest().authenticated() // 其他所有访问路径需要身份认证 .and() .formLogin() .loginPage("/signIn.html") // 指定登录请求地址 .loginProcessingUrl("/authentication/form") .permitAll(); */ }

测试ok

简化认证失败处理信息

public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException authenticationException ) throws IOException, ServletException { logger.info("登录失败"); if(LoginType.JSON.equals(securityProperties.getBrowser().getLoginType())) { httpServletResponse.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value()); httpServletResponse.setContentType("application/json;charset=UTF-8"); // httpServletResponse.getWriter().write(objectMapper.writeValueAsString(authenticationException));//打印的信息太多 简化如下 httpServletResponse.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse(authenticationException.getMessage()))); }else { super.onAuthenticationFailure(httpServletRequest,httpServletResponse,authenticationException); } }

重构图形验证码接口

验证码基本参数可配置

默认配置

ImageCodeProperties

public class ImageCodeProperties { private int width = 67; private int height = 23; private int length = 4; private int expireIn = 60;

再封装一层ValidateCodeProperties

public class ValidateCodeProperties { private ImageCodeProperties image = new ImageCodeProperties(); public ImageCodeProperties getImage() { return image; } public void setImage(ImageCodeProperties image) { this.image = image; } }

SecurityProperties加入ValidateCodeProperties

@ConfigurationProperties(prefix = "whale.security") //这个类会读取以whale.security开头的配置项 public class SecurityProperties { //浏览器配置 private BrowserProperties browser = new BrowserProperties(); //验证码配置 private ValidateCodeProperties code = new ValidateCodeProperties();

应用级配置

demo application

whale.security.code.image.length = 6#验证码长度、 whale.security.code.image.width = 200

请求级配置

<tr> <td>验证码:</td> <td> <input type="text" name="imageCode"> <img src="/code/image?width=200"> </td> </tr>

拦截器引用配置

@RestController public class ValidateCodeController implements Serializable { public static final String SESSION_KEY ="SESSION_KEY_IMAGE_CODE";//key //spring 操作session的工具类 private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @Autowired private SecurityProperties securityProperties; @RequestMapping("/code/image") private void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException { //1根据请求中的随机数生成图片 // ImageCode imageCode = createImageCode(request); ImageCode imageCode = createImageCode(new ServletWebRequest(request)); //2将随机数放到session中 sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode); //3将生成的图片写到接口的响应中 ImageIO.write(imageCode.getImage(),"jpeg",response.getOutputStream()); } /** * * @param request(HttpServletRequest) * @return */ private ImageCode createImageCode(ServletWebRequest request) { //生成一个图片对象 // int width = 67; int width = ServletRequestUtils.getIntParameter(request.getRequest(),"width",securityProperties.getCode().getImage().getWidth()); // int height =23; int height = ServletRequestUtils.getIntParameter(request.getRequest(),"height",securityProperties.getCode().getImage().getHeight()); 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 <4; i++) { // 验证码的长度不应该在请求中配置 for (int i = 0; i <securityProperties.getCode().getImage().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 ImageCode(image, sRand, 60); return new ImageCode(image, sRand, securityProperties.getCode().getImage().getExpireIn()); } /** * 生成随机背景条纹 * * @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); } }

测试

验证码拦截的接口可配置

ImageCodeProperties加属性url

private String url;

验证码匹配路径以逗号隔开

demo application 中配置路径

whale.security.code.image.url = /user,/user/*

拦截器处理

/** * 继承spring中的OncePerRequestFilter,确保每次请求调用一次过滤器 */ //InitializingBean 实现此接口中的 afterPropertiesSet 初始化方法在其中初始化图片验证码拦截路径 public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean { private AuthenticationFailureHandler authenticationFailureHandler; public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) { this.authenticationFailureHandler = authenticationFailureHandler; } private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); //验证码路径,需要初始化 private Set<String> urls = new HashSet<>(); //配置 private SecurityProperties securityProperties; //路径正则匹配工具 private AntPathMatcher pathMatcher = new AntPathMatcher(); @Override public void afterPropertiesSet() throws ServletException { super.afterPropertiesSet(); String[] configUrls = StringUtils.splitByWholeSeparatorPreserveAllTokens(securityProperties.getCode().getImage().getUrl(),","); for (String url : configUrls) { urls.add(url); } //这个路径是默认的 urls.add("/authentication/form"); } @Override protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException { System.out.println(httpServletRequest.getRequestURI()); System.out.println(httpServletRequest.getRequestURL()); //如果请求路径满足匹配模式 则需要验证码 boolean action = false; for (String url : urls) { if(pathMatcher.match(url,httpServletRequest.getRequestURI())){ action = true; } } // if(StringUtils.equals("/authentication/form",httpServletRequest.getRequestURI()) // && StringUtils.equalsAnyIgnoreCase(httpServletRequest.getMethod(),"post")){ if(action){ try { validate(new ServletWebRequest(httpServletRequest)); }catch (ValidateCodeException e){ authenticationFailureHandler.onAuthenticationFailure(httpServletRequest,httpServletResponse,e); return;//失败后直接返回,不再走下面的过滤器 } } //如果不是登录请求,直接放行 filterChain.doFilter(httpServletRequest,httpServletResponse); } private void validate(ServletWebRequest request) throws ServletRequestBindingException { ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY); String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode"); if(StringUtils.isBlank(codeInRequest)){ throw new ValidateCodeException("验证码的值不能为空"); } if(codeInSession == null){ throw new ValidateCodeException("验证码不存在"); } if(codeInSession.isExpried()){ sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY); throw new ValidateCodeException("验证码已过期"); } if(! StringUtils.equals(codeInSession.getCode(),codeInRequest)){ throw new ValidateCodeException("验证码不匹配"); } sessionStrategy.removeAttribute(request,ValidateCodeController.SESSION_KEY); } public SecurityProperties getSecurityProperties() { return securityProperties; } public void setSecurityProperties(SecurityProperties securityProperties) { this.securityProperties = securityProperties; } }

BrowserSecurityConfig中配置拦截器

················ @Override protected void configure(HttpSecurity http) throws Exception { //http.formLogin() //指定身份认证的方式为表单登录 //http.httpBasic() ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter(); validateCodeFilter.setAuthenticationFailureHandler(whaleAuthenctiationFailureHandler);//设置错误过滤器 validateCodeFilter.setSecurityProperties(securityProperties); validateCodeFilter.afterPropertiesSet(); ············

测试

验证码的生成逻辑可配置

创建验证码生成器接口及实现类

把com.whale.security.core.validate.ValidateCodeController#createImageCode中生成图片验证码的逻辑搬到验证码生成器接口及实现类中 如下 ValidateCodeGenerator

public interface ValidateCodeGenerator { ImageCode generate(ServletWebRequest request); }

ImageCodeGenerator

public class ImageCodeGenerator implements ValidateCodeGenerator { /** * 系统配置 */ @Autowired private SecurityProperties securityProperties; /* * (non-Javadoc) * * @see * com.imooc.security.core.validate.code.ValidateCodeGenerator#generate(org. * springframework.web.context.request.ServletWebRequest) */ @Override public ImageCode generate(ServletWebRequest request) {

ValidateCodeController中调用验证码生成器接口

@RestController public class ValidateCodeController implements Serializable { public static final String SESSION_KEY ="SESSION_KEY_IMAGE_CODE";//key //spring 操作session的工具类 private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); @Autowired private SecurityProperties securityProperties; @Autowired private ValidateCodeGenerator imageCodeGenerator; @RequestMapping("/code/image") private void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException { //1根据请求中的随机数生成图片 // ImageCode imageCode = createImageCode(request); // ImageCode imageCode = createImageCode(new ServletWebRequest(request)); ImageCode imageCode = imageCodeGenerator.generate(new ServletWebRequest(request)); //2将随机数放到session中 sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode); //3将生成的图片写到接口的响应中 ImageIO.write(imageCode.getImage(),"jpeg",response.getOutputStream()); }

imageCodeGenerator 图片生成器接口实现类的初始化和可配置

imageCodeGenerator是如何注入进去的呢 如下 @ConditionalOnMissingBean(name = “imageCodeGenerator”)

有了这个配置,跟在ImageCodeGenerator上面写一个@Component效果一样在这配置可以加上@ConditionalOnMissingBean(name = “imageCodeGenerator”)作用是:spring容器启动前,先去容器里面找是否有一个名字叫做imageCodeGenerator这样一个Bean如果可以找到则用找到那个Bean,将不会用这个类的Bean @Configuration public class ValidateCodeBeanConfig { @Autowired private SecurityProperties securityProperties; @Bean @ConditionalOnMissingBean(name = "imageCodeGenerator") //为啥这样配置 //@ConditionalOnMissingBean(name = "imageCodeGenerator")spring初始化这个类之前会判断容器中是否有名字为imageCodeGenerator的bean, //若果有就用已经初始化的bean,没有的话才初始化当前bean //这样 这个接口就可被用户覆盖 public ValidateCodeGenerator imageCodeGenerator() { //方法的名字就是spring容器中bean的名字 ImageCodeGenerator codeGenerator = new ImageCodeGenerator(); codeGenerator.setSecurityProperties(securityProperties); return codeGenerator; } }

测试demo模块中覆盖验证码生成器

@Component("imageCodeGenerator") public class DemoImageCodeGenerator implements ValidateCodeGenerator { @Override public ImageCode generate(ServletWebRequest request) { System.out.println("更高级的图形验证码生成代码"); return null; } }

访问报错 ok 主要是设计思想

以增量的方式去适应变化 当需求逻辑发生变化时,我们不是改变原来得代码,而是加一段代码:很重要

最新回复(0)