溫馨提示×

溫馨提示×

您好,登錄后才能下訂單哦!

密碼登錄×
登錄注冊×
其他方式登錄
點(diǎn)擊 登錄注冊 即表示同意《億速云用戶服務(wù)條款》

Spring Security散列加密中如何實(shí)現(xiàn)自動登錄功能

發(fā)布時間:2021-09-26 13:39:50 來源:億速云 閱讀:152 作者:柒染 欄目:開發(fā)技術(shù)

這篇文章給大家介紹Spring Security散列加密中如何實(shí)現(xiàn)自動登錄功能,內(nèi)容非常詳細(xì),感興趣的小伙伴們可以參考借鑒,希望對大家能有所幫助。

前言

在前面的2個章節(jié)中,一一哥 帶大家實(shí)現(xiàn)了在Spring Security中添加圖形驗(yàn)證碼校驗(yàn)功能,其實(shí)Spring Security的功能不僅僅是這些,還可以實(shí)現(xiàn)很多別的效果,比如實(shí)現(xiàn)自動登錄,注銷登錄等。

有的小伙伴會問,我們?yōu)槭裁匆獙?shí)現(xiàn)自動登錄???這個需求其實(shí)還是很常見的,因?yàn)閷τ谟脩魜碚f,他可能經(jīng)常需要進(jìn)行登錄以及退出登錄,你想想,如果用戶每次登錄時都要輸入自己的用戶名和密碼,是不是很煩,用戶體驗(yàn)是不是很不好?

所以為了提高項(xiàng)目的用戶體驗(yàn),我們可以在項(xiàng)目中添加自動登錄功能,當(dāng)然也要給用戶提供退出登錄的功能。接下來就跟著 一一哥 來學(xué)習(xí)如何實(shí)現(xiàn)這些功能吧!

一. 自動登錄簡介

1. 為什么要自動登錄

我們在訪問網(wǎng)站或app時,一般都會要求我們注冊一個賬號,包含用戶名和密碼信息,其中密碼還會有長度及取值范圍的限制。很多時候,我們在不同的網(wǎng)站上注冊的賬號,可能密碼也不同,這就導(dǎo)致我們必須記住這些不同網(wǎng)站上的用戶信息。那么在下次登錄時,因?yàn)槲覀兊拿艽a太多了,很有可能會記不起這些賬號密碼。所以在幾次嘗試登錄失敗之后,很多人都會選擇找回密碼,從而再次陷入如何設(shè)置密碼的循環(huán)里。

為了盡可能減少用戶重新登錄的頻率,提高用戶的使用體驗(yàn),我們可以提供自動登錄這樣一個會給用戶帶來便利,同時也會給用戶帶來風(fēng)險的體驗(yàn)性功能。

2. 自動登錄的實(shí)現(xiàn)方案

了解了自動登錄出現(xiàn)的背景及作用后,那么我們該怎么實(shí)現(xiàn)自動登錄呢?

首先我們知道,自動登錄是將用戶的登錄信息保存在用戶瀏覽器的cookie中,當(dāng)用戶下次訪問時,自動實(shí)現(xiàn)校驗(yàn)并建立登錄狀態(tài)的一種機(jī)制。

所以基于上面的原理,Spring Security 就為我們提供了兩種比較好的實(shí)現(xiàn)自動登錄的方案:

  • 基于散列加密算法機(jī)制:加密用戶必要的登錄信息,并生成令牌來實(shí)現(xiàn)自動登錄,利用TokenBasedRememberMeServices類來實(shí)現(xiàn)。

  • 基于數(shù)據(jù)庫等持久化數(shù)據(jù)存儲機(jī)制:生成持久化令牌來實(shí)現(xiàn)自動登錄,利用PersistentTokenBasedRememberMeServices來實(shí)現(xiàn)。

我上面提到的2個實(shí)現(xiàn)類,其實(shí)都是AbstractRememberMeServices的子類,如下圖所示:

Spring Security散列加密中如何實(shí)現(xiàn)自動登錄功能

Spring Security散列加密中如何實(shí)現(xiàn)自動登錄功能

了解了這些核心API之后,我們就可以利用這兩個API來實(shí)現(xiàn)自動登錄了。

二. 基于散列加密方案實(shí)現(xiàn)自動登錄

我先帶各位利用第1種實(shí)現(xiàn)方案,即基于散列加密方案來實(shí)現(xiàn)自動登錄。

首先我們還是在之前的案例基礎(chǔ)之上進(jìn)行開發(fā),具體的項(xiàng)目創(chuàng)建過程略過,請參考之前的章節(jié)內(nèi)容。

1. 配置加密令牌的key

首先我們創(chuàng)建一個application.yml文件,在其中添加數(shù)據(jù)庫配置,以及一個用來加密令牌的key字符串,字符串的值隨便自定義就行

spring:
  datasource:
    url: jdbc:mysql://localhost:3306/db-security?useUnicode=true&characterEncoding=utf-8&useSSL=false&serverTimezone=GMT
    username: root
    password: syc
  security:
    remember-me:
      key: yyg

2. 配置SecurityConfig類

跟之前的案例一樣,我還是要創(chuàng)建一個SecurityConfig類,在其中的configure(HttpSecurity http)方法中,通過JdbcTokenRepositoryImpl關(guān)聯(lián)我們的數(shù)據(jù)庫,并且通過rememberMe()方法開啟“記住我”功能,另外還要把我們前面在配置文件中的rememberKey配置進(jìn)來,作為散列加密的key。

@EnableWebSecurity(debug = true)
public class SecurityConfig extends WebSecurityConfigurerAdapter {
 
    @Value("${spring.security.remember-me.key}")
    private String rememberKey;
 
    @Autowired
    private UserDetailsService userDetailsService;
 
    @Override
    protected void configure(HttpSecurity http) throws Exception {
        //利用JdbcTokenRepositoryImpl關(guān)聯(lián)數(shù)據(jù)源
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
 
        http.authorizeRequests()
                .antMatchers("/admin/**")
                .hasRole("ADMIN")
                .antMatchers("/user/**")
                .hasRole("USER")
                .antMatchers("/app/**")
                .permitAll()
                .anyRequest()
                .authenticated()
                .and()
                .formLogin()
                .permitAll()
                .and()
            	//開啟“記住我”功能
                .rememberMe()
                .userDetailsService(userDetailsService)
                //配置散列加密用的key
                .key(rememberKey)
                .and()
                .csrf()
                .disable();
    }
 
    @Bean
    public PasswordEncoder passwordEncoder() {
        //不對登錄密碼進(jìn)行加密
        return NoOpPasswordEncoder.getInstance();
    }
    
}

3. 添加測試接口

為了方便后續(xù)的測試,我隨便編寫一個測試用的web接口。

@RestController
@RequestMapping("/user")
public class UserController {
 
    @GetMapping("hello")
    public String hello() {
 
        return "hello, user";
    }
 
}

4. 啟動項(xiàng)目測試

然后我們把項(xiàng)目啟動起來進(jìn)行測試,當(dāng)然你別忘了編寫項(xiàng)目入口類,這里我就不粘貼相關(guān)代碼了。

我們訪問一下/user/hello接口,會先重定向到/login接口,這時候會發(fā)現(xiàn)在默認(rèn)的登錄頁面上多了一個“記住我”功能。

Spring Security散列加密中如何實(shí)現(xiàn)自動登錄功能

此時如果我們打開 開發(fā)者調(diào)試工具,并且勾選“記住我”,然后發(fā)起請求,這時候我們會在控制臺看到remember-me的cookie信息,說明Spring Security已經(jīng)自動生成了remember-me這個cookie,且表單中的remember-me參數(shù)也處于了“on”狀態(tài)。

Spring Security散列加密中如何實(shí)現(xiàn)自動登錄功能

Spring Security散列加密中如何實(shí)現(xiàn)自動登錄功能

也就是說,我們利用簡單的幾行代碼,就實(shí)現(xiàn)了基于散列加密方案的自動登錄。

三. 散列加密方案實(shí)現(xiàn)原理

你可能會很好奇,散列加密方案到底是怎么實(shí)現(xiàn)自動登錄的呢?別急,接下來 壹哥就為你分析一下散列加密的實(shí)現(xiàn)原理。

1. cookie的加密原理分析

我在前面給各位說過,自動登錄其實(shí)就是將用戶的登錄信息保存在用戶瀏覽器的cookie中,當(dāng)用戶下次訪問時,自動實(shí)現(xiàn)校驗(yàn)并建立登錄狀態(tài)的一種機(jī)制。所以在自動登錄后,肯定會生成代表用戶的cookie信息,但是為了安全,這個cookie肯定不會明文存儲,需要把這個cookie進(jìn)行加密處理,當(dāng)然也會解碼處理。所以接下來我就給各位分析一下這個cookie的加密和解碼過程。

首先 壹哥 給各位解釋一下所謂的散列加密算法,其實(shí)質(zhì)就是把 username、expirationTime、password等字段,再加上自定義的key字段合并起來,在每個字段之間用 ":" 分隔,最后利用md5算法進(jìn)行哈希運(yùn)算,這樣就可以得到一個加密后的字符串。Spring Security把這個加密的字符串存儲到cookie中,作為用戶已登錄的標(biāo)識信息。

然后 壹哥 帶你看看TokenBasedRememberMeServices源碼類中的makeTokenSignature()方法,你會看到散列加密算法的具體加密實(shí)現(xiàn)過程,源碼如下圖所示:

Spring Security散列加密中如何實(shí)現(xiàn)自動登錄功能

2. cookie的解碼原理分析

上面利用MD5進(jìn)行了加密,用戶在下次登錄后,肯定需要進(jìn)行信息的比對,以判斷用戶信息是否一致。Spring Security是先對cookie中的信息進(jìn)行解碼,然后與之前記錄的登錄信息進(jìn)行比對,以此判斷用戶是否已登錄。

Spring Security是在AbstractRememberMeServices類的decodeCookie()方法中,利用Base64對cookie進(jìn)行解碼,如下圖所示:

Spring Security散列加密中如何實(shí)現(xiàn)自動登錄功能

對于以上2個源碼方法,我們可以簡化抽取出如下兩行代碼:

//對各字段進(jìn)行散列加密
hashInfo=md5Hex(username +":"+expirationTime +":"password+":"+key)

//利用base64進(jìn)行解碼
rememberCookie=base64(username+":"+expirationrime+":"+hashInfo)

其中,expirationTime是指本次自動登錄的有效期,key是自己指定的一個散列鹽值,用于防止令牌被修改。利用以上兩個

分析完源碼之后,壹哥給各位簡單總結(jié)一下cookie的生成驗(yàn)證原理:

  • 首先利用上面的源碼生成cookie,并保存在瀏覽器中;

  • 在瀏覽器關(guān)閉并重新打開之后,用戶再去訪問 /user/hello 接口時,此時就會攜帶remember-me這個cookie到服務(wù)端;

  • 服務(wù)器端拿到cookie之后,利用Base64進(jìn)行解碼,計算出用戶名和過期時間,再根據(jù)用戶名查詢到用戶密碼;

  • 最后還要通過 MD5 散列函數(shù)計算出散列值,并將計算出的散列值和瀏覽器傳遞來的散列值進(jìn)行對比,以此確認(rèn)這個令牌是否有效。

3. 自動登錄的源碼分析

上面分析完cookie信息的加密和解碼之后,接下來我再結(jié)合源碼,從兩個方面來介紹自動登錄的實(shí)現(xiàn)過程,一個是 remember-me 令牌的生成的過程,另一個則是該令牌的解析過程。

3.1 令牌生成的源碼分析

我們要想知道源碼中是如何生成remember-me自動登錄令牌的,首先得知道Spring Security是如何進(jìn)入到該令牌所在代碼的,這個代碼的執(zhí)行與我們前一章節(jié)所講的Spring Security的認(rèn)證授權(quán)有關(guān),請進(jìn)入到前面查看。

AbstractAuthenticationProcessingFilter#doFilter ->
AbstractAuthenticationProcessingFilter#successfulAuthentication ->
AbstractRememberMeServices#loginSuccess ->
TokenBasedRememberMeServices#onLoginSuccess

這個令牌生成的核心處理方法定義在:TokenBasedRememberMeServices#onLoginSuccess。

@Override
public void onLoginSuccess(HttpServletRequest request, HttpServletResponse response,
  Authentication successfulAuthentication) {
    //從認(rèn)證對象中獲取用戶名
 	String username = retrieveUserName(successfulAuthentication);
    //從認(rèn)證對象中獲取密碼
 	String password = retrievePassword(successfulAuthentication);
    
    ......
    
 	if (!StringUtils.hasLength(password)) {
        //根據(jù)用戶名查詢出對應(yīng)的用戶
 	 	UserDetails user = getUserDetailsService().loadUserByUsername(username);
        //獲取到用戶身上的密碼
  		password = user.getPassword();
 	}
    
    //獲取登錄過期時間,默認(rèn)是2周
 	int tokenLifetime = calculateLoginLifetime(request, successfulAuthentication);
 	long expiryTime = System.currentTimeMillis();
 	expiryTime += 1000L * (tokenLifetime < 0 ? TWO_WEEKS_S : tokenLifetime);
    
    //生成remember-me簽名信息
 	String signatureValue = makeTokenSignature(expiryTime, username, password);
    
    //保存cookie
 	setCookie(new String[] { username, Long.toString(expiryTime), signatureValue },
    tokenLifetime, request, response);
}
 
protected String makeTokenSignature(long tokenExpiryTime, String username,
  String password) {
 	String data = username + ":" + tokenExpiryTime + ":" + password + ":" + getKey();
 	MessageDigest digest;
 	digest = MessageDigest.getInstance("MD5");
 	return new String(Hex.encode(digest.digest(data.getBytes())));
}

以上源碼的實(shí)現(xiàn)邏輯很好理解:

  1.  首先從登錄成功的 Authentication 對象中提取出用戶名/密碼;

  2. 由于登錄成功之后,密碼可能被擦除了,所以如果一開始沒有拿到密碼,就再從 UserDetailsService 中重新加載用戶并重新獲取密碼;

  3. 接下來獲取令牌的有效期,令牌有效期默認(rèn)是兩周;

  4. 再接下來調(diào)用 makeTokenSignature()方法 去計算散列值,實(shí)際上就是根據(jù) username、令牌有效期以及 password、key 一起計算一個散列值。如果我們沒有自己去設(shè)置這個 key,默認(rèn)是在 RememberMeConfigurer#getKey 方法中進(jìn)行設(shè)置的,它的值是一個 UUID 字符串。但是如果服務(wù)端重啟,這個默認(rèn)的 key 是會變的,這樣就導(dǎo)致之前派發(fā)出去的所有 remember-me 自動登錄令牌失效,所以我們可以指定這個 key。

  5. 最后,將用戶名、令牌有效期以及計算得到的散列值放入 Cookie 中并隨response返回。

3.2 令牌解析的源碼分析

對于RememberMe 這個功能,Spring Security提供了 RememberMeAuthenticationFilter 這個過濾器類來處理相關(guān)功能,我們來看下 RememberMeAuthenticationFilter 的 doFilter() 方法:

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) {
            //處理自動登錄的業(yè)務(wù)邏輯
			Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
					response);
 
			if (rememberMeAuth != null) {
				// Attempt authenticaton via AuthenticationManager
				try {
					rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
 
					// Store to SecurityContextHolder
					SecurityContextHolder.getContext().setAuthentication(rememberMeAuth);
 
					onSuccessfulAuthentication(request, response, rememberMeAuth);
 
					if (logger.isDebugEnabled()) {
						logger.debug("SecurityContextHolder populated with remember-me token: '"
								+ SecurityContextHolder.getContext().getAuthentication()
								+ "'");
					}
 
					// Fire event
					if (this.eventPublisher != null) {
						eventPublisher
								.publishEvent(new InteractiveAuthenticationSuccessEvent(
										SecurityContextHolder.getContext()
												.getAuthentication(), this.getClass()));
					}
 
					if (successHandler != null) {
						successHandler.onAuthenticationSuccess(request, response,
								rememberMeAuth);
 
						return;
					}
 
				}
				catch (AuthenticationException authenticationException) {
					if (logger.isDebugEnabled()) {
						logger.debug(
								"SecurityContextHolder not populated with remember-me token, as "
										+ "AuthenticationManager rejected Authentication returned by RememberMeServices: '"
										+ rememberMeAuth
										+ "'; invalidating remember-me token",
								authenticationException);
					}
 
					rememberMeServices.loginFail(request, response);
 
					onUnsuccessfulAuthentication(request, response,
							authenticationException);
				}
			}
 
			chain.doFilter(request, response);
		}
		else {
			if (logger.isDebugEnabled()) {
				logger.debug("SecurityContextHolder not populated with remember-me token, as it already contained: '"
						+ SecurityContextHolder.getContext().getAuthentication() + "'");
			}
 
			chain.doFilter(request, response);
		}
	}

這個方法最關(guān)鍵的地方在于,如果從 SecurityContextHolder 中無法獲取到當(dāng)前登錄用戶實(shí)例,那么就調(diào)用 rememberMeServices.autoLogin()邏輯進(jìn)行登錄,我們來看下這個方法:

@Override
	public final Authentication autoLogin(HttpServletRequest request,
			HttpServletResponse response) {
		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 {
			String[] cookieTokens = decodeCookie(rememberMeCookie);
			user = processAutoLoginCookie(cookieTokens, request, response);
			userDetailsChecker.check(user);
 
			logger.debug("Remember-me cookie accepted");
 
			return createSuccessfulAuthentication(request, user);
		}
		......
 
		cancelCookie(request, response);
		return null;
	}

Spring Security就是在這里提取出 cookie 信息,并對 cookie 信息進(jìn)行解碼。解碼之后,再調(diào)用 processAutoLoginCookie()方法去做校驗(yàn)。processAutoLoginCookie() 方法的代碼我就不貼了,核心流程就是首先獲取用戶名和過期時間,再根據(jù)用戶名查詢到用戶密碼,然后通過 MD5 散列函數(shù)計算出散列值。最后再將拿到的散列值和瀏覽器傳遞來的散列值進(jìn)行對比,就能確認(rèn)這個令牌是否有效,進(jìn)而確認(rèn)登錄是否有效。

關(guān)于Spring Security散列加密中如何實(shí)現(xiàn)自動登錄功能就分享到這里了,希望以上內(nèi)容可以對大家有一定的幫助,可以學(xué)到更多知識。如果覺得文章不錯,可以把它分享出去讓更多的人看到。

向AI問一下細(xì)節(jié)

免責(zé)聲明:本站發(fā)布的內(nèi)容(圖片、視頻和文字)以原創(chuàng)、轉(zhuǎn)載和分享為主,文章觀點(diǎn)不代表本網(wǎng)站立場,如果涉及侵權(quán)請聯(lián)系站長郵箱:is@yisu.com進(jìn)行舉報,并提供相關(guān)證據(jù),一經(jīng)查實(shí),將立刻刪除涉嫌侵權(quán)內(nèi)容。

AI