rememberme关闭浏览器时再重启应用时无法登录,提示达到最大登录数,需要等待session超时才能登录。
原因是SessionManagementFilter在调用ConcurrentSessionControlAuthenticationStrategy.onAuthentication()时,由于直接关闭浏览器,当时的session仍然保存在SessionRegistry中,在ConcurrentSessionControlAuthenticationStrategy进行验证时,误认为已经存在当前用户的活跃session,从而抛出最大登录数异常。
解决方法:
1. 设置<security:concurrency-control max-sessions="1" error-if-maximum-exceeded="false"/>,使用后登陆挤掉先登录用户的策略。
2. session靠Cookie来维持,每次给客户端一个cookie里面存放session id,然后请求的时候,服务器根据session id找到对应的session。这个cookie是在浏览器关闭的时候就失效了,自动登录的cookie需要设置成为关闭浏览器后cookie还有效。
3. 在当前窗口的关闭事件中发送请求主动使当前session失效,调用session.invalidate()。
4. 使用自定义的ConcurrentSessionControlAuthenticationStrategy、RegisterSessionAuthenticationStrategy和SessionRegistry的实现,保存用户的ip地址,在进行验证发现到达最大登录数时,在SessionRegistry中保存的当前用户的session id列表,判断是否存在session对应的ip与当前用户的ip相等,如果存在则使该session失效,并通过用户的验证。
package com.jaeson.springstudy.security; import java.util.ArrayList; import java.util.Collections; import java.util.Date; import java.util.List; import java.util.Map; import java.util.Set; import java.util.concurrent.ConcurrentHashMap; import java.util.concurrent.ConcurrentMap; import java.util.concurrent.CopyOnWriteArraySet; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.ApplicationListener; import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.core.session.SessionDestroyedEvent; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.util.Assert; public class MySessionRegistryImpl implements SessionRegistry, ApplicationListener<SessionDestroyedEvent> { private static final Logger logger = LoggerFactory.getLogger(MySessionRegistryImpl.class); /** <principal:Object,SessionIdSet> */ private final ConcurrentMap<Object, Set<String>> principals = new ConcurrentHashMap<Object, Set<String>>(); /** <sessionId:Object,SessionInformation> */ private final Map<String, SessionInformation> sessionIds = new ConcurrentHashMap<String, SessionInformation>(); //for ipAddress private final Map<String, String> ipAddr = new ConcurrentHashMap<String, String>(); @Override public void onApplicationEvent(SessionDestroyedEvent event) { String sessionId = event.getId(); removeSessionInformation(sessionId); logger.debug("onApplicationEvent fired, sessionId = {}", sessionId); } @Override public List<Object> getAllPrincipals() { return new ArrayList<Object>(principals.keySet()); } @Override public List<SessionInformation> getAllSessions(Object principal, boolean includeExpiredSessions) { final Set<String> sessionsUsedByPrincipal = principals.get(principal); if (sessionsUsedByPrincipal == null) { return Collections.emptyList(); } List<SessionInformation> list = new ArrayList<SessionInformation>( sessionsUsedByPrincipal.size()); for (String sessionId : sessionsUsedByPrincipal) { SessionInformation sessionInformation = getSessionInformation(sessionId); if (sessionInformation == null) { continue; } if (includeExpiredSessions || !sessionInformation.isExpired()) { list.add(sessionInformation); } } return list; } @Override public SessionInformation getSessionInformation(String sessionId) { Assert.hasText(sessionId, "SessionId required as per interface contract"); return sessionIds.get(sessionId); } @Override public void refreshLastRequest(String sessionId) { Assert.hasText(sessionId, "SessionId required as per interface contract"); SessionInformation info = getSessionInformation(sessionId); if (info != null) { info.refreshLastRequest(); } } @Override public void registerNewSession(String sessionId, Object authentication) { Assert.hasText(sessionId, "SessionId required as per interface contract"); Assert.notNull(authentication, "Authentication required as per interface contract"); if (logger.isDebugEnabled()) { logger.debug("Registering session " + sessionId + ", for authentication " + authentication); } Authentication auth = null; Object principal = authentication; if (authentication instanceof Authentication) { auth = (Authentication) authentication; principal = auth.getPrincipal(); } if (getSessionInformation(sessionId) != null) { removeSessionInformation(sessionId); //for ip address removeIpAddress(sessionId); } sessionIds.put(sessionId, new SessionInformation(principal, sessionId, new Date())); if (auth != null) { Object details = auth.getDetails(); if (details instanceof WebAuthenticationDetails) addIpAddress(sessionId, ((WebAuthenticationDetails) details).getRemoteAddress()); } Set<String> sessionsUsedByPrincipal = principals.get(principal); if (sessionsUsedByPrincipal == null) { sessionsUsedByPrincipal = new CopyOnWriteArraySet<String>(); Set<String> prevSessionsUsedByPrincipal = principals.putIfAbsent(principal, sessionsUsedByPrincipal); if (prevSessionsUsedByPrincipal != null) { sessionsUsedByPrincipal = prevSessionsUsedByPrincipal; } } sessionsUsedByPrincipal.add(sessionId); if (logger.isTraceEnabled()) { logger.trace("Sessions used by '" + principal + "' : " + sessionsUsedByPrincipal); } } @Override public void removeSessionInformation(String sessionId) { Assert.hasText(sessionId, "SessionId required as per interface contract"); SessionInformation info = getSessionInformation(sessionId); if (info == null) { return; } logger.debug("begin remove sessionId: {}", info.getSessionId()); logger.debug("before remove sessionIds sessionIds.size() = {}, principals.size() = {}, ipAddress.size() = {} ", sessionIds.size(), principals.size(), ipAddr.size()); sessionIds.remove(sessionId); removeIpAddress(sessionId); logger.debug("after remove sessionIds sessionIds.size() = {}, principals.size() = {}, ipAddress.size() = {} ", sessionIds.size(), principals.size(), ipAddr.size()); Set<String> sessionsUsedByPrincipal = principals.get(info.getPrincipal()); if (sessionsUsedByPrincipal == null) { return; } if (logger.isDebugEnabled()) { logger.debug("Removing session " + sessionId + " from principal's set of registered sessions"); } sessionsUsedByPrincipal.remove(sessionId); if (sessionsUsedByPrincipal.isEmpty()) { // No need to keep object in principals Map anymore if (logger.isDebugEnabled()) { logger.debug("Removing principal " + info.getPrincipal() + " from registry"); } principals.remove(info.getPrincipal()); } logger.debug("after remove principals sessionIds.size() = {}, principals.size() = {}, ipAddress.size() = {} ", sessionIds.size(), principals.size(), ipAddr.size()); } public String getIpAddress(String sessionId){ if (ipAddr.containsKey(sessionId)) return ipAddr.get(sessionId); return null; } protected void removeIpAddress(String sessionId) { if (ipAddr.containsKey(sessionId)) ipAddr.remove(sessionId); } protected void addIpAddress(String sessionId, String ipAddress) { ipAddr.put(sessionId, ipAddress); } }
package com.jaeson.springstudy.security; import java.util.List; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.context.MessageSource; import org.springframework.context.MessageSourceAware; import org.springframework.context.support.MessageSourceAccessor; import org.springframework.security.core.Authentication; import org.springframework.security.core.SpringSecurityMessageSource; import org.springframework.security.core.session.SessionInformation; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.web.authentication.WebAuthenticationDetails; import org.springframework.security.web.authentication.session.SessionAuthenticationException; import org.springframework.security.web.authentication.session.SessionAuthenticationStrategy; import org.springframework.util.Assert; public class MyConcurrentSessionControlAuthenticationStrategy implements MessageSourceAware, SessionAuthenticationStrategy { private static final Logger logger = LoggerFactory.getLogger(MyConcurrentSessionControlAuthenticationStrategy.class); protected MessageSourceAccessor messages = SpringSecurityMessageSource.getAccessor(); private final SessionRegistry sessionRegistry; private boolean exceptionIfMaximumExceeded = false; private int maximumSessions = 1; public MyConcurrentSessionControlAuthenticationStrategy(SessionRegistry sessionRegistry) { Assert.notNull(sessionRegistry, "The sessionRegistry cannot be null"); this.sessionRegistry = sessionRegistry; } public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) { final List<SessionInformation> sessions = sessionRegistry.getAllSessions( authentication.getPrincipal(), false); int sessionCount = sessions.size(); int allowedSessions = getMaximumSessionsForThisUser(authentication); if (sessionCount < allowedSessions) { // They haven't got too many login sessions running at present return; } if (allowedSessions == -1) { // We permit unlimited logins return; } if (sessionCount == allowedSessions) { HttpSession session = request.getSession(false); if (session != null) { // Only permit it though if this request is associated with one of the // already registered sessions for (SessionInformation si : sessions) { if (si.getSessionId().equals(session.getId())) { return; } } } // If the session is null, a new one will be created by the parent class, // exceeding the allowed number } //判断是否来自同一个ip的request,如果是则使同一个ip的用户session失效 Object details = authentication.getDetails(); if (details instanceof WebAuthenticationDetails && sessionRegistry instanceof MySessionRegistryImpl) { logger.debug("using MySessionRegistryImpl to detect where request from same ip address..."); MySessionRegistryImpl mySessionRegistry = (MySessionRegistryImpl) sessionRegistry; String ipAddress = ((WebAuthenticationDetails) details).getRemoteAddress(); logger.debug("request ip address: {} ", ipAddress); for (SessionInformation session : sessions) { logger.debug("user({}) login in session info:{}", authentication.getName(), session); if (ipAddress.equals(mySessionRegistry.getIpAddress(session.getSessionId()))) { logger.debug("session: {} expired now", session.getSessionId()); session.expireNow(); return; } } } allowableSessionsExceeded(sessions, allowedSessions, sessionRegistry); } protected int getMaximumSessionsForThisUser(Authentication authentication) { return maximumSessions; } protected void allowableSessionsExceeded(List<SessionInformation> sessions, int allowableSessions, SessionRegistry registry) throws SessionAuthenticationException { if (exceptionIfMaximumExceeded || (sessions == null)) { throw new SessionAuthenticationException(messages.getMessage( "ConcurrentSessionControlAuthenticationStrategy.exceededAllowed", new Object[] { Integer.valueOf(allowableSessions) }, "Maximum sessions of {0} for this principal exceeded")); } // Determine least recently used session, and mark it for invalidation SessionInformation leastRecentlyUsed = null; for (SessionInformation session : sessions) { if ((leastRecentlyUsed == null) || session.getLastRequest() .before(leastRecentlyUsed.getLastRequest())) { leastRecentlyUsed = session; } } leastRecentlyUsed.expireNow(); } public void setExceptionIfMaximumExceeded(boolean exceptionIfMaximumExceeded) { this.exceptionIfMaximumExceeded = exceptionIfMaximumExceeded; } public void setMaximumSessions(int maximumSessions) { Assert.isTrue( maximumSessions != 0, "MaximumLogins must be either -1 to allow unlimited logins, or a positive integer to specify a maximum"); this.maximumSessions = maximumSessions; } public void setMessageSource(MessageSource messageSource) { Assert.notNull(messageSource, "messageSource cannot be null"); this.messages = new MessageSourceAccessor(messageSource); } }
package com.jaeson.springstudy.security; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import org.springframework.security.core.Authentication; import org.springframework.security.core.session.SessionRegistry; import org.springframework.security.web.authentication.session.RegisterSessionAuthenticationStrategy; public class MyRegisterSessionAuthenticationStrategy extends RegisterSessionAuthenticationStrategy { private SessionRegistry sessionRegistry; public MyRegisterSessionAuthenticationStrategy(SessionRegistry sessionRegistry) { super(sessionRegistry); this.sessionRegistry = sessionRegistry; } @Override public void onAuthentication(Authentication authentication, HttpServletRequest request, HttpServletResponse response) { if (sessionRegistry instanceof MySessionRegistryImpl) sessionRegistry.registerNewSession(request.getSession().getId(), authentication); else sessionRegistry.registerNewSession(request.getSession().getId(), authentication.getPrincipal()); } }
<beans xmlns="http://www.springframework.org/schema/beans" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:security="http://www.springframework.org/schema/security" xsi:schemaLocation=" http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.0.xsd http://www.springframework.org/schema/security http://www.springframework.org/schema/security/spring-security.xsd "> <security:http pattern="/resources/**" security="none" /> <security:http pattern="/*.html" security="none" /> <!-- 默认auto-config="false",设置auto-config="true"时,自动注册form-login、basic authentication、logout。 默认use-expressions="true" 需要使用表达式hasRole('ROLE_USER') --> <security:http auto-config="true" use-expressions="false"> <!-- 设置没有访问权限时转向的提示页面 --> <security:access-denied-handler error-page="/accessDenied.html" /> <!-- 设置匿名用户可以访问的url --> <security:intercept-url pattern="/login*" access="IS_AUTHENTICATED_ANONYMOUSLY" /> <!-- 设置相应角色可以访问的url --> <security:intercept-url pattern="/security/**" access="ROLE_ADMIN" /> <security:intercept-url pattern="/**" access="ROLE_USER" /> <!-- 设置自定义的登录页面和登录后的缺省home主页 login-page不设置时,spring自动使用"/login"。 default-target-url不设置时,登录成功后转向登录之前的请求url,如果没有则指向根目录"/"。 always-use-default-target="true"登录成功后始终转向default-target-url。 authentication-failure-url设置登录失败时转向的页面,如果不设置spring会自动转向"/login?error"。 --> <security:form-login login-page="/login.jsp" authentication-failure-url="/login.jsp?error=1" default-target-url="/" always-use-default-target="true" /> <!-- 默认invalidate-session="true"在logout时使session失效,logout-success-url设置logout成功后转向的页面--> <security:logout logout-success-url="/logout.html" invalidate-session="true"/> <!-- data-source-ref="dataSource"使用数据库持久化remember me标记 --> <security:remember-me data-source-ref="dataSource" user-service-ref="userDetailsService" /> <security:session-management session-authentication-strategy-ref="sessionAuthenticationStrategy"/> </security:http> <security:authentication-manager alias="authenticationManager"> <security:authentication-provider user-service-ref="userDetailsService"> <security:password-encoder ref="bcryptEncoder"/> </security:authentication-provider> </security:authentication-manager> <bean id="sessionAuthenticationStrategy" class="org.springframework.security.web.authentication.session.CompositeSessionAuthenticationStrategy"> <constructor-arg> <list> <ref bean="concurrentSessionControlAuthenticationStrategy" /> <ref bean="sessionFixationProtectionStrategy" /> <ref bean="registerSessionAuthenticationStrategy" /> </list> </constructor-arg> </bean> <bean id="sessionFixationProtectionStrategy" class="org.springframework.security.web.authentication.session.SessionFixationProtectionStrategy" /> <bean id="concurrentSessionControlAuthenticationStrategy" class="com.jaeson.springstudy.security.MyConcurrentSessionControlAuthenticationStrategy"> <constructor-arg ref="sessionRegistry"/> <property name="maximumSessions" value="1"/> <property name="exceptionIfMaximumExceeded" value="true"/> </bean> <bean id="registerSessionAuthenticationStrategy" class="com.jaeson.springstudy.security.MyRegisterSessionAuthenticationStrategy"> <constructor-arg ref="sessionRegistry"/> </bean> <bean id="sessionRegistry" class="com.jaeson.springstudy.security.MySessionRegistryImpl" /> <bean id="bcryptEncoder" class="org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder"/> <!-- 配置UserDetailsService实现,可以使用自定义的UserDetailsService实现获得数据库的用户信息UserDetails --> <bean id="userDetailsService" class="org.springframework.security.core.userdetails.jdbc.JdbcDaoImpl"> <property name="dataSource" ref="dataSource" /> <property name="usersByUsernameQuery" value="SELECT username, password, enable FROM user WHERE username=?" /> <property name="authoritiesByUsernameQuery" value="SELECT u.username as username, r.rolename as rolename FROM user u JOIN user_group ug ON u.id=ug.user_id JOIN groups g ON ug.group_id=g.id JOIN group_role gr ON g.id=gr.group_id JOIN role r ON gr.role_id=r.id WHERE u.username=?" /> </bean> </beans>
相关推荐
- 关闭浏览器并重启,再次访问网站时应该能够自动登录。 - 检查自动登录是否正常工作,并确认用户是否正确地被识别。 #### 五、标签解释 - **symfony**:指代Symfony框架本身,这是一个流行的PHP Web开发框架。 -...
在Spring Security中,`rememberMe`功能用于实现在用户关闭浏览器后仍能保持登录状态,即用户下次访问时无需再次输入用户名和密码。这通常通过在客户端(浏览器)存储一个安全的cookie来实现。本文将深入探讨如何在...
"RememberMe"是一个常见的功能,通常在Web应用中用于实现用户登录的持久化,以便用户在下次访问时无需重新输入用户名和密码。这个功能主要基于cookie和服务器端的数据存储来实现,涉及到的主要技术包括JavaScript、...
.rememberMe().rememberMeServices(rememberMeServices()) .key("key") // 同上面的key .rememberMeParameter("remember-me") // 前端提交的参数名 .tokenRepository(persistentTokenRepository()) ....
它的核心功能之一是“RememberMe”服务,该服务允许用户在关闭浏览器后仍然保持登录状态,无需在下次访问时重新登录。这一特性通过存储在Cookie中的特定字段"rememberMe"实现。 然而,在Apache Shiro 1.2.4及其更早...
至于RememberMe服务,它是Acegi提供的一个特性,允许用户在关闭浏览器后仍能保持登录状态。为了实现RememberMe功能,我们需要使用`RememberMeProcessingFilter`。这个过滤器会检查请求中的特定Cookie(通常称为...
"RememberMe"功能是Spring Security提供的一种便捷的认证机制,用于在用户关闭浏览器后仍然保持登录状态。当用户勾选了"记住我"选项,Spring Security会在用户的浏览器中存储一个长期有效的cookie。下次用户访问时,...
本文将详细介绍 Shiro 如何实现“记住登录信息”(RememberMe)的功能,帮助用户在关闭浏览器后仍能保持登录状态,以便在下次打开时无需重新登录。 1. **RememberMe 的工作原理** RememberMe 功能主要依赖于 ...
除了基本的登录功能,Django还支持记住我(Remember Me)功能,通过在cookie中存储一个长期有效的令牌,使用户在关闭浏览器后再次访问时仍能保持登录状态。还可以实现注册功能,允许新用户创建账户,并添加自定义的...
基于Spring Boot框架的Spring Security项目 ... Remember Me功能支持“记住我”功能,用户在关闭浏览器后仍然保持登录状态。 2. 缓存管理 Redis缓存使用Redis作为缓存存储介质,提高系统性能。
“Remember Me”服务:类似于购物车的功能,可以让用户在关闭浏览器后仍保持登录状态,下次访问时无需重新登录。 Web支持:Shiro提供了Web应用的API支持,帮助开发者保护Web应用程序的安全。 缓存(Caching):...
1. **"Remember Me"功能**:Laravel提供了一个"记住我"功能,允许用户在关闭浏览器后仍能保持登录状态。这主要通过在用户登录时生成并存储一个长期有效的"remember_token"来实现。 2. **代币生成**:当用户选择...
Remember Me允许用户在关闭浏览器后仍然保持登录状态,以便下次访问时自动登录。为了实现这一功能,Shiro会将加密后的用户信息序列化并存储在一个名为`remember-me`的Cookie中。问题在于,攻击者可以利用Shiro的默认...
<label><input type="checkbox" id="rememberMe" /> 记住我 登录 ``` 然后,利用JQuery的`.show()`函数将隐藏的登录框显示出来,同时可以添加一些动画效果,如淡入淡出,以增加用户体验: ```javascript $("#...
4. **Remember Me**:Spring Security允许配置“记住我”功能,这样用户在关闭浏览器后再次访问时无需重新登录。这通过在用户成功登录时创建一个长期有效的cookie实现。 5. **Session Management**:Spring ...
6. **Remember Me & SSO**:Remember Me 功能允许用户在一次登录后,即使关闭浏览器再次打开也能保持登录状态。而Single Sign-On(单点登录)则是用户在一次认证后,可以在多个相互信任的应用系统间自由切换,无需...
“记住我”功能允许用户在关闭浏览器后再次打开时仍保持登录状态,提高了用户体验。SpringSecurity通过使用持久化的令牌来实现这一功能。下面是实现过程: 1. **启用记住我**:在配置安全过滤器链时,添加`remember...
登录窗体设计是软件开发中的基础模块,尤其在桌面应用和网页应用中广泛存在。它为用户提供了一个安全的入口,验证用户的身份以便访问系统资源。本教程将针对初学者,详细讲解如何设计一个具备基本功能的登录窗体,并...
3. Remember Me服务:用户登录后,可以通过Cookie记住用户的登录状态,下次访问时自动登录。 安全性和性能是考虑使用Session还是Cookie时的重要因素: 1. 安全性:Session存储在服务器端,安全性通常高于存储在...
- Remember-Me服务允许用户选择“记住我”,这样即使关闭浏览器后,下次打开时仍然保持登录状态。 - Spring Security提供了Remember-Me服务的实现,可以通过配置来启用。 - 默认的Remember-Me服务使用了`...