Spring Security 如何防止暴力破解

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

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

一 封装登录计数器

@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 地址的失败尝试次数。

@Component
public class AuthenticationFailureListener implements
        ApplicationListener<AuthenticationFailureBadCredentialsEvent> {

    @Autowired
    private LoginAttemptService loginAttemptService;

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

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

@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 地址是否已被封锁。

@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 已被封锁。

@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 中实现了一个基本的暴力破解防御机制。不过在实际开发中,我们还可以采用更复杂的策略,如结合验证码、多因素认证等。

转发于:江南一点雨

扫码领红包

微信赞赏支付宝扫码领红包

发表回复

后才能评论