[Java实战]Spring Security 添加验证码(二十三)

发布于:2025-05-15 ⋅ 阅读:(88) ⋅ 点赞:(0)

[Java实战]Spring Security 添加验证码(二十三)

在现代的 Web 应用中,验证码是防止恶意攻击(如暴力破解、自动注册等)的重要手段之一。Spring Security 是一个功能强大的安全框架,提供了用户认证、授权等功能。本文将详细介绍如何在 Spring Security 中添加验证码功能,从而进一步增强应用的安全性。

一. 环境准备

  • openJDK 17+:Spring Boot 3 要求 Java 17 及以上。
  • Spring Boot 3.4.5:使用最新稳定版。
  • 构建工具:Maven 或 Gradle(本文以 Maven 为例)。

二、验证码的作用

验证码(CAPTCHA,Completely Automated Public Turing test to tell Computers and Humans Apart)是一种区分用户是人类还是机器人的测试。常见的验证码类型包括:

  • 图片验证码:用户需要识别并输入图片中的字符。
  • 短信验证码:用户需要输入发送到手机的验证码。
  • 邮箱验证码:用户需要输入发送到邮箱的验证码。

验证码的主要作用是:

  • 防止暴力破解:限制非法登录尝试。
  • 防止自动注册:限制恶意用户批量注册账号。
  • 防止垃圾评论:限制自动发布垃圾评论。

三、Spring Security 添加验证码

1. 添加依赖

在 Spring Boot 项目中,添加验证码功能需要一些额外的依赖。首先,确保你的项目中已经添加了 Spring Security 和 Spring Web 的依赖。

<dependencies>
    <!-- Spring Boot Starter Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>

    <!-- Spring Boot Starter Security -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>

    <!-- Thymeleaf -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-thymeleaf</artifactId>
    </dependency>

    <!-- 图片验证码库 -->
    <dependency>
        <groupId>com.github.penggle</groupId>
        <artifactId>kaptcha</artifactId>
        <version>2.3.2</version>
    </dependency>
    
     <dependency>
            <groupId>jakarta.servlet</groupId>
            <artifactId>jakarta.servlet-api</artifactId>
            <version>6.0.0</version>
            <scope>provided</scope>
      </dependency>
</dependencies>

2. 配置验证码生成器

使用 Kaptcha 库生成图片验证码。首先,配置 Kaptcha 的 Bean。

@Configuration
public class KaptchaConfig {

    @Bean
    public Producer kaptchaProducer() {
        Properties properties = new Properties();
        properties.put("kaptcha.border", "no");
        properties.put("kaptcha.textproducer.font.color", "black");
        properties.put("kaptcha.textproducer.char.space", "5");
        properties.put("kaptcha.image.width", "125");
        properties.put("kaptcha.image.height", "45");
        properties.put("kaptcha.textproducer.char.len", "5");
        properties.put("kaptcha.noise.impl", "com.google.code.kaptcha.impl.NoNoise");

        DefaultKaptcha kaptcha = new DefaultKaptcha();
        kaptcha.setConfig(new Config(properties));
        return kaptcha;
    }
}

3. 创建验证码控制器

创建一个控制器,用于生成和显示验证码图片。

@RestController
public class KaptchaController {
    @Autowired
    private Producer kaptchaProducer;

    @GetMapping("/captcha")
    public void getKaptcha(HttpServletResponse response, HttpSession session) throws IOException, IOException {
        response.setDateHeader("Expires", 0);
        response.setHeader("Cache-Control", "no-store, no-cache, must-revalidate");
        response.addHeader("Cache-Control", "post-check=0, pre-check=0");
        response.setHeader("Pragma", "no-cache");
        response.setContentType("image/jpeg");

        String capText = kaptchaProducer.createText();
        session.setAttribute("captcha", capText);

        BufferedImage bi = kaptchaProducer.createImage(capText);
        ServletOutputStream out = response.getOutputStream();
        ImageIO.write(bi, "jpg", out);
        try {
            out.flush();
        } finally {
            out.close();
        }
    }
}

4. 修改登录页面

在登录页面login.html中添加验证码输入框和验证码图片。

<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head>
    <title>Login</title>
</head>
<body>
<form th:action="@{/login}" method="post">
    <div><label>Username: <input type="text" name="username"></label></div>
    <div><label>Password: <input type="password" name="password"></label></div>
    <div><label>Captcha: <input type="text" name="captcha"></label></div>
    <div><img th:src="@{/captcha}" alt="captcha" onclick="this.src='/captcha?'+new Date()" style="cursor:pointer;"></div>
    <div><input type="submit" value="Sign In"></div>
</form>
</body>
</html>

在这里插入图片描述
在这里插入图片描述

5. 修改 SecurityConfig

SecurityConfig 中添加验证码校验逻辑。

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    @Autowired
    private CustomUserDetailsService userDetailsService;

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
                .csrf(csrf -> csrf.disable())
                .authorizeHttpRequests(authorize -> authorize
                        .requestMatchers("/admin/**").hasRole("ADMIN")
                        .requestMatchers("/user/**").hasRole("USER")
                        .requestMatchers("/", "/home", "/register", "/captcha").permitAll()
                        .anyRequest().authenticated()
                )
                .formLogin(form -> form
                        .loginPage("/login").permitAll()
                        .defaultSuccessUrl("/home", true)
                )
                .logout(logout -> logout.permitAll())
                .addFilterBefore(new CaptchaFilter(), UsernamePasswordAuthenticationFilter.class);

        return http.build();
    }

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

    @Bean
    public AuthenticationManager authenticationManager(AuthenticationConfiguration config) throws Exception {
        return config.getAuthenticationManager();
    }

6. 创建验证码过滤器

创建一个自定义过滤器,用于校验验证码。


public class CaptchaFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(
            HttpServletRequest request,
            HttpServletResponse response,
            FilterChain filterChain
    ) throws ServletException, IOException {

        // 仅拦截登录请求(POST 方法)
        if ("POST".equalsIgnoreCase(request.getMethod())
                && "/login".equals(request.getRequestURI())) {

            // 从前端获取验证码参数(根据方案一或二调整名称)
            String inputCaptcha = request.getParameter("captcha");
            String sessionCaptcha = (String) request.getSession().getAttribute("captcha");

            // 校验逻辑
            if (inputCaptcha == null || inputCaptcha.isEmpty()
                    || !inputCaptcha.equalsIgnoreCase(sessionCaptcha)) {

                // 清除旧验证码并记录错误
                request.getSession().removeAttribute("captcha");
                request.getSession().setAttribute("captchaError", "验证码错误");
                response.sendRedirect("/login?error");
                return;
            }

            // 验证通过后清除验证码(避免重复使用)
            request.getSession().removeAttribute("captcha");
        }

        filterChain.doFilter(request, response);
    }
}

四、高级用法

1. 短信验证码

除了图片验证码,你还可以使用短信验证码。以下是一个简单的实现示例:

添加依赖
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-core</artifactId>
    <version>4.5.0</version>
</dependency>
<dependency>
    <groupId>com.aliyun</groupId>
    <artifactId>aliyun-java-sdk-dysmsapi</artifactId>
    <version>1.1.0</version>
</dependency>
配置短信服务
@Service
public class SmsService {

    public void sendSms(String phone, String code) {
        DefaultProfile profile = DefaultProfile.getProfile("cn-hangzhou", "your-access-key-id", "your-access-key-secret");
        IAcsClient client = new DefaultAcsClient(profile);

        CommonRequest request = new CommonRequest();
        request.setMethod(MethodType.POST);
        request.setDomain("dysmsapi.aliyuncs.com");
        request.setVersion("2017-05-25");
        request.setAction("SendSms");

        request.putQueryParameter("RegionId", "cn-hangzhou");
        request.putQueryParameter("PhoneNumbers", phone);
        request.putQueryParameter("SignName", "your-sign-name");
        request.putQueryParameter("TemplateCode", "your-template-code");
        request.putQueryParameter("TemplateParam", "{\"code\":\"" + code + "\"}");

        try {
            CommonResponse response = client.getCommonResponse(request);
            System.out.println(response.getData());
        } catch (ServerException e) {
            e.printStackTrace();
        } catch (ClientException e) {
            e.printStackTrace();
        }
    }
}
修改登录页面

在登录页面中添加短信验证码输入框。

<div><label>SMS Captcha: <input type="text" name="smsCaptcha"></label></div>
修改验证码过滤器

在验证码过滤器中添加短信验证码校验逻辑。

public class CaptchaFilter extends OncePerRequestFilter {

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
        if ("/login".equals(request.getRequestURI())) {
            String captcha = request.getParameter("captcha");
            String smsCaptcha = request.getParameter("smsCaptcha");
            String sessionCaptcha = (String) request.getSession().getAttribute("captcha");
            String sessionSmsCaptcha = (String) request.getSession().getAttribute("smsCaptcha");

            if (captcha == null || !captcha.equalsIgnoreCase(sessionCaptcha)) {
                request.getSession().setAttribute("captchaError", "Invalid captcha");
                response.sendRedirect("/login");
                return;
            }

            if (smsCaptcha == null || !smsCaptcha.equalsIgnoreCase(sessionSmsCaptcha)) {
                request.getSession().setAttribute("smsCaptchaError", "Invalid SMS captcha");
                response.sendRedirect("/login");
                return;
            }
        }
        filterChain.doFilter(request, response);
    }
}

五、常见问题与解决方案

1. 验证码不显示

原因:Kaptcha 配置不正确,或服务器未正确返回图片。

解决方案

  • 检查 Kaptcha 的配置是否正确。
  • 确保 KaptchaController 中的 getKaptcha 方法返回正确的图片。

2. 验证码校验失败

原因:验证码输入错误,或验证码已过期。

解决方案

  • 确保用户输入的验证码正确。
  • 检查验证码的有效期是否过期。

3. 短信验证码发送失败

原因:短信服务配置不正确,或网络问题。

解决方案

  • 检查短信服务的配置是否正确。
  • 确保网络连接正常。

六、总结

本文详细介绍了如何在 Spring Security 中添加验证码功能,包括图片验证码和短信验证码。通过合理使用验证码,可以显著增强应用的安全性。希望本文能帮助你更好地理解和使用 Spring Security 添加验证码。

如果你在使用过程中遇到任何问题,欢迎在评论区留言交流。感谢你的阅读,希望这篇文章对你有所帮助!


网站公告

今日签到

点亮在社区的每一天
去签到