Code ví dụ Spring Boot Concurrent Session Control/ Max Session
(Xem lại: Spring MVC Security Concurrent Session Control/ Max Session (XML config))
Trong nhiều trường hợp, bạn muốn tài khoản người dùng chỉ có thể login tại một vị trí (để tránh xung đột dữ liệu, bảo mật…). Ví dụ như nick chơi game, nếu bạn đăng nhập trên máy tính A rồi lại đăng nhập trên máy tính B thì nó sẽ tự động logout khỏi máy tính A. Trong bài này ta sẽ thực hiện ví dụ cài đặt để giới hạn mỗi tài khoản chỉ được login tại một nơi.
Các công nghệ sử dụng:
- Spring Boot 2.0.1
- Maven
- JDK 1.8
- Eclipse + Spring Tool Suite
Tạo Spring Boot Project
Cấu trúc Project
File Spring Security Config
package stackjava.com.sbmaxsession.config; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.web.session.HttpSessionEventPublisher; import stackjava.com.sbmaxsession.controller.CustomAuthenticationFailureHandler; @Configuration @EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { @Bean public HttpSessionEventPublisher httpSessionEventPublisher() { return new HttpSessionEventPublisher(); } @Bean public BCryptPasswordEncoder passwordEncoder() { BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder(); return bCryptPasswordEncoder; } @Autowired public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception { auth.inMemoryAuthentication().passwordEncoder(passwordEncoder()). withUser("kai").password("$2a$04$Q2Cq0k57zf2Vs/n3JXwzmerql9RzElr.J7aQd3/Sq0fw/BdDFPAj.").roles("ADMIN"); // auth.inMemoryAuthentication().passwordEncoder(NoOpPasswordEncoder.getInstance()).withUser("kai").password("123456").roles("USER"); } @Autowired private CustomAuthenticationFailureHandler customAuthenticationFailureHandler; @Override protected void configure(HttpSecurity http) throws Exception { // Chỉ cho phép user đã đăng nhập mới được truy cập đường dẫn /user/** http.authorizeRequests().antMatchers("/user/**").authenticated(); // Cấu hình concurrent session http.sessionManagement().sessionFixation().newSession() .invalidSessionUrl("/login?message=timeout") .maximumSessions(1).expiredUrl("/login?message=max_session").maxSessionsPreventsLogin(true); // Cấu hình cho Login Form. http.authorizeRequests().and().formLogin()// .loginProcessingUrl("/j_spring_security_login")// .loginPage("/login")// .defaultSuccessUrl("/user")// .failureHandler(customAuthenticationFailureHandler) .usernameParameter("username")// .passwordParameter("password") // Cấu hình cho Logout Page. .and().logout().logoutUrl("/j_spring_security_logout").logoutSuccessUrl("/login?message=logout"); } }
Bean httpSessionEventPublisher
để enable khả năng hỗ trợ concurrent session-control.
Method sessionManagement() dùng để quản lý session,
- Method invalidSessionUrl() dùng để chỉ định url sẽ chuyển hướng tới nếu request chứa session đã hết hạn
- Method maxSessionsPreventsLogin(): xác định số lượng session lớn nhất có thể hoạt động đồng thời, ở đây mình để là 1 tức là 1 tài khoản chỉ cho phép hoạt động tại một nơi duy nhất.
- Method maxSessionsPreventsLogin(): nếu tham số đầu vào là true thì không thể login ở nơi khác khi đã đạt max session, nếu bằng false thì cho phép login ở nơi khác còn nơi login trước đó sẽ bị hết hạn.
- Method expiredUrl(): chỉ định đường dẫn sẽ chuyển hướng trong trường hợp login thất bại do tình huống bị timeout do login ở nơi khác.
File controller
package stackjava.com.sbmaxsession.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("timeout")) { model.addAttribute("message", "Time out"); } if (message.equals("max_session")) { model.addAttribute("message", "This accout has been login from another device!"); } if (message.equals("logout")) { model.addAttribute("message", "Logout!"); } if (message.equals("error")) { model.addAttribute("message", "Login Failed!"); } } return "login"; } @RequestMapping("/user") public String user() { return "user"; } }
File xử lý sự kiện login thất bại
package stackjava.com.sbmaxsession.controller; import java.io.IOException; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler; import org.springframework.security.web.authentication.session.SessionAuthenticationException; import org.springframework.stereotype.Component; @Component public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler { public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException { // Login failed by max session if (exception.getClass().isAssignableFrom(SessionAuthenticationException.class)) { response.sendRedirect(request.getContextPath() + "/login?message=max_session"); return; } response.sendRedirect(request.getContextPath() + "/login?message=error"); } }
Các file views:
<!DOCTYPE html> <html xmlns:th="http://www.thymeleaf.org"> <head> <title>Spring Boot Security Hello</title> </head> <body> <h2>Demo Spring Boot - Remember me</h2> <h3><p th:text="${message}"></p></h3> <form name='login-form' th:action="@{/j_spring_security_login}" method='POST'> <table> <tr> <td>Username:</td> <td><input type='text' name='username' value=''></td> </tr> <tr> <td>Password:</td> <td><input type='password' name='password' /></td> </tr> <tr> <td><input name="submit" type="submit" value="submit" /></td> </tr> </table> <input type="hidden" name="${_csrf.parameterName}" value="${_csrf.token}" /> </form> </body> </html>
<!DOCTYPE html> <html> <head> <title>Spring Boot Security</title> </head> <body> <h2>User Page</h2> <h3> Welcome : <span th:utext="${#request.userPrincipal.name}"></span> </h3> <br/><br/> <form th:action="@{/j_spring_security_logout}" method="post"> <input type="hidden" th:name="${_csrf.parameterName}" th:value="${_csrf.token}" /> <input type="submit" value="Logout" /> </form> </body> </html>
Demo:
Mình login trên cả 2 trình duyệt chrome à firefox:
Login trên chrome: (kai/123456)
Sau đây tiếp tục login với tài khoản kai/123456 trên firefox.
Vì mình đang để maxSessionsPreventsLogin(true) nên sẽ không thể login ở nơi khác, muốn login ta phải logout khỏi chrome.
Code ví dụ Spring Boot Concurrent Session Control/ Max Session stackjava.com
Okay, done!
Download code ví dụ trên tại đây.
References:
https://docs.spring.io/spring-security/site/docs/5.0.0.RELEASE/reference/htmlsingle