Spring Security系列之記住我(十二)

蔣老溼發表於2018-12-24

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

記住我基本原理

Spring Security系列之記住我(十二)

  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);
    
    複製程式碼
  2. 登陸頁面新增記住我複選款(name必須是remeber-me)
    <input name="remember-me" type="checkbox"> 下次自動登入
    複製程式碼
  3. 配置MerryyouSecurityConfig
    http.
    ......
                 .and()
                 .rememberMe()
                 .tokenRepository(persistentTokenRepository())//設定操作表的Repository
                 .tokenValiditySeconds(securityProperties.getRememberMeSeconds())//設定記住我的時間
                 .userDetailsService(userDetailsService)//設定userDetailsService
                 .and()
     ......
    複製程式碼

效果如下

Spring Security系列之記住我(十二)

原始碼分析

首次登入

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…

文章來源

相關文章