知識庫 / Spring / Spring Security RSS 訂閱

阻止暴力破解身份驗證嘗試(使用 Spring Security)

Spring Security
HongKong
4
03:01 PM · Dec 06 ,2025

1. 概述

在本快速教程中,我們將使用 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 || xfHeader.isEmpty() || !xfHeader.contains(request.getRemoteAddr())) {
            loginAttemptService.loginFailed(request.getRemoteAddr());
        } else {
            loginAttemptService.loginFailed(xfHeader.split(",")[0]);
        }
    }
}

請注意,當身份驗證失敗時,我們通知 LoginAttemptService 以失敗嘗試的來源 IP 地址。這裏,我們從 HttpServletRequest Bean 中獲取 IP 地址,該 Bean 還為通過例如代理服務器轉發的請求提供了原始地址,該地址位於 X-Forwarded-For 標頭中。

我們也注意到,X-Forwarded-For 標頭是多值的,並且可以輕鬆地偽造原始 IP 地址。因此,我們不應假設該標頭是可信的;相反,我們必須檢查它是否包含請求的遠程地址。否則,攻擊者可以在標頭的第一個索引中設置與自己不同的 IP 地址,以避免阻止自己的 IP 地址。如果我們在這些 IP 地址中阻止一個,攻擊者可以添加另一個 IP 地址,依此類推。這意味着他可以暴力破解標頭的 IP 地址,從而偽造請求。

3. 登錄嘗試服務 (LoginAttemptService)

現在,我們來討論一下我們的 LoginAttemptService 實現;簡單來説,我們記錄每個 IP 地址在 24 小時內錯誤的嘗試次數。該塊方法將檢查給定 IP 地址的請求是否超過了允許的限制。

@Service
public class LoginAttemptService {

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

    @Autowired
    private HttpServletRequest request;

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

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

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

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

以下是 getClientIP() 方法:

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

請注意,我們有額外的邏輯來識別客户端的原始 IP 地址。在大多數情況下,這並不必要,但在某些網絡場景中,卻是必需的。

對於這些罕見場景,我們使用 X-Forwarded-For 標頭來獲取原始 IP 地址;以下是該標頭的語法:

X-Forwarded-For: clientIpAddress, proxy1, proxy2

請注意,一次未成功的身份驗證嘗試會增加該 IP 地址的嘗試次數,但對於成功的身份驗證,計數器不會被重置。

從這一點上講,它只是簡單地檢查身份驗證時計數器的過程。

4. 用户詳情服務 (UserDetailsService)

現在,讓我們在自定義的 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 {
        if (loginAttemptService.isBlocked()) {
            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);
        }
    }
}

此外,請注意 Spring 還有一個非常有趣的特性:我們需要 HTTP 請求,因此我們只是將其硬編碼進去。

現在,這很酷。我們需要在我們的 web.xml 中添加一個快速監聽器,才能使其生效,並且這使得事情變得更加簡單。

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

大致就是這樣——我們已經定義了這個新的 RequestContextListener 在我們的 web.xml 中,以便能夠訪問來自 UserDetailsService 的請求。

5. 修改 AuthenticationFailureHandler

最後,讓我們修改我們的 CustomAuthenticationFailureHandler 以自定義我們的新錯誤消息。

我們處理用户實際被阻止 24 小時的情況——並且向用户告知其 IP 被阻止,因為他超過了允許的最大錯誤的身份驗證嘗試。在此類中,我們還會在每次失敗時檢查,以確定用户是否被阻止:

@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private MessageSource messages;

    @Autowired
    private LocaleResolver localeResolver;

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private LoginAttemptService loginAttemptService;

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

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

        ...
    }
}

6. 結論

重要的是要理解,這只是應對暴力破解密碼嘗試的初步步驟,但同時也意味着還有改進的空間。一套生產級別的暴力破解防禦策略可能比僅限IP阻止包含更多元素。

user avatar
0 位用戶收藏了這個故事!
收藏

發佈 評論

Some HTML is okay.