Spring Boot如何防暴力破解攻擊?

banq發表於2021-05-17

Spring Security在身份驗證和授權過程中為我們完成了許多工作。暴力破解是Web應用程式上的常見攻擊,惡意使用者會嘗試將密碼猜測作為暴力破解。Spring安全性是一個靈活的框架,並提供擴充套件點來擴充套件或使用核心功能。Spring Security不提供任何現成的功能來進行暴力保護,但提供了一些我們可以使用的擴充套件點。
在本文中,我們將構建Spring Security暴力保護以處理暴力攻擊。有多種處理這些攻擊的選項。
  1. 某些失敗的嘗試後鎖定帳戶。
  2. 裝置Cookie –鎖定未知裝置。
  3. 使用驗證碼可以防止自動攻擊。

在本文中,我們將研究第一個選項。我們將保留每次失敗和成功登入嘗試的艱苦跋涉,如果連續失敗的登入嘗試增加了一定的閾值,我們將鎖定/禁用該帳戶,並讓使用者進行密碼重置或任何其他步驟來響應該帳戶。
本文是Spring Security初學者課程的一部分,您可以從GitHub儲存庫中下載原始碼。
 

1. Spring安全認證事件
我們將使用Spring安全事件釋出功能來構建我們的暴力保護服務。對於成功或失敗的每個身份驗證,Spring安全性都會發布AuthenticationSuccessEvent或AuthenticationFailureEvent。我們將使用此功能來構建Spring安全性的暴力保護。這是我們策略的高階工作流程。

  1. 我們將編寫一個自定義身份驗證失敗事件偵聽器。該偵聽器將與基礎服務一起使用,以保持嘗試失敗次數的迷航,並在超過該次數時將其鎖定。
  2. 成功的身份驗證偵聽器可以重置任何失敗的計數(我們會將失敗的計數器重置為零)。

讓我們構建AuthenticationFailureEventListner一個偵聽特定事件的通知,並在任何身份驗證失敗的情況下通知我們。

package com.javadevjournal.core.security.event;

import com.javadevjournal.core.security.bruteforce.BruteForceProtectionService;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationListener;
import org.springframework.security.authentication.event.AuthenticationFailureBadCredentialsEvent;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;

@Component
public class AuthenticationFailureListener implements ApplicationListener < AuthenticationFailureBadCredentialsEvent > {

    private static Logger LOG = LoggerFactory.getLogger(AuthenticationFailureListener.class);

    @Resource(name = "bruteForceProtectionService")
    private BruteForceProtectionService bruteForceProtectionService;

    @Override
    public void onApplicationEvent(AuthenticationFailureBadCredentialsEvent event) {
        String username = event.getAuthentication().getName();
        LOG.info("********* login failed for user {} ", username);
        bruteForceProtectionService.registerLoginFailure(username);

    }

}

在此事件偵聽器中,監聽AuthenticationFailureBadCredentialsEvent,BruteForceProtectionService會將使用者ID資訊傳遞給我們,該資訊將檢查並在需要時禁用使用者帳戶。
與失敗事件處理程式類似,Spring安全性還將在成功身份驗證時釋出事件,我們將建立一個自定義成功處理程式。該處理程式會將控制元件移交給BruteForceProtectionService,以重置失敗的計數器。


@Component
public class AuthenticationSuccessListener implements ApplicationListener < AuthenticationSuccessEvent > {

    private static Logger LOG = LoggerFactory.getLogger(AuthenticationSuccessListener.class);

    @Resource(name = "bruteForceProtectionService")
    private BruteForceProtectionService bruteForceProtectionService;

    @Override
    public void onApplicationEvent(AuthenticationSuccessEvent event) {
        String username = event.getAuthentication().getName();
        LOG.info("********* login successful for user {} ", username);
        bruteForceProtectionService.resetBruteForceCounter(username);
    }
}

BruteForceProtectionService是Spring的安全蠻力保護的一部分,會執行以下任務:
  1. 對於每次失敗的登入嘗試,增加失敗的計數器。
  2. 檢查失敗計數是否超過允許的最大配置。
  3. 如果計數器失敗超過最大限制,則禁用該帳戶。
  4. BruteForceProtectionService還將在成功登入後重置計數器。


@Service("bruteForceProtectionService")
public class DefaultBruteForceProtectionService implements BruteForceProtectionService {

    @Value("${jdj.security.failedlogin.count}")
    private int maxFailedLogins;

    @Autowired
    UserRepository userRepository;

    @Value("${jdj.brute.force.cache.max}")
    private int cacheMaxLimit;

    private final ConcurrentHashMap < String, FailedLogin > cache;

    public DefaultBruteForceProtectionService() {
        this.cache = new ConcurrentHashMap < > (cacheMaxLimit); //setting max limit for cache
    }

    @Override
    public void registerLoginFailure(String username) {

        UserEntity user = getUser(username);
        if (user != null && !user.isLoginDisabled()) {
            int failedCounter = user.getFailedLoginAttempts();
            if (maxFailedLogins < failedCounter + 1) {
                user.setLoginDisabled(true); //disabling the account
            } else {
                //let's update the counter
                user.setFailedLoginAttempts(failedCounter + 1);
            }
            userRepository.save(user);
        }
    }

    @Override
    public void resetBruteForceCounter(String username) {
        UserEntity user = getUser(username);
        if (user != null) {
            user.setFailedLoginAttempts(0);
            user.setLoginDisabled(false);
            userRepository.save(user);
        }
    }

    @Override
    public boolean isBruteForceAttack(String username) {
        UserEntity user = getUser(username);
        if (user != null) {
            return user.getFailedLoginAttempts() >= maxFailedLogins ? true : false;
        }
        return false;
    }

    protected FailedLogin getFailedLogin(final String username) {
        FailedLogin failedLogin = cache.get(username.toLowerCase());

        if (failedLogin == null) {
            //setup the initial data
            failedLogin = new FailedLogin(0, LocalDateTime.now());
            cache.put(username.toLowerCase(), failedLogin);
            if (cache.size() > cacheMaxLimit) {

                // add the logic to remve the key based by timestamp
            }
        }
        return failedLogin;
    }

    private UserEntity getUser(final String username) {
        return userRepository.findByEmail(username);
    }

    public int getMaxFailedLogins() {
        return maxFailedLogins;
    }

    public void setMaxFailedLogins(int maxFailedLogins) {
        this.maxFailedLogins = maxFailedLogins;
    }

    public class FailedLogin {

        private int count;
        private LocalDateTime date;

        public FailedLogin() {
            this.count = 0;
            this.date = LocalDateTime.now();
        }

        public FailedLogin(int count, LocalDateTime date) {
            this.count = count;
            this.date = date;
        }

        public int getCount() {
            return count;
        }

        public void setCount(int count) {
            this.count = count;
        }

        public LocalDateTime getDate() {
            return date;
        }

        public void setDate(LocalDateTime date) {
            this.date = date;
        }
    }
}

確保我們能夠儲存和更新計數以及禁用帳戶,透過UserEntity和UserDetailsS​​ervice實現。

@Entity
@Table(name = "user")
public class UserEntity {

    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String firstName;
    private String lastName;
    @Column(unique = true)
    private String email;
    private String password;
    private String token;
    private boolean accountVerified;

    //new fields
    private int failedLoginAttempts;
    private boolean loginDisabled;
}
@Service("userDetailsService")
public class CustomUserDetailService implements UserDetailsService {

    @Autowired
    UserRepository userRepository;

    @Override
    public UserDetails loadUserByUsername(String email) throws UsernameNotFoundException {
        final UserEntity customer = userRepository.findByEmail(email);
        if (customer == null) {
            throw new UsernameNotFoundException(email);
        }
        boolean enabled = !customer.isAccountVerified();
        UserDetails user = User.withUsername(customer.getEmail())
            .password(customer.getPassword())
            .disabled(customer.isLoginDisabled())
            .authorities("USER").build();
        return user;
    }
}

請記住,一旦禁用帳戶,即使使用成功憑據進行的登入嘗試也不會解鎖該帳戶。要求使用者使用重置密碼功能來解鎖帳戶。
 

在登入頁面上顯示錯誤
上面的配置可以確保在兩次彈簧登入失敗嘗試後帳戶都被鎖定,這可能是由於Spring Security蠻力保護。我們還可能希望在我們的登入頁面上顯示自定義錯誤訊息,讓我們對自定義登入頁面及其控制器進行一些更改將以下條目新增到messages.properties檔案:

user.account.locked = Your account has been locked due to multiple failed login attempts.

一旦使用者超過失敗的登入嘗試,我們將在登入頁面上顯示以上訊息。下一步是對登入頁面控制器進行一些更改。

@Controller
@RequestMapping("/login")
public class LoginPageController {

    public static final String LAST_USERNAME_KEY = "LAST_USERNAME";

    @Resource(name = "customerAccountService")
    private CustomerAccountService customerAccountService;

    @GetMapping
    public String login(@RequestParam(value = "error", defaultValue = "false") boolean loginError,
        @RequestParam(value = "invalid-session", defaultValue = "false") boolean invalidSession,
        final Model model, HttpSession session) {

        String userName = getUserName(session);
        if (loginError) {
            if (StringUtils.isNotEmpty(userName) && customerAccountService.loginDisabled(userName)) {
                model.addAttribute("accountLocked", Boolean.TRUE);
                model.addAttribute("forgotPassword", new ResetPasswordData());
                return "account/login";
            }
        }
    }


    final String getUserName(HttpSession session) {
        final String username = (String) session.getAttribute(LAST_USERNAME_KEY);
        if (StringUtils.isNotEmpty(username)) {
            session.removeAttribute(LAST_USERNAME_KEY); // we don't need it and removing it.
        }
        return username;
    }
}

在登入控制器中做了一些重要的事情。
  1. 如果正在不同地處理登入錯誤請求引數。獲取此引數後,我們檢查是否鎖定了使用者帳戶。
  2. 如果它鎖定了使用者帳戶,我們會向客戶顯示不同的錯誤訊息。
  3. 如果您檢視getUserName()方法,我們將從會話中獲取使用者名稱。

預設情況下,一旦我們在登入控制器中收到控制元件,使用者名稱將在請求中不可用。要顯示自定義訊息,我們需要使用者名稱。我們正在使用Spring安全性失敗處理程式將使用者名稱儲存在會話中。
我們只將使用者名稱儲存在會話中,並讓預設的身份驗證處理程式AuthenticationFailureHandler在身份驗證失敗時執行其工作:

import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;

public class LoginAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    public static final String LAST_USERNAME_KEY = "LAST_USERNAME";

    public void onAuthenticationFailure(HttpServletRequest request,
        HttpServletResponse response, AuthenticationException exception)
    throws IOException, ServletException {
        request.getSession().setAttribute(LAST_USERNAME_KEY, request.getParameter("username"));
        super.onAuthenticationFailure(request, response, exception);
    }
}

在登入HTML中新增條件,以在前端顯示自定義錯誤訊息。我們在登入頁面新增了以下條件。

<div th:if="${param.error!=null and accountLocked ==true}">
   <div class="alert alert-danger">
      <span th:text="#{user.account.locked}"/>
   </div>
</div>

 

相關文章