SpringSecurity學習筆記三

基於用戶名、密碼的"記住我"功能

SpringBoot 2.2.0.RELEASE

   <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.2</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-jdbc</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
        </dependency>
    </dependencies>

記住我功能流程圖
"記住我"功能的流程是:

  • 輸入驗證信息,如用戶名、密碼、驗證碼等
  • 選中 “記住我”,系統後臺根據登陸成功的用戶信息 生成token信息 放到cookie和數據庫表中
  • 【系統重新啓動或退出系統重新訪問】,在cookie的過期時間內 重新登錄時 都不需要輸入用戶名、密碼,可以直接訪問請求資源。

首先,需要明確的是"記住我"是SpringSecurity默認帶的,過濾器和認證類分別是RememberMeAuthenticationFilterRememberMeAuthenticationProvider我們只需要配置即可

需要注意的是,使用記住我功能,前端頁面必須要有一個name=remember-me的checkbox,
因爲SpringSecurity中與"記住我"有關的功能,將remember-me作爲參數名來進行業務處理,如AbstractRememberMeServices#rememberMeRequested()

<input name="remember-me" type="checkbox" value="true"/>記住我</td>

講到"記住我"功能,還要從之前的`UserNamePasswordAuthenticationFilter`說起,當執行了`attemptAuthentication`之後,會執行``successfulAuthentication(request, response, chain, authResult);``

有代碼爲證 AbstractAuthenticationProcessFilter
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {

		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;

		if (!requiresAuthentication(request, response)) {
			chain.doFilter(request, response);

			return;
		}

		if (logger.isDebugEnabled()) {
			logger.debug("Request is to process authentication");
		}

		Authentication authResult;

		//調用UserNamePasswordAuthenticationFilter方法
			authResult = attemptAuthentication(request, response);
		
		//此處省略一些代碼
		
		successfulAuthentication(request, response, chain, authResult);
	}

着重看一下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);
		}
		//將驗證過後的信息 存到SecurityContextHolder中
	SecurityContextHolder.getContext().setAuthentication(authResult);

		rememberMeServices.loginSuccess(request, response, authResult);

		// Fire event
		if (this.eventPublisher != null) {
			eventPublisher.publishEvent(new InteractiveAuthenticationSuccessEvent(
					authResult, this.getClass()));
		}
		//調用成功處理器
		successHandler.onAuthenticationSuccess(request, response, authResult);
	}

會發現,該方法除了將認證信息放到SecurityContext以外,還調用了rememberMeServices.loginSuccess方法

其中rememberMeServices.loginSuccess的調用過程如下:
AbstractRememberMeServices#loginSuccess==>PersistentTokenBasedRememberMeServices#onLoginSuccess

AbstractRememberMeServices

public final void loginSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication successfulAuthentication) {
		//這裏就是上面說的檢驗request中是否含有remember-me參數,
		//如果有 才往下執行
		if (!rememberMeRequested(request, parameter)) {
			logger.debug("Remember-me login not requested.");
			return;
		}

		onLoginSuccess(request, response, successfulAuthentication);
	}

可以從上面看到 parameter的默認是指remember-me。如果沒有選中‘記住我’複選框 則終止執行。

PersistentTokenBasedRememberMeServices

protected void onLoginSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication successfulAuthentication) {
		String username = successfulAuthentication.getName();

		logger.debug("Creating new persistent login for user " + username);

		PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
				username, generateSeriesData(), generateTokenData(), new Date());
		try {
			tokenRepository.createNewToken(persistentToken);
			addCookie(persistentToken, request, response);
		}
		catch (Exception e) {
			logger.error("Failed to save persistent token ", e);
		}
	}

這個方法的主要含義:
從登陸成功的信息中拿到用戶名,隨後生成一條插入語句 將token等信息插入到persistent_logins這個表中。並將token信息添加到cookie中。

persistent_logins這個是SpringSecurity默認提供出來針對“記住我”功能來記錄token和cookie的數據表

tokenRepository這個在該類中的默認實現是new InMemoryTokenRepositoryImpl(),我們可以在Security配置中指定我們的tokenRepository。


PersistentTokenRepository有兩種實現,分別是InMemoryTokenRepositoryImpl和JdbcTokenRepositoryImpl。兩者的區別不言而喻,一個是將token信息放到內存中,一個是將其持久化到數據庫



說了這麼多,我們來配置下

protected void configure(HttpSecurity http) throws Exception {

//        ValidateCodeFilter validateCodeFilter=new ValidateCodeFilter(failureHandler);

        //表單登錄 任何請求均進行認證
       http.formLogin()
       //默認登錄請求是/login,這裏重置爲/authentication/form 並不影響流程
                .loginProcessingUrl("/authentication/form")
                .successHandler(successHandler)
                .failureHandler(failureHandler)
                .and()
                .rememberMe()
                .userDetailsService(userDetailsService)
                .tokenRepository(persistentTokenRepository()).
                and()
                .authorizeRequests()
                .anyRequest().authenticated()
                .and()
                .csrf()
                .disable();
    }
		
@Autowired
    private DataSource dataSource;

    //這個Bean 就是上文我們說的替代默認tokenRepository的配置
	@Bean
    @ConditionalOnMissingBean(PersistentTokenRepository.class)
    public PersistentTokenRepository persistentTokenRepository() {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
      //這句話 只有在第一次啓動時 才放開註釋 因爲會創建表,再次運行時 註釋,因爲存在的表不能被再次創建
       // jdbcTokenRepository.setCreateTableOnStartup(true);
        return jdbcTokenRepository;
    }

關於數據源的配置

spring.datasource.url=jdbc:mysql://localhost:3306/study
spring.datasource.username=root
spring.datasource.password=root
spring.datasource.driver-class-name=com.mysql.jdbc.Driver

系統運行起來後:

在這裏插入圖片描述
點擊登錄,認證成功處理器關於自定義認證處理器,請參考這篇文章會將用戶信息打印到頁面
在這裏插入圖片描述

前面我們分析過,由於選中了"remember-me",用戶名、密碼登錄成功後,會把用戶的token存到表裏 並且寫入到cookie裏一份。
我們去persistent_logins表中看下,發現token計入了表中
在這裏插入圖片描述
重啓系統,發現不需要登錄,直接可以請求接口,這說明了我們成功了。



源碼分析
退出系統,重新再次訪問系統資源時,會先進入到RememberMeAutnenticationFilter

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain)
			throws IOException, ServletException {
		HttpServletRequest request = (HttpServletRequest) req;
		HttpServletResponse response = (HttpServletResponse) res;
	
  //系統重新啓動或用戶退出過,所以SecurityContextHolder中沒有用戶的認證信息
		if (SecurityContextHolder.getContext().getAuthentication() == null) {
			Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
					response);

			if (rememberMeAuth != null) {
			
				try {
          //根據自動登錄獲取到的用戶信息 進行認證 這裏認證就很簡單了 判斷hashcode是否一致
					rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);

					
//將用戶認證信息重新放到SecurityContextHolder中			
//SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
					onSuccessfulAuthentication(request, response, rememberMeAuth);
     ...

			chain.doFilter(request, response);
		}
	}

rememberServices#autoLogin方法如下

public final Authentication autoLogin(HttpServletRequest request,
			HttpServletResponse response) {
  //從request中獲取cookie
		String rememberMeCookie = extractRememberMeCookie(request);
 ...
			user = processAutoLoginCookie(cookieTokens, request, response);
...
  //封裝用戶認證信息 這裏省略了
	}

processAutoLoginCookie方法
PersistentTokenBasedRememberMeServices

 UserDetails processAutoLoginCookie(String[] cookieTokens,
			HttpServletRequest request, HttpServletResponse response) {
	//存到數據庫中的信息如下
//	前文中提到的`persistent_logins`表中series、token 分別代表下面的兩個參數
		final String presentedSeries = cookieTokens[0];
		final String presentedToken = cookieTokens[1];
		
  //從表中取出token數據
		PersistentRememberMeToken token = tokenRepository
				.getTokenForSeries(presentedSeries);
...
		PersistentRememberMeToken newToken = new PersistentRememberMeToken(
				token.getUsername(), token.getSeries(), generateTokenData(), new Date());

		try {
      //更新表中的token
			tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
					newToken.getDate());
      //重新將新token 添加到cookie中
			addCookie(newToken, request, response);
		}
	...
    //獲取user信息,從這裏可以看出UserDetailsService或自定義UserDetailsService必須在RememberMe配置中配置
		return getUserDetailsService().loadUserByUsername(token.getUsername());
	}

RememberMeAuthenticationProvider的認證過程相較於其他provider,比較簡單 這裏一筆帶過了


以上,是個人對”記住我“功能的個人實踐與源碼理解。如有問題,請指出,謝謝。
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章