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)
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值