Spring Security 图形验证码校验

验证码校验也是通过Spring Security中的过滤器链来进行校验的。

编写图形验证码服务

1、创建 ImageCode 实体类

该实体类用于存储验证码的相关数据。

@Data
public class ImageCode {
    /**
     *  图形验证码
     */
    private BufferedImage imageCode;
    /**
     *  验证码
     */
    private String code;

    /**
     *  过期时间
     */
    private LocalDateTime expireTime;

    public ImageCode(BufferedImage imageCode, String code, int expireTime) {
        this.imageCode = imageCode;
        this.code = code;
        this.expireTime = LocalDateTime.now().plusSeconds(expireTime);
    }

    public ImageCode(BufferedImage imageCode, String code, LocalDateTime expireTime) {
        this.imageCode = imageCode;
        this.code = code;
        this.expireTime = expireTime;
    }

    public boolean isExpire() {
        return LocalDateTime.now().compareTo(this.expireTime) > 0;
    }
}

2、创建验证码服务
@RestController
@RequestMapping
public class ValidateCodeController {

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    /** ImageCode在session中的key */
    public static final String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE";

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 生成imageCode
        ImageCode imageCode = createImageCode(request);
        // 将imageCode 保存在session中
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode);
        ImageIO.write(imageCode.getImageCode(), "JPEG", response.getOutputStream());
    }

    public ImageCode createImageCode(HttpServletRequest request) {
        // 在内存中创建图像
        int width = 65, height = 20;
        BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        Graphics graphics = bufferedImage.getGraphics();
        // 设定背景色
        graphics.setColor(getRandColor(230, 255));
        graphics.fillRect(0, 0, 100, 25);
        // 设定字体
        graphics.setFont(new Font("Arial", Font.CENTER_BASELINE | Font.ITALIC, 18));
        // 产生0条干扰线,
        graphics.drawLine(0, 0, 0, 0);
        // 随机产生四位验证码
        String sRand = "";
        Random random = new Random();
        for (int i = 0; i < 4; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            // 将认证码显示到图象中
            graphics.setColor(getRandColor(100, 150));// 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
            graphics.drawString(rand, 15 * i + 6, 16);
        }
        graphics.dispose();
        return new ImageCode(bufferedImage, sRand, 60);
    }

    /**
     * 给定范围获得随机颜色
     *
     * @param fc
     * @param bc
     * @return
     */
    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);
    }
}
3、在 Security 的配置文件中,将获取验证码的请求,进行认证过滤。
.antMatchers("/code/image","/authentication/require",securityProperties.getBrowser().getLoginpage()).permitAl()
4、登录界面的html代码
 <form action="/authentication/form" method="post">
        <table>
            <tr>
                <td>用户名:</td>
                <td><input type="text" name="username" value="user"></td>
            </tr>
            <tr>
                <td>密码:</td>
                <td><input type="password" name="password" value="123456"></td>
            </tr>
            <tr>
                <td>图形验证码:</td>
                <td>
                    <input type="text" name="imageCode">
                    <img src="/code/image">
                </td>
            </tr>
            <tr>
                <td colspan="2"><button type="submit">登陆</button></td>
            </tr>
        </table>
    </form>

验证码校验

通过Filter实现,验证码的校验逻辑。自定义的验证码过滤器,前置到UsernamePasswordAuthenticationFilter之前。

1、自定义验证码校验过滤器
public class ValidateCodeFilter  extends OncePerRequestFilter {

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    private AuthenticationFailureHandler authenticationFailureHandler;

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        if("/authentication/form".equals(httpServletRequest.getRequestURI())
          && "POST".equalsIgnoreCase(httpServletRequest.getMethod())) {
            try {
                validate(new ServletWebRequest(httpServletRequest));
            } catch (ValidateCodeException e) {
                // 调用失败处理器
               authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
               return;
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

    private void validate(ServletWebRequest servletWebRequest) throws ServletRequestBindingException, ValidateCodeException {
        ImageCode imageCodeSession = (ImageCode)sessionStrategy.getAttribute(servletWebRequest,ValidateCodeController.SESSION_KEY_IMAGE_CODE);
        String code = ServletRequestUtils.getStringParameter(servletWebRequest.getRequest(),"imageCode");
        if (StrUtil.isBlank(code)) {
            throw new ValidateCodeException("验证码的值不能为空");
        }
        if (imageCodeSession == null) {
            throw new ValidateCodeException("验证码不存在");
        }
        if (imageCodeSession.isExpire()) {
            sessionStrategy.removeAttribute(servletWebRequest,ValidateCodeController.SESSION_KEY_IMAGE_CODE);
            throw new ValidateCodeException("验证码已过期");
        }
        if (!StrUtil.equals(imageCodeSession.getCode(), code)) {
            throw new ValidateCodeException("验证码不匹配");
        }
        sessionStrategy.removeAttribute(servletWebRequest,ValidateCodeController.SESSION_KEY_IMAGE_CODE);
    }

    public AuthenticationFailureHandler getAuthenticationFailureHandler() {
        return authenticationFailureHandler;
    }

    public void setAuthenticationFailureHandler(AuthenticationFailureHandler authenticationFailureHandler) {
        this.authenticationFailureHandler = authenticationFailureHandler;
    }
}
2、自定义的异常类
public class ValidateCodeException extends AuthenticationException {

    public ValidateCodeException(String msg) {
        super(msg);
    }
}

3、配置验证码校验过滤器
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private SecurityProperties securityProperties;

    @Autowired
    private LoginSuccessHandler loginSuccessHandler;

    @Autowired
    private LoginFailureHandler loginFailureHandler;
    /**
     *  为减少代码重复开发,多个应用使用同一个认证中心,每个应用需要自己指定登录页面。
     *  这里需要将 loginpage 指向一个controlelr地址。
     *  如果是html页面,就跳转到指定的登录页。
     *  如果不是html页面,就提示401 没有认证信息。
     *  如果有应用有指定的就使用自己的。如果没指定就使用本认证模块默认的登录页。
     * @param http
     * @throws Exception
     */
    @Override
    protected void configure(HttpSecurity http) throws Exception {
    	// 引入验证码过滤器
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(loginFailureHandler);
        // 配置过滤器的位置
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);

        http.formLogin()
                .loginPage("/authentication/require")
                .loginProcessingUrl("/authentication/form")
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailureHandler)
                .successForwardUrl("/index")
//                .defaultSuccessUrl("/index")
                .and()
                .authorizeRequests()
                .antMatchers("/code/image","/authentication/require",securityProperties.getBrowser().getLoginpage()).permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable();
    }
     }

验证码代码重构

重构代码将一些参数,改为可配置的,降低代码耦合度; 在这个Spring Security的案例中,是把当前应用作为可以重用的应用架构,进而可以引入到其他应用中去,减少用户认证的重复开发。每个应用可能可能自己的验证码生成逻辑不同,可以通过代码重构,让第三方应用实现自己的生成验证码的逻辑。

验证码基本参数配置

应用级别配置:配置在引用的第三方项目中
默认级别配置:给配置值指定设置默认值

1、验证码参数配置如下:

security:
  browser:
    logintype: html
  validate_code:
    image:
      width: 80
      height: 40
      length: 5
      expire_time: 100

2、对应的配置实体类

@ConfigurationProperties(prefix = "security")
public class SecurityProperties {

    private BrowserProperties browser = new BrowserProperties();

    private ValidateCodeProperties validateCode = new ValidateCodeProperties();
  // 省略getter和setter方法
}
@Data
public class ValidateCodeProperties {

    ImageCodeProperties image = new ImageCodeProperties();

}
@Data
public class ImageCodeProperties {

    private int width = 67;

    private int height = 23;

    private int length = 4; // 验证码长度

    private int expireTime = 60; // 过期时间

}

3、在ValidateCodeController中,将对应的验证码参数改为从配置类中读取。

 public ImageCode createImageCode(HttpServletRequest request) {
        ImageCodeProperties image = securityProperties.getValidateCode().getImage();
        // 在内存中创建图像
//        int width = 65, height = 20;
        int width = ServletRequestUtils.getIntParameter(request, "width", image.getWidth());
        int height = ServletRequestUtils.getIntParameter(request, "height", image.getHeight());
        int length = ServletRequestUtils.getIntParameter(request, "length", image.getLength());
        int expireTime = ServletRequestUtils.getIntParameter(request, "expireTime", image.getExpireTime());
        BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);

        Graphics graphics = bufferedImage.getGraphics();
        // 设定背景色
        graphics.setColor(getRandColor(230, 255));
        graphics.fillRect(0, 0, 100, 25);
        // 设定字体
        graphics.setFont(new Font("Arial", Font.CENTER_BASELINE | Font.ITALIC, 18));
        // 产生0条干扰线,
        graphics.drawLine(0, 0, 0, 0);
        // 随机产生四位验证码
        String sRand = "";
        Random random = new Random();
        for (int i = 0; i < length; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            // 将认证码显示到图象中
            graphics.setColor(getRandColor(100, 150));// 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
            graphics.drawString(rand, 15 * i + 6, 16);
        }
        graphics.dispose();
        return new ImageCode(bufferedImage, sRand, expireTime);
    }
验证码拦截的接口可配置

如果 第三方应用认证的接口和默认的路径不一样呢,这时候就需要改为可配置的了。

在上面的配置类中添加url属性。

在验证码校验过滤器的 ValidateCodeFilter类中,实现 接口 InitializingBean的方法afterPropertiesSet()

实现了 InitializingBean接口的类,实例化bean时,会自动执行,afterPropertiesSet()方法。

public class ValidateCodeFilter  extends OncePerRequestFilter implements InitializingBean {

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    private AuthenticationFailureHandler authenticationFailureHandler;

    private Set<String> urls; // 存储需要拦截的url
    
    private SecurityProperties securityProperties;
    
    

    @Override
    public void afterPropertiesSet() throws ServletException {
        super.afterPropertiesSet();
        String url = securityProperties.getValidateCode().getImage().getUrl();
        String[] configUrl = url.split(",");
        urls = Stream.of(configUrl).collect(Collectors.toSet());
        // 添加默认的地址
        urls.add("/authentication/form");
    }

    /**
     *   重构
     * @param httpServletRequest
     * @param httpServletResponse
     * @param filterChain
     * @throws ServletException
     * @throws IOException
     */
    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        boolean action = false;
        for (String url : urls) {
            if (url.equals(httpServletRequest.getRequestURI())) {
                action = true;
                break;
            }
        }
        if (action) {
            try {
                validate(new ServletWebRequest(httpServletRequest));
            } catch (ValidateCodeException e) {
                // 调用失败处理器
               authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
               return;
            }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }
}

ValidateCodeFilter 类没有通过Spring Bean的方式注入,还是需要在SecurityConfig中进行手动调用 afterPropertiesSet()

 protected void configure(HttpSecurity http) throws Exception {
        ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
        validateCodeFilter.setAuthenticationFailureHandler(loginFailureHandler);

        validateCodeFilter.setSecurityProperties(securityProperties);
        validateCodeFilter.afterPropertiesSet();
        // 配置过滤器的位置
        http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class);

        http.formLogin()
                .loginPage("/authentication/require")
                .loginProcessingUrl("/authentication/form")
                .successHandler(loginSuccessHandler)
                .failureHandler(loginFailureHandler)
                .successForwardUrl("/index")
//                .defaultSuccessUrl("/index")
                .and()
                .authorizeRequests()
                .antMatchers("/code/image","/authentication/require",securityProperties.getBrowser().getLoginpage()).permitAll()
                .anyRequest().authenticated()
                .and().csrf().disable();
    }
验证码实现逻辑可配

思路: 逻辑可配,就是抽象成接口,实现由客户端实现。

1、 定义生成验证码逻辑的接口

/**
 * @Author L.jg
 * @Title       抽象接口,让客户端可配置接口
 * @Date 2021/5/24 11:42
 */
public interface ValidateCodeGenerate {

    ImageCode generate(HttpServletRequest request);

}

2、实现接口,自定义验证码的实现逻辑

public class ImageCodeGenerate implements  ValidateCodeGenerate {

    private ImageCodeProperties imageCodeProperties;


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

    @Override
    public ImageCode generate(HttpServletRequest request) {
        int width = ServletRequestUtils.getIntParameter(request, "width", imageCodeProperties.getWidth());
        int height = ServletRequestUtils.getIntParameter(request, "height", imageCodeProperties.getHeight());
        int length = ServletRequestUtils.getIntParameter(request, "length", imageCodeProperties.getLength());
        int expireTime = ServletRequestUtils.getIntParameter(request, "expireTime", imageCodeProperties.getExpireTime());
        return createImageCode(width, height, length, expireTime);
    }
    public ImageCode createImageCode(int width, int height, int length, int expireTime) {
        // 在内存中创建图像
        // int width = 65, height = 20;
        BufferedImage bufferedImage = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
        Graphics graphics = bufferedImage.getGraphics();
        // 设定背景色
        graphics.setColor(getRandColor(230, 255));
        graphics.fillRect(0, 0, 100, 25);
        // 设定字体
        graphics.setFont(new Font("Arial", Font.CENTER_BASELINE | Font.ITALIC, 18));
        // 产生0条干扰线,
        graphics.drawLine(0, 0, 0, 0);
        // 随机产生四位验证码
        String sRand = "";
        Random random = new Random();
        for (int i = 0; i < length; i++) {
            String rand = String.valueOf(random.nextInt(10));
            sRand += rand;
            // 将认证码显示到图象中
            graphics.setColor(getRandColor(100, 150));// 调用函数出来的颜色相同,可能是因为种子太接近,所以只能直接生成
            graphics.drawString(rand, 15 * i + 6, 16);
        }
        graphics.dispose();
        return new ImageCode(bufferedImage, sRand, expireTime);
    }

    /**
     * 给定范围获得随机颜色
     *
     * @param fc
     * @param bc
     * @return
     */
    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);
    }
}

3、 在Spring boot中注入Bean

@Configuration
public class VlidateCodeConfig {

    @Autowired
    private SecurityProperties securityProperties;

    @Bean
    // 如果Spring 容易中存在 imageCodeGenerate 的bean就不会再初始化该bean了
    @ConditionalOnMissingBean(name = "imageCodeGenerate")
    public ValidateCodeGenerate imageCodeGenerate() {
        ImageCodeGenerate imageCodeGenerate = new ImageCodeGenerate(securityProperties.getValidateCode().getImage());
        return imageCodeGenerate;
    }
}

4、 在验证码controller中,注入验证码实现类

@RestController
@RequestMapping
public class ValidateCodeController {

    private SessionStrategy sessionStrategy = new HttpSessionSessionStrategy();

    @Autowired
    private SecurityProperties securityProperties;
    
    @Autowired
    private ValidateCodeGenerate validateCodeGenerate;

    /** ImageCode在session中的key */
    public static final String SESSION_KEY_IMAGE_CODE = "SESSION_KEY_IMAGE_CODE";

    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws IOException {
        // 生成imageCode
//        ImageCode imageCode = createImageCode(request);
        ImageCode imageCode = validateCodeGenerate.generate(request);
        // 将imageCode 保存在session中
        sessionStrategy.setAttribute(new ServletWebRequest(request), SESSION_KEY_IMAGE_CODE, imageCode);
        ImageIO.write(imageCode.getImageCode(), "JPEG", response.getOutputStream());
    }
    }
  • 1
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值