Spring Security(七):實現“記住我”功能

這章繼續擴展功能,來一個“記住我”的功能實現,就是說用戶在登錄一次以後,系統會記住這個用戶一段時間,這段時間內用戶不需要重新登錄就可以使用系統。

記住我功能基本原理

原理說明

  • 用戶登錄發送認證請求的時候會被UsernamePasswordAuthenticationFilter認證攔截,認證成功以後會調用一個RememberMeService服務,服務裏面有一個TokenRepository,這個服務會生成一個Token,然後將Token寫入到瀏覽器的Cookie同時會使用TokenRepository把生成的Token寫到數據庫裏面,因爲這個動作是在認證成功以後做的,所以在Token寫入數據庫的時候會把用戶名同時寫入數據庫。
  • 假如瀏覽器關了重新訪問系統,用戶不需要再次登錄就可以訪問,這個時候請求在過濾器鏈上會經過RememberMeAuthenticationFilter,這個過濾器的作用是讀取Cookie中的Token交給RemeberMeService,RemeberMeService會用TokenRepository到數據庫裏去查這個Token在數據庫裏有沒有記錄,如果有記錄就會把用戶名取出來,取出來以後會進行各種校驗然後生成新Token再調用之前的UserDetailService,去獲取用戶的信息,然後把用戶信息放到SecurityContext裏面,到這裏就把用戶給登錄上了。

圖解說明

流程圖解

RememberMeAuthenticationFilter位於過濾器鏈的哪一環?

  • 圖解
    在這裏插入圖片描述
  • 首先其他認證過濾器會先進行認證,當其他過濾器都無法認證時,RememberMeAuthenticationFilter會嘗試去做認證。

記住我功能具體實現

前端頁面

  • 登錄的時候加上一行記住我的勾選按鈕,這裏要注意,name一定要是remember-me,下面源碼部分會提到。
			<tr>
				<td colspan='2'><input name="remember-me" type="checkbox" value="true" />記住我</td>
			</tr>

後臺

  • 首先配置TokenRepositoryBean
	/**
	 * 記住我功能的Token存取器配置
	 * 
	 * @return
	 */
	@Bean
	public PersistentTokenRepository persistentTokenRepository() {
		JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
		tokenRepository.setDataSource(dataSource);
		// 啓動的時候自動創建表,建表語句 JdbcTokenRepositoryImpl 已經都寫好了
		tokenRepository.setCreateTableOnStartup(true);
		return tokenRepository;
	}
  • 然後需要在 configure 配置方法那邊進行記住我功能所有組件的配置
	protected void configure(HttpSecurity http) throws Exception {
		ValidateCodeFilter validateCodeFilter = new ValidateCodeFilter();
		http.addFilterBefore(validateCodeFilter, UsernamePasswordAuthenticationFilter.class)
				.formLogin()
				.loginPage("/authentication/require")
				.loginProcessingUrl("/authentication/form")
				.successHandler(meicloudAuthenticationSuccessHandler)
				.failureHandler(meicloudAuthenticationFailureHandler)
				// 配置記住我功能
				.and()
				.rememberMe()
				// 配置TokenRepository
				.tokenRepository(persistentTokenRepository())
				// 配置Token過期時間
				.tokenValiditySeconds(3600)
				// 最終拿到用戶名之後,使用UserDetailsService去做登錄
				.userDetailsService(userDetailsService)
				.and()
				.authorizeRequests()
				.antMatchers("/authentication/require", securityProperties.getBrowser().getSignInPage(), "/code/image").permitAll()
				.anyRequest()
				.authenticated()
				.and()
				.csrf().disable();

	}

記住我功能Spring Security源碼解析

登錄之前“記住我”源碼流程

  • 在認證成功之後,會調用successfulAuthentication方法(這些第五章源碼部分已經學習過),在將認證信息保存到Context後,RememberMeServices就會調用它的loginSuccess方法
    protected void successfulAuthentication(HttpServletRequest request, HttpServletResponse response, FilterChain chain, Authentication authResult) throws IOException, ServletException {
        if (this.logger.isDebugEnabled()) {
            this.logger.debug("Authentication success. Updating SecurityContextHolder to contain: " + authResult);
        }

        SecurityContextHolder.getContext().setAuthentication(authResult);
        this.rememberMeServices.loginSuccess(request, response, authResult);
        if (this.eventPublisher != null) {
            this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(authResult, this.getClass()));
        }

        this.successHandler.onAuthenticationSuccess(request, response, authResult);
    }
  • loginSuccess方法裏面會先檢查請求中是否有name爲remember-me的參數,有才進行下一步。
    public final void loginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
    	// this.parameter = "remember-me"
        if (!this.rememberMeRequested(request, this.parameter)) {
            this.logger.debug("Remember-me login not requested.");
        } else {
            this.onLoginSuccess(request, response, successfulAuthentication);
        }
    }
  • 再進入onLoginSuccess方法,裏面主要就是進行寫庫和寫Cookie的操作。
    protected void onLoginSuccess(HttpServletRequest request, HttpServletResponse response, Authentication successfulAuthentication) {
        String username = successfulAuthentication.getName();
        this.logger.debug("Creating new persistent login for user " + username);
        // 生成Token
        PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(username, this.generateSeriesData(), this.generateTokenData(), new Date());
        try {
        	// 將Token和userName插入數據庫
            this.tokenRepository.createNewToken(persistentToken);
            // 將Token寫到Cookie中
            this.addCookie(persistentToken, request, response);
        } catch (Exception var7) {
            this.logger.error("Failed to save persistent token ", var7);
        }
    }

登錄之後“記住我”源碼流程

  • 首先會進入RememberMeAuthenticationFilter,會先判斷前面的過濾器是否進行過認證(Context中是否有認證信息),未進行過認證的話會調用RememberMeServices的autoLogin方法。
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        if (SecurityContextHolder.getContext().getAuthentication() == null) {
            Authentication rememberMeAuth = this.rememberMeServices.autoLogin(request, response);
            if (rememberMeAuth != null) {
                try {
                    rememberMeAuth = this.authenticationManager.authenticate(rememberMeAuth);
                    SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
                    this.onSuccessfulAuthentication(request, response, rememberMeAuth);
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("SecurityContextHolder populated with remember-me token: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
                    }

                    if (this.eventPublisher != null) {
                        this.eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(SecurityContextHolder.getContext().getAuthentication(), this.getClass()));
                    }

                    if (this.successHandler != null) {
                        this.successHandler.onAuthenticationSuccess(request, response, rememberMeAuth);
                        return;
                    }
                } catch (AuthenticationException var8) {
                    if (this.logger.isDebugEnabled()) {
                        this.logger.debug("SecurityContextHolder not populated with remember-me token, as AuthenticationManager rejected Authentication returned by RememberMeServices: '" + rememberMeAuth + "'; invalidating remember-me token", var8);
                    }

                    this.rememberMeServices.loginFail(request, response);
                    this.onUnsuccessfulAuthentication(request, response, var8);
                }
            }
            chain.doFilter(request, response);
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '" + SecurityContextHolder.getContext().getAuthentication() + "'");
            }
            chain.doFilter(request, response);
        }
    }
  • autoLogin方法裏面,主要調用this.processAutoLoginCookie(cookieTokens, request, response)這個方法獲取數據庫中的用戶信息,其步驟是:
  • 解析前端傳來的Cookie,裏面包含了Token和seriesId,它會使用seriesId查找數據庫的Token
  • 檢查Cookie中的Token和數據庫查出來的Token是否一樣
  • 一樣的話再檢查數據庫中的Token是否已過期
  • 如果以上都符合的話,會使用舊的用戶名和series重新new一個Token,這時過期時間也重新刷新
  • 然後將新的Token保存回數據庫,同時添加回Cookie
  • 最後再調用UserDetailsService的loadUserByUsername方法返回UserDetails
protected UserDetails processAutoLoginCookie(String[] cookieTokens, HttpServletRequest request, HttpServletResponse response) {
        if (cookieTokens.length != 2) {
            throw new InvalidCookieException("Cookie token did not contain 2 tokens, but contained '" + Arrays.asList(cookieTokens) + "'");
        } else {
            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);
            } else 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."));
            } else if (token.getDate().getTime() + (long)this.getTokenValiditySeconds() * 1000L < System.currentTimeMillis()) {
                throw new RememberMeAuthenticationException("Remember-me login has expired");
            } else {
                if (this.logger.isDebugEnabled()) {
                    this.logger.debug("Refreshing persistent login token for user '" + token.getUsername() + "', series '" + token.getSeries() + "'");
                }
                PersistentRememberMeToken newToken = new PersistentRememberMeToken(token.getUsername(), token.getSeries(), this.generateTokenData(), new Date());
                try {
                    this.tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(), newToken.getDate());
                    this.addCookie(newToken, request, response);
                } catch (Exception var9) {
                    this.logger.error("Failed to update token: ", var9);
                    throw new RememberMeAuthenticationException("Autologin failed due to data access problem");
                }

                return this.getUserDetailsService().loadUserByUsername(token.getUsername());
            }
        }
    }
  • 回到RememberMeAuthenticationFilter,在調用了autoLogin方法之後得到了rememberMeAuth,然後再對其進行一個認證,認證成功之後保存到SecurityContext中,至此整個RememberMe自動登錄流程源碼結束。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章