Prevent Brute Force Authentication Attempts with Spring Security – 用Spring Security防止蛮力认证的尝试

最后修改: 2015年 2月 14日

中文/混合/英文(键盘快捷键:t)

1. Overview

1.概述

In this quick tutorial, we’ll implement a basic solution for preventing brute force authentication attempts using Spring Security.

在这个快速教程中,我们将使用Spring Security实现一个基本的解决方案,以防止brute force认证尝试

Simply put – we’ll keep a record of the number of failed attempts originating from a single IP address. If that particular IP goes over a set number of requests – it will be blocked for 24 hours.

简单地说–我们会记录下来自一个IP地址的失败尝试次数。如果这个特定的IP超过了设定的请求数,它将被封锁24小时。

2. An AuthenticationFailureListener

2.一个AuthenticationFailureListener

Let’s start by defining an AuthenticationFailureListener – to listen to AuthenticationFailureBadCredentialsEvent events and notify us of an authentication failure:

让我们先定义一个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]);
        }
    }
}

Note how, when authentication fails, we inform the LoginAttemptService of the IP address from where the unsuccessful attempt originated. Here, we get the IP address from the HttpServletRequest bean, which also gives us the originating address in the X-Forwarded-For header for requests that are forwarded by e.g. a proxy server.

请注意,当认证失败时,我们如何通知LoginAttemptService这个不成功的尝试来自哪个IP地址。在这里,我们从HttpServletRequest Bean中获得了IP地址,它也在X-Forwarded-For头中为由代理服务器等转发的请求提供了原发地址。

3. An AuthenticationSuccessEventListener

3.一个AuthenticationSuccessEventListener

Let’s also define an AuthenticationSuccessEventListener – which listens for AuthenticationSuccessEvent events and notifies us of a successful authentication:

让我们也定义一个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]);
        }
    }
}

Note how – similar to the failure listener, we’re notifying the LoginAttemptService of the IP address from which the authentication request originated.

请注意–与失败监听器类似,我们要通知LoginAttemptService认证请求来源的IP地址。

4. The LoginAttemptService

4、LoginAttemptService

Now – let’s discuss our LoginAttemptService implementation; simply put – we keep the number of wrong attempts per IP address for 24 hours:

现在–让我们讨论一下我们的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;
        }
    }
}

Notice how an unsuccessful authentication attempt increases the number of attempts for that IP, and the successful authentication resets that counter.

请注意,不成功的认证尝试会增加该IP的尝试次数,而成功的认证则会重置该计数器。

From this point, it’s simply a matter of checking the counter when we authenticate.

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

5. The UserDetailsService

5.UserDetailsService

Now, let’s add the extra check in our custom UserDetailsService implementation; when we load the UserDetails, we first need to check if this IP address is blocked:

现在,让我们在我们的自定义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);
        }
    }
}

And here is getClientIP() method:

这里是getClientIP()方法。

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

Notice that we have some extra logic to identify the original IP address of the Client. In most cases, that’s not going to be necessary, but in some network scenarios, it is.

注意,我们有一些额外的逻辑来识别客户的原始IP地址。在大多数情况下,这没有必要,但在一些网络场景中,这是必要的。

For these rare scenarios, we’re using the X-Forwarded-For header to get to the original IP; here’s the syntax for this header:

对于这些罕见的情况,我们使用X-Forwarded-For头来获得原始IP;这里是这个头的语法。

X-Forwarded-For: clientIpAddress, proxy1, proxy2

Also, notice another super-interesting capability that Spring has – we need the HTTP request, so we’re simply wiring it in.

此外,注意到Spring拥有的另一个超级有趣的能力–我们需要HTTP请求,所以我们简单地将它连接起来。

Now, that’s cool. We’ll have to add a quick listener into our web.xml for that to work, and it makes things a whole lot easier.

现在,这很酷。我们必须在我们的web.xml中添加一个快速监听器,这样才能工作,它使事情变得简单了许多。

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

That’s about it – we’ve defined this new RequestContextListener in our web.xml to be able to access the request from the UserDetailsService.

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

6. Modify AuthenticationFailureHandler

6.修改AuthenticationFailureHandler

Finally – let’s modify our CustomAuthenticationFailureHandler to customize our new error message.

最后–让我们修改我们的CustomAuthenticationFailureHandler以定制我们的新错误信息。

We’re handling the situation when the user actually does get blocked for 24 hours – and we’re informing the user that his IP is blocked because he exceeded the maximum allowed wrong authentication attempts:

我们要处理的情况是,用户确实被封锁了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. Conclusion

7.结论

It’s important to understand that this is a good first step in dealing with brute-force password attempts, but also that there’s room for improvement. A production-grade brute-force prevention strategy may involve more elements than an IP block.

重要的是要明白,这是处理暴力破解密码尝试的良好第一步,但也有改进的空间。一个生产级的暴力破解策略可能涉及比IP块更多的元素。

The full implementation of this tutorial can be found in the github project.

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