SpringSecurity图片验证码
in JavaDevelop with 0 comment

SpringSecurity图片验证码

in JavaDevelop with 0 comment

图片验证码

在登录界面图形添加验证码

开发生成图像验证码接口

思路:

这里相当于功能,生成图片什么的不记录了。网上一大堆,记录下这里的一些代码思路

由于是公用的,把该服务写在core中

图片验证码信息类

package cn.mrcode.imooc.springsecurity.securitycore.validate.code;

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

/**
 * 图形验证码
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/3 22:44
 */
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 boolean isExpried() {
      return this.expireTime.isBefore(LocalDateTime.now());
  }

验证码服务

package cn.mrcode.imooc.springsecurity.securitycore.validate.code;
/**
 * 验证码服务
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/3 22:48
 */
@RestController
public class ValidateCodeController {
    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 = createImageCode(request);
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY, imageCode);
        response.setContentType("image/jpeg");
        //禁止图像缓存。
        response.setHeader("Pragma", "no-cache");
        response.setHeader("Cache-Control", "no-cache");
        ImageIO.write(imageCode.getImage(), "JPEG", response.getOutputStream());
    }

    private ImageCode createImageCode(HttpServletRequest request) throws IOException {
        String code = RandomStringUtils.randomAlphanumeric(4);
        BufferedImage image = createImageCode(80, 40, code);
        return new ImageCode(image, code, 60);
    }

需要把该服务路径在 cn.mrcode.imooc.springsecurity.securitybrowser.BrowserSecurityConfig 中配置放行。

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

步骤:
由之前的源码的探索发现,只要把过滤器添加到spring现有的过滤器链上就可以了;

  1. 编写验证码过滤器
  2. 放在UsernamePasswordAuthenticationFilter过滤器之前
package cn.mrcode.imooc.springsecurity.securitycore.validate.code;

import org.apache.commons.lang3.StringUtils;
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;

/**
 * 图片验证码验证过滤器
 * OncePerRequestFilter spring提供的,保证在一个请求中只会被调用一次
 * @author : zhuqiang
 * @version : V1.0
 * @date : 2018/8/3 23:24
 */
public class ValidateCodeFilter extends OncePerRequestFilter {
    // 在初始化本类的地方进行注入
    // 一般在配置security http的地方进行添加过滤器
    private AuthenticationFailureHandler failureHandler;
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 为登录请求,并且为post请求
        if (StringUtils.equals("/authentication/form", request.getRequestURI())
                && StringUtils.equalsAnyIgnoreCase(request.getMethod(), "post")) {
            try {
                validate(request);
            } catch (ValidateCodeException e) {
                failureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }
        filterChain.doFilter(request, response);
    }

    private void validate(HttpServletRequest request) throws ServletRequestBindingException {
        // 拿到之前存储的imageCode信息
        ServletWebRequest swr = new ServletWebRequest(request);
        ImageCode codeInSession = (ImageCode) sessionStrategy.getAttribute(swr, ValidateCodeController.SESSION_KEY);
        // 又是一个spring中的工具类,
        // 试问一下,如果不看源码怎么可能知道有这些工具类可用?
        String codeInRequest = ServletRequestUtils.getStringParameter(request, "imageCode");

        if (StringUtils.isBlank(codeInRequest)) {
            throw new ValidateCodeException("验证码的值不能为空");
        }
        if (codeInSession == null) {
            throw new ValidateCodeException("验证码不存在");
        }
        if (codeInSession.isExpried()) {
            sessionStrategy.removeAttribute(swr, ValidateCodeController.SESSION_KEY);
            throw new ValidateCodeException("验证码已过期");
        }
        if (!StringUtils.equals(codeInSession.getCode(), codeInRequest)) {
            throw new ValidateCodeException("验证码不匹配");
        }
        sessionStrategy.removeAttribute(swr, ValidateCodeController.SESSION_KEY);
    }

    public AuthenticationFailureHandler getFailureHandler() {
        return failureHandler;
    }

    public void setFailureHandler(AuthenticationFailureHandler failureHandler) {
        this.failureHandler = failureHandler;
    }
}

把过滤器添加到现有认证流程中

ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
validateCodeFilter.setFailureHandler(myAuthenticationFailureHandler);
http
        // 由源码得知,在最前面的是UsernamePasswordAuthenticationFilter
        .addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
        // 定义表单登录 - 身份认证的方式
        .formLogin()
        .loginPage("/authentication/require")
        .loginProcessingUrl("/authentication/form")

还需要注意的一个地方就是myAuthenticationFailureHandler中。因为失败会调用这个处理器;
这里和视频中演示的不一样。不会再把异常信息打印到前段页面了。

后补:视频中不知道什么时候把LoginType变成了json类型,所以会抛出异常

if (securityProperties.getBrowser().getLoginType() == LoginType.JSON) {
    response.setContentType("application/json;charset=UTF-8");
    response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
    response.getWriter().write(objectMapper.writeValueAsString(exception));
} else {
    // 在这里失败跳转不回去了。而且异常信息也没有打印出来。父类默认打印了死的一句话
    // 在这里就不往上面扔了,这里就先当做 defaultFailureUrl 不存在吧
    // 模拟打印异常信息
    response.setContentType("text/html;charset=UTF-8");
    response.sendError(HttpStatus.UNAUTHORIZED.value(),
            exception.getLocalizedMessage());
//            super.onAuthenticationFailure(request, response, exception);
}

图片验证码重构

验证码基本参数配置

三级覆盖:在最上面的会覆盖下级的配置

↓ 请求级配置 :配置值在调用接口的时候传递
↓ 应用级配置 :配置写在security-demo项目中
↓ 默认配置   :配置值写在security-core项目中

图形验证码配置类

package cn.mrcode.imooc.springsecurity.securitycore.properties;

/**
 * 图形验证码
 * @author zhuqiang
 * @version 1.0.1 2018/8/4 10:03
 * @date 2018/8/4 10:03
 * @since 1.0
 */
public class ImageCodeProperties {
    private int width = 67;
    private int height = 23;
    private int length = 4;  // 验证码长度
    private int expireIn = 60;  // 过期时间

验证码配置类

package cn.mrcode.imooc.springsecurity.securitycore.properties;

/**
 * 用来封装验证码相关的配置
 * @author zhuqiang
 * @version 1.0.1 2018/8/4 10:06
 * @date 2018/8/4 10:06
 * @since 1.0
 */
public class ValidateCodeProperties {
    private ImageCodeProperties image = new ImageCodeProperties();
    // 后面还会新增短信验证码的配置

加入总配置类中

@ConfigurationProperties(prefix = "imooc.security")
public class SecurityProperties {
    /** imooc.security.browser 路径下的配置会被映射到该配置类中 */
    private BrowserProperties browser = new BrowserProperties();
    private ValidateCodeProperties code = new ValidateCodeProperties();

修改处理逻辑处

cn.mrcode.imooc.springsecurity.securitycore.validate.code.ValidateCodeController

private ImageCode createImageCode(HttpServletRequest request) throws IOException {
    ImageCodeProperties imageProperties = securityProperties.getCode().getImage();
    // 先从请求中获取,然后从配置中获取
    // 如果配置中的没有被覆盖则是默认配置
    int width = ServletRequestUtils.getIntParameter(request, "width", imageProperties.getWidth());
    int height = ServletRequestUtils.getIntParameter(request, "height", imageProperties.getHeight());
    int length = ServletRequestUtils.getIntParameter(request, "length", imageProperties.getLength());
    int expireIn = ServletRequestUtils.getIntParameter(request, "expireIn", imageProperties.getExpireIn());
    String code = RandomStringUtils.randomNumeric(length);
    BufferedImage image = createImageCode(width, height, code);
    return new ImageCode(image, code, expireIn);
}

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

实现思路:

  1. 提供url拦截地址配置属性
  2. 过滤器中获取配置的属性,并且循环匹配

增加url配置属性

public class ImageCodeProperties {
    private int width = 67;
    private int height = 23;
    private int length = 4;  // 验证码长度
    private int expireIn = 60;  // 过期时间
    private String url;  // 要验证的接口url路径,逗号隔开

过滤器中对目标url进行匹配逻辑

public class ValidateCodeFilter extends OncePerRequestFilter implements InitializingBean {
    // 在初始化本类的地方进行注入
    // 一般在配置security http的地方进行添加过滤器
    private AuthenticationFailureHandler failureHandler;
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    // 由初始化的地方传递进来
    private SecurityProperties securityProperties;
    // 存储所有需要拦截的url
    private Set<String> urls;

    private AntPathMatcher pathMatcher = new AntPathMatcher();

    /**
     * org.springframework.beans.factory.InitializingBean 保证在其他属性都设置完成后,有beanFactory调用
     * 但是在这里目前还是需要初始化处调用该方法
     * @throws ServletException
     */
    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        String url = securityProperties.getCode().getImage().getUrl();
        String[] configUrl = StringUtils.split(url, ",");
        urls = Stream.of(configUrl).collect(Collectors.toSet());
        urls.add("/authentication/form"); // 登录请求
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        // 为登录请求,并且为post请求
        boolean action = false;
        for (String url : urls) {
            // org.springframework.util.AntPathMatcher 能匹配spring中的url模式
            // 支持通配符路径那种
            if (pathMatcher.match(url, request.getRequestURI())) {
                action = true;
            }
        }
        if (action) {
            try {
                validate(request);
            } catch (ValidateCodeException e) {
                failureHandler.onAuthenticationFailure(request, response, e);
                return;
            }
        }
        filterChain.doFilter(request, response);
    }

原配置中心进行属性注入

cn.mrcode.imooc.springsecurity.securitybrowser.BrowserSecurityConfig
// 有三个configure的方法,这里使用http参数的
@Override
protected void configure(HttpSecurity http) throws Exception {
    // 最简单的修改默认配置的方法
    // 在v5+中,该配置(表单登录)应该是默认配置了
    // basic登录(也就是弹框登录的)应该是v5-的版本默认

    ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
    validateCodeFilter.setFailureHandler(myAuthenticationFailureHandler);
    validateCodeFilter.setSecurityProperties(securityProperties);  // 注入配置属性类
    validateCodeFilter.afterPropertiesSet(); // 初始化url配置

测试:security-demo/application.yml

imooc:
  security:
    browser:
     # loginPage: /demo-signIn.html
     # loginType: REDIRECT
      loginType: JSON
    code:
      image:
        width: 100
        height: 50
        url: /order,/user/*   # 对订单和所有user路径进行验证码拦截 */

验证码的生成逻辑可配

思路:逻辑可配,就是抽象成接口,然后由客户端提供

这里的思路很强大,让我学习到了 spring中的默认配置是怎么实现的

提供一个生成图片信息的接口

package cn.mrcode.imooc.springsecurity.securitycore.validate.code;

import javax.servlet.http.HttpServletRequest;
import java.io.IOException;

public interface ValidateCodeGenerate {
    ImageCode generate(HttpServletRequest request) throws IOException;
}

实现默认图片生成接口类

cn.mrcode.imooc.springsecurity.securitycore.validate.code.ImageCodeGenerate

public class ImageCodeGenerate implements ValidateCodeGenerate {
    private ImageCodeProperties imageProperties;

    public ImageCodeGenerate(ImageCodeProperties imageProperties) {
        this.imageProperties = imageProperties;
    }

    @Override
    public ImageCode generate(HttpServletRequest request) throws IOException {
        return createImageCode(request);
    }

    public ImageCode createImageCode(HttpServletRequest request) throws IOException {
        int width = ServletRequestUtils.getIntParameter(request, "width", imageProperties.getWidth());
        int height = ServletRequestUtils.getIntParameter(request, "height", imageProperties.getHeight());
        int length = ServletRequestUtils.getIntParameter(request, "length", imageProperties.getLength());
        int expireIn = ServletRequestUtils.getIntParameter(request, "expireIn", imageProperties.getExpireIn());
        String code = RandomStringUtils.randomNumeric(length);
        BufferedImage image = createImageCode(width, height, code);
        return new ImageCode(image, code, expireIn);
    }

    ...后面的就是具体的工具类代码 不贴了

增加配置类,初始化图片生成器实例;这个是重点!!

package cn.mrcode.imooc.springsecurity.securitycore.validate.code;

import cn.mrcode.imooc.springsecurity.securitycore.properties.SecurityProperties;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class ValidateCodeConfig {
    @Autowired
    private SecurityProperties securityProperties;

    @Bean
    // spring 容器中如果存在imageCodeGenerate的bean就不会再初始化该bean了
    // 条件注解
    @ConditionalOnMissingBean(name = "imageCodeGenerate")
    public ValidateCodeGenerate imageCodeGenerate() {
        ImageCodeGenerate imageCodeGenerate = new ImageCodeGenerate(securityProperties.getCode().getImage());
        return imageCodeGenerate;
    }
}

之前调用处修改成调用接口

public class ValidateCodeController {
    public static final String SESSION_KEY = "SESSION_KEY_IMAGE_CODE";
    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired  // 使用生成接口处理信息
    private ValidateCodeGenerate validateCodeGenerate;

最后测试思路:

  1. 先保证重构后的代码功正常工作
  2. 在demo项目中 实现兵初始化一个ValidateCodeGenerate实现类,调试看看是否走进了我们自己的生成逻辑

小结

本章知识点