Spring Security入门(二十)-实现图形验证码功能

    xiaoxiao2021-04-16  226

    一.导学

    开发生成图形验证码接口 根据随机数生成图片将随机数存到session中将生成的图片写入响应中 由于不论app还是浏览器 都要用到 所以写到core项目里面去

    二.实现图形验证码

    图片验证码类一般包含三个属性:图片 随机数 过期时间一般过期时间不是直接设置的 而是设置的多少时间过期 当前时间+多少时间过期 public class ImageCode { private BufferedImage image;//验证码图片 private String code;//验证码 private LocalDateTime expireTime;//过期时间 public ImageCode(BufferedImage image, String code,int expireIn) {//超时时间 this.image = image; this.code = code; this.expireTime = LocalDateTime.now().plusSeconds(expireIn); } public BufferedImage getImage() { return image; } public String getCode() { return code; } public LocalDateTime getExpireTime() { return expireTime; } public boolean isExpire(){//判断当前时间是否过期 return this.expireTime.isBefore(LocalDateTime.now()); } } 生成验证码的控制类 @RestController public class ValidateCodeController { public static final String SESSION_KEY="SESSION_KEY_IMAGE_CODE"; private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();//spring操作session提供的类 @GetMapping("/code/image") public void createCode(HttpServletResponse response, HttpServletRequest request) throws IOException { ImageCode imageCode = createImageCode(); sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);//存入session中 ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());//写到相应的输出流中 中间是格式 } private ImageCode createImageCode() {//创建验证码 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);//60s过期 } /* 生成随机背景条纹 */ 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); } } 登录页 <!DOCTYPE html> <html lang="en"> <head> <meta charset="UTF-8"> <title>标准登录页面</title> </head> <body> <h1>标准登录页面</h1> <form action="/authentication/form" method="post"> <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> </form> </body> </html>

    如何验证提交上面的验证码,之前扩展自定义登录的时候都是实现的spring的接口

    但是spring并没有提供实现了一个接口就能实现验证码校验

    spring security就是一条过滤器链,我们可以在这条链中加入自己写的过滤器

    在这里加一个自己写的过滤器 对应的校验逻辑是在该过滤器中完成

    验证通过再进入后面的过滤器 验证不过就抛异常

    验证码过滤器类

    public class ValidateCodeFilter extends OncePerRequestFilter {//spring提供的,保证在一个请求中只会被调用一次 private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy(); private AuthenticationFailureHandler failureHandler; public AuthenticationFailureHandler getFailureHandler() { return failureHandler; } public void setFailureHandler(AuthenticationFailureHandler failureHandler) { this.failureHandler = failureHandler; } @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain) throws ServletException, IOException { if(StringUtils.equals(request.getRequestURI(),"/authentication/form") &&StringUtils.endsWithIgnoreCase(request.getMethod(),"post")){//判断请求 try{ validate(request); }catch(ValidateCodeException e){ failureHandler.onAuthenticationFailure(request,response,e);//交给错误处理器 return; } } chain.doFilter(request,response); } public void validate(HttpServletRequest request) throws ServletRequestBindingException {//校验逻辑在这个方法内 ServletWebRequest req = new ServletWebRequest(request); ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(req,ValidateCodeController.SESSION_KEY); String codeInRequest = ServletRequestUtils.getStringParameter(request,"imageCode"); if(StringUtils.isBlank(codeInRequest)){ throw new ValidateCodeException("验证码不能为空"); } if(codeInSession==null){ throw new ValidateCodeException("验证码不存在"); } if(codeInSession.isExpire()){ sessionStrategy.removeAttribute(req,ValidateCodeController.SESSION_KEY); throw new ValidateCodeException("验证码已过期"); } if(!StringUtils.equals(codeInSession.getCode(),codeInRequest)){ throw new ValidateCodeException("验证码不匹配"); } sessionStrategy.removeAttribute(req,ValidateCodeController.SESSION_KEY); } } 配置 protected void configure(HttpSecurity http) throws Exception { ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter(); validateCodeFilter.setFailureHandler(playmakerAuthenticationFailureHandler);//设置失败处理器 http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)//设置这个过滤器在这个过滤器之前执行 .formLogin()//form表单 高版本中默认是表单 低版本中默认是HttpBasic .loginPage("/authentication/require") //指定登录页面的url .loginProcessingUrl("/authentication/form") //指定处理登录请求的url .successHandler(playmakerAuthenticationSuccessHandler) .failureHandler(playmakerAuthenticationFailureHandler) //httpBasic() //httpBasic .and() //对请求授权配置 .authorizeRequests() .antMatchers("/playmaker-signIn.html", "/authentication/require", "/error", "/code/image", securityProperties.getBrowser().getLoginPage()).permitAll()//当访问这些url时不需要身份认证 .anyRequest()//对任意请求都必须是已认证才能访问 .authenticated() .and() .csrf().disable();//关闭csrf } 一旦抛出异常错误信息太多了 所以我们只让其返回错误的消息注意要把LoginType改成json @Component("playmakerAuthenticationFailureHandler") public class PlaymakerAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {//这是spring security默认的失败处理器 处理方式是跳转 private Logger logger = LoggerFactory.getLogger(getClass()); @Autowired private ObjectMapper objectMapper; @Autowired private SecurityProperties securityProperties; @Override public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { logger.info("登录失败"); if(securityProperties.getBrowser().getLoginType().equals(LoginType.JSON)){ response.setContentType("application/json;charset=utf-8"); response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());//默认200 因为登录失败 所以设置为500 response.getWriter().write(objectMapper.writeValueAsString(new SimpleResponse(exception.getMessage())));//打印错误信息太多 只返回错误消息 } else{ super.onAuthenticationFailure(request,response,exception);//直接调用父类 } } }

    最新回复(0)