9.图形验证码

Posted popopopopo

tags:

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

图形验证码

图形验证码一般是防止恶意,人眼看起来都费劲,何况是机器。不少网站为了防止用户利用机器人自动注册、登录、灌水,都采用了验证码技术。所谓验证码,就是将一串随机产生的数字或符号,生成一幅图片, 图片里加上一些干扰, 也有目前需要手动滑动的图形验证码. 这种可以有专门去做的第三方平台. 比如极验(https://www.geetest.com/), 那么本次课程讲解主要针对图形验证码.

spring security添加验证码大致可以分为三个步骤:

1. 根据随机数生成验证码图片;

2. 将验证码图片显示到登录页面;

3. 认证流程中加入验证码校验。

Spring Security的认证校验是由UsernamePasswordAuthenticationFilter过滤器完成的,所以我们的验证码校验逻辑应该在这个过滤器之前。验证码通过后才能到后续的操作. 流程如下: 

代码实现:

验证码生成类

package com.po.controller;

import com.po.domain.ImageCode;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.io.IOException;
import java.util.Random;
import java.util.concurrent.TimeUnit;

/**
 * 处理生成验证码的请求
 */
@RestController
@RequestMapping("/code")
public class ValidateCodeController 

    public final static String REDIS_KEY_IMAGE_CODE = "REDIS_KEY_IMAGE_CODE";
    public final static int expireIn = 60;  // 验证码有效时间 60s

    //使用sessionStrategy将生成的验证码对象存储到Session中,并通过IO流将生成的图片输出到登录页面上。
    @Autowired
    public StringRedisTemplate stringRedisTemplate;

    @RequestMapping("/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException 
        //获取访问IP
        String remoteAddr = request.getRemoteAddr();
        //生成验证码对象
        ImageCode imageCode = createImageCode();
        //生成的验证码对象存储到redis中 KEY为REDIS_KEY_IMAGE_CODE+IP地址
        stringRedisTemplate.boundValueOps(REDIS_KEY_IMAGE_CODE + "-" + remoteAddr)
                .set(imageCode.getCode(), expireIn, TimeUnit.SECONDS);
        //通过IO流将生成的图片输出到登录页面上
        ImageIO.write(imageCode.getImage(), "jpeg", response.getOutputStream());
    

    /**
     * 用于生成验证码对象
     *
     * @return
     */
    private ImageCode createImageCode() 

        int width = 100;    // 验证码图片宽度
        int height = 36;    // 验证码图片长度
        int length = 4;     // 验证码位数
        //创建一个带缓冲区图像对象
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        //获得在图像上绘图的Graphics对象
        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);
        

        //生成随机数 并绘制
        StringBuilder sRand = new StringBuilder();
        for (int i = 0; i < length; i++) 
            String rand = String.valueOf(random.nextInt(10));
            sRand.append(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.toString());
    

    /**
     * 获取随机演示
     *
     * @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);
    


自定义验证码过滤器ValidateCodeFilter

package com.po.filter;

import com.po.controller.ValidateCodeController;
import com.po.exception.ValidateCodeException;
import com.po.service.impl.MyAuthenticationService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import org.springframework.web.filter.OncePerRequestFilter;

import javax.servlet.FilterChain;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/**
 * 自定义验证码过滤器,OncePerRequestFilter 一次请求只会经过一次过滤器
 */
@Service
public class ValidateCodeFilter extends OncePerRequestFilter 

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException 
        // 判断是否是登录请求
        if (request.getRequestURI().equals("/login") &&
                request.getMethod().equalsIgnoreCase("post")) 
            String imageCode = request.getParameter("imageCode");
            System.out.println(imageCode);
            // 具体的验证流程
            try 
                validate(request, imageCode);
             catch (ValidateCodeException e) 
                myAuthenticationService.onAuthenticationFailure(request, response, e);
                return;
            
        
        // 如果不是登录请求,直接放行
        filterChain.doFilter(request, response);
    

    @Autowired
    StringRedisTemplate stringRedisTemplate;
    @Autowired
    MyAuthenticationService myAuthenticationService;

    private void validate(HttpServletRequest request, String imageCode) 
        // 从redis中获取验证码
        String redisKey = ValidateCodeController.REDIS_KEY_IMAGE_CODE + "-" + request.getRemoteAddr();
        String redisImageCode = stringRedisTemplate.boundValueOps(redisKey).get();
        // 验证码的判断
        if (!StringUtils.hasText(redisImageCode)) 
            throw new ValidateCodeException("验证码的值不能为空");
        
        if (redisImageCode == null) 
            throw new ValidateCodeException("验证码已过期");
        
        if (!redisImageCode.equals(imageCode)) 
            throw new ValidateCodeException("验证码不正确");
        
        // 从redis中删除验证码
        stringRedisTemplate.delete(redisKey);
    

自定义验证码异常类

package com.po.exception;

import org.springframework.security.core.AuthenticationException;

public class ValidateCodeException extends AuthenticationException 
    public ValidateCodeException(String msg) 
        super(msg);
    

security配置类(里面添加我们自定义过滤器的顺序)

package com.po.config;

import com.po.filter.ValidateCodeFilter;
import com.po.service.impl.MyAuthenticationService;
import com.po.service.impl.MyUserDetailsService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.builders.WebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.rememberme.JdbcTokenRepositoryImpl;
import org.springframework.security.web.authentication.rememberme.PersistentTokenRepository;

import javax.sql.DataSource;


@Configuration
public class SecurityConfiguration extends WebSecurityConfigurerAdapter 
    @Autowired
    private MyUserDetailsService myUserDetailsService;

    @Autowired
    ValidateCodeFilter validateCodeFilter;

    /**
     * http请求方法
     *
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception 
        /** http.httpBasic() //开启httpBasic认证
         .and().authorizeRequests().anyRequest().authenticated(); //所有请求都需要认证之后访问
         */
        // 将验证码过滤器添加在UsernamePasswordAuthenticationFilter过滤器的前面
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);

/*        http.formLogin().loginPage("/login.html")//开启表单认证
              //  .and().authorizeRequests() //放行登录页面
            //    .anyRequest().authenticated();
             //   .and().authorizeRequests().antMatchers("/login.html").permitAll() //放行登录页面
                .and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
                .anyRequest().authenticated();*/
                 http.formLogin() //开启表单认证
                .loginPage("/toLoginPage") // 自定义登陆页面
                .loginProcessingUrl("/login") //表单提交路径
                .usernameParameter("username").passwordParameter("password") //自定义input额name值和password
                .successForwardUrl("/") //登录成功之后跳转的路径
                         .successHandler(myAuthenticationService) // 登录成功处理
                         .failureHandler(myAuthenticationService) //登录失败处理
                         .and().logout().logoutUrl("/logout") //退出
                         .logoutSuccessHandler(myAuthenticationService) //退出后处理
                .and().authorizeRequests().antMatchers("/toLoginPage").permitAll() //放行登录页面
                .anyRequest().authenticated()
                 .and().rememberMe() //开启记住我功能
                  .tokenValiditySeconds(1209600) //token失效时间,默认失效时间是两周
                  .rememberMeParameter("remember-me") // 自定义表单name值
                  .tokenRepository(getPersistentTokenRepository()) //设置PersistentTokenRepository
                 .and().headers().frameOptions().sameOrigin() //加载同源域名下iframe页面
                .and().csrf().disable();//关闭csrf防护
    

    @Override
    public void configure(WebSecurity web) throws Exception 
        //解决静态资源被拦截的问题
        web.ignoring().antMatchers("/css/**","/images/**","/js/**","/code/**");
    

    /**
     *身份安全管理器
     * @param auth
     * @throws Exception
     */
    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception 
        auth.userDetailsService(myUserDetailsService);
    

    @Autowired
    DataSource dataSource;
    /**
     * 负责token与数据库之间的操作
     * @return
     */
    @Bean
    public PersistentTokenRepository getPersistentTokenRepository()
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource); //设置数据源
        tokenRepository.setCreateTableOnStartup(false); //启动时帮助我们自动创建一张表,第一次启动设置为true,第二次启动程序的时候设置false或者注释掉;
        return tokenRepository;
    

    @Autowired
    private MyAuthenticationService myAuthenticationService;


  

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

逻辑漏洞---登录验证码安全

如何开发图形验证码?

本人初学Java,有啥好的图形验证码推荐吗?

selenium基础-图形验证码

验证码识别图形验证码识别01

Python验证码识别:利用pytesser识别简单图形验证码