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:
- Spring 5.0.2.RELEASE
- Spring Security 5.0.2.RELEASE
- Maven
- Tomcat
- JDK 1.8
- Eclipse + Spring Tool Suite
Tạo Maven Project
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)
Login trên firefox (kai/123456)
(mình đang để error-if-maximum-exceeded=”false” nên vẫn login ở nơi khác được)
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 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