Spring Security 防止暴力认证尝试

一、概述

在本快速教程中,我们将实施一个基本解决方案,以防止使用 Spring Security进行**暴力认证尝试。**

简单地说——我们将记录来自单个 IP 地址的失败尝试次数。如果该特定 IP 超过一定数量的请求 - 它将被阻止 24 小时。

2. AuthenticationFailureListener

让我们从定义AuthenticationFailureListener开始——监听AuthenticationFailureBadCredentialsEvent事件并通知我们身份验证失败:

@Component
public class AuthenticationFailureListener implements 
  ApplicationListener<AuthenticationFailureBadCredentialsEvent> {

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private LoginAttemptService loginAttemptService;

    @Override
    public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent e) {
        final String xfHeader = request.getHeader("X-Forwarded-For");
        if (xfHeader == null) {
            loginAttemptService.loginFailed(request.getRemoteAddr());
        } else {
            loginAttemptService.loginFailed(xfHeader.split(",")[0]);
        }
    }
}

请注意,当身份验证失败时,我们如何通知LoginAttemptService失败尝试来自的IP地址。在这里,我们从HttpServletRequest bean中获得IP地址,它也为我们提供了X-Forwarded-For报头中的原始地址,用于由例如代理服务器转发的请求。

3. AuthenticationSuccessEventListener

我们还定义一个AuthenticationSuccessEventListener——它监听AuthenticationSuccessEvent事件并通知我们身份验证成功:

@Component
public class AuthenticationSuccessEventListener implements 
  ApplicationListener<AuthenticationSuccessEvent> {
    
    @Autowired
    private HttpServletRequest request;

    @Autowired
    private LoginAttemptService loginAttemptService;

    @Override
    public void onApplicationEvent(final AuthenticationSuccessEvent e) {
        final String xfHeader = request.getHeader("X-Forwarded-For");
        if (xfHeader == null) {
            loginAttemptService.loginSucceeded(request.getRemoteAddr());
        } else {
            loginAttemptService.loginSucceeded(xfHeader.split(",")[0]);
        }
    }
}

请注意 - 与失败侦听器类似,我们通知LoginAttemptService身份验证请求源自的 IP 地址。

4. 登录尝试服务

现在——让我们讨论我们的LoginAttemptService实现;简单地说——我们将每个 IP 地址的错误尝试次数保留 24 小时:

@Service
public class LoginAttemptService {

    private final int MAX_ATTEMPT = 10;
    private LoadingCache<String, Integer> attemptsCache;

    public LoginAttemptService() {
        super();
        attemptsCache = CacheBuilder.newBuilder().
          expireAfterWrite(1, TimeUnit.DAYS).build(new CacheLoader<String, Integer>() {
            public Integer load(String key) {
                return 0;
            }
        });
    }

    public void loginSucceeded(String key) {
        attemptsCache.invalidate(key);
    }

    public void loginFailed(String key) {
        int attempts = 0;
        try {
            attempts = attemptsCache.get(key);
        } catch (ExecutionException e) {
            attempts = 0;
        }
        attempts++;
        attemptsCache.put(key, attempts);
    }

    public boolean isBlocked(String key) {
        try {
            return attemptsCache.get(key) >= MAX_ATTEMPT;
        } catch (ExecutionException e) {
            return false;
        }
    }
}

请注意不成功的身份验证尝试如何增加该 IP 的尝试次数,而成功的身份验证会重置该计数器。

从这一点来看,这只是我们验证时检查计数器的问题。

5. 用户详情服务

现在,让我们在自定义的UserDetailsService实现中添加额外的检查;当我们加载UserDetails时,我们首先需要检查这个 IP 地址是否被阻止

@Service("userDetailsService")
@Transactional
public class MyUserDetailsService implements UserDetailsService {
 
    @Autowired
    private UserRepository userRepository;
 
    @Autowired
    private RoleRepository roleRepository;
 
    @Autowired
    private LoginAttemptService loginAttemptService;
 
    @Autowired
    private HttpServletRequest request;
 
    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        String ip = getClientIP();
        if (loginAttemptService.isBlocked(ip)) {
            throw new RuntimeException("blocked");
        }
 
        try {
            User user = userRepository.findByEmail(email);
            if (user == null) {
                return new org.springframework.security.core.userdetails.User(
                  " ", " ", true, true, true, true, 
                  getAuthorities(Arrays.asList(roleRepository.findByName("ROLE_USER"))));
            }
 
            return new org.springframework.security.core.userdetails.User(
              user.getEmail(), user.getPassword(), user.isEnabled(), true, true, true, 
              getAuthorities(user.getRoles()));
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
}

这是*getClientIP()*方法:

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

请注意,我们有一些额外的逻辑来识别 Client 的原始 IP 地址。在大多数情况下,这不是必需的,但在某些网络场景中,它是必需的。

对于这些罕见的情况,我们使用X-Forwarded-For标头来获取原始 IP;这是此标头的语法:

X-Forwarded-For: clientIpAddress, proxy1, proxy2

另外,请注意 Spring 的另一个非常有趣的功能——我们需要 HTTP 请求,所以我们只是将它连接起来。

现在,这很酷。我们必须在我们的web.xml中添加一个快速侦听器才能使其工作,它使事情变得容易得多。

<listener>
    <listener-class>
        org.springframework.web.context.request.RequestContextListener
    </listener-class>
</listener>

就是这样——我们在web.xml中定义了这个新的**RequestContextListener以便能够访问来自UserDetailsService的请求。

6. 修改AuthenticationFailureHandler

最后 - 让我们修改我们的CustomAuthenticationFailureHandler以自定义我们的新错误消息。

我们正在处理用户确实被阻止 24 小时的情况——我们通知用户他的 IP 被阻止是因为他超过了允许的最大错误身份验证尝试次数:

@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private MessageSource messages;

    @Override
    public void onAuthenticationFailure(...) {
        ...

        String errorMessage = messages.getMessage("message.badCredentials", null, locale);
        if (exception.getMessage().equalsIgnoreCase("blocked")) {
            errorMessage = messages.getMessage("auth.message.blocked", null, locale);
        }

        ...
    }
}

7. 结论

了解这是处理暴力密码尝试的良好开端很重要,但还有改进的余地。生产级暴力破解策略可能涉及比 IP 块更多的元素。

本教程的完整实现可以在github 项目中找到。

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值