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
Trường hợp login thành công (kai/123456)
Logout:
Trường hợp nhập sai username/password 5 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