`
jaesonchen
  • 浏览: 310019 次
  • 来自: ...
社区版块
存档分类
最新评论

rememberme关闭浏览器时重新登录应用时无法登录

 
阅读更多

 

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>	

 

 

 

 

 

分享到:
评论

相关推荐

    81、Remember me设置1

    - 关闭浏览器并重启,再次访问网站时应该能够自动登录。 - 检查自动登录是否正常工作,并确认用户是否正确地被识别。 #### 五、标签解释 - **symfony**:指代Symfony框架本身,这是一个流行的PHP Web开发框架。 -...

    Spring Security学习之rememberMe自动登录的实现

    在Spring Security中,`rememberMe`功能用于实现在用户关闭浏览器后仍能保持登录状态,即用户下次访问时无需再次输入用户名和密码。这通常通过在客户端(浏览器)存储一个安全的cookie来实现。本文将深入探讨如何在...

    rememberMe

    "RememberMe"是一个常见的功能,通常在Web应用中用于实现用户登录的持久化,以便用户在下次访问时无需重新输入用户名和密码。这个功能主要基于cookie和服务器端的数据存储来实现,涉及到的主要技术包括JavaScript、...

    spring security 3.0x remember-me 免登陆

    .rememberMe().rememberMeServices(rememberMeServices()) .key("key") // 同上面的key .rememberMeParameter("remember-me") // 前端提交的参数名 .tokenRepository(persistentTokenRepository()) ....

    Apache shiro1.2.4反序列化漏洞介绍.docx

    它的核心功能之一是“RememberMe”服务,该服务允许用户在关闭浏览器后仍然保持登录状态,无需在下次访问时重新登录。这一特性通过存储在Cookie中的特定字段"rememberMe"实现。 然而,在Apache Shiro 1.2.4及其更早...

    spring-Acgei的一个小例子之四

    至于RememberMe服务,它是Acegi提供的一个特性,允许用户在关闭浏览器后仍能保持登录状态。为了实现RememberMe功能,我们需要使用`RememberMeProcessingFilter`。这个过滤器会检查请求中的特定Cookie(通常称为...

    SpringSecurity function ModifyAuthroties RemebeMe

    "RememberMe"功能是Spring Security提供的一种便捷的认证机制,用于在用户关闭浏览器后仍然保持登录状态。当用户勾选了"记住我"选项,Spring Security会在用户的浏览器中存储一个长期有效的cookie。下次用户访问时,...

    shiro之记住登录信息

    本文将详细介绍 Shiro 如何实现“记住登录信息”(RememberMe)的功能,帮助用户在关闭浏览器后仍能保持登录状态,以便在下次打开时无需重新登录。 1. **RememberMe 的工作原理** RememberMe 功能主要依赖于 ...

    python-Django实现用户登录

    除了基本的登录功能,Django还支持记住我(Remember Me)功能,通过在cookie中存储一个长期有效的令牌,使用户在关闭浏览器后再次访问时仍能保持登录状态。还可以实现注册功能,允许新用户创建账户,并添加自定义的...

    基于Spring Boot框架的Spring Security项目.zip

    基于Spring Boot框架的Spring Security项目 ... Remember Me功能支持“记住我”功能,用户在关闭浏览器后仍然保持登录状态。 2. 缓存管理 Redis缓存使用Redis作为缓存存储介质,提高系统性能。

    Shiro开发文档.pdf

    “Remember Me”服务:类似于购物车的功能,可以让用户在关闭浏览器后仍保持登录状态,下次访问时无需重新登录。 Web支持:Shiro提供了Web应用的API支持,帮助开发者保护Web应用程序的安全。 缓存(Caching):...

    Laravel开发-session-tokens

    1. **"Remember Me"功能**:Laravel提供了一个"记住我"功能,允许用户在关闭浏览器后仍能保持登录状态。这主要通过在用户登录时生成并存储一个长期有效的"remember_token"来实现。 2. **代币生成**:当用户选择...

    061-Apache Shiro 反序列化之殇.pdf

    Remember Me允许用户在关闭浏览器后仍然保持登录状态,以便下次访问时自动登录。为了实现这一功能,Shiro会将加密后的用户信息序列化并存储在一个名为`remember-me`的Cookie中。问题在于,攻击者可以利用Shiro的默认...

    弹出浮动层漂亮登录框

    &lt;label&gt;&lt;input type="checkbox" id="rememberMe" /&gt; 记住我 登录 ``` 然后,利用JQuery的`.show()`函数将隐藏的登录框显示出来,同时可以添加一些动画效果,如淡入淡出,以增加用户体验: ```javascript $("#...

    springsecurity3.0.5应用

    4. **Remember Me**:Spring Security允许配置“记住我”功能,这样用户在关闭浏览器后再次访问时无需重新登录。这通过在用户成功登录时创建一个长期有效的cookie实现。 5. **Session Management**:Spring ...

    shiro-master

    6. **Remember Me & SSO**:Remember Me 功能允许用户在一次登录后,即使关闭浏览器再次打开也能保持登录状态。而Single Sign-On(单点登录)则是用户在一次认证后,可以在多个相互信任的应用系统间自由切换,无需...

    SpringSecurity5.7.11实现用户认证和记住我功能

    “记住我”功能允许用户在关闭浏览器后再次打开时仍保持登录状态,提高了用户体验。SpringSecurity通过使用持久化的令牌来实现这一功能。下面是实现过程: 1. **启用记住我**:在配置安全过滤器链时,添加`remember...

    登录窗体设计

    登录窗体设计是软件开发中的基础模块,尤其在桌面应用和网页应用中广泛存在。它为用户提供了一个安全的入口,验证用户的身份以便访问系统资源。本教程将针对初学者,详细讲解如何设计一个具备基本功能的登录窗体,并...

    asp.net讲解

    3. Remember Me服务:用户登录后,可以通过Cookie记住用户的登录状态,下次访问时自动登录。 安全性和性能是考虑使用Session还是Cookie时的重要因素: 1. 安全性:Session存储在服务器端,安全性通常高于存储在...

    spring-security cookie认证

    - Remember-Me服务允许用户选择“记住我”,这样即使关闭浏览器后,下次打开时仍然保持登录状态。 - Spring Security提供了Remember-Me服务的实现,可以通过配置来启用。 - 默认的Remember-Me服务使用了`...

Global site tag (gtag.js) - Google Analytics