通过这个 Spring 安全性教程,我将指导您如何通过实现限制登录尝试功能来加强 Spring Boot 应用程序的安全性,该功能可防止暴力猜测可能被黑客利用的密码。您将学习如何使用以下策略实现限制登录尝试次数功能:
假设您正在开发一个基于 Spring Boot 的 Java Web 应用程序,并且已经使用 Spring 安全性和 MySQL 数据库(存储用户信息)实现了身份验证。作为通用标准,Thymeleaf用作模板引擎,Spring Data JPA和Hibernate用于数据访问层,HTML 5和Bootstrap用于UI。
假设用户信息存储在名为users 的表中。您需要添加 3 个额外的列才能实现限制登录尝试功能。它们是:
下图显示了包含 3 个新列的用户表的结构:然后更新实体类User,如下所示:在这里,我们添加 3 个新字段,这些字段相应地映射到users表中的 3 个新列。

|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
import javax.persistence.*;
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Integer id;
private String email;
private String password;
private String firstName;
private String lastName;
private boolean enabled;
@Column(name = "account_non_locked")
private boolean accountNonLocked;
@Column(name = "failed_attempt")
private int failedAttempt;
@Column(name = "lock_time")
private Date lockTime;
// constructors...
// getters...
// setters...
}
|
使用 Spring 安全性实现身份验证时,您已经创建了一个类型为“用户详细信息”的类来保存经过身份验证的用户的详细信息。因此,请确保isAccountNonLocked() 方法更新如下:这很重要,因为如果此方法返回 false,Spring Security 将拒绝身份验证。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
|
import org.springframework.security.core.userdetails.UserDetails;
public class ShopmeUserDetails implements UserDetails {
private User user;
public ShopmeUserDetails(User user) {
this.user = user;
}
@Override
public boolean isAccountNonLocked() {
return user.isAccountNonLocked();
}
// other overridden methods...
}
|
接下来,您需要在UserRepository类中声明一个方法,以根据用户的电子邮件更新用户的失败登录尝试次数,如下所示:如您所见,我们使用自定义查询(JPA 查询)。并通过实现 4 个新方法和几个常量来更新业务类UserServices,如下所示:让我解释一下这个新代码。首先,我们声明允许的最大失败登录尝试次数:以及锁定时间的持续时间(以毫秒为单位):因此很容易配置/更改允许的最大失败登录次数和锁定持续时间。让我们来看看新方法:
|
1
2
3
4
5
6
7
8
9
|
import org.springframework.data.jpa.repository.*;
public interface UserRepository extends JpaRepository
@Query("UPDATE User u SET u.failedAttempt = ?1 WHERE u.email = ?2")
@Modifying
public void updateFailedAttempts(int failAttempts, String email);
}
|
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
@Service
@Transactional
public class UserServices {
public static final int MAX_FAILED_ATTEMPTS = 3;
private static final long LOCK_TIME_DURATION = 24 * 60 * 60 * 1000; // 24 hours
@Autowired
private UserRepository repo;
public void increaseFailedAttempts(User user) {
int newFailAttempts = user.getFailedAttempt() + 1;
repo.updateFailedAttempts(newFailAttempts, user.getEmail());
}
public void resetFailedAttempts(String email) {
repo.updateFailedAttempts(0, email);
}
public void lock(User user) {
user.setAccountNonLocked(false);
user.setLockTime(new Date());
repo.save(user);
}
public boolean unlockWhenTimeExpired(User user) {
long lockTimeInMillis = user.getLockTime().getTime();
long currentTimeInMillis = System.currentTimeMillis();
if (lockTimeInMillis + LOCK_TIME_DURATION < currentTimeInMillis) {
user.setAccountNonLocked(true);
user.setLockTime(null);
user.setFailedAttempt(0);
repo.save(user);
return true;
}
return false;
}
}
|
|
1
|
public static final int MAX_FAILED_ATTEMPTS = 3;
|
|
1
|
private static final long LOCK_TIME_DURATION = 24 * 60 * 60 * 1000; // 24 hours
|
在自定义登录页面中,确保它包含以下代码片段,以便在登录失败时显示异常消息。在登录页面中包含此代码很重要,因此它将显示 Spring 生成的原始错误消息 Security.To 了解有关使用 Spring 安全性编写自定义登录页面的更多信息,请参阅本文。
|
1
2
3
|
<div th:if="${param.error}">
<p class="text-danger">[[${session.SPRING_SECURITY_LAST_EXCEPTION.message}]]p>
div>
|
接下来,我们需要编写一个自定义身份验证失败处理程序类来干预 Spring Security 的身份验证过程,以便更新失败的登录尝试次数,锁定和解锁用户帐户。因此,使用以下代码创建一个新类CustomLoginFailureHandler:让我解释一下这段代码。首先,它根据在登录页面中输入的电子邮件获取一个 User 对象。如果在数据库中找到该用户,并且该用户已启用且未锁定:
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
|
package com.shopme.admin.security;
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.LockedException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;
@Component
public class CustomLoginFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private UserServices userService;
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
String email = request.getParameter("email");
User user = userService.getByEmail(email);
if (user != null) {
if (user.isEnabled() && user.isAccountNonLocked()) {
if (user.getFailedAttempt() < UserServices.MAX_FAILED_ATTEMPTS - 1) {
userService.increaseFailedAttempts(user);
} else {
userService.lock(user);
exception = new LockedException("Your account has been locked due to 3 failed attempts."
+ " It will be unlocked after 24 hours.");
}
} else if (!user.isAccountNonLocked()) {
if (userService.unlockWhenTimeExpired(user)) {
exception = new LockedException("Your account has been unlocked. Please try to login again.");
}
}
}
super.setDefaultFailureUrl("/login?error");
super.onAuthenticationFailure(request, response, exception);
}
}
|
如果用户的帐户被锁定,它将在锁定持续时间到期时尝试解锁帐户。请注意,我们抛出LockedException(由 Spring Security 定义)以及自定义错误消息,该错误消息将显示在登录页面中。阅读本文以了解有关 Spring 安全性中的身份验证失败处理程序的更多信息。
在某些情况下,用户第一次(或第二次)登录失败,但下次登录成功。因此,应用程序应在用户成功登录后立即清除失败的登录尝试次数。为此,您需要使用以下代码创建自定义身份验证处理程序类:如您所见,用户成功登录后,应用程序会将失败的登录尝试次数重置为零。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
|
import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;
@Component
public class CustomLoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
@Autowired
private UserServices userService;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {
ShopmeUserDetails userDetails = (ShopmeUserDetails) authentication.getPrincipal();
User user = userDetails.getUser();
if (user.getFailedAttempt() > 0) {
userService.resetFailedAttempts(user.getEmail());
}
super.onAuthenticationSuccess(request, response, authentication);
}
}
|
要启用上面的自定义身份验证失败和成功处理程序,您需要更新 Spring 安全性配置类,如下所示:这是为编码完成的。接下来,我们准备测试限制登录尝试函数。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
|
@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
@Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests()
.antMatchers("/login").permitAll()
...
.anyRequest().authenticated()
...
.formLogin()
.loginPage("/login")
.usernameParameter("email")
.failureHandler(loginFailureHandler)
.successHandler(loginSuccessHandler)
.permitAll()
...
}
@Autowired
private CustomLoginFailureHandler loginFailureHandler;
@Autowired
private CustomLoginSuccessHandler loginSuccessHandler;
}
|
在测试之前,请确保新列具有默认值:0 表示failed_attempt,true 表示account_non_locked,null 表示lock_time。启动您的 Spring 引导应用程序并转到登录页面。输入正确的用户名但输入错误的密码,您将在第一次失败的登录尝试中看到以下错误:检查数据库,您应该看到failed_attempt= 1。现在尝试再次使用错误的密码登录。您会看到相同的错误,但failed_attempt= 2.接下来,尝试进行第三次失败的登录,它说用户帐户已被锁定:检查数据库,您应该看到failed_attempt= 2,account_non_locked= 0(false)并且lock_time设置为特定时间。然后,用户将无法在 24 小时内登录,因为他的帐户被锁定。等待锁定时间到期(为了快速测试,您可以将锁定持续时间更改为 5 分钟),然后使用正确的凭据再次登录。用户将看到此屏幕:并且用户必须再次登录(使用正确的凭据)。检查数据库,您应该看到新 3 列的值已设置为默认值。这是一些代码示例和参考,供您在 Spring 引导应用程序中实现限制登录尝试功能。要了解实际编码,我建议您观看以下视频:


