溫馨提示×

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

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

如何進(jìn)行個(gè)性化認(rèn)證以及RememberMe實(shí)現(xiàn)

發(fā)布時(shí)間:2021-10-20 16:30:02 來(lái)源:億速云 閱讀:132 作者:柒染 欄目:大數(shù)據(jù)

如何進(jìn)行個(gè)性化認(rèn)證以及RememberMe實(shí)現(xiàn),相信很多沒(méi)有經(jīng)驗(yàn)的人對(duì)此束手無(wú)策,為此本文總結(jié)了問(wèn)題出現(xiàn)的原因和解決方法,通過(guò)這篇文章希望你能解決這個(gè)問(wèn)題。

Spring Security 解析(三) —— 個(gè)性化認(rèn)證 以及 RememberMe 實(shí)現(xiàn)

在學(xué)習(xí)Spring Cloud 時(shí),遇到了授權(quán)服務(wù)oauth 相關(guān)內(nèi)容時(shí),總是一知半解,因此決定先把Spring Security 、Spring Security Oauth3 等權(quán)限、認(rèn)證相關(guān)的內(nèi)容、原理及設(shè)計(jì)學(xué)習(xí)并整理一遍。本系列文章就是在學(xué)習(xí)的過(guò)程中加強(qiáng)印象和理解所撰寫(xiě)的,如有侵權(quán)請(qǐng)告知。

項(xiàng)目環(huán)境: > - JDK1.8 > - Spring boot 2.x > - Spring Security 5.x

一、個(gè)性化認(rèn)證

(一) 配置登錄

?? 在 授權(quán)過(guò)程 和 認(rèn)證過(guò)程 中我們都是使用的 Security 默認(rèn)的一個(gè)登錄頁(yè)面(/login),那么如果我們想自定義一個(gè)登錄頁(yè)面該如何實(shí)現(xiàn)呢?其實(shí)很簡(jiǎn)單,我們新建 FormAuthenticationConfig 配置類(lèi),然后在configure(HttpSecurity http) 方法中實(shí)現(xiàn)以下設(shè)置:

        http.formLogin()
                //可以設(shè)置自定義的登錄頁(yè)面 或者 (登錄)接口
                // 注意1: 一般來(lái)說(shuō)設(shè)置成(登錄)接口后,該接口會(huì)配置成無(wú)權(quán)限即可訪問(wèn),所以會(huì)走匿名filter, 也就意味著不會(huì)走認(rèn)證過(guò)程了,所以我們一般不直接設(shè)置成接口地址
                // 注意2: 這里配置的 地址一定要配置成無(wú)權(quán)限訪問(wèn),否則將出現(xiàn) 一直重定向問(wèn)題(因?yàn)闊o(wú)權(quán)限后又會(huì)重定向到這里配置的登錄頁(yè)url)
                .loginPage(securityProperties.getLogin().getLoginPage())
                //.loginPage("/loginRequire")
                // 指定驗(yàn)證憑據(jù)的URL(默認(rèn)為 /login) ,
                // 注意1:這里修改后的 url 會(huì)意味著  UsernamePasswordAuthenticationFilter 將 驗(yàn)證此處的 url
                // 注意2: 與 loginPage設(shè)置的接口地址是有 區(qū)別, 一但 loginPage 設(shè)置了的是訪問(wèn)接口url,那么此處配置將無(wú)任何意義
                // 注意3: 這里設(shè)置的 Url 是有默認(rèn)無(wú)權(quán)限訪問(wèn)的
                .loginProcessingUrl(securityProperties.getLogin().getLoginUrl())
                //分別設(shè)置成功和失敗的處理器
                .successHandler(customAuthenticationSuccessHandler)
                .failureHandler(customAuthenticationFailureHandler);

??最后在 SpringSecurityConfig 的 configure(HttpSecurity http) 方法中 調(diào)用 formAuthenticationConfig.configure(http) 即可;

?? 正如看到的一樣,我們通過(guò) loginPage()設(shè)置 登錄頁(yè)面接口, 通過(guò) loginProcessingUrl() 設(shè)置 UsernamePasswordAuthenticationFilter 要匹配的 接口地址(一定是Post)(看過(guò)授權(quán)過(guò)程的同學(xué)應(yīng)該都知道其默認(rèn)的是/login)。 這里有以下幾點(diǎn)值得注意:

> - loginPage() 這里配置的 地址(不管是接口url還是登錄頁(yè)面)一定要配置成無(wú)權(quán)限訪問(wèn),否則將出現(xiàn) 一直重定向問(wèn)題(因?yàn)闊o(wú)權(quán)限后又會(huì)重定向到這里配置的登錄頁(yè)url > - loginPage() 一般來(lái)說(shuō)不直接設(shè)置成(登錄)接口,因?yàn)樵O(shè)置了接口會(huì)配置成無(wú)權(quán)限即可訪問(wèn)(當(dāng)然設(shè)置成登錄頁(yè)面也需要配置無(wú)權(quán)限訪問(wèn)),所以會(huì)走匿名filter, 也就意味著不會(huì)走認(rèn)證過(guò)程了,所以我們一般不直接設(shè)置成接口地址 > - loginProcessingUrl() 這里修改后的 url 會(huì)意味著 UsernamePasswordAuthenticationFilter 將 驗(yàn)證此處的 url > - loginProcessingUrl() 這里設(shè)置的 Url 是有默認(rèn)無(wú)權(quán)限訪問(wèn)的,與 loginPage設(shè)置的接口地址是有 區(qū)別, 一但 loginPage 設(shè)置了的是接口url,那么此處配置將無(wú)任何意義 > - successHandler() 和 failureHandler 分別 設(shè)置認(rèn)證成功處理器 和 認(rèn)證失敗處理器 (如果對(duì)這2個(gè)處理器沒(méi)印象的話(huà),建議回顧下授權(quán)過(guò)程)

(二) 配置成功和失敗處理器

?? 在授權(quán)過(guò)程中,我們?cè)龊?jiǎn)單提及到過(guò)這2個(gè)處理器,在Security中默認(rèn)的處理器分別是SavedRequestAwareAuthenticationSuccessHandler 和 SimpleUrlAuthenticationFailureHandler ,這次我們自定義這2個(gè)處理器,分別為 CustomAuthenticationSuccessHandler ( extends SavedRequestAwareAuthenticationSuccessHandler ) 重寫(xiě) onAuthenticationSuccess() 方法 :

@Component("customAuthenticationSuccessHandler")
@Slf4j
public class CustomAuthenticationSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler {
    @Autowired
    private SecurityProperties securityProperties;

    private RequestCache requestCache = new HttpSessionRequestCache();

    @Override
    public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
                                        Authentication authentication) throws IOException, ServletException {
        logger.info("登錄成功");
        // 如果設(shè)置了loginSuccessUrl,總是跳到設(shè)置的地址上
        // 如果沒(méi)設(shè)置,則嘗試跳轉(zhuǎn)到登錄之前訪問(wèn)的地址上,如果登錄前訪問(wèn)地址為空,則跳到網(wǎng)站根路徑上
        if (!StringUtils.isEmpty(securityProperties.getLogin().getLoginSuccessUrl())) {
            requestCache.removeRequest(request, response);
            setAlwaysUseDefaultTargetUrl(true);
            setDefaultTargetUrl(securityProperties.getLogin().getLoginSuccessUrl());
        }
        super.onAuthenticationSuccess(request, response, authentication);
    }

}

和 CustomAuthenticationFailureHandler( extends SimpleUrlAuthenticationFailureHandler) 重寫(xiě) onAuthenticationFailure() 方法 :

@Component("customAuthenticationFailureHandler")
@Slf4j
public class CustomAuthenticationFailureHandler extends SimpleUrlAuthenticationFailureHandler {

    @Autowired
    private ObjectMapper objectMapper;

    @Autowired
    private SecurityProperties securityProperties;

    private RedirectStrategy redirectStrategy = new DefaultRedirectStrategy();

    @Override
    public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
                                        AuthenticationException exception) throws IOException {

        logger.info("登錄失敗");
        if (StringUtils.isEmpty(securityProperties.getLogin().getLoginErrorUrl())){

            response.setStatus(HttpStatus.INTERNAL_SERVER_ERROR.value());
            response.setContentType("application/json;charset=UTF-8");
            response.getWriter().write(objectMapper.writeValueAsString(exception.getMessage()));

        } else {
            // 跳轉(zhuǎn)設(shè)置的登陸失敗頁(yè)面
            redirectStrategy.sendRedirect(request,response,securityProperties.getLogin().getLoginErrorUrl());
        }

    }
}
(三) 自定義的登陸頁(yè)面

這里就不再描述,直接貼代碼:

<meta charset="UTF-8">
    <title>登錄</title>


<h3>登錄頁(yè)面</h3>
<form action="/loginUp" method="post">  
    <table>
        <tbody><tr>
            <td>用戶(hù)名:</td>
            <td><input type="text" name="username"></td>
        </tr>
        <tr>
            <td>密碼:</td>
            <td><input type="password" name="password"></td>
        </tr>
        <tr>
            <td colspan="2"><input name="remember-me" type="checkbox" value="true">記住我</td>
        </tr>
        <tr>
            <td colspan="2">
                <button type="submit">登錄</button>
            </td>
        </tr>
    </tbody></table>
</form>

??注意這里請(qǐng)求的地址是 loginProcessingUrl() 配置的地址

(四)測(cè)試驗(yàn)證

??這里就不在貼結(jié)果圖了,只要我們明白結(jié)果流程就行是這樣的就可以: localhost:8080 ——> 點(diǎn)擊 測(cè)試驗(yàn)證Security 權(quán)限控制 ————> 跳轉(zhuǎn)到 我們自定義的 /loginUp.html 登錄頁(yè),登錄后 ————> 有配置loginSuccessUrl,則跳轉(zhuǎn)到 loginSuccess.html;反之則直接跳轉(zhuǎn)到 /get_user/test 接口返回結(jié)果。 整個(gè)流程就全面涉及到了我們自定義的登錄頁(yè)面、自定義的登錄成功/失敗處理器。

二、 RememberMe (記住我)功能解析

(一)RememberMe 功能實(shí)現(xiàn)配置
首先我們一股腦的將rememberMe配置加上,然后看下現(xiàn)象:

1、 創(chuàng)建 persistent_logins 表,用于存儲(chǔ)token和用戶(hù)的關(guān)聯(lián)信息:

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 、 添加rememberMe配置 信息

    @Bean
    public PersistentTokenRepository persistentTokenRepository(){
        JdbcTokenRepositoryImpl tokenRepository = new JdbcTokenRepositoryImpl();
        tokenRepository.setDataSource(dataSource);
        // 如果token表不存在,使用下面語(yǔ)句可以初始化 persistent_logins(ddl在db目錄下) 表;若存在,請(qǐng)注釋掉這條語(yǔ)句,否則會(huì)報(bào)錯(cuò)。
        //tokenRepository.setCreateTableOnStartup(true);
        return tokenRepository;
    }
    
     @Override
    protected void configure(HttpSecurity http) throws Exception {

        formAuthenticationConfig.configure(http);
        http.   ....
                .and()
                // 開(kāi)啟 記住我功能,意味著 RememberMeAuthenticationFilter 將會(huì) 從Cookie 中獲取token信息
                .rememberMe()
                // 設(shè)置 tokenRepository ,這里默認(rèn)使用 jdbcTokenRepositoryImpl,意味著我們將從數(shù)據(jù)庫(kù)中讀取token所代表的用戶(hù)信息
                .tokenRepository(persistentTokenRepository())
                // 設(shè)置  userDetailsService , 和 認(rèn)證過(guò)程的一樣,RememberMe 有專(zhuān)門(mén)的 RememberMeAuthenticationProvider ,也就意味著需要 使用UserDetailsService 加載 UserDetails 信息
                .userDetailsService(userDetailsService)
                // 設(shè)置 rememberMe 的有效時(shí)間,這里通過(guò) 配置來(lái)設(shè)置
                .tokenValiditySeconds(securityProperties.getLogin().getRememberMeSeconds())
                .and()
                .csrf().disable(); // 關(guān)閉csrf 跨站(域)攻擊防控
    }

這里解釋下配置:

  • rememberMe() 開(kāi)啟 記住我功能,意味著 RememberMeAuthenticationFilter 將會(huì) 從Cookie 中獲取token信息

  • tokenRepository() 配置 token的獲取策略,這里配置成從數(shù)據(jù)庫(kù)中讀取

  • userDetailsService() 配置 UserDetaisService (如果不熟悉該對(duì)象,建議回顧認(rèn)證過(guò)程)

  • tokenValiditySeconds() 設(shè)置 rememberMe 的有效時(shí)間,這里通過(guò) 配置來(lái)設(shè)置

另一個(gè)重要的配置在登錄頁(yè)面,這里的 必須是 name="remember-me" ,rememberMe就是通過(guò)驗(yàn)證這個(gè)配置來(lái)開(kāi)啟remermberMe功能的。

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

??實(shí)操結(jié)果應(yīng)該為:進(jìn)入登陸頁(yè)面 ——> 勾選記住我后登錄 ——> 成功后,查看persistent_logins 表發(fā)現(xiàn)有一條數(shù)據(jù)——> 重啟項(xiàng)目 ——> 重新訪問(wèn)需要登錄才能訪問(wèn)的頁(yè)面,發(fā)現(xiàn)無(wú)需登錄即可訪問(wèn)——> 刪除 persistent_logins 表數(shù)據(jù),等待token設(shè)置的有效時(shí)間過(guò)期,然后重新刷新頁(yè)面發(fā)現(xiàn)跳轉(zhuǎn)到登陸頁(yè)面。

(二) RembemberMe 實(shí)現(xiàn)源碼解析

?? 首先我們查看UsernamePasswordAuthenticationFiler(AbstractAuthenticationProcessingFilter) 的 successfulAuthentication() 方法內(nèi)部源碼:

protected void successfulAuthentication(HttpServletRequest request,
			HttpServletResponse response, FilterChain chain, Authentication authResult)
			throws IOException, ServletException {
        
        // 1 設(shè)置 認(rèn)證成功的Authentication對(duì)象到SecurityContext中
		SecurityContextHolder.getContext().setAuthentication(authResult);
        
        // 2 調(diào)用 RememberMe 相關(guān)service處理
		rememberMeServices.loginSuccess(request, response, authResult);

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

其中我們發(fā)現(xiàn)我們本次重點(diǎn)關(guān)注的一行代碼: rememberMeServices.loginSuccess(request, response, authResult) , 查看這個(gè)方法內(nèi)部源碼:

@Override
	public final void loginSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication successfulAuthentication) {
        // 這里就在判斷用戶(hù)是否勾選了記住我
		if (!rememberMeRequested(request, parameter)) {
			logger.debug("Remember-me login not requested.");
			return;
		}

		onLoginSuccess(request, response, successfulAuthentication);
	}

通過(guò) rememberMeRequested() 判斷是否勾選了記住我。 onLoginSuccess() 方法 最終會(huì)調(diào)用到 PersistentTokenBasedRememberMeServices 的 onLoginSuccess() 方法,貼出其方法源碼如下:

protected void onLoginSuccess(HttpServletRequest request,
			HttpServletResponse response, Authentication successfulAuthentication) {
	    // 1 獲取賬戶(hù)名
		String username = successfulAuthentication.getName();
        
        // 2 創(chuàng)建  PersistentRememberMeToken 對(duì)象
		PersistentRememberMeToken persistentToken = new PersistentRememberMeToken(
				username, generateSeriesData(), generateTokenData(), new Date());
		try {
		    // 3 通過(guò) tokenRepository 存儲(chǔ) persistentRememberMeToken 信息
			tokenRepository.createNewToken(persistentToken);
			// 4 將 persistentRememberMeToken 信息添加到Cookie中
			addCookie(persistentToken, request, response);
		}
		catch (Exception e) {
			logger.error("Failed to save persistent token ", e);
		}
	}

分析下源碼步驟:

  • 獲取 賬戶(hù)信息 username

  • 傳入 username 創(chuàng)建 PersistentRememberMeToken 對(duì)象

  • 通過(guò) tokenRepository 存儲(chǔ) persistentRememberMeToken信息

  • 將 persistentRememberMeToken 信息添加到Cookie中

??這里的 tokenRepository 就是我們配置 rememberMe功能所設(shè)置的。經(jīng)過(guò)上面的解析我們看到了rememberServices 將 創(chuàng)建一個(gè) token 信息,并存儲(chǔ)到數(shù)據(jù)庫(kù)(因?yàn)槲覀兣渲玫氖菙?shù)據(jù)庫(kù)存儲(chǔ)方式 JdbcTokenRepositoryImpl )中,并將token信息添加到Cookie中了。到這里,我們看到了RememberMe實(shí)現(xiàn)前的一些業(yè)務(wù)處理,那么后面如何實(shí)現(xiàn)RememberMe,我想大家心里大概都有個(gè)底了。這里直接拋出之前授權(quán)過(guò)程中我們沒(méi)有提及到的 filter 類(lèi) RememberMeAuthenticationFilter,它是介于 UsernamePasswordAuthenticationFilter 和 AnonymousAuthenticationFilter 之間的一個(gè)filter,它主要負(fù)責(zé)的就是前面的filter都沒(méi)有認(rèn)證成功后從Cookie中獲取token信息然后再通過(guò)tokenRepository 獲取 登錄用戶(hù)名,然后UserDetailsServcie 加載 UserDetails 信息 ,最后創(chuàng)建 Authticaton(RememberMeAuthenticationToken) 信息再調(diào)用 AuthenticationManager.authenticate() 進(jìn)行認(rèn)證過(guò)程。

RememberMeAuthenticationFilter

??我們來(lái)看下 RememberMeAuthenticationFilter 的dofiler方法源碼:

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) {
		    //  1 調(diào)用 rememberMeServices.autoLogin() 獲取Authtication 信息
			Authentication rememberMeAuth = rememberMeServices.autoLogin(request,
					response);

			if (rememberMeAuth != null) {
				// Attempt authenticaton via AuthenticationManager
				try {
				    // 2 調(diào)用 authenticationManager.authenticate() 認(rèn)證
					rememberMeAuth = authenticationManager.authenticate(rememberMeAuth);
                    
                    ......
					}

				}
				catch (AuthenticationException authenticationException) {
                .....
			}

			chain.doFilter(request, response);
		}

我們主要關(guān)注 rememberMeServices.autoLogin(request,response) 方法實(shí)現(xiàn),查看器源碼:

@Override
	public final Authentication autoLogin(HttpServletRequest request,
			HttpServletResponse response) {
		// 1 從Cookie 中獲取 token 信息
		String rememberMeCookie = extractRememberMeCookie(request);

		if (rememberMeCookie == null) {
			return null;
		}
		
		if (rememberMeCookie.length() == 0) {
			cancelCookie(request, response);
			return null;
		}

		UserDetails user = null;

		try {
		    // 2 解析 token信息
			String[] cookieTokens = decodeCookie(rememberMeCookie);
			// 3 通過(guò) token 信息 生成 Uerdetails 信息
			user = processAutoLoginCookie(cookieTokens, request, response);
			userDetailsChecker.check(user);

			logger.debug("Remember-me cookie accepted");
            // 4 通過(guò) UserDetails 信息創(chuàng)建 Authentication 
			return createSuccessfulAuthentication(request, user);
		} 
		.....
	}

內(nèi)部實(shí)現(xiàn)步驟:

  • 從Cookie中獲取 token 信息并解析

  • 通過(guò) 解析的token 生成 UserDetails (processAutoLoginCookie() 方法實(shí)現(xiàn) )

  • 通過(guò) UserDetails 生成 Authentication ( createSuccessfulAuthentication() 創(chuàng)建 RememberMeAuthenticationToken )

其中最關(guān)鍵的一部是 processAutoLoginCookie() 方法是如何生成UserDetails 對(duì)象的,我們查看這個(gè)方法源碼實(shí)現(xiàn):

protected UserDetails processAutoLoginCookie(String[] cookieTokens,
			HttpServletRequest request, HttpServletResponse response) {
		final String presentedSeries = cookieTokens[0];
		final String presentedToken = cookieTokens[1];
        // 1 通過(guò) tokenRepository 加載數(shù)據(jù)庫(kù)token信息
		PersistentRememberMeToken token = tokenRepository
				.getTokenForSeries(presentedSeries);

		PersistentRememberMeToken newToken = new PersistentRememberMeToken(
				token.getUsername(), token.getSeries(), generateTokenData(), new Date());
        // 2 判斷 用戶(hù)傳入token和數(shù)據(jù)中的token是否一致,不一致可能存在安全問(wèn)題
        if (!presentedToken.equals(token.getTokenValue())) {
			tokenRepository.removeUserTokens(token.getUsername());
			throw new CookieTheftException(
					messages.getMessage(
							"PersistentTokenBasedRememberMeServices.cookieStolen",
							"Invalid remember-me token (Series/token) mismatch. Implies previous cookie theft attack."));
		}
		try {
		    // 3 更新 token 并添加到Cookie中
			tokenRepository.updateToken(newToken.getSeries(), newToken.getTokenValue(),
					newToken.getDate());
			addCookie(newToken, request, response);
		}
		catch (Exception e) {
			throw new RememberMeAuthenticationException(
					"Autologin failed due to data access problem");
		}
        // 4 通過(guò) UserDetailsService().loadUserByUsername() 方法加載UserDetails 信息并返回
		return getUserDetailsService().loadUserByUsername(token.getUsername());
	}

我們看下其內(nèi)部步驟:

  • 通過(guò) tokenRepository 加載數(shù)據(jù)庫(kù)token信息

  • 判斷 用戶(hù)傳入token和數(shù)據(jù)中的token是否一致,不一致可能存在安全問(wèn)題

  • 更新 token 并添加到Cookie中

  • 通過(guò) UserDetailsService().loadUserByUsername() 方法加載UserDetails 信息并返回

?? 看到這里相信大家以下就明白了,當(dāng)初為啥在啟用rememberMe功能時(shí)要配置 tokenRepository 和 UserDetailsService了。

這里我就不再演示整個(gè)實(shí)現(xiàn)的流程了,老規(guī)矩,上流程圖:

如何進(jìn)行個(gè)性化認(rèn)證以及RememberMe實(shí)現(xiàn)

看完上述內(nèi)容,你們掌握如何進(jìn)行個(gè)性化認(rèn)證以及RememberMe實(shí)現(xiàn)的方法了嗎?如果還想學(xué)到更多技能或想了解更多相關(guān)內(nèi)容,歡迎關(guān)注億速云行業(yè)資訊頻道,感謝各位的閱讀!

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

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

AI