Code ví dụ Spring Security Prevent Brute Force (Ngăn login sai nhiều lần)

Code ví dụ Spring Security Prevent Brute Force.

Tổng quan

Trên một số trang web hoặc một số thiết bị như iphone, máy tính… khi login nếu thực hiện nhập sai password nhiều lần nó sẽ lock máy và chỉ cho phép login lại sau một khoảng thời gian tầm 5 phút, 30 phút… tùy theo tầm quan trọng của dữ liệu.

Việc lock thiết bị/trang web như thế nhằm tránh việc tấn công vét cạn (Prevent Brute Force).

Ở bài này mình sẽ làm ví dụ Prevent Brute Force với Spring Security, cách làm của chúng ta sẽ là lưu lại IP của client, kiểm tra nếu cùng 1 IP mà  login sai quá 5 lần thì sẽ ngăn không cho login tiếp.

Mình sẽ sử dụng lại code ở ví dụ Spring Security Hibernate

Tạo Maven Project

Các thư viện sử dụng:

<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
  <modelVersion>4.0.0</modelVersion>
  <groupId>stackjava.com</groupId>
  <artifactId>SpringSecurityPreventBruteForce</artifactId>
  <version>0.0.1-SNAPSHOT</version>
  <packaging>war</packaging>

  <properties>
    <spring.version>5.0.2.RELEASE</spring.version>
    <spring.security.version>5.0.2.RELEASE</spring.security.version>
     <hibernate.version>5.2.12.Final</hibernate.version>
    <jstl.version>1.2</jstl.version>

  </properties>

  <dependencies>

    <!-- Spring Web -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-webmvc</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!-- Spring ORM -->
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-orm</artifactId>
      <version>${spring.version}</version>
    </dependency>

    <!-- Spring Security -->
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-web</artifactId>
      <version>${spring.security.version}</version>
    </dependency>
    <dependency>
      <groupId>org.springframework.security</groupId>
      <artifactId>spring-security-config</artifactId>
      <version>${spring.security.version}</version>
    </dependency>

    <!-- Hibernate -->
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-core</artifactId>
      <version>${hibernate.version}</version>
    </dependency>
    <dependency>
      <groupId>org.hibernate</groupId>
      <artifactId>hibernate-entitymanager</artifactId>
      <version>${hibernate.version}</version>
    </dependency>
    <dependency>
      <groupId>commons-dbcp</groupId>
      <artifactId>commons-dbcp</artifactId>
      <version>1.4</version>
    </dependency>
    <!-- MySQL -->
    <dependency>
      <groupId>mysql</groupId>
      <artifactId>mysql-connector-java</artifactId>
      <version>5.1.45</version>
    </dependency>

    <!-- JSP - Servlet Lib -->
    <dependency>
      <groupId>javax.servlet.jsp</groupId>
      <artifactId>jsp-api</artifactId>
      <version>2.2</version>
      <scope>provided</scope>
    </dependency>
    <dependency>
      <groupId>javax.servlet</groupId>
      <artifactId>servlet-api</artifactId>
      <version>2.5</version>
      <scope>provided</scope>
    </dependency>

    <!-- jstl for jsp page -->
    <dependency>
      <groupId>jstl</groupId>
      <artifactId>jstl</artifactId>
      <version>${jstl.version}</version>
    </dependency>
    
    <!-- cached guava -->
    <dependency>
        <groupId>com.google.guava</groupId>
        <artifactId>guava</artifactId>
        <version>23.0</version>
    </dependency>
    

  </dependencies>
</project>

Ở đây mình dùng thêm thư viện guava để lưu lại cached IP.

File Cấu hình spring security

<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/security"
  xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:beans="http://www.springframework.org/schema/beans"
  xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
    http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd">

  <http auto-config="true">
    <intercept-url pattern="/admin**" access="hasRole('ROLE_ADMIN')" />
    <intercept-url pattern="/user**" access="hasRole('ROLE_ADMIN') or hasRole('ROLE_USER')" />
    
    <form-login login-page="/login" login-processing-url="/j_spring_security_login"
      default-target-url="/user" authentication-failure-handler-ref="customAuthenticationFailureHandler"
      username-parameter="username" password-parameter="password" />
    
    <logout logout-url="/j_spring_security_logout"
      logout-success-url="/login?message=logout" delete-cookies="JSESSIONID" />
  </http>

  <authentication-manager>
    <authentication-provider user-service-ref="myUserDetailsService">
      <password-encoder hash="bcrypt"/>
    </authentication-provider>
  </authentication-manager>
</beans:beans>

authentication-failure-handler-ref="customAuthenticationFailureHandler"  nếu login failed thì nó sẽ được xử lý bởi bean customAuthenticationFailureHandler

Trường hợp login thất bại:

package stackjava.com.preventbruteforce.security;

import java.io.IOException;

import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.authentication.BadCredentialsException;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import stackjava.com.preventbruteforce.utils.RequestUtils;

@Component
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private LoginAttemptService loginAttemptService;
  
  public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
      AuthenticationException exception) throws IOException, ServletException {
    
    // Login failed by BadCredentialsException (Username or password incorrect)
    if (exception.getClass().isAssignableFrom(BadCredentialsException.class)) {
      loginAttemptService.loginFailed(RequestUtils.getClientIP(request));
    }
    
    // Login failed by Blocked IP
    if (exception.getMessage() != null && exception.getMessage().equals("block_ip")) {
      response.sendRedirect(request.getContextPath() + "/login?message=block_ip");
      return;
    } 

        response.sendRedirect(request.getContextPath() + "/login?message=error");
  }
  
}

Nếu nhập sai username/password ta sẽ lưu lại ip và số lần login thất bại của ip đó

Trường hợp login thành công

package stackjava.com.preventbruteforce.security;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationListener;
import org.springframework.security.authentication.event.AuthenticationSuccessEvent;
import org.springframework.stereotype.Component;

import stackjava.com.preventbruteforce.utils.RequestUtils;

@Component
public class AuthenticationSuccessEventListener implements ApplicationListener<AuthenticationSuccessEvent> {

  @Autowired
  private LoginAttemptService loginAttemptService;

  @Autowired
  private HttpServletRequest request;

  public void onApplicationEvent(AuthenticationSuccessEvent e) {
    loginAttemptService.loginSucceeded(RequestUtils.getClientIP(request));
  }
}

Nếu ip đó login thành công thì ta xóa lịch sử login trước đó đi.

Class LoginAttemptService.java

package stackjava.com.preventbruteforce.security;

import java.util.concurrent.ExecutionException;
import java.util.concurrent.TimeUnit;

import org.springframework.stereotype.Service;

import com.google.common.cache.CacheBuilder;
import com.google.common.cache.CacheLoader;
import com.google.common.cache.LoadingCache;

@Service
public class LoginAttemptService {

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

  public LoginAttemptService() {
    attemptsCache = CacheBuilder.newBuilder().expireAfterWrite(30, TimeUnit.MINUTES)
        .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;
    }
  }
}

Class RequestUtils.java

package stackjava.com.preventbruteforce.utils;

import javax.servlet.http.HttpServletRequest;

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

Class MyUserDetailsService.java

package stackjava.com.preventbruteforce.security;

import javax.servlet.http.HttpServletRequest;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import stackjava.com.preventbruteforce.dao.UserDAO;
import stackjava.com.preventbruteforce.utils.RequestUtils;

@Service
public class MyUserDetailsService implements UserDetailsService {

  @Autowired
  private UserDAO userDAO;

    @Autowired
    private LoginAttemptService loginAttemptService;
  
    @Autowired
    private HttpServletRequest request;
    
  public UserDetails loadUserByUsername(final String username) {

    String ip = RequestUtils.getClientIP(request);
        if (loginAttemptService.isBlocked(ip)) {
            throw new RuntimeException("block_ip");
        }
    
    stackjava.com.preventbruteforce.entities.User user = userDAO.loadUserByUsername(username);
    if (user == null) {
      throw new UsernameNotFoundException("user_not_found");
    }

    boolean enabled = true;
    boolean accountNonExpired = true;
    boolean credentialsNonExpired = true;
    boolean accountNonLocked = true;

    return new User(username, user.getPassword(), enabled, accountNonExpired, credentialsNonExpired,
        accountNonLocked, user.getAuthorities());
  }
  
}

File Controller

package stackjava.com.preventbruteforce.controller;

import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;

@Controller
public class BaseController {
  
  @RequestMapping(value = { "/login", "/" })
  public String login(@RequestParam(required=false) String message, final Model model) {
    if (message != null && !message.isEmpty()) {
      if (message.equals("block_ip")) {
        model.addAttribute("message", "Your IP has been blocked! Pls try again in 30 minutes");
      }
      if (message.equals("logout")) {
        model.addAttribute("message", "Logout!");
      }
      if (message.equals("error")) {
        model.addAttribute("message", "Login Failed!");
      }
    }
    return "login";
  }

  @RequestMapping("/admin")
  public String admin() {
    return "admin";
  }

  @RequestMapping("/user")
  public String user() {
    return "user";
  }

}

File views:

<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
<title>login</title>
</head>
<body>
  <h1>Spring MVC - Security 5 : Prevent Brute Force</h1>
  <h3>${message}</h3>

  <form name='loginForm' action="<c:url value='j_spring_security_login' />" method='POST'>
    <table>
      <tr>
        <td>User:</td>
        <td><input type='text' name='username'></td>
      </tr>
      <tr>
        <td>Password:</td>
        <td><input type='password' name='password' /></td>
      </tr>
      <tr>
        <td colspan='2'><input name="submit" type="submit" value="login" /></td>
      </tr>
    </table>
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />

  </form>
</body>
</html>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
<title>User Page</title>
</head>
<body>
  <h1>User Page</h1>
  <h2>Welcome: ${pageContext.request.userPrincipal.name}</h2>

  <a href="<c:url value="/admin" />">Admin Page</a> <br/>

  <form action="<c:url value="/j_spring_security_logout" />" method="post">
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
    <input type="submit" value="Logout" />
  </form>

</body>
</html>
<%@taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<html>
<head>
<title>Admin Page</title>
</head>
<body>
  <h1>Admin Page</h1>
  <h2>Welcome: ${pageContext.request.userPrincipal.name}</h2>

  <form action="<c:url value="/j_spring_security_logout" />" method="post">
    <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" />
    <input type="submit" value="Logout" />
  </form>

</body>
</html>

Demo:

Trường hợp login sai username/password

Code ví dụ Spring Security Prevent Brute Force (Ngăn login sai nhiều lần)

Code ví dụ Spring Security Prevent Brute Force (Ngăn login sai nhiều lần)

Trường hợp login thành công (kai/123456)

Code ví dụ Spring Security Prevent Brute Force (Ngăn login sai nhiều lần)

Logout:

Code ví dụ Spring Security Prevent Brute Force (Ngăn login sai nhiều lần)

Trường hợp nhập sai username/password 5 lần

Code ví dụ Spring Security Prevent Brute Force (Ngăn login sai nhiều lần)

Code ví dụ Spring Security Prevent Brute Force stackjava.com

Okay, Done!

Download code ví dụ trên tại đây.

 

 

References:

https://docs.spring.io/spring-security/site/docs/current/reference/htmlsingle

stackjava.com