Spring Security之RememberMe

前言

今天我们来聊聊RemenberMe功能,他的实现或许跟你的最初的想法不一样哦。

什么是RememberMe

其实就是“记住我”功能。在我们工作/生活中,总会存在被打断的情况,临时需要去做其他事情。而当我们想回来继续处理的时候,通常都会发现,网页已经退出登录态了。也就是开发同学常说的,session超时了。而“记住我”则可以完美解决该问题。

除此之外,对于移动端的APP而言,也有同样的妙用。可以让用户长时间保持登录态。

“记住我”意味着,只要用户不是主动退出的,都应该认为用户还处于登录态。

如何实现RememberMe

我们来看看实现RememberMe需要做什么?一个完整流程是怎么样的?

  1. 登录认证成功后,我们需要生成一个RememberMe的凭证,然后返回给浏览器。这里通常是一个长期有效的Cookie,有效期与你预期的RememberMe的时间一样。
  2. 检测用户是否处于登录态。当用户没有处于登录态,且cookie中存在RememberMe凭证,在确认凭证有效之后,自动恢复登录态。
  3. 用户主动注销登录态,我们就要清理cookie,销毁凭证。
  • 对于步骤一,SpringSecurity提供了一个新的组件:RememberMeServices。在我们专栏的Spring Security之认证过滤器UsernamePasswordAuthenticationFilter中我们也看到了源码在认证完成后调用该组件完成RememberMe的凭证处理。

    PS: 可能有同学会问为什么不用AuthenticationSuccessHandler。如果你看过他的类注释,你就能找到答案了。这个组件的设计本意是完成登录后给用户呈现的内容。而我们这里要做的与之无关。

  • 对于步骤二,我们则需要一个新的过滤器,用来检查每个请求的登录态,以及处理登录态恢复。这便是RememberMeAuthenticationFilter。

  • 而步骤三,通过LogoutHandler就行。实际上,RememberMeServices也是他的子类。这一点体现了功能的高内聚,将与RememberMe相关的内容都放在一起了。

PS:题外话,对于软件而言,当功能足够小的时候,可以放在同一个类中。可当功能随着发展,细节就会增加,类就会显得臃肿。我们应当在嗅到代码的坏味道时,重新对功能进行审视,进行必要的新的抽象,大胆定义新的组件,以满足新的业务诉求。

Spring Security的设计

按照“高内聚低耦合”原则,我们应当把RememberMe相关的功能都放在同一个组件里。SpringSecurity则设计了两层结构。首先是RememberMeServices接口:

public interface RememberMeServices {
	// 自动登录。这自然是与session超时之后,从cookie中读取凭证自动恢复登录态有关
	Authentication autoLogin(HttpServletRequest request, HttpServletResponse response);
	
	// 登录失败的处理。要是自动登录失败了,那必须把cookie清理了哇
	void loginFail(HttpServletRequest request, HttpServletResponse response);
	
	// 登录成功。那就是要生成RememberMe凭证并丢到cookie了。
	void loginSuccess(HttpServletRequest request, HttpServletResponse response,
				Authentication successfulAuthentication);
}

除了自动登录(基于RememberMe的凭证),还有与认证过滤器配合的登录成功与登录失败的处理(涉及凭证的生成与清理)。

这第二层便是AbstractRememberMeServices

public abstract class AbstractRememberMeServices
		implements RememberMeServices, InitializingBean, LogoutHandler, MessageSourceAware {
	@Override
	public Authentication autoLogin(HttpServletRequest request, HttpServletResponse response) {
		// 寻找目标cookie
		String rememberMeCookie = extractRememberMeCookie(request);
		if (rememberMeCookie == null) {
			return null;
		}
		if (rememberMeCookie.length() == 0) {
			// 清理重置cookie
			cancelCookie(request, response);
			return null;
		}
		try {
			// 解析凭证
			String[] cookieTokens = decodeCookie(rememberMeCookie);
			// 通过凭证处理自动登录-这是抽象方法,由子类实现
			UserDetails user = processAutoLoginCookie(cookieTokens, request, response);
			// 检查用户状态
			this.userDetailsChecker.check(user);
			// 创建登录成功的认证信息
			return createSuccessfulAuthentication(request, user);
		}
		catch (CookieTheftException ex) {
			// 被攻击了,清理cookie
			cancelCookie(request, response);
			throw ex;
		}
		catch (UsernameNotFoundException ex) {
			// 没解析到用户
		}
		catch (InvalidCookieException ex) {
			// cookie已失效
		}
		catch (AccountStatusException ex) {
			// 用户状态异常
		}
		catch (RememberMeAuthenticationException ex) {
			// 登录异常
		}
		// 清理cookie
		cancelCookie(request, response);
		// 返回空,意味着通过rememberMe登录失败了。交由原来的登录过滤器处理。
		return null;
	}
	@Override
	public void loginSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication successfulAuthentication) {
		// 检查是否勾选了rememberMe
		if (!rememberMeRequested(request, this.parameter)) {
			this.logger.debug("Remember-me login not requested.");
			return;
		}
		// 完成登录后的处理。这是原来的登录认证后的操作。需要为止生成凭证。这是个抽象方法,由子类实现
		onLoginSuccess(request, response, successfulAuthentication);
	}
	@Override
	public void loginFail(HttpServletRequest request, HttpServletResponse response) {
		// 重置cookie
		cancelCookie(request, response);
		// 登录失败后处理。这是个空方法,也是个钩子方法。不过目前子类都没有使用到。
		onLoginFail(request, response);
	}
	@Override
	public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
		// 清理保存凭证的cookie
		cancelCookie(request, response);
	}
	protected void setCookie(String[] tokens, int maxAge, HttpServletRequest request, HttpServletResponse response) {
		String cookieValue = encodeCookie(tokens);
		Cookie cookie = new Cookie(this.cookieName, cookieValue);
		cookie.setMaxAge(maxAge);
		cookie.setPath(getCookiePath(request));
		if (this.cookieDomain != null) {
			cookie.setDomain(this.cookieDomain);
		}
		if (maxAge < 1) {
			cookie.setVersion(1);
		}
		cookie.setSecure((this.useSecureCookie != null) ? this.useSecureCookie : request.isSecure());
		// 设置了httpOnly后,js脚本将无法读取到cookie信息
		// {@link https://cloud.tencent.com/developer/article/2097036}
		cookie.setHttpOnly(true);
		response.addCookie(cookie);
	}
}

除了实现RememberMeServices之外,还实现了LogoutHandler。如此一来,才能完成我们上面的流程分析的功能。这里面的还有个门道,就是为什么不将RememberMeServices直接继承LogoutHandler。因为这两本就是两个不同的功能,从组件设计上就应该解耦隔离。但在是实现上又需要配合,单独设计一个实现类来完成在用户注销登录态时清理RememberMe凭证也不是不可以。但这样的话,意味着RememberMe的实现就散落在两个地方了,没有内聚。而这,可能就是AbstractRememberMeServices存在的意义之一。

好了,到这里,我们需要关心的还有如下问题:

  1. 怎么存储RememberMe的凭证?
  2. 怎么把凭证给前端?
  3. 前端怎么再把凭证给后端?

这听起来都是公共功能,因为我们前端的每个请求都可能会丢失登录态,需要通过RememberMe凭证完成登录态恢复。
庆幸的是,这些问题前辈们已经解决了,并且对前端完全无感。那就是基于一个长期有效的cookie,通过这个cookie把凭证给前端。而后端需要校验凭证时,也从该cookie中读取。

至于凭证的校验,这里有两种方式:

  • 基于令牌的 remember-me:
    这是最常见的实现方式。当用户选择“记住我”选项并成功登录后,系统会生成一个唯一的凭证(token),并将该令牌存储在数据库中。同时,这个令牌会被设置为一个 cookie 发送到用户的浏览器。
    当用户下次访问应用时,即使没有显式登录,系统也会检查这个 cookie 中的令牌,并与数据库中的令牌进行匹配。如果匹配成功,则自动登录用户。

  • 基于哈希的 remember-me:
    这种方式不需要在服务器端存储任何信息。它通过将用户的用户名、过期时间和一个密钥进行哈希计算,生成一个签名。这个签名会被设置为一个 cookie 发送到用户的浏览器。
    当用户下次访问应用时,系统会验证这个签名的有效性。如果签名有效,则自动登录用户。

由此,也引出SpringSecuirty的两个RememberMeServices的具体实现。

  1. PersistentTokenBasedRememberMeServices
    由服务端存储Token。因此他依赖于PersistentTokenRepository。Spring提供了两个实现:InMemoryTokenRepositoryImpl、JdbcTokenRepositoryImpl。如果你想自定义,又不知道怎么设计表结构,那么可以参考下JdbcTokenRepositoryImpl#CREATE_TABLE_SQL
    public class PersistentTokenBasedRememberMeServices extends AbstractRememberMeServices {
    	@Override
    	protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
    		Authentication successfulAuthentication) {
    		String username = successfulAuthentication.getName();
    		// 生成token,
    		// 第二个参数是唯一键,后面要通过他来从数据库中读取凭证
    		// 第三个参数是凭证
    		// 第四个参数是凭证创建时间
    		PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(),
    				generateTokenData(), new Date());
    		// 保存到数据库中
    		this.tokenRepository.createNewToken(persistentToken);
    		// 将凭证添加到cookie
    		addCookie(persistentToken, request, response);
    	}
    
    	protected String generateSeriesData() {
    		byte[] newSeries = new byte[this.seriesLength];
    		this.random.nextBytes(newSeries);
    		return new String(Base64.getEncoder().encode(newSeries));
    	}
    	protected String generateTokenData() {
    		byte[] newToken = new byte[this.tokenLength];
    		// 随机生成token
    		this.random.nextBytes(newToken);
    		return new String(Base64.getEncoder().encode(newToken));
    	}
    	@Override
    	protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
    		HttpServletResponse response) {
    		String presentedSeries = cookieTokens[0];
    		String presentedToken = cookieTokens[1];
    		// 从数据库中读取凭证
    		PersistentRememberMeToken token = this.tokenRepository.getTokenForSeries(presentedSeries);
    		if (token == null) {
    			// 抛出认证异常
    			throw new RememberMeAuthenticationException("No persistent token found for series id: " + presentedSeries);
    		}
    		// 校验凭证是否有效,即是否跟数据库中的一致
    		if (!presentedToken.equals(token.getTokenValue())) {
    			// 凭证与数据库中的不一致,意味着可能遭到攻击了,清理掉该用户所有凭证
    			this.tokenRepository.removeUserTokens(token.getUsername());
    			// 抛出认证异常
    			throw new CookieTheftException(this.messages.getMessage(
    					"PersistentTokenBasedRememberMeServices.cookieStolen",
    					"Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
    		}
    		// 检查token有效期
    		if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
    			// 抛出认证异常
    			throw new RememberMeAuthenticationException("Remember-me login has expired");
    		}
    		PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(),
    				generateTokenData(), new Date());
    		try {
    			// 刷新凭证有效期		
    			this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
    			// 重新放到token
    			addCookie(newToken, request, response);
    		}
    		catch (Exception ex) {
    			// 抛出认证异常
    			throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
    		}
    		// 自动认证成功,返回用户信息
    		return getUserDetailsService().loadUserByUsername(token.getUsername());
    	}
    	@Override
    	protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
    		Authentication successfulAuthentication) {
    	String username = successfulAuthentication.getName();
    	// 生成凭证 -- 随机的
    	PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, generateSeriesData(),
    			generateTokenData(), new Date());
    	try {
    		// 此处的方法名有点歧义,其实是保存将凭证保存到数据库中
    		this.tokenRepository.createNewToken(persistentToken);
    		// 将凭证通过cookie返回给浏览器
    		addCookie(persistentToken, request, response);
    	}
    	catch (Exception ex) {
    		// 单纯地吃掉异常,因为登录成功了。不能因为rememberMe异常导致登录失败。
    	}
    	@Override
    	public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
    		super.logout(request, response, authentication);
    		if (authentication != null) {
    			// 清理掉服务端保存的RememberMe凭证
    			this.tokenRepository.removeUserTokens(authentication.getName());
    		}
    	}
    }
    
    实现的关键就是,一个唯一值,用来从数据库中索引到当前凭证。另一个便是这凭证。因此这两个信息都是要保存在cookie中的。同样也要保存在数据库中。因为有效期是从数据库中读取到凭证的创建时间后再计算得到的。
  2. TokenBasedRememberMeServices
    public class TokenBasedRememberMeServices extends AbstractRememberMeServices {
    	
    	@Override
    	protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request,
    			HttpServletResponse response) {
    		// 获取凭证
    		if (!isValidCookieTokensLength(cookieTokens)) {
    			throw new InvalidCookieException(
    					"Cookie token did not contain 3 or 4 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
    		}
    		// 检查凭证是否有效
    		long tokenExpiryTime = getTokenExpiryTime(cookieTokens);
    		if (isTokenExpired(tokenExpiryTime)) {
    			throw new InvalidCookieException("Cookie token[1] has expired (expired on '" + new Date(tokenExpiryTime)
    					+ "'; current time is '" + new Date() + "')");
    		}
    		// 通过凭证查询用户
    		UserDetails userDetails = getUserDetailsService().loadUserByUsername(cookieTokens[0]);
    		// 检查凭证签名
    		String actualTokenSignature = cookieTokens[2];
    		// 默认的凭证签名算法为sha256
    		RememberMeTokenAlgorithm actualAlgorithm = this.matchingAlgorithm;
    		if (cookieTokens.length == 4) {
    			actualTokenSignature = cookieTokens[3];
    			// 指定了签名算法,解析
    			actualAlgorithm = RememberMeTokenAlgorithm.valueOf(cookieTokens[2]);
    		}
    		// 通过用户信息和凭证算法,计算得出一个预期的有效凭证
    		String expectedTokenSignature = makeTokenSignature(tokenExpiryTime, userDetails.getUsername(),
    				userDetails.getPassword(), actualAlgorithm);
    		// 凭证签名与计算的一致就是有效凭证
    		if (!equals(expectedTokenSignature, actualTokenSignature)) {
    			throw new InvalidCookieException("Cookie contained signature '" + actualTokenSignature + "' but expected '"
    					+ expectedTokenSignature + "'");
    		}
    		return userDetails;
    	}
    	@Override
    	public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
    		Authentication successfulAuthentication) {
    		String username = retrieveUserName(successfulAuthentication);
    		String password = retrievePassword(successfulAuthentication);
    		if (!StringUtils.hasLength(username)) {
    			// 用户名为空
    			return;
    		}
    		if (!StringUtils.hasLength(password)) {
    			// 密码为空,从数据库加载
    			UserDetails user = getUserDetailsService().loadUserByUsername(username);
    			password = user.getPassword();
    			if (!StringUtils.hasLength(password)) {
    				// 密码依然为空,就退出处理了。为了不影响正常的登录流程
    				return;
    			}
    		}
    		// 计算凭证有效期,以便刷新有效期
    		int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
    		long expiryTime = System.currentTimeMillis();
    		// SEC-949
    		expiryTime += 1000L * ((tokenLifetime < 0) ? TWO_WEEKS_S : tokenLifetime);
    		// 用新的有效期重新计算签名
    		String signatureValue = makeTokenSignature(expiryTime, username, password, this.encodingAlgorithm);
    		// 刷新凭证
    		setCookie(new String[] { username, Long.toString(expiryTime), this.encodingAlgorithm.name(), signatureValue },
    				tokenLifetime, request, response);
    	}
    	protected String makeTokenSignature(long tokenExpiryTime, String username, String password,
    		RememberMeTokenAlgorithm algorithm) {
    		// 拼接签名的目标数据
    		String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
    		try {
    			// 获取签名算法
    			MessageDigest digest = MessageDigest.getInstance(algorithm.getDigestAlgorithm());
    			// 完成签名
    			return new String(Hex.encode(digest.digest(data.getBytes())));
    		}
    		catch (NoSuchAlgorithmException ex) {
    			throw new IllegalStateException("No " + algorithm.name() + " algorithm available!");
    		}
    	}
    }
    
    可以看到签名的数据甚至包括用户密码,而其内部类TokenBasedRememberMeServices.RememberMeTokenAlgorithm提供的两个算法是MD5和SHA256安全性都不是很好。虽然其父类setCookie方法中将该cookie设置为httpOnly,相对提升安全性。

小结

rememberMe功能的核心主键RememberMeServices有两个实现:

RememberMeServices原理劣势优势
PersistentTokenBasedRememberMeServices通过随机产生凭证,保存到数据库中。占用服务端存储资源提高安全性,凭证不包含任何用户信息
TokenBasedRememberMeServices以时间换空间,将用户信息存储于凭证中,而凭证是保存在cookie中的。cookie需要在网络中传输,存在暴露用户信息风险。所幸spring将cookie设置为httpOnly。不占用服务端的存储资源

完整流程源码

  1. 登录成功后,会调用this.rememberMeServices.loginSuccess(request, response, authResult);,这点在我们专栏的Spring Security之认证过滤器UsernamePasswordAuthenticationFilter之中也看到了。
  2. RememberMeAuthenticationFilter
    public class RememberMeAuthenticationFilter extends GenericFilterBean implements ApplicationEventPublisherAware {
    
    	@Override
    	public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain)
    			throws IOException, ServletException {
    		doFilter((HttpServletRequest) request, (HttpServletResponse) response, chain);
    	}
    
    	private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
    			throws IOException, ServletException {
    		if (this.securityContextHolderStrategy.getContext().getAuthentication() != null) {
    			// 处于登录态中,无需处理
    			chain.doFilter(request, response);
    			return;
    		}
    		// 通过rememberMeServices完成凭证校验
    		Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
    		if (rememberMeAuth != null) {
    			// 通过AuthenticationManager进行校验,这里校验的凭证本身的合法性。对于RememberMe有对应的RememberMeAuthenticationProvider。
    			try {
    				rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
    				// 与认证过滤器一样,需要完成如下事项
    				// 1. 保存安全上下文
    				SecurityContext context = this.securityContextHolderStrategy.createEmptyContext();
    				context.setAuthentication(rememberMeAuth);
    				this.securityContextHolderStrategy.setContext(context);
    				// 这里是个空方法
    				onSuccessfulAuthentication(request, response, rememberMeAuth);
    				if (this.eventPublisher != null) {
    					// 发布登录成功事件
    					this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
    							this.securityContextHolderStrategy.getContext().getAuthentication(), this.getClass()));
    				}
    				if (this.successHandler != null) {
    					// 执行登录处理器,跳转到指定地址
    					this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
    					return;
    				}
    			}
    			catch (AuthenticationException ex) {
    				// 登录失败,注意这里可是通过AuthenticationManager登录失败,不是自动登录失败,因为自动登录不在这个try-catch代码块中。
    				this.rememberMeServices.loginFail(request, response);
    				onUnsuccessfulAuthentication(request, response, ex);
    			}
    		}
    		chain.doFilter(request, response);
    	}
    }
    
    这里必须要说一下,如果RememberMeServices#autoLogin失败了,抛出的异常是如何处理的。这将由专栏之前的安全异常处理完成。当抛出的是认证异常,将会跳转到登录页面。如果是持久化的,那么数据库中的凭证也不会被清理。关于这点,我推测Spring考虑的是,只有认证(非RememberMe)失败或者用户主动注销才清理。至于自动认证失败,也就意味着凭证已经无效了,清理与否也就无关紧要了。换而言之,只有有效的凭证才有清理的意义。

3.登出/登录态注销。
这个前面已经看到AbstractRememberServices也是LogoutHandler,将会被LogoutFilter调用。

配置RememberMe

HttpSecuirty.rememberMe(
customizer -> customizer.tokenValiditySeconds((int) Duration.ofDays(7).toSeconds())
);

该配置引入了RememberMeConfigurer,同时指定了凭证的有效时间。接下来我们看看RememberMeConfigurer。

public final class RememberMeConfigurer<H extends HttpSecurityBuilder<H>>
		extends AbstractHttpConfigurer<RememberMeConfigurer<H>, H> {
	@Override
	public void init(H http) throws Exception {
		validateInput();
		String key = getKey();
		// 获取RememberMeServices
		RememberMeServices rememberMeServices = getRememberMeServices(http, key);
		// 放入共享对象中,因为认证过滤器需要与之协同
		http.setSharedObject(RememberMeServices.class, rememberMeServices);
		LogoutConfigurer<H> logoutConfigurer = http.getConfigurer(LogoutConfigurer.class);
		if (logoutConfigurer != null && this.logoutHandler != null) {
			// 登记RememberMe的登出处理器	
			logoutConfigurer.addLogoutHandler(this.logoutHandler);
		}
		// 这个用来校验RememberMeAuthentication的
		// 无论那种凭证都需要这个Provider来鉴定
		// 他会被放到ProviderManager(他是AuthenticationManager)中
		RememberMeAuthenticationProvider authenticationProvider = new RememberMeAuthenticationProvider(key);
		authenticationProvider = postProcess(authenticationProvider);
		http.authenticationProvider(authenticationProvider);
		initDefaultLoginFilter(http);
	}
	private RememberMeServices getRememberMeServices(H http, String key) throws Exception {
		// 如果用户指定了RememberMeServices 
		if (this.rememberMeServices != null) {
			if (this.rememberMeServices instanceof LogoutHandler && this.logoutHandler == null) {
				// 确认实现了logoutHandler
				this.logoutHandler = (LogoutHandler) this.rememberMeServices;
			}
			return this.rememberMeServices;
		}
		// 创建
		AbstractRememberMeServices tokenRememberMeServices = createRememberMeServices(http, key);
		// 指定rememberMe参数,因为这是用户选择勾选才有的
		tokenRememberMeServices.setParameter(this.rememberMeParameter);
		tokenRememberMeServices.setCookieName(this.rememberMeCookieName);
		if (this.rememberMeCookieDomain != null) {
			tokenRememberMeServices.setCookieDomain(this.rememberMeCookieDomain);
		}
		if (this.tokenValiditySeconds != null) {
			// 指定了凭证有效期
			tokenRememberMeServices.setTokenValiditySeconds(this.tokenValiditySeconds);
		}
		if (this.useSecureCookie != null) {
			// 指定使用https
			tokenRememberMeServices.setUseSecureCookie(this.useSecureCookie);
		}
		if (this.alwaysRemember != null) {
			// 配置了总是rememberMe
			tokenRememberMeServices.setAlwaysRemember(this.alwaysRemember);
		}
		tokenRememberMeServices.afterPropertiesSet();
		this.logoutHandler = tokenRememberMeServices;
		this.rememberMeServices = tokenRememberMeServices;
		return tokenRememberMeServices;
	}	
	private AbstractRememberMeServices createRememberMeServices(H http, String key) {
		// 配置了tokenRepository,那就是持久化的,否则就是基于Hash算法,后端不存在凭证
		return (this.tokenRepository != null) ? createPersistentRememberMeServices(http, key)
				: createTokenBasedRememberMeServices(http, key);
	}
	@Override
	public void configure(H http) {
		// 前面的源码我们也发现需要来AuthenticationManager,因此在构建时需要从共享对象中获取
		RememberMeAuthenticationFilter rememberMeFilter = new RememberMeAuthenticationFilter(
				http.getSharedObject(AuthenticationManager.class), this.rememberMeServices);
		if (this.authenticationSuccessHandler != null) {
			// 如果指定了rememberMe的认证成功处理器则配置。这个可以与认证过滤器不一样。如果没有配置的话,按照流程会继续执行原请求(还在请求的处理过程中)
			rememberMeFilter.setAuthenticationSuccessHandler(this.authenticationSuccessHandler);
		}
		SecurityContextConfigurer<?> securityContextConfigurer = http.getConfigurer(SecurityContextConfigurer.class);
		if (securityContextConfigurer != null && securityContextConfigurer.isRequireExplicitSave()) {
			SecurityContextRepository securityContextRepository = securityContextConfigurer
				.getSecurityContextRepository();
			// 配置SecurityContextPepository,这与认证过滤器类似,认证成功需要保存安全上下文
			rememberMeFilter.setSecurityContextRepository(securityContextRepository);
		}
		rememberMeFilter.setSecurityContextHolderStrategy(getSecurityContextHolderStrategy());
		rememberMeFilter = postProcess(rememberMeFilter);
		// 增加过滤器。
		http.addFilter(rememberMeFilter);
	}
}

就这样,我们把完成自动认证-恢复登录态的RememberMeAuthenticationFilter、清理凭证的LogoutHandler、和与认证过滤器协作的负责创建RememberMe凭证的RememberMeServices就都配置好了。
如果想要使用服务端存储方案的PersistentTokenBasedRememberMeServices,需要声明一个PersistentTokenRepository对象。例如:

@Bean
public PersistentTokenRepository persistentTokenRepository() {
	return new InMemoryTokenRepositoryImpl();
}

总结

  1. RememberMe可以作为保持登录态的一种手段,减少用户频繁使用系统的操作。
  2. RememberMe功能需要认证过滤器和RememberMeAuthenticationFilter、LogoutFilter(体现在LogoutHandler上)的协同。
  3. RememberMe最核心的凭证校验能力,分为:PersistentTokenBasedRememberMeServices(基于服务端存储凭证)TokenBasedRememberMeServices(基于Hash算法验证凭证)

后记

至此,Spring Security的核心内容基本就介绍完毕了。至于Logout这个太过于简单,不准备单独介绍了,无非就是清理登录态,也就是安全上下文。下次就是要进行功能定制了,例如:验证码校验、自定义AuthorizationManager(鉴权/授权过滤器)

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值