[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 添加验证码。
如果你在使用过程中遇到任何问题,欢迎在评论区留言交流。感谢你的阅读,希望这篇文章对你有所帮助!