Code ví dụ Spring Security Concurrent Session Control/ Max Session (Chỉ login tại một nơi)

Code ví dụ Spring Security Concurrent Session Control/ Max Session (Chỉ login tại một nơi).

(Xem thêm: Ví dụ Spring Boot Concurrent Session Control/ Max Session (annotation 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.

Các công nghệ sử dụng:

Tạo Maven Project

Code ví dụ Spring Security Concurrent Session Control/ Max Session (Chỉ login tại một nơi)

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>SpringSecurityMaxSession</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>
    <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 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>

    <!-- 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>

  </dependencies>
</project>

File web.xml

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
  xmlns="http://java.sun.com/xml/ns/javaee"
  xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd"
  version="2.5">
  <display-name>SpringSecurityMaxSession</display-name>
  <servlet>
    <servlet-name>spring-mvc</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <init-param>
      <param-name>contextConfigLocation</param-name>
      <param-value></param-value>
    </init-param>
    <load-on-startup>1</load-on-startup>
  </servlet>
  <servlet-mapping>
    <servlet-name>spring-mvc</servlet-name>
    <url-pattern>/</url-pattern>
  </servlet-mapping>
  <context-param>
    <param-name>contextConfigLocation</param-name>
    <param-value>
      /WEB-INF/spring-mvc-servlet.xml,
      /WEB-INF/spring-security.xml 
    </param-value>
  </context-param>
  <listener>
    <listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
  </listener>
  <listener>
    <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
  </listener>
  <listener>
    <listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
  </listener>
  <filter>
    <filter-name>springSecurityFilterChain</filter-name>
    <filter-class>org.springframework.web.filter.DelegatingFilterProxy</filter-class>
  </filter>
  <filter-mapping>
    <filter-name>springSecurityFilterChain</filter-name>
    <url-pattern>/*</url-pattern>
  </filter-mapping>
</web-app>

* Lưu ý: để hỗ trợ concurrent session-control ta thêm listener sau vào file web.xml

<listener>
  <listener-class>org.springframework.security.web.session.HttpSessionEventPublisher</listener-class>
</listener>

File Spring Config

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

  <context:component-scan base-package="stackjava.com.springsecuritymaxsession" />
  <mvc:annotation-driven />

  <bean
    class="org.springframework.web.servlet.view.InternalResourceViewResolver">
    <property name="prefix">
      <value>/WEB-INF/views/jsp/</value>
    </property>
    <property name="suffix">
      <value>.jsp</value>
    </property>
  </bean>

</beans>

File Spring Security Config

<?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="/user**" access="isAuthenticated()" />

    <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" />

    <session-management invalid-session-url="/login?message=timeout"
      session-fixation-protection="newSession">
      <concurrency-control error-if-maximum-exceeded="false" expired-url="/login?message=max_session"
        max-sessions="1" />
    </session-management>
    
  </http>

  <authentication-manager>
    <authentication-provider>
      <user-service>
        <user name="kai" password="{noop}123456" authorities="ROLE_ADMIN" />
      </user-service>
    </authentication-provider>
  </authentication-manager>
</beans:beans>

Thẻ session-management dùng để quản lý session, thuộc tính invalid-session-url dùng để chỉ định url sẽ chuyển hướng tới nếu request chứa session đã hết hạn

Thẻ concurrency-control dùng để quản lý số lượng session của một tài khoản hoạt động đồng thời

  • max-sessions: là 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.
  • error-if-maximum-exceeded: nếu bằng 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.
  • expired-url: 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.springsecuritymaxsession.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ý hành động login thất bại:

package stackjava.com.springsecuritymaxsession.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:

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

  <form name='loginForm' action="<c:url value='j_spring_security_login' />" method='POST'>
    <table>
      <tr>
        <td>Username:</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>

  <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:

Mình login trên cả 2 trình duyệt chrome à firefox:

Login trên chrome: (kai/1234567)

Code ví dụ Spring Security Concurrent Session Control/ Max Session (Chỉ login tại một nơi)

Code ví dụ Spring Security Concurrent Session Control/ Max Session (Chỉ login tại một nơi)

Login trên firefox (kai/123456)

(mình đang để error-if-maximum-exceeded=”false” nên vẫn login ở nơi khác được)

Code ví dụ Spring Security Concurrent Session Control/ Max Session (Chỉ login tại một nơi)

Code ví dụ Spring Security Concurrent Session Control/ Max Session (Chỉ login tại một nơi)

Sau khi login trên firefox ra refresh lại trang trên chrome thì sẽ thấy nó đã bị logout:

Code ví dụ Spring Security Concurrent Session Control/ Max Session (Chỉ login tại một nơi)

Code ví dụ Spring Security 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