SpringSecurity入門5---自動登錄(RememberMe)

代碼地址

實現方式

SpringSecurity提供了兩種令牌

  1. 散列算法加密用戶必要的登錄信息並生成令牌
  2. 數據庫等持久性數據存儲機制用的持久化令牌

散列加密方式

使用方式很簡單,修改配置文件,加入RememberMe即可

protected void configure(HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/css/**", "/img/**", "/js/**", "/bootstrap/**", "/captcha.jpg").permitAll()
                .antMatchers("/app/api/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/myLogin.html")
                .loginProcessingUrl("/login")
                .successHandler(successHandler)
                .failureHandler(failureHandler)
                .authenticationDetailsSource(myWebAuthenticationDetailsSource)
                .permitAll()
                .and()
                // 添加記住我,需要提供UserDetailService
                .rememberMe().userDetailsService(userDetailService)
                // 使登錄頁不受限
                .and()
                .csrf().disable();
    }

修改前端代碼,添加記住我選擇框,name要爲remember-me

							<div class="form-group">
                                <label for="captcha">驗證碼
                                </label>
                                <input id="captcha" type="text" class="form-control" name="captcha" required>
                                <img src="/captcha.jpg" alt="captcha" height="50px" width="150px">
                            </div>
                            <div class="form-group">
                                <label>
                                    <input type="checkbox" name="remember-me" value="true"> Remember Me
                                </label>
                            </div>

重啓項目登錄系統,勾選記住我
在這裏插入圖片描述
關閉瀏覽器後再重新訪問路徑,可以看到不需要再次登錄就可以訪問
在這裏插入圖片描述
F12打開谷歌瀏覽器的開發者工具,查看cookie中,除了JSessionId之外還多出了一個Remember-me的值,這個就是令牌
在這裏插入圖片描述
生成和驗證Token的邏輯在AbstractRememberMeServices中有基本的實現,我們來看一下它的子類TokenBasedRememberMeServices,它有一個方法用於生成Token

protected String makeTokenSignature(long tokenExpiryTime, String username,
			String password) {
		String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
		MessageDigest digest;
		try {
			digest = MessageDigest.getInstance("MD5");
		}
		catch (NoSuchAlgorithmException e) {
			throw new IllegalStateException("No MD5 algorithm available!");
		}

		return new String(Hex.encode(digest.digest(data.getBytes())));
	}

先將用戶名、過期時間、密碼、key(salt)進行一個MD5加密,再用base64加密用戶名+過期時間+上一步md5加密的值。

當驗證的時候,使用base64對令牌進行解密獲取到用戶名、過期時間和加密的散列值,在通過用戶名(我們注入了UserDetailService)查詢到用戶密碼,再次正向進行一次散列算法,與之前加密散列值進行對比來判斷令牌是否有效。

需要注意的是,我們用到的getKey(),key是在構造函數傳入的,通過我們配置的Remember點進去,查找到RememberMeConfigurer,裏面對我們的RememberService進行了初始化,傳入了Key值,其中的getKey方法如下

private String getKey() {
        if (this.key == null) {
            if (this.rememberMeServices instanceof AbstractRememberMeServices) {
                this.key = ((AbstractRememberMeServices)this.rememberMeServices).getKey();
            } else {
                this.key = UUID.randomUUID().toString();
            }
        }

        return this.key;
    }

當我們未指定key的情況下,key會被設置爲UUID,也就是說當系統每次重啓之後的key值是不一樣的,當我們系統重啓後,用戶之前的RememberMe的令牌肯定就失效了,爲了避免這個問題我們可以自己指定key的值

.rememberMe().userDetailsService(userDetailService).key("anntly")

在該類中,當登陸成功後,會刷新我們的令牌並將其放入Cookie中

@Override
	public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
			Authentication successfulAuthentication) {

		String username = retrieveUserName(successfulAuthentication);
		String password = retrievePassword(successfulAuthentication);

		// If unable to find a username and password, just abort as
		// TokenBasedRememberMeServices is
		// unable to construct a valid token in this case.
		if (!StringUtils.hasLength(username)) {
			logger.debug("Unable to retrieve username");
			return;
		}

		if (!StringUtils.hasLength(password)) {
			UserDetails user = getUserDetailsService().loadUserByUsername(username);
			password = user.getPassword();

			if (!StringUtils.hasLength(password)) {
				logger.debug("Unable to obtain password for user: " + username);
				return;
			}
		}

		int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
		long expiryTime = System.currentTimeMillis();
		// 計算過期時間,在AbstractRememberMeServices默認設置的是兩週
		expiryTime += 1000L * (tokenLifetime < 0 ? TWO_WEEKS_S : tokenLifetime);
		// 生成Token
		String signatureValue = makeTokenSignature(expiryTime, username, password);
		// 放入cookie
		setCookie(new String[] { username, Long.toString(expiryTime), signatureValue },
				tokenLifetime, request, response);

		if (logger.isDebugEnabled()) {
			logger.debug("Added remember-me cookie for user '" + username
					+ "', expiry: '" + new Date(expiryTime) + "'");
		}
	}

使用非持久化的方式不用使用其他的存儲空間,但是隻要獲取到用戶的RememberId之後,把他放入cookie中,在不同的session中也可以進行訪問,存在一定的安全隱患,需要保證在記住我功能下對敏感操作的限制,下圖即爲使用MsEdge瀏覽器放入令牌後訪問路徑成功
在這裏插入圖片描述

持久化方案

保存兩個令牌,series和token,均爲MD5散列值,series在用戶使用密碼重新登錄時更新,token在每次創建一個新的session的時候就會重新生成。

在非持久化方案中有一個問題是多個客戶端使用同一個token都可以登錄,在持久化方案中,每個新的session都會導致token的更新,也就是說token只支持單實例登錄。

series不會因爲自動登錄而更改,當自動登錄的時候,會驗證series和token兩個值,當用戶在未使用過自動登錄的時候被盜,會刷新token值,此時用戶的token已經失效,當用戶使用自動登錄的時候由於在數據庫中存儲的和series匹配的token不一致,系統就會腿短令牌是否已經被盜用,作出對應的操作

實現

在SpringSecurity中使用PersistentRememberMeToken封裝series和token,對應的表如下,在數據庫中創建對應的表,可以看到主鍵是series,當自動登錄解析出series時從數據庫查詢對應的信息就可以比對登錄時的token和數據庫的是否一致了

DROP TABLE IF EXISTS `persistent_logins`;
CREATE TABLE `persistent_logins`  (
  `username` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `series` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `token` varchar(64) CHARACTER SET utf8 COLLATE utf8_general_ci NOT NULL,
  `last_used` timestamp(0) NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP(0),
  PRIMARY KEY (`series`) USING BTREE
) ENGINE = InnoDB CHARACTER SET = utf8 COLLATE = utf8_general_ci ROW_FORMAT = Dynamic;

SET FOREIGN_KEY_CHECKS = 1;

修改配置文件

	@Autowired
    private DataSource dataSource;

    @Override
    protected void configure(HttpSecurity http) throws Exception {
        JdbcTokenRepositoryImpl jdbcTokenRepository = new JdbcTokenRepositoryImpl();
        jdbcTokenRepository.setDataSource(dataSource);
        http.authorizeRequests()
                .antMatchers("/css/**", "/img/**", "/js/**", "/bootstrap/**", "/captcha.jpg").permitAll()
                .antMatchers("/app/api/**").permitAll()
                .anyRequest().authenticated()
                .and()
                .formLogin().loginPage("/myLogin.html")
                .loginProcessingUrl("/login")
                .successHandler(successHandler)
                .failureHandler(failureHandler)
                .authenticationDetailsSource(myWebAuthenticationDetailsSource)
                .permitAll()
                .and()
                .rememberMe().userDetailsService(userDetailsService()).tokenRepository(jdbcTokenRepository)
                //.rememberMe().userDetailsService(userDetailService).key("anntly")
                // 使登錄頁不受限
                .and()
                .csrf().disable();
    }

使用默認提供的JdbcTokenRepositoryImpl來獲取表的相關信息,如果需要我們也可以自己實現PersistentTokenRepository接口

配置完畢後重啓項目,登錄,查看生成的remember-me ,並將其使用base64解密

// remember-me
b01hMmlVaXprcnloblFHNFFlcGc1QSUzRCUzRDpta2xTN1ljaEZVMWpoVWNzZmdzekpnJTNEJTNE
// 解密後
oMa2iUizkryhnQG4Qepg5A%3D%3D:mklS7YchFU1jhUcsfgszJg%3D%3D

解密後的值,冒號前半部分就是我們的series,後半部分當然就是token,可以查看我們創建的表中數據是否一致

AbstractRememberMeServices的另外一個實現類PersistentTokenBasedRememberMeServices中的processAutoLoginCookie方法中可以查看自動登錄令牌校驗和更新的情況

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) + "'");
		}
		// series
		final String presentedSeries = cookieTokens[0];
		// token
		final String presentedToken = cookieTokens[1];
		// 根據series獲取令牌信息
		PersistentRememberMeToken token = tokenRepository
				.getTokenForSeries(presentedSeries);

		if (token == null) {
			// No series match, so we can't authenticate using this cookie
			throw new RememberMeAuthenticationException(
					"No persistent token found for series id: " + presentedSeries);
		}
		// 當數據庫查詢出來token和當前token不匹配
		// We have a match for this user/series combination
		if (!presentedToken.equals(token.getTokenValue())) {
			// Token doesn't match series value. Delete all logins for this user and throw
			// an exception to warn them.
			tokenRepository.removeUserTokens(token.getUsername());

			throw new CookieTheftException(
					messages.getMessage(
							"PersistentTokenBasedRememberMeServices.cookieStolen",
							"Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
		}
		//處理過期時間
		if (token.getDate().getTime() + getTokenValiditySeconds() * 1000L < System
				.currentTimeMillis()) {
			throw new RememberMeAuthenticationException("Remember-me login has expired");
		}

		// Token also matches, so login is valid. Update the token value, keeping the
		// *same* series number.
		if (logger.isDebugEnabled()) {
			logger.debug("Refreshing persistent login token for user '"
					+ token.getUsername() + "', series '" + token.getSeries() + "'");
		}

		PersistentRememberMeToken newToken = new PersistentRememberMeToken(
				token.getUsername(), token.getSeries(), generateTokenData(), new Date());

		try {
			// 刷新token
			tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
					newToken.getDate());
			addCookie(newToken, request, response);
		}
		catch (Exception e) {
			logger.error("Failed to update token: ", e);
			throw new RememberMeAuthenticationException(
					"Autologin failed due to data access problem");
		}

		return getUserDetailsService().loadUserByUsername(token.getUsername());
	}

當登錄成功時令牌的生成

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);
		}
	}
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章