当前位置: 首页 > article >正文

SpringSecurity使用过滤器实现图形验证码

1、图形验证码的作用

图形验证码(CAPTCHA,Completely Automated Public Turing test to tell Computers and Humans Apart)是一种用于区分用户是人类还是计算机程序的自动化测试。它通常用于防止自动化软件(如机器人或爬虫程序)进行恶意操作,如滥用在线服务、暴力破解密码或进行垃圾邮件发送等。

图形验证码的工作原理基于一个假设:计算机程序难以自动识别和处理复杂的图像或模式,而人类则相对容易。因此,图形验证码通常包含扭曲的文字、数字、图像或它们的组合,这些元素对人类来说相对容易辨认,但对计算机程序来说则非常困难。

下面将介绍 Spring Boot 整合 Spring Security 实现图形验证码功能,执行结果如下如:

(1)登录页面

(2)登录成功后,跳转至首页

2、创建项目

【示例】SpringBoot 整合 SpringSecurity  使用过滤器实现图形验证码功能。

2.1 创建 Spring Boot 项目

创建 SpringBoot 项目,项目结构如下图:

2.2 添加 Maven 依赖

在 pom.xml 配置文件中添加 Spring Security、谷歌 Kaptcha 图形验证码。

<!-- Spring Security 依赖 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-security</artifactId>
    <version>2.7.18</version>
</dependency>

<!-- 谷歌 Kaptcha 图形验证码 -->
<dependency>
    <groupId>com.github.penggle</groupId>
    <artifactId>kaptcha</artifactId>
    <version>2.3.2</version>
</dependency>

3、整合 Spring Security 框架实现认证与授权

3.1 配置类(Config 层)

创建 WebSecurityConfig 类(Spring Security 配置类),并添加 @EnableWebSecurity 注解和继承 WebSecurityConfigurerAdapter 类。

package com.pjb.securitydemo.config;

import com.pjb.securitydemo.filter.VerificationCodeFilter;
import com.pjb.securitydemo.handler.LoginFailureHandler;
import com.pjb.securitydemo.handler.LoginSuccessHandler;
import com.pjb.securitydemo.handler.PermissionDeniedHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.password.NoOpPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;

/**
 * Spring Security 配置类
 * @author pan_junbiao
 **/
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter
{
    @Autowired
    private LoginSuccessHandler loginSuccessHandler;

    @Autowired
    private LoginFailureHandler loginFailureHandler;

    @Autowired
    private PermissionDeniedHandler permissionDeniedHandler;

    @Override
    protected void configure(HttpSecurity http) throws Exception
    {
        http.authorizeRequests() //返回一个URL拦截注册器
                .antMatchers("/captcha.jpg").permitAll() //公开其权限
                .anyRequest() //匹配所有的请求
                .authenticated() //所有匹配的URL都需要被认证才能访问
                .and() //结束当前标签,让上下文回到 HttpSecurity
                .formLogin() //启动表单认证
                .loginPage("/myLogin.html") //自定义登录页面
                .loginProcessingUrl("/auth/form") //指定处理登录请求路径
                .permitAll() //使登录页面不设限访问
                //.defaultSuccessUrl("/index") //登录认证成功后的跳转页面
                .successHandler(loginSuccessHandler) //指定登录成功时的处理
                .failureHandler(loginFailureHandler) //指定登录失败时的处理
                .and()
                .exceptionHandling().accessDeniedHandler(permissionDeniedHandler) //403无权时的返回操作
                .and().csrf().disable(); //关闭CSRF的防御功能
        
        //图形验证码过滤器(核心代码):将自定义过滤器添加在UsernamePasswordAuthenticationFilter之前
        http.addFilterBefore(new VerificationCodeFilter(), UsernamePasswordAuthenticationFilter.class);
    }

    /**
     * 内存中添加登录账号
     */
    @Bean
    public UserDetailsService userDetailsService()
    {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("admin").password("123456").roles("ADMIN").build());
        manager.createUser(User.withUsername("user").password("123456").roles("USER").build());
        manager.createUser(User.withUsername("panjunbiao").password("123456").roles("USER").build());
        return manager;
    }

    /**
     * 密码编译器
     * 由于5.x版本之后默认启用了委派密码编译器,
     * 因而按照以往的方式设置内存密码将会读取异常,
     * 所以需要暂时将密码编码器设置为 NoOpPasswordEncoder
     */
    @Bean
    public PasswordEncoder passwordEncoder()
    {
        return NoOpPasswordEncoder.getInstance();
    }
}

3.2 处理类(Handler 层)

(1)登录成功处理类

package com.pjb.securitydemo.handler;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

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

/**
 * 登录成功处理类
 */
@Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler
{
    @Override
    public void onAuthenticationSuccess(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, Authentication authentication) throws IOException, ServletException
    {
        //重定向至首页
        httpServletResponse.sendRedirect("/");
    }
}

(2)登录失败处理类

package com.pjb.securitydemo.handler;

import com.pjb.securitydemo.exception.VerificationCodeException;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.authentication.CredentialsExpiredException;
import org.springframework.security.authentication.DisabledException;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 登录失败处理类
 */
@Component
public class LoginFailureHandler implements AuthenticationFailureHandler
{
    @Override
    public void onAuthenticationFailure(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AuthenticationException authenticationException) throws IOException, ServletException
    {
        //获取登录失败原因
        String errorMessage = "";
        if(authenticationException instanceof BadCredentialsException){
            errorMessage = "用户名或密码不正确";
        }else if(authenticationException instanceof DisabledException){
            errorMessage = "账号被禁用";
        }else if(authenticationException instanceof UsernameNotFoundException){
            errorMessage = "用户名不存在";
        }else if(authenticationException instanceof CredentialsExpiredException){
            errorMessage = "密码已过期";
        }else if(authenticationException instanceof LockedException) {
            errorMessage = "账号被锁定";
        }else if(authenticationException instanceof VerificationCodeException){
            errorMessage = "无效的图形验证码";
        }else{
            errorMessage = "未知异常";
        }

        //设置响应编码
        httpServletResponse.setContentType("application/json;charset=utf-8");
        PrintWriter out = httpServletResponse.getWriter();
        out.write(errorMessage);
    }
}

(3)403无权限处理类

package com.pjb.securitydemo.handler;

import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.stereotype.Component;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.io.PrintWriter;

/**
 * 403无权限处理类
 */
@Component
public class PermissionDeniedHandler implements AccessDeniedHandler
{
    @Override
    public void handle(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, AccessDeniedException e) throws IOException, ServletException
    {
        httpServletResponse.setContentType("application/json;charset=utf-8");
        PrintWriter out = httpServletResponse.getWriter();
        out.write("403无权限");
    }
}

 

4、整合 Kaptcha 框架实现图形验证码

4.1 配置类(Config 层)

创建 KaptchaConfig 类(Kaptcha 图形验证码配置类),设置图形验证码相关属性。

package com.pjb.securitydemo.config;

import com.google.code.kaptcha.Producer;
import com.google.code.kaptcha.impl.DefaultKaptcha;
import com.google.code.kaptcha.util.Config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.Properties;

/**
 * 谷歌Kaptcha图形验证码配置类
 */
@Configuration
public class KaptchaConfig
{
    @Bean
    public Producer captcha()
    {
        //配置图形验证码的基本参数
        Properties properties = new Properties();
        //图片宽度
        properties.setProperty("kaptcha.image.width","150");
        //图片长度
        properties.setProperty("kaptcha.image.height","50");
        //字符集(从哪些字符中产生)
        properties.setProperty("kaptcha.textproducer.char.string", "0123456789");
        //字符长度
        properties.setProperty("kaptcha.textproducer.char.length", "4");
        //字体颜色
        properties.put("kaptcha.textproducer.font.color", "red");
        // 文字间隔,这里设置为10px
        properties.put("kaptcha.textproducer.char.space", "10");

        // 背景颜色渐变开始
        properties.put("kaptcha.background.clear.from", "yellow");
        // 背景颜色渐变结束
        properties.put("kaptcha.background.clear.to", "green");

        //初始化配置
        Config config = new Config(properties);

        //使用默认的图形验证码实现,当然也可以自定义实现
        DefaultKaptcha defaultKaptcha = new DefaultKaptcha();
        defaultKaptcha.setConfig(config);
        return defaultKaptcha;
    }
}

图形验证码配置属性表

属性名属性作用默认值
kaptcha.border图片边框,合法值:yes , noyes
kaptcha.border.color边框颜色,合法值: r,g,b (and optional alpha) 或者 white,black,blue.black
kaptcha.image.width图片宽200
kaptcha.image.height图片高50
kaptcha.producer.impl图片实现类com.google.code.kaptcha.impl.DefaultKaptcha
kaptcha.textproducer.impl文本实现类com.google.code.kaptcha.text.impl.DefaultTextCreator
kaptcha.textproducer.char.string文本集合,验证码值从此集合中获取abcde2345678gfynmnpwx
kaptcha.textproducer.char.length验证码长度5
kaptcha.textproducer.font.names字体Arial, Courier
kaptcha.textproducer.font.size字体大小40px.
kaptcha.textproducer.font.color字体颜色,合法值: r,g,b 或者 white,black,blue.black
kaptcha.textproducer.char.space文字间隔2
kaptcha.noise.impl干扰实现类com.google.code.kaptcha.impl.DefaultNoise
kaptcha.noise.color干扰 颜色,合法值: r,g,b 或者 white,black,blue.black
kaptcha.obscurificator.impl

图片样式:<br />水纹 com.google.code.kaptcha.impl.WaterRipple <br />

鱼眼 com.google.code.kaptcha.impl.FishEyeGimpy <br />

阴影 com.google.code.kaptcha.impl.ShadowGimpy

com.google.code.kaptcha.impl.WaterRipple
kaptcha.background.impl背景实现类com.google.code.kaptcha.impl.DefaultBackground
kaptcha.background.clear.from背景颜色渐变,开始颜色light grey
kaptcha.background.clear.to背景颜色渐变, 结束颜色white
kaptcha.word.impl文字渲染器com.google.code.kaptcha.text.impl.DefaultWordRenderer
kaptcha.session.keysession keyKAPTCHA_SESSION_KEY
kaptcha.session.datesession date

4.2 控制器层(Controller 层)

创建 CaptchaController 类(验证码控制器),实现生成验证码图片方法。

package com.pjb.securitydemo.controller;

import com.google.code.kaptcha.Producer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

import javax.imageio.ImageIO;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.awt.image.BufferedImage;
import java.io.IOException;

/**
 * 验证码控制器
 */
@Controller
public class CaptchaController
{
    @Autowired
    private Producer captchaProducer;

    @GetMapping("/captcha.jpg")
    public void getCaptcha(HttpServletRequest request, HttpServletResponse response) throws IOException
    {
        //设置内容类型
        response.setContentType("image/jpeg");
        //创建验证码文本
        String capText = captchaProducer.createText();

        //将验证码文本保存到Session中
        request.getSession().setAttribute("captcha", capText);
        //创建验证码图片
        BufferedImage bufferedImage = captchaProducer.createImage(capText);
        //获取响应输出流
        ServletOutputStream out = response.getOutputStream();
        //将图片验证码数据写入响应输出流
        ImageIO.write(bufferedImage,"jpg",out);
        //推送并关闭响应输出流
        try
        {
            out.flush();
        }
        finally
        {
            out.close();
        }
    }
}

4.3 自定义异常类(Exception 层)

自定义异常类 VerificationCodeException(验证码校验失败的异常类),继承 AuthenticationException 类。

package com.pjb.securitydemo.exception;

import org.springframework.security.core.AuthenticationException;

/**
 * 验证码校验失败的异常类
 */
public class VerificationCodeException extends AuthenticationException
{
    public VerificationCodeException()
    {
        super("图形验证码校验失败");
    }
}

4.4 自定义过滤器(Filter 层)

自定义过滤器类 VerificationCodeFilter (验证码校验过滤器),继承 OncePerRequestFilter 类。

有了图形验证码的 API 之后,就可以自定义验证码校验过滤器了。虽然 Spring Security 的过滤器链对过滤器没有特殊要求,只要继承 Filter 接口即可,但是在 Spring 体系中,推荐使用  OncePerRequestFilter 类来实现,它可以确保一次请求只会通过一次该过滤器(Filter 实际上并不能保证这一点)。

package com.pjb.securitydemo.filter;

import com.pjb.securitydemo.exception.VerificationCodeException;
import com.pjb.securitydemo.handler.LoginFailureHandler;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.util.StringUtils;
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 javax.servlet.http.HttpSession;
import java.io.IOException;

/**
 * 验证码校验过滤器
 */
public class VerificationCodeFilter extends OncePerRequestFilter
{
    private AuthenticationFailureHandler authenticationFailureHandler = new LoginFailureHandler();

    @Override
    protected void doFilterInternal(HttpServletRequest httpServletRequest, HttpServletResponse httpServletResponse, FilterChain filterChain) throws ServletException, IOException
    {
        //非登录请求不校验验证码
        String requestURI = httpServletRequest.getRequestURI();
        if(!"/auth/form".equals(requestURI))
        {
            filterChain.doFilter(httpServletRequest,httpServletResponse);
        }
        else
        {
            try
            {
                //验证码校验
                verificationCode(httpServletRequest);

                //验证成功
                filterChain.doFilter(httpServletRequest,httpServletResponse);
            }
            catch (VerificationCodeException ex)
            {
                //验证失败
                authenticationFailureHandler.onAuthenticationFailure(httpServletRequest, httpServletResponse, ex);
            }
        }
    }

    /**
     * 验证码校验
     */
    public void verificationCode(HttpServletRequest httpServletRequest) throws VerificationCodeException
    {
        String requestCode = httpServletRequest.getParameter("captcha");
        HttpSession session = httpServletRequest.getSession();
        String savedCode = (String)session.getAttribute("captcha");
        if(!StringUtils.isEmpty(savedCode))
        {
            //随手清除验证码,无论是失败,还是成功。客户端应在登录失败时刷新验证码
            session.removeAttribute("captcha");
        }

        //验证不通过,抛出异常
        if(StringUtils.isEmpty(requestCode) || StringUtils.isEmpty(savedCode) || !requestCode.equals(savedCode))
        {
            throw new VerificationCodeException();
        }
    }
}

至此整合 Kaptcha 框架实现图形验证码已完成,最后注意,一定要把自定义过滤器类 VerificationCodeFilter 添加到 Spring Security 的过滤器链中。

打开 WebSecurityConfig 类(Spring Security 配置类),将自定义过滤器类 VerificationCodeFilter 添加到过滤器链中,如下:

5、前端页面

5.1 控制器层(Controller 层)

创建 IndexController 类(首页控制器),实现获取当前登录用户名并跳转至首页。

package com.pjb.securitydemo.controller;
 
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
 
import javax.servlet.http.HttpServletRequest;
import java.security.Principal;
 
/**
 * 首页控制器
 * @author pan_junbiao
 **/
@Controller
public class IndexController
{
    /**
     * 首页
     */
    @RequestMapping("/")
    public String index(HttpServletRequest request)
    {
        //获取当前登录人
        String userName = "未登录";
        Principal principal = request.getUserPrincipal();
        if(principal!=null)
        {
            userName = principal.getName();
        }
 
        //返回页面
        request.setAttribute("userName",userName);
        return "/index.html";
    }
 
}

5.2 编写登录页面

在 resources\static 静态资源目录下,创建 myLogin.html 页面。

注意:myLogin.html 页面必须放在 resources\static 静态资源目录下,否则页面无法加载。

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <title>登录</title>
    <meta name="author" content="pan_junbiao的博客">
</head>
<body>
<form name="myForm" action="/auth/form" method="post">
    <table align="center">
        <caption>用户登录</caption>
        <tr>
            <td>登录账户:</td>
            <td>
                <input type="text" name="username" placeholder="请输入登录账户" value="panjunbiao" />
            </td>
        </tr>
        <tr>
            <td>登录密码:</td>
            <td>
                <input type="password" name="password" placeholder="请输入登录密码" value="123456" />
            </td>
        </tr>
        <tr>
            <td>验证码:</td>
            <td>
                <!-- 新增图形验证码的输入框 -->
                <input type="text" name="captcha" placeholder="请输入验证码" />

                <!-- 图片指向图形验证码API -->
                <img src="/captcha.jpg" alt="captch" height="50px" width="150px" style="margin-left:20px;" >
            </td>
        </tr>
        <!-- 以下是提交、取消按钮 -->
        <tr>
            <td colspan="2" style="text-align: center; padding: 5px;">
                <input type="submit" value="提交" />
                <input type="reset" value="重置" />
            </td>
        </tr>
    </table>
</form>
</body>
</html>

5.3 编写首页

在 resources\templates 资源目录下,创建 index.html 页面。

注意:首页 index.html 页面中使用 Thymeleaf 模板 。

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>首页</title>
    <meta name="author" content="pan_junbiao的博客">
</head>
<body>
    <h1 style="color: red">Hello,Spring Security</h1>
    <p>博客信息:您好,欢迎访问 pan_junbiao的博客</p>
    <p>博客地址:https://blog.csdn.net/pan_junbiao</p>
    <p th:text="'当前登录人:' + ${userName}"></p>
    <a href="/logout" onclick="return confirm('确认注销吗?');">登出</a>
</body>
</html>

6、运行项目

6.1 登录页面

6.2 图形验证码校验失败

6.3 登录成功后,跳转至首页

 


http://www.kler.cn/a/457348.html

相关文章:

  • 爬虫的工作原理
  • Vue3 内置组件之component
  • 【新教程】华为昇腾NPU的pytorch环境搭建
  • 交换机关于环路、接口绑定、链路聚合的相关知识
  • 安全运营 -- splunk restapi 最小权限
  • OSPF特殊区域(open shortest path first LSA Type7)
  • matlab smith自适应模糊PID房间湿度控制
  • 基于TCP的Qt网络通信
  • 【论文解读】Arbitrary-steps Image Super-resolution via Diffusion Inversion
  • UE4 编译报错 “Error LNK2019 : 无法解析的外部符号” 一种可能的原因
  • Flask使用的正例和反例
  • SpringBoot整合篇 05、Springboot整合Redission
  • flask-admin 模型视图(modelView)中重写after_model_delete与on_model_delete
  • 力扣-数据结构-6【算法学习day.77】
  • 李永乐线性代数:A可逆,AX=B相关推论和例题解题思路
  • 【探花交友】day06—即时通信
  • [openGauss 学废系列]- openGauss体系结构-多个用户访问同一个数据库
  • Mooncake:kimi后端推理服务的架构设计
  • DOM解析:深入理解文档对象模型
  • Elasticsearch 数据存储底层机制详解
  • C++进阶-【高级语法】
  • 使用GitHub Pages部署静态网站:简易指南
  • 《Vue进阶教程》第二十四课:优化
  • c++ 里 常量转换 const_cast < T > ,要给模板参数 T 传递什么类型呢?
  • iClient3D for Cesium 加载shp数据并拉伸为白模
  • Node.js 工具:在 Windows 11 中配置 Node.js 的详细步骤