上一篇:https://lawsssscat.blog.csdn.net/article/details/105316362

前面的代码下载:https://github.com/LawssssCat/v-security/tree/v2.2

实现图形验证码功能,三步:

  • 开发生成图形验证码接口
  • 在认证流程中加入图形认证码校验
  • 重构代码

开发生成图形验证码接口

生成图形验证码

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

<mark>因为验证码不管是在浏览器或者是手机中都会用到,所以我们还是会写到 v-security-core 中</mark>

封装 ImageCode 类
包含 验证码图片的缓冲区(BufferedImage)、验证码(code)、过期时间(expire)

package cn.vshop.security.core.validate.code;

import lombok.AllArgsConstructor;
import lombok.Getter;
import lombok.Setter;

import java.awt.image.BufferedImage;
import java.time.LocalDateTime;

/** * @author alan smith * @version 1.0 * @date 2020/4/5 1:17 */
@Getter
@Setter
@AllArgsConstructor
public class ImageCode {

    /** * 图片,展示给用户看的 */
    private BufferedImage image;

    /** * 随机数,需要存在session中,作为验证的依据 */
    private String code;

    /** * 过期时间 * <p> * LocalDateTime 只存储如期,时间,不存储时区,JDK8后用以替代Date * 当时间不需要跟其他应用交互,建议使用。 */
    private LocalDateTime expireTime;

    /** * 构造函数 * * @param image 图片 * @param code 验证码 * @param expireIn 期望多长时间后过期(seconds) */
    public ImageCode(BufferedImage image, String code, int expireIn) {
        this.image = image;
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireIn);
    }

}

编写接口 GET /code/image

通过这个接口,前端可以获取二维码图片

<mark>在重构时候,会把这里写死的值抽离成参数</mark>

package cn.vshop.security.core.validate.code;

import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

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;

/** * @author alan smith * @version 1.0 * @date 2020/4/5 1:33 */
@RestController
public class ValidateCodeController {

    /** * 校验码信息存入session中的key */
    private 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 imageCode = createImageCode(request);
        // 获取session,并把键值对存入session
        sessionStrategy.setAttribute(
                // ServletWebRequest 是一个适配器(Adapter),把servlet封装成spring的WebRequest(继承了RequestAttributes)
                // 通过把请求传进来,sessionStrategy会从请求中获取session
                new ServletWebRequest(request),
                // 存入session中的key
                SESSION_KEY,
                // 存入session中的值
                imageCode);
        // javax的io工具包
        // 将BufferedImage以指定格式写入输出流中
        ImageIO.write(
                // 以BufferedImage类型的图片
                imageCode.getImage(),
                // 输出的图片格式
                "JPEG",
                // 输出流,输出到响应体中
                response.getOutputStream());

    }

    private Random random = new Random();

    /** * 生成验证码图片的逻辑代码 * * @param request 请求 * @return 将验证码图片封装为 ImageCode 返回 */
    private ImageCode createImageCode(HttpServletRequest request) {
        int width = 67;
        int height = 23;
        // 图片的缓冲区(buffer)
        // BufferedImage类是具有缓冲区的Image类,Image类是用于描述图像信息的类
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        // 图片的画笔
        // 产生Image对象的Graphics对象,该对象可以在图像上进行各种绘制操作
        Graphics graphics = image.getGraphics();
        /** * 背景颜色 */
        // 设置画笔的颜色
        graphics.setColor(getRandColor(200, 250));
        // 用画笔填充图片
        graphics.fillRect(0, 0, width, height);

        // 绘制干扰线
        // 设置画笔的颜色
        graphics.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);
            // 用画笔画直线
            graphics.drawLine(x, y, xl, yl);
        }

        // 设置画笔的字体、粗细、大小
        graphics.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        String sRand = "";
        for (int i = 0; i < 4; i++) {
            // 生成随机字符
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            // 设置画笔颜色
            graphics.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            // 画笔填充字符
            graphics.drawString(rand, 13 * i + 6, 16);
        }

        // 执行前面设置好的画笔脚本
        graphics.dispose();
        // 将图片、字符串、过期时间封装成ImageCode
        return new ImageCode(image, sRand, 60);
    }

    /** * 生成随机颜色 * * @param fc frontcolor * @param bc backcolor * @return 颜色实体 */
    private Color getRandColor(int fc, int bc) {
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc - 16);
        int g = fc + random.nextInt(bc - fc - 14);
        int b = fc + random.nextInt(bc - fc - 18);
        return new Color(r, g, b);
    }


}

修改 browser 配置,使其不拦截 “/code/image” 接口

修改登录界面
使其请求接口,显示图片

访问页面

http://localhost:8080/login2.html


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

如何添加图形验证码校验?
最直接的方式是在 用户名密码验证过滤器前添加图形校验码校验过滤器

编写 ValidateCodeFilter

package cn.vshop.security.core.validate.code;

import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.access.method.P;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
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;

/** * @author alan smith * @version 1.0 * @date 2020/4/5 11:39 */
public class ValidateCodeFilter
        // SpringMVC 提供的工具类,能确保此Filter每次请求只被调用一次
        extends OncePerRequestFilter {

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    // 我们前面自定义的验证失败处理器
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 如果请求的是执行登录URL
        if (StringUtils.equals("/authentication/form", request.getRequestURI())
                // 并且是POST请求
                && StringUtils.equalsIgnoreCase(request.getMethod(), "post")) {
            // 尝试校验
            try {
                // 封装成spring的request,方便后续操作
                validate(new ServletWebRequest(request));
            } catch (ValidateCodeException e) {
                // 捕获到自定义的验证码校验异常
                // 用我们之前自定义的错误处理器,进行校验失败的处理
                authenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return ;
            }
        }
        // 校验通过or不是登录请求
        filterChain.doFilter(request, response);
    }

    /** * 校验的逻辑 * * @param request */
    private void validate(ServletWebRequest request) throws ServletRequestBindingException {
        // 从session中获取封装好的ImageCode
        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
        // 从request中获取请求参数imageCode
        String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException("验证码的值不能为空");
        }
        if (codeInSession == null) {
            throw new ValidateCodeException("验证码不存在");
        }
        if (codeInSession.isExpired()) {
            // 如果过期了,就移除验证码
            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 void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationFailureHandler = authenticationFailureHandler;
    }
}

ImageCode 涉及到的过期判断

自定义验证码校验异常

package cn.vshop.security.core.validate.code;

import org.springframework.security.core.AuthenticationException;

/** * 验证码校验异常 * * @author alan smith * @version 1.0 * @date 2020/4/5 11:54 */
public class ValidateCodeException
        // Spring Security所有校验异常的基类
        extends AuthenticationException {
    public ValidateCodeException(String msg) {
        super(msg);
    }
}

在 BrowserSecurityConfig 中注册过滤器

测试

来到登录页面

http://localhost:8080/login2.html

登录失败

直接点登录,会报错,并且能看到错误信息

输入正确验证码

去到了UsernamePasswordAuthenticationFilter

正常登陆

访问成功

如果错误时,返回的不是json

可能是没有把返回的类型改为JSON

如果返回的信息过多
可以用自定义的 SimpleResponse 只封装想返回的错误信息

重构代码

下面重构代码,使代码更加灵活(能被引用)

  • 验证码基本参数可配置

    如:

  • 验证码拦截的接口可配置

    除了说接口可变外,还要可以让不同服务能选择性的使用验证码
    如:

  • 验证码的生成逻辑可配置
    有的情况可能需要更复杂的验证码,(如能使用不同字符,限定校验次数等)

# 图形验证码基本参数配置

  • 默认配置
    即如果使用安全模块,不做其他配置,默认使用写在 core 里面的配置
  • 应用及的配置
    如果使用了安全模块的demo做了一些配置,可以覆盖上面的默认配置
  • 请求级配置
    配置值在请求接口中传递,可以在调用请求接口时候覆盖上面的配置

<mark>这也是做通用代码的通用逻辑,能在不同级别中做不同的配置的覆盖</mark>

编写 ValidateCodeProperties 和 ImageCodeProperties

ValidateCodeProperties 包含 ImageCodeProperties

前者包含 手机验证码和图形验证码的配置(即包含后者)
后者包含 图形验证码配置

package cn.vshop.security.core.properties;

import lombok.Getter;
import lombok.Setter;

/** * 图形验证码 * * @author alan smith * @version 1.0 * @date 2020/4/5 14:47 */
@Getter
@Setter
public class ImageCodeProperties {

    /** * 验证码宽 */
    private int width = 67;

    /** * 验证码高 */
    private int height = 23;

    /** * 验证码个数 */
    private int length = 4;

    /** * 验证按失效时间 */
    private int expireIn = 60;

}

package cn.vshop.security.core.properties;

import lombok.Getter;
import lombok.Setter;

/** * 验证码配置 * 包含: * 1. 图形验证码 * 2. 短信验证码 * * @author alan smith * @version 1.0 * @date 2020/4/5 14:54 */
@Getter
@Setter
public class ValidateCodeProperties {

    private ImageCodeProperties image = new ImageCodeProperties();

}

修改 application.yml 配置

为了能看出效果,我们把图形验证码的长度改为 6

v:
  security:
    code: 
      image: 
        length: 6

修改 ValidateCodeController
在ValidateCodeController中使用上面写好的配置信息

package cn.vshop.security.core.validate.code;

import cn.vshop.security.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

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;

/** * @author alan smith * @version 1.0 * @date 2020/4/5 1:33 */
@RestController
public class ValidateCodeController {

    @Autowired
    private SecurityProperties securityProperties;

    /** * 校验码信息存入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 imageCode = createImageCode(request);
        // 获取session,并把键值对存入session
        sessionStrategy.setAttribute(
                // ServletWebRequest 是一个适配器(Adapter),把servlet封装成spring的WebRequest(继承了RequestAttributes)
                // 通过把请求传进来,sessionStrategy会从请求中获取session
                new ServletWebRequest(request),
                // 存入session中的key
                SESSION_KEY,
                // 存入session中的值
                imageCode);
        // javax的io工具包
        // 将BufferedImage以指定格式写入输出流中
        ImageIO.write(
                // 以BufferedImage类型的图片
                imageCode.getImage(),
                // 输出的图片格式
                "JPEG",
                // 输出流,输出到响应体中
                response.getOutputStream());

    }

    private Random random = new Random();

    /** * 生成验证码图片的逻辑代码 * * @param request 请求 * @return 将验证码图片封装为 ImageCode 返回 */
    private ImageCode createImageCode(HttpServletRequest request) {
        // 验证码图片宽度
        // 借助工具类,中request中获取
        int width = ServletRequestUtils.getIntParameter(
                // 从请求中分析width参数,从而获取width值
                request, "width",
                // 如果请求中没有值,就从配置中获取
                securityProperties.getCode().getImage().getWidth());
        int height = ServletRequestUtils.getIntParameter(
                request, "height",
                securityProperties.getCode().getImage().getHeight());
        // 验证码长度
        int length = securityProperties.getCode().getImage().getLength();
        // 验证码有效时间
        int expiredIn = securityProperties.getCode().getImage().getExpireIn();

        // 图片的缓冲区(buffer)
        // BufferedImage类是具有缓冲区的Image类,Image类是用于描述图像信息的类
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        // 图片的画笔
        // 产生Image对象的Graphics对象,该对象可以在图像上进行各种绘制操作
        Graphics graphics = image.getGraphics();
        /** * 背景颜色 */
        // 设置画笔的颜色
        graphics.setColor(getRandColor(200, 250));
        // 用画笔填充图片
        graphics.fillRect(0, 0, width, height);

        // 绘制干扰线
        // 设置画笔的颜色
        graphics.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);
            // 用画笔画直线
            graphics.drawLine(x, y, xl, yl);
        }

        // 设置画笔的字体、粗细、大小
        graphics.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        String sRand = "";
        for (int i = 0; i < length; i++) {
            // 生成随机字符
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            // 设置画笔颜色
            graphics.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            // 画笔填充字符
            graphics.drawString(rand, 13 * i + 6, 16);
        }

        // 执行前面设置好的画笔脚本
        graphics.dispose();
        // 将图片、字符串、过期时间封装成ImageCode
        return new ImageCode(image, sRand, expiredIn);
    }

    /** * 生成随机颜色 * * @param fc frontcolor * @param bc backcolor * @return 颜色实体 */
    private Color getRandColor(int fc, int bc) {
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc - 16);
        int g = fc + random.nextInt(bc - fc - 14);
        int b = fc + random.nextInt(bc - fc - 18);
        return new Color(r, g, b);
    }


}

修改登录页

最后,修改图片请求,看请求长度是否有效果

测试
来到登录页面

http://localhost:8080/login2.html

# 验证码拦截的接口可配置

<mark>我们希望能指定哪些接口需要验证码校验,哪些接口不需要验证码校验</mark>

修改 application.yml

希望下面的配置生效,即***会对下面指定的接口进行拦截和验证码的校验

v:
  security:
    code:
      image:
      # 指定要进行验证码验证的url
        url:
        - /user
        - /user/*

在 ImageCodeProperties 中添加

    /** * 要进行图形验证码校验的url */
    private String[] urls ;

修改 ValidateCodeFilter

  • 添加接口 InitializingBean : 可以在其他配置注入完成后,通过afterPropertiesSet方法进行初始化
    (因为这个Filter不在Spring容器中,所以InitializingBean 实际不生效,但我们可以手动调用它的方法实现)

  • 添加属性:urls 指定要过滤哪些url

  • 添加属性:securityProperties 把配置中指定的urls注入(需要手动注入,因为该Filter不在Spring容器中)

  • 通过修改方法doFilterInternal,修改过滤条件。(通过Spring的工具类AntPathMatcher)

package cn.vshop.security.core.validate.code;

import cn.vshop.security.core.properties.SecurityProperties;
import lombok.Setter;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang.StringUtils;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.util.AntPathMatcher;
import org.springframework.web.bind.ServletRequestBindingException;
import org.springframework.web.bind.ServletRequestUtils;
import org.springframework.web.context.request.ServletWebRequest;
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;
import java.util.HashSet;
import java.util.Set;

/** * 图形校验码的过滤器 * * @author alan smith * @version 1.0 * @date 2020/4/5 11:39 */
@Slf4j
@Setter
public class ValidateCodeFilter
        // SpringMVC 提供的工具类,能确保此Filter每次请求只被调用一次
        extends OncePerRequestFilter
        // 在其他参数都加载完毕时,组装URLs的值
        implements InitializingBean {

    /** * 要拦截的url */
    private Set<String> urls = new HashSet<>();

    private SecurityProperties securityProperties;

    /** * spring的工具类,用来匹配Ant风格路径,如:“/user/*” */
    private AntPathMatcher pathMatcher = new AntPathMatcher();

    /** * 操作session的工具类 */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    // 我们前面自定义的验证失败处理器
    private AuthenticationFailureHandler authenticationFailureHandler;

    @Override
    public void afterPropertiesSet() throws ServletException {
        // 父类实现了
        super.afterPropertiesSet();
        String[] configUrls = securityProperties.getCode().getImage().getUrls();
        for (String url : configUrls) {
            urls.add(url);
        }
        urls.add("/authentication/form");
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {

        // 循环判断是否执行过滤
        boolean action = false;
        for (String url : urls) {
            if (pathMatcher.match(url, request.getRequestURI())) {
                action = true;
                break;
            }
        }


        // 如果请求的是执行登录URL
        if (action) {
            // 尝试校验
            try {
                // 封装成spring的request,方便后续操作
                validate(new ServletWebRequest(request));
            } catch (ValidateCodeException e) {
                // 捕获到自定义的验证码校验异常
                // 用我们之前自定义的错误处理器,进行校验失败的处理
                authenticationFailureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }
        // 校验通过or不是登录请求
        filterChain.doFilter(request, response);
    }

    /** * 校验的逻辑 * * @param request */
    private void validate(ServletWebRequest request) throws ServletRequestBindingException {

        // 从session中获取封装好的ImageCode
        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);
        // 从request中获取请求参数imageCode
        String codeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException("验证码的值不能为空");
        }
        if (codeInSession == null) {
            throw new ValidateCodeException("验证码不存在");
        }
        if (codeInSession.isExpired()) {
            // 如果过期了,就移除验证码
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
            // 然后再抛异常
            throw new ValidateCodeException("验证码已过期");
        }
        if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
            throw new ValidateCodeException("验证码不匹配");
        }
        sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
    }

}

修改 BrowserSecurityConfig 配置

测试

访问 http://localhost:8080/user

当我们把配置中的路径注释掉

等看到(下图),说明配置生效了

# 验证码的生成逻辑可配置

<mark>要把一段逻辑做成可以配置的,我们需要把这段代码封装到一个接口的后面</mark>

就像我们前面实现 Spring Security 的一些逻辑(如登录成功、失败逻辑)一样,我们需要实现 Spring Security 的一些接口

因此,我们需要生成一个接口,让别人能覆盖我们的逻辑,同时给出一个默认的实现

<mark>其实就是把之前写在Controller里面的代码,抽离出来,作为一个接口的实现</mark>

生成接口 ValidateCodeGenerator

package cn.vshop.security.core.validate.code;

import javax.servlet.http.HttpServletRequest;

/** * 校验码生成器 * * @author alan smith * @version 1.0 * @date 2020/4/5 17:25 */
public interface ValidateCodeGenerator {
    /** * 生成验证码图片的逻辑代码 * * @param request 请求 * @return 将验证码图片封装为 ImageCode 返回 */
    ImageCode createImageCode(HttpServletRequest request);
}

给出默认实现 ImageCodeGenerator

把Controller 里面相关的代码挪进来

package cn.vshop.security.core.validate.code;

import cn.vshop.security.core.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.ServletRequestUtils;

import javax.servlet.http.HttpServletRequest;
import java.awt.*;
import java.awt.image.BufferedImage;
import java.util.Random;

/** * @author alan smith * @version 1.0 * @date 2020/4/5 17:30 */
public class ImageCodeGenerator implements ValidateCodeGenerator {

    @Autowired
    private SecurityProperties securityProperties;

    private Random random = new Random();

    @Override
    public ImageCode createImageCode(HttpServletRequest request) {
        // 验证码图片宽度
        // 借助工具类,中request中获取
        int width = ServletRequestUtils.getIntParameter(
                // 从请求中分析width参数,从而获取width值
                request, "width",
                // 如果请求中没有值,就从配置中获取
                securityProperties.getCode().getImage().getWidth());
        int height = ServletRequestUtils.getIntParameter(
                request, "height",
                securityProperties.getCode().getImage().getHeight());
        // 验证码长度
        int length = securityProperties.getCode().getImage().getLength();
        // 验证码有效时间
        int expiredIn = securityProperties.getCode().getImage().getExpireIn();

        // 图片的缓冲区(buffer)
        // BufferedImage类是具有缓冲区的Image类,Image类是用于描述图像信息的类
        BufferedImage image = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        // 图片的画笔
        // 产生Image对象的Graphics对象,该对象可以在图像上进行各种绘制操作
        Graphics graphics = image.getGraphics();
        /** * 背景颜色 */
        // 设置画笔的颜色
        graphics.setColor(getRandColor(200, 250));
        // 用画笔填充图片
        graphics.fillRect(0, 0, width, height);

        // 绘制干扰线
        // 设置画笔的颜色
        graphics.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);
            // 用画笔画直线
            graphics.drawLine(x, y, xl, yl);
        }

        // 设置画笔的字体、粗细、大小
        graphics.setFont(new Font("Times New Roman", Font.ITALIC, 20));
        String sRand = "";
        for (int i = 0; i < length; i++) {
            // 生成随机字符
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            // 设置画笔颜色
            graphics.setColor(new Color(20 + random.nextInt(110), 20 + random.nextInt(110), 20 + random.nextInt(110)));
            // 画笔填充字符
            graphics.drawString(rand, 13 * i + 6, 16);
        }

        // 执行前面设置好的画笔脚本
        graphics.dispose();
        // 将图片、字符串、过期时间封装成ImageCode
        return new ImageCode(image, sRand, expiredIn);
    }

    /** * 生成随机颜色 * * @param fc frontcolor * @param bc backcolor * @return 颜色实体 */
    private Color getRandColor(int fc, int bc) {
        if (fc > 255) {
            fc = 255;
        }
        if (bc > 255) {
            bc = 255;
        }
        int r = fc + random.nextInt(bc - fc - 16);
        int g = fc + random.nextInt(bc - fc - 14);
        int b = fc + random.nextInt(bc - fc - 18);
        return new Color(r, g, b);
    }
}

修改 ValidateCodeController

将Controller原本的代码抽离到 接口实现中后,注入那个接口

package cn.vshop.security.core.validate.code;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.social.connect.web.HttpSessionSessionStrategy;
import org.springframework.social.connect.web.SessionStrategy;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.context.request.ServletWebRequest;

import javax.imageio.ImageIO;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

/** * @author alan smith * @version 1.0 * @date 2020/4/5 1:33 */
@RestController
public class ValidateCodeController {

    /** * 校验码信息存入session中的key */
    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";

    /** * 注入接口,接口的实现完成验证码的生成和封装 */
    @Autowired
    private ValidateCodeGenerator validateCodeGenerator ;


    /** * Spring 的工具类,用以操作session */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 生成随机的验证码,并封装为 ImageCode
        ImageCode imageCode = validateCodeGenerator.createImageCode(request);
        // 获取session,并把键值对存入session
        sessionStrategy.setAttribute(
                // ServletWebRequest 是一个适配器(Adapter),把servlet封装成spring的WebRequest(继承了RequestAttributes)
                // 通过把请求传进来,sessionStrategy会从请求中获取session
                new ServletWebRequest(request),
                // 存入session中的key
                SESSION_KEY,
                // 存入session中的值
                imageCode);
        // javax的io工具包
        // 将BufferedImage以指定格式写入输出流中
        ImageIO.write(
                // 以BufferedImage类型的图片
                imageCode.getImage(),
                // 输出的图片格式
                "JPEG",
                // 输出流,输出到响应体中
                response.getOutputStream());

    }

} 

编写配置类 ValidateCodeBeanConfig

通过配置类,使接口的实现注入到spring容器中,从而实现controller中的注入

package cn.vshop.security.core.validate.code;

import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

/** * 校验码生成逻辑的配置类 * * @author alan smith * @version 1.0 * @date 2020/4/5 17:39 */
@Configuration
public class ValidateCodeBeanConfig {
    /** * 如果用户没有注入名字为imageCodeGenerator的bean,那么就使用默认的 * * @return 默认的 ValidateCodeGenerator 实现 */
    @Bean("imageCodeGenerator")
    @ConditionalOnMissingBean(name = "imageCodeGenerator")
    public ValidateCodeGenerator imageCodeGenerator() {
        return new ImageCodeGenerator();
    }
}

<mark>至此,就实现了验证码的生成可配置</mark>

下面,就尝试覆盖原有的生成逻辑

其他形式验证码,参考:SpringBoot——登录验证码实现

这里就不展示了

package cn.vshop.security.core.validate.code;

import cn.vshop.security.core.properties.SecurityProperties;
import org.springframework.beans.factory.InitializingBean;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;

/** * @author alan smith * @version 1.0 * @date 2020/4/5 19:44 */
// 名字必须为 imageCodeGenerator。
// 当然,也可以自定义覆盖规则
@Component("imageCodeGenerator")
public class DemoImageCodeGenerator implements ValidateCodeGenerator, InitializingBean {

    @Autowired
    private SecurityProperties securityProperties;

    private ImageCodeGenerator imageCodeGenerator;

    @Override
    public void afterPropertiesSet() throws Exception {
        imageCodeGenerator = new ImageCodeGenerator();
        imageCodeGenerator.setSecurityProperties(securityProperties);
    }

    @Override
    public ImageCode createImageCode(HttpServletRequest request) {
        System.out.println("更高级的图形验证码生成代码");
        return imageCodeGenerator.createImageCode(request);
    }
}

<mark>这里体现一种代码思想,即以增量的方式来适应变化。</mark>
当有代码有新的需求,不是重写原有代码,而是借助提供好的接口,在原有代码上添加新增的业务逻辑

前面的代码下载:https://github.com/LawssssCat/v-security/tree/v2.3

done~