有关springSceurity之前有写过两篇文章:

1、SpringSecurity(1)---认证+授权代码实现

2、SpringSecurity(2)---记住我功能实现

这篇我们来讲图形验证码功能实现。

一、思路

我整理下springSceurity整合图形验证码的大致思路:

1、首先对于验证码本身而言,应该有三部分组成 1、存放验证码的背景图片 2、验证码 3、验证码的有效时间。

2、对于springSceurity而言,验证码的执行校验顺序肯定是在UsernamePasswordAuthenticationFilter之前的,因为如果验证码都不对,那么
根本都不需要验证账号密码。所以我们需要自定义一个验证码过滤器,并且配置在UsernamePasswordAuthenticationFilter之前执行。

3、对于获取验证码的接口,肯定是不需要进行认证限制的。

4、对于获取验证码的接口的时候,需要把该验证码信息+当前浏览器的SessonId绑定在一起存在Seesion中,为了后面校验的时候通过SessonId
去取这个验证码信息。

5、登陆请求接口,除了带上用户名和密码之外,还需要带上验证码信息。在进入验证码过滤器的时候,首先通过SessonId获取存在Sesson中的
验证码信息,拿到验证码信息之后首先还要校验该验证码是否在有效期内。之后再和当前登陆接口带来的验证码进行对比,如果一致,那么当前
验证码这一关就过了,就开始验证下一步账号和密码是否正确了。

整个流程大致就是这样。下面现在是具体代码,然后进行测试。


二、代码展示

这里只展示一些核心代码,具体完整项目会放到github上。

1、ImageCodeProperties

这个是一个bean实体,是一个图形验证码的默认配置。

@Data
public class ImageCodeProperties {
    /**
     * 验证码宽度
     */
    private int width = 67;
    /**
     * 验证码高度
     */
    private int height = 23;
    /**
     * 验证码长度
     */
    private int length = 4;
    /**
     * 验证码过期时间
     */
    private int expireIn = 60;
    /**
     * 需要验证码的请求url字符串,用英文逗号隔开
     */
    private String url = "/login";

}

2、ImageCode

这个是图片验证码的完整信息,也会将这个完整信息存放于Sesson中。

图片验证码信息 由三部分组成 :

1.图片信息(长、宽、背景色等等)。2.code就是真正的验证码,用来验证用。3.该验证码的有效时间。

@Data
@AllArgsConstructor
@NoArgsConstructor
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 boolean isExpired() {
        return LocalDateTime.now().isAfter(expireTime);
    }
}

3、ValidateCodeGeneratorService

获取验证码的接口

public interface ValidateCodeGeneratorService {

    /**
     * 生成图片验证码
     *
     * @param request 请求
     * @return ImageCode实例对象
     */
    ImageCode generate(ServletWebRequest request);
}

4、ImageCodeGeneratorServiceImpl

获取图片验证码的接口的实现类

@Data
public class ImageCodeGeneratorServiceImpl implements ValidateCodeGeneratorService {

    private static final String IMAGE_WIDTH_NAME = "width";
    private static final String IMAGE_HEIGHT_NAME = "height";
    private static final Integer MAX_COLOR_VALUE = 255;

    private ImageCodeProperties imageCodeProperties;

    @Override
    public ImageCode generate(ServletWebRequest request) {
        //设置图片的宽度和高度
        int width = ServletRequestUtils.getIntParameter(request.getRequest(), IMAGE_WIDTH_NAME, imageCodeProperties.getWidth());
        int height = ServletRequestUtils.getIntParameter(request.getRequest(), IMAGE_HEIGHT_NAME, imageCodeProperties.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);
        }

        // 生成数字验证码
        StringBuilder sRand = new StringBuilder();
        for (int i = 0; i < imageCodeProperties.getLength(); 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(), imageCodeProperties.getExpireIn());
    }

    /**
     * 生成随机背景条纹
     *
     * @param fc 前景色
     * @param bc 背景色
     * @return RGB颜色
     */
    private Color getRandColor(int fc, int bc) {
        Random random = new Random();
        if (fc > MAX_COLOR_VALUE) {
            fc = MAX_COLOR_VALUE;
        }
        if (bc > MAX_COLOR_VALUE) {
            bc = MAX_COLOR_VALUE;
        }
        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);
    }
}

5、ValidateCodeController

获取验证码的请求接口。

@RestController
public class ValidateCodeController {

    /**
     * 前缀
     */
   public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
    private static final String FORMAT_NAME = "JPEG";
   @Autowired
    private ValidateCodeGeneratorService imageCodeGenerator;
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 第一步:根据请求生成一个图形验证码对象
        ImageCode imageCode = imageCodeGenerator.generate(new ServletWebRequest(request));
        // 第二步:将图形验证码对象存到session中,第一个参数可以从传入的请求中获取session
        sessionStrategy.setAttribute(new ServletRequestAttributes(request), SESSION_KEY, imageCode);
        // 第三步:将生成的图片写到接口的响应中
        ImageIO.write(imageCode.getImage(), FORMAT_NAME, response.getOutputStream());
    }

}

到这里,我们可用请求获取图片验证码的信息了。接下来我们就要登陆请求部分的代码。

6、ValidateCodeFilter

自定义过滤器,这里面才是核心的代码,首先继承OncePerRequestFilter(直接继承Filter也是可用的),实现InitializingBean是为了初始化一些初始数据。

这里走的逻辑就是把存在session中的图片验证码和当前请求的验证码进行比较,如果相同则放行,否则直接抛出异常

@Data
@Slf4j
@Component
public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {

    private static final String SUBMIT_FORM_DATA_PATH = "/login";

    /**
     * 失败处理器
     */
    @Autowired
    private AuthenctiationFailHandler authenctiationFailHandler;
    /**
     * 验证码属性类
     */
    @Autowired
    private ImageCodeProperties imageCodeProperties;

    /**
     * 存放需要走验证码请求url
     */
    private Set<String> urls = new HashSet<>();

    /**
     * 处理session工具类
     */
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();
    /**
     * 正则配置工具
     */
    private final AntPathMatcher antPathMatcher = new AntPathMatcher();
    /**
     * 在初始化bean的时候都会执行该方法
     */
    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        String[] configUrls = StringUtils.split(imageCodeProperties.getUrl(), ",");
        // 登录的链接是必须要进行验证码验证的
        urls.addAll(Arrays.asList(configUrls));
    }

    /**
     * 拦截请求进来的方法。
     */
    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        boolean action = false;
        for (String url : urls) {
            // 如果实际访问的URL可以与用户在imageCodeProperties中url配置的相同,那么就进行验证码校验
            log.info("request.getRequestURI = {}",request.getRequestURI());
            if (antPathMatcher.match(url, request.getRequestURI())) {
                action = true;
            }
        }
        //说明需要校验
        if (action) {
            try {
                validate(new ServletWebRequest(request));
            } catch (ValidateCodeException e) {
                authenctiationFailHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }

        //进入下一个过滤器
        filterChain.doFilter(request, response);
    }

    /**
     * 验证码校验逻辑
     *
     */
    private void validate(ServletWebRequest request) throws ServletRequestBindingException {
        // 从session中获取图片验证码
        ImageCode imageCodeInSession = (ImageCode) sessionStrategy.getAttribute(request, ValidateCodeController.SESSION_KEY);

        // 从请求中获取用户填写的验证码
        String imageCodeInRequest = ServletRequestUtils.getStringParameter(request.getRequest(), "imageCode");
        if (StringUtils.isBlank(imageCodeInRequest)) {
            throw new ValidateCodeException("验证码不能为空");
        }
        if (null == imageCodeInSession) {
            throw new ValidateCodeException("验证码不存在");
        }
        if (imageCodeInSession.isExpired()) {
            sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
            throw new ValidateCodeException("验证码已过期");
        }

        log.info("session中获取的验证码={},sessionId ={}",imageCodeInSession.getCode(),request.getSessionId());
        log.info("登陆操作传来的验证码={}",imageCodeInRequest);
        if (!StringUtils.equalsIgnoreCase(imageCodeInRequest, imageCodeInSession.getCode())) {
            throw new ValidateCodeException("验证码不匹配");
        }
        // 验证成功,删除session中的验证码
        sessionStrategy.removeAttribute(request, ValidateCodeController.SESSION_KEY);
    }
}

7、WebSecurityConfig

SpringSecurity的Java 配置类也需要做一点点改动。那就是需要设置ValidateCodeFilter要在UsernamePasswordAuthenticationFilter之前进行拦截过滤。

SpringSceurity(3)---图形验证码功能实现-LMLPHP

到这里整个图形验证码的功能就开发完成了,具体代码放在github上下面进行测试。


三、测试

说明下我这里懒的写前端相关代码了,所以直接用posman用请求来获取验证码,获取验证码之后再进行登陆操作。

1、获取验证码

SpringSceurity(3)---图形验证码功能实现-LMLPHP

这里验证码code为:1848

2、登陆成功

SpringSceurity(3)---图形验证码功能实现-LMLPHP

输入的验证码也是1848,显示登陆成功。

3、登陆失败

因为配置的时候图片验证码有效期为60秒,所以在我们获取验证码后,过60秒再去登陆,就能发现,验证码已过期。

SpringSceurity(3)---图形验证码功能实现-LMLPHP

整个验证码功能大致就是这样。


Github地址 : spring-boot-security-study-03



别人骂我胖,我会生气,因为我心里承认了我胖。别人说我矮,我就会觉得好笑,因为我心里知道我不可能矮。这就是我们为什么会对别人的攻击生气。
攻我盾者,乃我内心之矛(19)
06-08 04:52