Spring Security 如何防止暴力破解?

在构建安全的 Web 应用时,防御暴力破解攻击是一个关键的考虑因素。Spring Security 提供了一套灵活的机制来帮助我们实现这一目标。本文将介绍如何使用 Spring Security 来记录失败的登录尝试,并在尝试次数超过一定阈值时封锁攻击者的 IP 地址。

亦可基于本文的思路实现封锁攻击者的账号。

一 封装登录计数器

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
@Service
public class LoginAttemptService {

@Autowired
RedisTemplate redisTemplate;

public static final int MAX_ATTEMPT = 10;

@Autowired
HttpServletRequest request;

public void loginFailed() {
Integer attempts = null;
attempts = (Integer) redisTemplate.opsForValue().get(getClientIP());
if (attempts == null) {
attempts = 0;
}
attempts++;
redisTemplate.opsForValue().set(getClientIP(), attempts);
redisTemplate.expire(getClientIP(), 1, TimeUnit.DAYS);
}

public boolean isBlocked() {
Integer attempts = (Integer) redisTemplate.opsForValue().get(getClientIP());
return attempts != null && attempts >= MAX_ATTEMPT;
}

private String getClientIP() {
final String xfHeader = request.getHeader("X-Forwarded-For");
if (xfHeader != null) {
return xfHeader.split(",")[0];
}
return request.getRemoteAddr();
}

public void loginSuccess() {
redisTemplate.delete(getClientIP());
}
}

这里核心方法就三个:

  1. loginFailed:登录失败的时候调用该方法,以当前登录 IP 为 key,在 Redis 上保存登录失败的次数,并设置过期时间为 1 天。
  2. isBlocked:判断当前登录 IP 是否已经达到最大重试次数,达到了就要禁用该 IP 地址了(禁止时间为 1 天)。
  3. loginSuccess:登录成功后,清除该 IP 地址登录失败的记录。

二 监听器

前面 LoginAttemptService 中的方法,我们分别在登录成功和登录失败的监听器中进行调用。

首先,我们将创建一个 AuthenticationFailureListener,用于监听认证失败事件,并记录相关 IP 地址的失败尝试次数。

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class AuthenticationFailureListener implements
ApplicationListener<AuthenticationFailureBadCredentialsEvent> {

@Autowired
private LoginAttemptService loginAttemptService;

@Override
public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent e) {
loginAttemptService.loginFailed();
}
}

接下来再创建一个登录成功的监听器,用来清除当前 IP 登录失败的次数:

1
2
3
4
5
6
7
8
9
10
11
12
@Component
public class AuthenticationSuccessListener implements
ApplicationListener<AuthenticationSuccessEvent> {

@Autowired
private LoginAttemptService loginAttemptService;

@Override
public void onApplicationEvent(AuthenticationSuccessEvent event) {
loginAttemptService.loginSuccess();
}
}

Spring Security 在登录成功或者失败的时候都会自动发布相关事件,我们这里只需要定义监听器就可以获取到事件,相关源码在 ProviderManager#authenticate 方法中,如下:

三 修改登录逻辑

在自定义的 UserDetailsService 实现中,我们需要在加载用户详情之前检查请求的 IP 地址是否已被封锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
@Service
public class UserService implements UserDetailsService {

@Autowired
LoginAttemptService loginAttemptService;
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
if (loginAttemptService.isBlocked()) {
throw new RuntimeException("blocked");
}
return User.builder().username(username).password("{noop}123").authorities("admin").build();
}
}

四 自定义认证失败处理器

最后,我们将修改 CustomAuthenticationFailureHandler,以自定义错误消息并通知用户其 IP 已被封锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {
@Autowired
private MessageSource messages;
@Autowired
private HttpServletRequest request;
@Autowired
private LoginAttemptService loginAttemptService;

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
// ... 错误处理逻辑
}
}

五 结论

通过上述步骤,我们已经在 Spring Security 中实现了一个基本的暴力破解防御机制。不过在实际开发中,我们还可以采用更复杂的策略,如结合验证码、多因素认证等。

如果小伙伴们想要深入全面的学习 Spring Security,可以看看我最近录的最新版的 Spring Security+OAuth2 视频教程,这是视频目录:

这套视频教程基本上把 Spring Security 的方方面面以及 OAuth2 都讲到了。最重要的是,贴合了很多小伙伴们日常常见的开发场景,比如短信验证码登录,微信 OAuth2 登录等等都有讲到。

这套视频是基于目前最新版的 Spring Security6 录制的,Spring Security6 在 API 层面的变化还是蛮大的,引入了大量的 Lambda 表达式去简化配置,很多旧版的写法在 Spring Security6 中被废弃,并在将来在 Spring Security7 中会移除相关的 API,所以说最近的 Spring Security 更新还是蛮激进的。

另一方面就是这套视频包含了全新的基于目前最新版 Spring Security 录制的 OAuth2 教程,松哥在 2020 年的时候出过图文版的 OAuth2 教程,但是,现在新版的 OAuth2 也有很多变化,不仅仅是 API 层面的变化,授权模式也发生了一些变化,传统的密码模式、简化模式现在都不再支持,转而引入了 PKCE 模式,并且利用 OIDC 简化用户信息获取。同时,在 2020 年还处于萌芽状态的 Spring Security OAuth2 Server 这个项目,目前也趋于成熟,也可以直接使用了,这些松哥都在视频中和大家做了详细介绍。在 OAuth2 环节我也和大家分享了如何使用微信的 OAuth2 登录。

总之这一套教程,让大家彻底理解系统的安全管理。

之前有小伙伴说我直接自己写过滤器,既灵活还简单。我并不反对这种做法,但是有一个前提就是你很牛,你自己写的过滤器有考虑到计时攻击,有考虑到 XSS 攻击,有考虑到点击劫持,有考虑跨站请求伪造。。。等等太多了。当然,这些问题如果你都没有考虑到,那么 Spring Security 都有帮你考虑到并提供解决方案!

理解了 Spring Security,再去看市面上其他的安全管理框架,都会豁然开朗。

这套教程目前给小伙伴们提供了两个试看视频:

这套视频是付费的,¥499,有需要的小伙伴加微信备注 499 上车。