文档章节

精通Spring Boot——第十九篇:Spring Security 整合验证码登录

liululee
 liululee
发布于 02/06 16:53
字数 1743
阅读 97
收藏 5

献上一句格言,来自马克·扎克伯格的座右铭: Stay foucsed, Keep shipping(保持专注,持续交付)

1.引言

回到本章节我们将要学习的内容,现在使用验证码登录方式是再常见不过了,图形验证码,手机短信,邮箱验证码啊诸如此类的。今天我们以图形验证码为例,介绍下如何在Spring Security中添加验证码。与之前文章不同的是,这篇文章也将与数据库结合,模拟真实的开发环境。

2.准备工作

1.首先使用spring boot starter jpa 帮助我们通过实体类在数据库中简历对应的表结构,以及插入用户一条数据。

3.配置UserDetails, UserDeatilsService

在前面两篇文章中都有详细介绍过如何配置UserDetails以及UserDetailsService,这里也就不在赘述了

4.生成随机验证码

在生成验证码的同时,将验证码放入session中。

/**
 * @author developlee
 * @since 2019/1/14 16:23
 */
@RestController
public class CaptchaController {

    /**
     * 用于生成验证码图片
     *
     * @param request
     * @param response
     */
    @GetMapping("/code/image")
    public void createCode(HttpServletRequest request, HttpServletResponse response) throws Exception {
        HttpSession httpSession = request.getSession();
        Object[] objects = ValidateUtil.createImage();
        httpSession.setAttribute("imageCode", objects[0]);
        BufferedImage bufferedImage = (BufferedImage) objects[1];
        response.setContentType("image/png");
        OutputStream os = response.getOutputStream();
        ImageIO.write(bufferedImage, "png", os);
    }
}

工具类的实现,这个网上有很多种,大家可以搜一下看看

/**
 * @author developlee
 * @since 2019/1/18 17:24
 */
public class ValidateUtil {
    // 验证码字符集
    private static final char[] chars = {
            '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
            'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n',
            'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
            'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N',
            'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
    // 字符数量
    private static final int SIZE = 4;
    // 干扰线数量
    private static final int LINES = 5;
    // 宽度
    private static final int WIDTH = 80;
    // 高度
    private static final int HEIGHT = 40;
    // 字体大小
    private static final int FONT_SIZE = 30;

    /**
     * 生成随机验证码及图片
     * Object[0]:验证码字符串;
     * Object[1]:验证码图片。
     */
    public static Object[] createImage() {
        StringBuffer sb = new StringBuffer();
        // 1.创建空白图片
        BufferedImage image = new BufferedImage(
                WIDTH, HEIGHT, BufferedImage.TYPE_INT_RGB);
        // 2.获取图片画笔
        Graphics graphic = image.getGraphics();
        // 3.设置画笔颜色
        graphic.setColor(Color.LIGHT_GRAY);
        // 4.绘制矩形背景
        graphic.fillRect(0, 0, WIDTH, HEIGHT);
        // 5.画随机字符
        Random ran = new Random();
        for (int i = 0; i <SIZE; i++) {
            // 取随机字符索引
            int n = ran.nextInt(chars.length);
            // 设置随机颜色
            graphic.setColor(getRandomColor());
            // 设置字体大小
            graphic.setFont(new Font(
                    null, Font.BOLD + Font.ITALIC, FONT_SIZE));
            // 画字符
            graphic.drawString(
                    chars[n] + "", i * WIDTH / SIZE, HEIGHT*2/3);
            // 记录字符
            sb.append(chars[n]);
        }
        // 6.画干扰线
        for (int i = 0; i < LINES; i++) {
            // 设置随机颜色
            graphic.setColor(getRandomColor());
            // 随机画线
            graphic.drawLine(ran.nextInt(WIDTH), ran.nextInt(HEIGHT),
                    ran.nextInt(WIDTH), ran.nextInt(HEIGHT));
        }
        // 7.返回验证码和图片
        return new Object[]{sb.toString(), image};
    }

    /**
     * 随机取色
     */
    public static Color getRandomColor() {
        Random ran = new Random();
        Color color = new Color(ran.nextInt(256),
                ran.nextInt(256), ran.nextInt(256));
        return color;
    }
}

配置好之后,在页面加上我们的验证码

 <input name="validateCode" type="text" placeholder="请输入验证码">
 <input type=image src="http://localhost:8080/code/image"/>

5.配置过滤器链

然后我们写一个filter拦截器,用来实现验证码的验证。

/**
 * @author developlee
 * @since 2019/1/14 16:42
 */
@Slf4j
public class CaptchaFilter extends OncePerRequestFilter {

    @Autowired
    private AppConfig appConfig;

    private AuthenticationFailureHandler authenticationFailureHandler;

    // 注入appConfig
    public CaptchaFilter (AppConfig appConfig, AuthenticationFailureHandler authenticationFailureHandler) {
        this.appConfig = appConfig;
        this.authenticationFailureHandler = authenticationFailureHandler;
    }

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException {
        if(httpServletRequest.getRequestURI().equals(appConfig.getLoginUri().trim()) && httpServletRequest.getMethod().equals(RequestMethod.POST.name())) {
           try {
               validateCode(httpServletRequest);
           } catch (ValidateException e) {
               authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, e);
               return;
           }
        }
        filterChain.doFilter(httpServletRequest, httpServletResponse);
    }

    /**
     * 验证码的认证
     * @param userValidateCode
     * @throws ValidateException
     */
    private void validateCode(HttpServletRequest httpServletRequest) throws ValidateException {
        // 如果是登录请求,并且是post方式访问,则校验验证码
        String userValidateCode = httpServletRequest.getParameter("validateCode");
        String sysValidateCode = (String) httpServletRequest.getSession().getAttribute("imageCode");
        log.info("用户输入的验证码是:{},系统保存的验证码是:{}", userValidateCode, sysValidateCode);
        // 和我们保存的验证码进行比较
        if(StringUtils.isEmpty(userValidateCode)) {
            throw new ValidateException("验证码信息不能为空");
        }
        if(!StringUtils.equalsIgnoreCase(userValidateCode, sysValidateCode)) {
            throw new ValidateException("验证码不正确");
        }
		// TODO 可加上对验证码有效时间的验证,有兴趣的话可以自己实现下。其实就在生成验证码时,记录下生成的时间戳就好了。
    }
}

这个类中定义了一个ValidateException,这个exception扩展了Spring Security 中的 AuthentionException,当抛出ValidateException,确保我们的异常能被Spring Security正常捕获。


public class ValidateException extends AuthenticationException {
    @Getter
    @Setter
    private String code;

    @Getter
    @Setter
    private String msg;

    @Getter
    @Setter
    private Exception exception;

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

    public ValidateException(String msg, Throwable t) {
        super(msg, t);
    }
}

OK,到这里我们还缺最后一步,那就是将ValidateFilter添加到Spring Security 的拦截器链中,先看下过滤器链的执行顺序:

图片来源网络。

我们应该在验证用户名和密码之前先对验证码进行校验,因此我们的CaptchaFilter应该在UsernamePasswordAuthenticationFilter之前执行。

@Slf4j
@Configuration
@EnableWebSecurity
public class SecurityConfig extends WebSecurityConfigurerAdapter {

    @Autowired
    private UserDetailsServiceImpl userDetailsService;

    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }

    @Autowired
    private AppConfig appConfig;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        http.formLogin().loginPage("/sign_in").loginProcessingUrl(appConfig.getLoginUri())
                .defaultSuccessUrl("/welcome").permitAll()
                .failureHandler(new MyFailureHandler())
                .and().authorizeRequests().antMatchers("/code/image").permitAll()
                .and().addFilterBefore(new CaptchaFilter(appConfig, new MyFailureHandler()), UsernamePasswordAuthenticationFilter.class) // 验证码过滤器加入过滤器链
                .logout().logoutUrl("/auth/logout").clearAuthentication(true)
                .and().authorizeRequests().anyRequest().authenticated();
    }

    @Override
    protected void configure(AuthenticationManagerBuilder auth) throws Exception {
        auth.userDetailsService(userDetailsService).passwordEncoder(passwordEncoder());
    }
}

到这里之后,我们已经完成了对验证码的验证,然后要处理当验证不通过,也就是抛出ValidateException时,返回信息给页面。 注意到,SecurityConfig中的MyFailureHandler这个类,AuthentionException异常将会在这个类中处理。

/**
 * 登录失败处理逻辑
 */
@Slf4j
public class MyFailureHandler implements AuthenticationFailureHandler {
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException e) throws IOException, ServletException {
        if (e instanceof ValidateException) {
            log.info("用户输入验证码错误,返回错误信息" + e.getMessage());
        }
        httpServletResponse.setHeader("content-type", "application/json");
        httpServletResponse.setCharacterEncoding("UTF-8");
        Writer writer = httpServletResponse.getWriter();
        writer.write(e.getMessage());
    }
}

到这编码部分基本就结束了。下面我们在页面做个测试

6.测试

测试验证码为空的情况

看到log窗口打印的日志如下:提示返回验证信息不能为空

界面显示错误信息也是一样。

测试下验证码错误的情况

返回的是验证码不正确

这里的错误提示信息我们可以做个优化,让其在登录页面时就显示,可以自己实现下,在MyFailureHandler中用response.forward并携带错误信息跳转到登录页,然后在登录页面显示异常信息即可。

另外也可以看到,验证码不正确时,我们并没有对用户信息进行验证。所以SecurityConfig中的addFilterBefore是生效的。

7.总结

这篇文中,主要介绍了Spring Security整合验证码实现登录的功能。要注意的地方就是CaptchaFilter是扩展OncePerRequestFilter,然后要将该Filter放在Spring Security 的过滤器链中,并在UsernamePasswordAuthenticationFilter之前执行,以及异常的处理是使用自定义的FailureHandler。具体代码可参看我的github.com,欢迎大家star和follow,感谢观看。

© 著作权归作者所有

liululee
粉丝 123
博文 49
码字总数 47801
作品 0
杭州
程序员
私信 提问
精通Spring Boot—— 第二十篇:Spring Security记住我功能

引言 本章的代码实现是在上一篇教程:精通Spring Boot——第十九篇:Spring Security 整合验证码登录基础上,如果感觉本篇跳跃幅度较大,可先阅读上一篇,或访问我的github.com(文末会附上地...

liu浪诗人
02/07
0
0
SpringBoot集成Spring Security(4)——自定义表单登录

通过前面三篇文章,应该大致了解了Spring Security的流程。你应该发现了,真正的登录请求是由Spring Security帮我们处理的,那么我们如何实现自定义表单登录呢,比如添加一个验证码… 源码地...

yuanlaijike
2018/05/09
0
0
Spring Boot学习笔记

多模块开发 [SpringBoot学习]-IDEA创建Gradle多Module结构的SpringBoot项目 RabbitMQ RabbitMQ 安装 linux安装RabbitMQ详细教程 Ubuntu 16.04 RabbitMq 安装与运行(安装篇) ubantu安装...

OSC_fly
2018/07/26
0
0
SpringBoot 集成 Spring Security(8)——短信验证码登录

版权声明:本文版权归Jitwxs所有,欢迎转载,但未经作者同意必须保留原文链接。 https://blog.csdn.net/yuanlaijike/article/details/86164160 经过前面七章的学习,我们已经算入门 Spring S...

Jitwxs
01/09
0
0
后台权限管理系统 FEBS 新增 Spring Security 版

FEBS-Security是一个简单高效的后台权限管理系统。项目基础框架采用全新的Java Web开发框架 —— Spring Boot2.0.4,消除了繁杂的XML配置,使得二次开发更为简单;数据访问层采用Mybatis,同...

mrbird
2018/09/19
1K
0

没有更多内容

加载失败,请刷新页面

加载更多

Linux 软链接和硬链接简介

本文主要介绍了Linux系统中的链接文件。 文件系统 在Linux系统中,将文件分为两个部分:用户数据和元数据。 元数据(inode) 元数据即文件的索引节点(inode),用来记录文件的权限(r、w、x...

问题终结者
37分钟前
1
0
RocketMQ的事务投递

RocketMQ的事务投递 这是阿里的分布式事务图: 1、A服务先发送个Half Message给Brock端,消息中携带 B服务 即将要+100元的信息。 2、当A服务知道Half Message发送成功后,那么开始第3步执行本...

春哥大魔王的博客
52分钟前
2
0
Qt编写自定义控件31-面板仪表盘控件

一、前言 在Qt自定义控件中,仪表盘控件是数量最多的,写仪表盘都写到快要吐血,可能是因为各种工业控制领域用的比较多吧,而且仪表盘又是比较生动直观的,这次看到百度的echart中有这个控件...

飞扬青云
57分钟前
5
0
DisplayPort 迎来重大更新,数据带宽性能提高3倍

VESA宣布了他们对DisplayPort接口三年来的第一次重大更新。 与DP 1.4a相比,DisplayPort 2.0提供了三倍于DP 1.4a的数据带宽性能,支持超过8K的分辨率,更高的刷新率和更高分辨率的HDR,以及其...

linuxCool
今天
2
0
《Linux就该这么学》2019年7月20日第八天上课笔记

du命令 du -sh /newFS/ 察看文件/文件夹数据占用量 SWAP 交换分区的设置 磁盘容量配额 RHEL 5/6 usrquota RHEL 7 quota 软硬连接 ln 软 指针指向inode 硬 建立新的inode RAID 0 1 5 1+0...

2lodoss
今天
2
0

没有更多内容

加载失败,请刷新页面

加载更多

返回顶部
顶部