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

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

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

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

Cấu trúc Project

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

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)

Demo Spring Boot Concurrent Session Control/ Max Session

Spring boot chỉ login tại một nơi

Sau đây tiếp tục login với tài khoản kai/123456 trên firefox.

Spring boot chỉ login tại một nơi

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.

Spring boot chỉ login tại một nơi

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

stackjava.com