SpringSecurity-Four(图形验证码接口)

一个完整的登录页面一定要有图形验证码。这里分为两步,首先是生成验证码然后将这个验证码并且将验证码中的值存放到session中。第二步就是验证验证码,从请求中获取到用户输入的验证码信息并和session中存放的信息进行比对。正确则通过验证。

1. 开发生成图形验证码接口

  • 在html中添加验证码链接
	<tr>
            <td>图形验证码:</td>
            <td>
                <input type="text" name="imageCode">
                <img src="/code/image">
            </td>
        </tr>
  • 编写验证码对象
public class ImageCode {

    private BufferedImage image;
    /**
     * 验证码随机数
     */
    private String code;
    /**
     * 验证码过期时间
     */
    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;
    }

    /**
     * 判断是否过期,过期时间expireTime如果在当前时间之后就没有过期
     * 否则就过期
     * @return
     */
    public boolean isExpried(){
        return LocalDateTime.now().isAfter(expireTime);
    }

    public BufferedImage getImage() {
        return image;
    }

    public void setImage(BufferedImage image) {
        this.image = image;
    }

    public String getCode() {
        return code;
    }

    public void setCode(String code) {
        this.code = code;
    }

    public LocalDateTime getExpireTime() {
        return expireTime;
    }

    public void setExpireTime(LocalDateTime expireTime) {
        this.expireTime = expireTime;
    }
}

  • 验证码生成工具类
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);
    }

    /**
     * 生成随机背景条纹
     *
     * @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);
    }

  • 获取验证码的接口,前端页面调取这个接口就可以将验证码展示在前端页面上
@RestController
public class ValidateCodeController {
    /**
     * 验证码存放在session中的key值
     */
    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
    /**
     * spring中的一个操作session的工具类
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        ImageCode imageCode = createImageCode(request);
        /**
         * 将验证码放到session中
         */
        sessionStrategy.setAttribute(new ServletWebRequest(request),SESSION_KEY,imageCode);
        /**
         * 写验证码图片
         */
        ImageIO.write(imageCode.getImage(),"JPEG",response.getOutputStream());
    }
}
  • 因为我们配置了所有的url都要通过权限验证码,所以要将获取验证码这个接口添加到不需要权限验证中
@Configuration
public class BrowserSecurityConfig extends WebSecurityConfigurerAdapter {
    @Autowired
    private SecurityProperties securityProperties;

    /**
     * 导入自己写的AuthenticationSuccessHandler的实现类
     */
    @Autowired
    private AuthenticationSuccessHandler zcfAuthenticationSuccessHandler;

    /**
     * 导入自己写的AuthenticationFailureHandler的实现类
     */
    @Autowired
    private AuthenticationFailureHandler zcfAuthenticationFailureHandler
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        ValidateCodeFilter filter = new ValidateCodeFilter();
        filter.setAuthenticationFailureHandler(zcfAuthenticationFailureHandler);
        http.addFilterBefore(filter, UsernamePasswordAuthenticationFilter.class)
                .formLogin()                                      //基于form表单的登录验证
                .loginPage("/authentication/require")                //指定登录页面
                .loginProcessingUrl("/authentication/form")   //表单的action路径
                .successHandler(zcfAuthenticationSuccessHandler)//配置自己写的登录成功处理器
                .failureHandler(zcfAuthenticationFailureHandler)
                .and()
                .authorizeRequests()        //关于请求的一些配置
                /**
                 * 这里需要注意一个错误,如果不适用antMatchers().permitAll()将登录的url设置为不需要身份验证的话
                 * 那么就会导致,请求重定向次数过多错误
                 */
                .antMatchers("/authentication/require",securityProperties.getBrowser().getLoginPage(),
                        "/code/image").permitAll() //访问这个路径不需要身份验证
                .anyRequest()               //所有的请求
                .authenticated()            //身份验证
                .and()
                .csrf().disable();          //关闭跨站防护
    }
}

  • 访问登录页面,就能看到验证码,这里的验证码就已经生成了,但是并没有完成验证码的验证 image.png

2. 在认证流程中加入图形验证码的认证

  • 因为springsecurity并没有提供能验证验证码的接口的,所以我们需要自己写一个验证验证码的过滤去,并将这个过滤器加入到springsecurity的过滤器链中 image.png
  • 考虑到验证码如果验证错误要抛出错误信息,先自定义一个异常用来抛出错误信息
public class ValidateCodeException extends AuthenticationException {
    /**
     * 自定义验证码异常
     */
    private static final long serialVersionUID = 7012308720900016006L;

    public ValidateCodeException(String msg) {
        super(msg);
    }
}
  • 当从请求中拿到验证码的信息之后,要完成验证逻辑
    /**
     * 从请求中拿到验证码,完成验证。
     * @param request
     * @throws ServletRequestBindingException
     */
    private void validate(ServletWebRequest request) throws ServletRequestBindingException {
        /**
         * 从session中拿到放进去的ImageCode
         */
        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request,ValidateCodeController.SESSION_KEY);

        /**
         * 从Form请求中拿到imageCode的值,
         * 这里imageCode是我们在<input type="text" name="imageCode">中name的值
         */
        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);

    }

  • 自定义验证验证码的过滤器
/**
 * 继承OncePerRequestFilter类,保证我们自己的过滤器只会被调用一次
 */
public class ValidateCodeFilter extends OncePerRequestFilter {

    private static AuthenticationFailureHandler authenticationFailureHandler;

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
	/**
         * 这里要进行判断一下,保证逻辑的严谨。
         * 请求的地址必须是"/authentication/form",而且要是post请求
         * 否则直接放行
         */
        if(StringUtils.equals("/authentication/form",request.getRequestURI()) &&
        StringUtils.endsWithIgnoreCase(request.getMethod(),"post")){

            try {
                /**
                 * 使用validate方法验证请求中验证码的信息
                 * 这里因为后边需要使用到ServletWebRequest
                 * 所以对我们的HttpServletRequest进行包装一下
                 */
                validate(new ServletWebRequest(request));

            }catch (ValidateCodeException e){
                /**
                 * 使用前边写的登录失败处理器处理异常
                 */
                authenticationFailureHandler.onAuthenticationFailure(request,response,e);
                return;
            }

        }

        filterChain.doFilter(request,response);
    }
}

上边就完成了登录验证码逻辑,其实上面验证码都是已经写死了,比如验证码生成类。可以对其重构完成一个框架,用户可以自己配置,也可使用默认的。

评论

Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×