Spring Security原始碼分析七:Spring Security 記住我

鄭龍飛發表於2018-01-18

有這樣一個場景——有個使用者初訪並登入了你的網站,然而第二天他又來了,卻必須再次登入。於是就有了“記住我”這樣的功能來方便使用者使用,然而有一件不言自明的事情,那就是這種認證狀態的”曠日持久“早已超出了使用者原本所需要的使用範圍。這意味著,他們可以關閉瀏覽器,然後再關閉電腦,下週或者下個月,乃至更久以後再回來,只要這間隔時間不要太離譜,該網站總會知道誰是誰,並一如既往的為他們提供所有相同的功能和服務——與許久前他們離開的時候別無二致。

記住我基本原理

https://user-gold-cdn.xitu.io/2018/1/17/161049fa614ae478?w=1083&h=589&f=png&s=35619
https://user-gold-cdn.xitu.io/2018/1/17/161049fa614ae478?w=1083&h=589&f=png&s=35619

  1. 使用者認證成功之後呼叫RemeberMeService根據使用者名稱名生成TokenTokenRepository寫入到資料庫,同時也將Token寫入到瀏覽器的Cookie
  2. 重啟服務之後,使用者再次登入系統會由RememberMeAuthenticationFilter攔截,從Cookie中讀取Token資訊,與persistent_logins表匹配判斷是否使用記住我功能。最中由UserDetailsService查詢使用者資訊

記住我實現

  1. 建立persistent_logins
create table persistent_logins (username varchar(64) not null, series varchar(64) primary key, token varchar(64) not null, last_used timestamp not null);
複製程式碼
  1. 登陸頁面新增記住我複選款(name必須是remeber-me)
<input name="remember-me" type="checkbox"> 下次自動登入
複製程式碼
  1. 配置MerryyouSecurityConfig
http.
......
                .and()
                .rememberMe()
                .tokenRepository(persistentTokenRepository())//設定操作表的Repository
                .tokenValiditySeconds(securityProperties.getRememberMeSeconds())//設定記住我的時間
                .userDetailsService(userDetailsService)//設定userDetailsService
                .and()
	......
複製程式碼

效果如下

https://user-gold-cdn.xitu.io/2018/1/17/161049fab609ee1f?w=1346&h=655&f=gif&s=3401498
https://user-gold-cdn.xitu.io/2018/1/17/161049fab609ee1f?w=1346&h=655&f=gif&s=3401498

原始碼分析

首次登入

AbstractAuthenticationProcessingFilter#successfulAuthentication
protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {

		if (logger.isDebugEnabled()) {
			logger.debug("Authentication success. Updating SecurityContextHolder to contain: "
					+ authResult);
		}
		//# 1.將已認證過的Authentication放入到SecurityContext中
		SecurityContextHolder.getContext().setAuthentication(authResult);
		//# 2.登入成功呼叫rememberMeServices
		rememberMeServices.loginSuccess(request, response, authResult);

		// Fire event
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}

		successHandler.onAuthenticationSuccess(request, response, authResult);
	}
複製程式碼
  1. 將已認證過的Authentication放入到SecurityContext中
  2. 登入成功呼叫rememberMeServices
AbstractRememberMeServices#loginSuccess
private String parameter = DEFAULT_PARAMETER;//remember-me

public final void loginSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication successfulAuthentication) {
		// #1.判斷是否勾選記住我
		if (!rememberMeRequested(request, parameter)) {
			logger.debug("Remember-me login not requested.");
			return;
		}

		onLoginSuccess(request, response, successfulAuthentication);
	}
複製程式碼
  1. 判斷是否勾選記住我
PersistentTokenBasedRememberMeServices#onLoginSuccess
protected void onLoginSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication successfulAuthentication) {
		//#1.獲取使用者名稱
		String username = successfulAuthentication.getName();

		logger.debug("Creating new persistent login for user " + username);
		//#2.建立Token
		PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
				username, generateSeriesData(), generateTokenData(), new Date());
		try {
			//#3.儲存都資料庫
			tokenRepository.createNewToken(persistentToken);
			//#4.寫入到瀏覽器的Cookie中
			addCookie(persistentToken, request, response);
		}
		catch (Exception e) {
			logger.error("Failed to save persistent token ", e);
		}
	}
複製程式碼
  1. 獲取使用者名稱
  2. 建立Token
  3. 儲存都資料庫
  4. 寫入到瀏覽器的Cookie中

二次登入Remember-me

RememberMeAuthenticationFilter#doFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
		//#1.判斷SecurityContext中沒有Authentication
		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			//#2.從Cookie查詢使用者資訊返回RememberMeAuthenticationToken
			Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
					response);

			if (rememberMeAuth != null) {
				// Attempt authenticaton via AuthenticationManager
				try {
					//#3.如果不為空則由authenticationManager認證
					rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);

					// Store to SecurityContextHolder
					SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);

					onSuccessfulAuthentication(request, response, rememberMeAuth);
......
複製程式碼
  1. 判斷SecurityContext中沒有Authentication
  2. 從Cookie查詢使用者資訊返回RememberMeAuthenticationToken
  3. 如果不為空則由authenticationManager認證
AbstractRememberMeServices#autoLogin
public final Authentication autoLogin(HttpServletRequest request,
			HttpServletResponse response) {
			//#1.獲取Cookie
		String rememberMeCookie = extractRememberMeCookie(request);

		if (rememberMeCookie == null) {
			return null;
		}

		logger.debug("Remember-me cookie detected");

		if (rememberMeCookie.length() == 0) {
			logger.debug("Cookie was empty");
			cancelCookie(request, response);
			return null;
		}

		UserDetails user = null;

		try {
			//#2.解析Cookie
			String[] cookieTokens = decodeCookie(rememberMeCookie);
			//#3.獲取使用者憑證
			user = processAutoLoginCookie(cookieTokens, request, response);
			//#4.檢查使用者憑證
			userDetailsChecker.check(user);

			logger.debug("Remember-me cookie accepted");
			//#5.返回Authentication
			return createSuccessfulAuthentication(request, user);
		}
		catch (CookieTheftException cte) {
			cancelCookie(request, response);
			throw cte;
		}
		catch (UsernameNotFoundException noUser) {
			logger.debug("Remember-me login was valid but corresponding user not found.",
					noUser);
		}
		catch (InvalidCookieException invalidCookie) {
			logger.debug("Invalid remember-me cookie: " + invalidCookie.getMessage());
		}
		catch (AccountStatusException statusInvalid) {
			logger.debug("Invalid UserDetails: " + statusInvalid.getMessage());
		}
		catch (RememberMeAuthenticationException e) {
			logger.debug(e.getMessage());
		}

		cancelCookie(request, response);
		return null;
	}
複製程式碼
  1. 獲取Cookie
  2. 解析Cookie
  3. 獲取使用者憑證
  4. 檢查使用者憑證

程式碼下載

從我的 github 中下載,github.com/longfeizhen…

相關文章