STACKJAVA

Code ví dụ Spring Boot Concurrent Session Control/ Max Session

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:

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,

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