【spring security】認證過程詳解

spring security 主要有兩大功能,即認證和授權

一、Spring Security 認證介紹

總體認證過程:

1、用戶使用用戶名和密碼進行登錄。
2、Spring Security 將獲取到的用戶名和密碼封裝成一個實現了 Authentication 接口的 UsernamePasswordAuthenticationToken。
3、將上述產生的 token 對象傳遞給 AuthenticationManager 進行登錄認證。
4、AuthenticationManager 認證成功後將會返回一個封裝了用戶權限等信息的 Authentication 對象。
5、通過調用 SecurityContextHolder.getContext().setAuthentication(…) 將 AuthenticationManager 返回的 Authentication 對象賦予給當前的 SecurityContext。
在認證成功後,用戶就可以繼續操作去訪問其它受保護的資源了,但是在訪問的時候將會使用保存在 SecurityContext 中的 Authentication 對象進行相關的權限鑑定。

Web 應用的認證過程:

如果用戶直接訪問登錄頁面,那麼認證過程跟上述描述的基本一致,只是在認證完成後將跳轉到指定的成功頁面,默認是應用的根路徑。如果用戶直接訪問一個受保護的資源,那麼認證過程將如下:
1、引導用戶進行登錄,通常是重定向到一個基於 form 表單進行登錄的頁面,具體視配置而定。
2、用戶輸入用戶名和密碼後請求認證,後臺還是會像上節描述的那樣獲取用戶名和密碼封裝成一個 UsernamePasswordAuthenticationToken 對象,然後把它傳遞給 AuthenticationManager 進行認證。
3、如果認證失敗將繼續執行步驟 1,如果認證成功則會保存返回的 Authentication 到 SecurityContext,然後默認會將用戶重定向到之前訪問的頁面。
4、用戶登錄認證成功後再次訪問之前受保護的資源時就會對用戶進行權限鑑定,如不存在對應的訪問權限,則會返回 403 錯誤碼。
在上述步驟中主要的參與的類是 ExceptionTranslationFilter,這裏着重介紹下。

ExceptionTranslationFilter:

ExceptionTranslationFilter 是用來處理來自 AbstractSecurityInterceptor 拋出的 AuthenticationException 和 AccessDeniedException 的。

AbstractSecurityInterceptor 是 Spring Security 用於攔截請求進行權限鑑定的,其擁有兩個具體的子類,攔截方法調用的 MethodSecurityInterceptor 和攔截 URL 請求的 FilterSecurityInterceptor。

當 ExceptionTranslationFilter 捕獲到的是 AuthenticationException 時,將調用 AuthenticationEntryPoint 引導用戶進行登錄;如果捕獲的是 AccessDeniedException,但是用戶還沒有通過認證,則調用 AuthenticationEntryPoint 引導用戶進行登錄認證,否則將返回一個表示不存在對應權限的 403 錯誤碼。

在 request 之間共享 SecurityContext是如何實現的:

既然 SecurityContext 是存放在 ThreadLocal 中的,而且在每次權限鑑定的時候都是從 ThreadLocal 中獲取 SecurityContext 中對應的 Authentication 所擁有的權限,並且不同的 request 是不同的線程,爲什麼每次都可以從 ThreadLocal 中獲取到當前用戶對應的 SecurityContext 呢?

在 Web 應用中這是通過 SecurityContextPersistentFilter 實現的,默認情況下其會在每次請求開始的時候從 session 中獲取 SecurityContext,然後把它設置給 SecurityContextHolder,在請求結束後又會將 SecurityContextHolder 所持有的 SecurityContext 保存在 session 中,並且清除 SecurityContextHolder 所持有的 SecurityContext。

這樣當我們第一次訪問系統的時候,SecurityContextHolder 所持有的 SecurityContext 肯定是空的,待我們登錄成功後,SecurityContextHolder 所持有的 SecurityContext 就不是空的了,且包含有認證成功的 Authentication 對象,待請求結束後我們就會將 SecurityContext 存在 session 中,等到下次請求的時候就可以從 session 中獲取到該 SecurityContext 並把它賦予給 SecurityContextHolder 了,由於 SecurityContextHolder 已經持有認證過的 Authentication 對象了,所以下次訪問的時候也就不再需要進行登錄認證了。

二、從源碼角度分析認證過程:

認證過程的整體流程圖如下:
在這裏插入圖片描述

下面依次介紹:

AbstractAuthenticationProcessingFilter 抽象類

 /**
     *  調用 #requiresAuthentication(HttpServletRequest, HttpServletResponse) 決定是否需要進行驗證操作。
     * 	如果需要驗證,則會調用 #attemptAuthentication(HttpServletRequest, HttpServletResponse) 方法。
     * 	有三種結果:
     * 	 1、返回一個 Authentication 對象。
     *      配置的 SessionAuthenticationStrategy` 將被調用,
     *      然後 然後調用 #successfulAuthentication(HttpServletRequest,HttpServletResponse,FilterChain,Authentication) 方法。
     *   2、驗證時發生 AuthenticationException。
     *      #unsuccessfulAuthentication(HttpServletRequest, HttpServletResponse, AuthenticationException) 方法將被調用。
     *   3、返回Null,表示身份驗證不完整。假設子類做了一些必要的工作(如重定向)來繼續處理驗證,方法將立即返回。
     * 	假設後一個請求將被這種方法接收,其中返回的Authentication對象不爲空。
     *   
     */
    public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest request = (HttpServletRequest)req;
        HttpServletResponse response = (HttpServletResponse)res;
        if (!this.requiresAuthentication(request, response)) {
            chain.doFilter(request, response);
        } else {
            if (this.logger.isDebugEnabled()) {
                this.logger.debug("Request is to process authentication");
            }

            Authentication authResult;
            try {
                authResult = this.attemptAuthentication(request, response);
                if (authResult == null) {
                    return;
                }

                this.sessionStrategy.onAuthentication(authResult, request, response);
            } catch (InternalAuthenticationServiceException var8) {
                this.logger.error("An internal error occurred while trying to authenticate the user.", var8);
                this.unsuccessfulAuthentication(request, response, var8);
                return;
            } catch (AuthenticationException var9) {
                this.unsuccessfulAuthentication(request, response, var9);
                return;
            }

            if (this.continueChainBeforeSuccessfulAuthentication) {
                chain.doFilter(request, response);
            }

            this.successfulAuthentication(request, response, chain, authResult);
        }
    }

UsernamePasswordAuthenticationFilter(AbstractAuthenticationProcessingFilter的子類)

public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
        if (this.postOnly && !request.getMethod().equals("POST")) {
            throw new AuthenticationServiceException("Authentication method not supported: " + request.getMethod());
        } else {
            String username = this.obtainUsername(request);
            String password = this.obtainPassword(request);
            if (username == null) {
                username = "";
            }

            if (password == null) {
                password = "";
            }

            username = username.trim();
            UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(username, password);
            this.setDetails(request, authRequest);
            return this.getAuthenticationManager().authenticate(authRequest);
        }
    }

attemptAuthentication () 方法將 request 中的 username 和 password 生成 UsernamePasswordAuthenticationToken 對象,用於 AuthenticationManager 的驗證(即 this.getAuthenticationManager().authenticate(authRequest) )。

默認情況下注入 Spring 容器的 AuthenticationManager 是 ProviderManager。

ProviderManager(AuthenticationManager的實現類)

/**
	 * 嘗試驗證 Authentication 對象
	 * AuthenticationProvider 列表將被連續嘗試,直到 AuthenticationProvider 表示它能夠認證傳遞的過來的Authentication 對象。然後將使用該 AuthenticationProvider 嘗試身份驗證。
	 * 如果有多個 AuthenticationProvider 支持驗證傳遞過來的Authentication 對象,那麼由第一個來確定結果,覆蓋早期支持AuthenticationProviders 所引發的任何可能的AuthenticationException。 成功驗證後,將不會嘗試後續的AuthenticationProvider。
	 * 如果最後所有的 AuthenticationProviders 都沒有成功驗證 Authentication 對象,將拋出 AuthenticationException。
	 */
	public Authentication authenticate(Authentication authentication)
			throws AuthenticationException {
		Class<? extends Authentication> toTest = authentication.getClass();
		AuthenticationException lastException = null;
		Authentication result = null;
		boolean debug = logger.isDebugEnabled();

		for (AuthenticationProvider provider : getProviders()) {
			if (!provider.supports(toTest)) {
				continue;
			}

			if (debug) {
				logger.debug("Authentication attempt using "
						+ provider.getClass().getName());
			}

			try {
				result = provider.authenticate(authentication);

				if (result != null) {
					copyDetails(authentication, result);
					break;
				}
			}
			catch (AccountStatusException e) {
				prepareException(e, authentication);
				// SEC-546: Avoid polling additional providers if auth failure is due to
				// invalid account status
				throw e;
			}
			catch (InternalAuthenticationServiceException e) {
				prepareException(e, authentication);
				throw e;
			}
			catch (AuthenticationException e) {
				lastException = e;
			}
		}

		if (result == null && parent != null) {
			// Allow the parent to try.
			try {
				result = parent.authenticate(authentication);
			}
			catch (ProviderNotFoundException e) {
				// ignore as we will throw below if no other exception occurred prior to
				// calling parent and the parent
				// may throw ProviderNotFound even though a provider in the child already
				// handled the request
			}
			catch (AuthenticationException e) {
				lastException = e;
			}
		}

		if (result != null) {
			if (eraseCredentialsAfterAuthentication
					&& (result instanceof CredentialsContainer)) {
				// Authentication is complete. Remove credentials and other secret data
				// from authentication
				((CredentialsContainer) result).eraseCredentials();
			}

			eventPublisher.publishAuthenticationSuccess(result);
			return result;
		}

		// Parent was null, or didn't authenticate (or throw an exception).

		if (lastException == null) {
			lastException = new ProviderNotFoundException(messages.getMessage(
					"ProviderManager.providerNotFound",
					new Object[] { toTest.getName() },
					"No AuthenticationProvider found for {0}"));
		}

		prepareException(lastException, authentication);

		throw lastException;
	}

從代碼中不難看出,由 provider 來驗證 authentication, 核心點方法是:

Authentication result = provider.authenticate(authentication);

此處的 provider 是 AbstractUserDetailsAuthenticationProvider, AbstractUserDetailsAuthenticationProvider 是AuthenticationProvider的實現,看看它的 #authenticate(authentication) 方法:

 //驗證 authentication
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        Assert.isInstanceOf(UsernamePasswordAuthenticationToken.class, authentication, this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.onlySupports", "Only UsernamePasswordAuthenticationToken is supported"));
        String username = authentication.getPrincipal() == null ? "NONE_PROVIDED" : authentication.getName();
        boolean cacheWasUsed = true;
        UserDetails user = this.userCache.getUserFromCache(username);
        if (user == null) {
            cacheWasUsed = false;

            try {
                //根據用戶輸入的用戶名,從數據庫中查詢出用戶實體對象
                user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            } catch (UsernameNotFoundException var6) {
                this.logger.debug("User '" + username + "' not found");
                if (this.hideUserNotFoundExceptions) {
                    throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
                }

                throw var6;
            }

            Assert.notNull(user, "retrieveUser returned null - a violation of the interface contract");
        }

        try {
            this.preAuthenticationChecks.check(user);
            //校驗用戶輸入的用戶名和密碼,與查詢出的用戶實體對象用戶名密碼是否一致
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        } catch (AuthenticationException var7) {
            if (!cacheWasUsed) {
                throw var7;
            }

            cacheWasUsed = false;
            user = this.retrieveUser(username, (UsernamePasswordAuthenticationToken)authentication);
            this.preAuthenticationChecks.check(user);
            this.additionalAuthenticationChecks(user, (UsernamePasswordAuthenticationToken)authentication);
        }

        this.postAuthenticationChecks.check(user);
        if (!cacheWasUsed) {
            this.userCache.putUserInCache(user);
        }

        Object principalToReturn = user;
        if (this.forcePrincipalAsString) {
            principalToReturn = user.getUsername();
        }

        return this.createSuccessAuthentication(principalToReturn, authentication, user);

    }

上述代碼中:

1、首先看下this.retrieveUser方法,該方法是根據用戶輸入的用戶名,從數據庫中查詢出用戶實體對象。

AbstractUserDetailsAuthenticationProvider 內置了緩存機制,從緩存中獲取不到的 UserDetails 信息的話,就調用如下方法獲取用戶信息:

// 獲取用戶信息
UserDetails user = retrieveUser(username,
(UsernamePasswordAuthenticationToken) authentication);

retrieveUser() 方法在 DaoAuthenticationProvider 中實現,DaoAuthenticationProvider 是AbstractUserDetailsAuthenticationProvider 的子類。具體實現如下:

protected final UserDetails retrieveUser(String username, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        UserDetails loadedUser;
        try {
        //根據用戶名,獲取用戶信息
            loadedUser = this.getUserDetailsService().loadUserByUsername(username);
        } catch (UsernameNotFoundException var6) {
            if (authentication.getCredentials() != null) {
                String presentedPassword = authentication.getCredentials().toString();
                this.passwordEncoder.isPasswordValid(this.userNotFoundEncodedPassword, presentedPassword, (Object)null);
            }

            throw var6;
        } catch (Exception var7) {
            throw new InternalAuthenticationServiceException(var7.getMessage(), var7);
        }

        if (loadedUser == null) {
            throw new InternalAuthenticationServiceException("UserDetailsService returned null, which is an interface contract violation");
        } else {
            return loadedUser;
        }
    }

2、再看下this.additionalAuthenticationChecks方法,該方法是把1中查詢出的用戶信息與用戶輸入的信息(用戶名和密碼)進行比對判斷是否驗證成功。retrieveUser() 方法也是在 DaoAuthenticationProvider 中實現。

protected void additionalAuthenticationChecks(UserDetails userDetails, UsernamePasswordAuthenticationToken authentication) throws AuthenticationException {
        Object salt = null;
        if (this.saltSource != null) {
            salt = this.saltSource.getSalt(userDetails);
        }

        if (authentication.getCredentials() == null) {
            this.logger.debug("Authentication failed: no credentials provided");
            throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
        } else {
            String presentedPassword = authentication.getCredentials().toString();
            if (!this.passwordEncoder.isPasswordValid(userDetails.getPassword(), presentedPassword, salt)) {
                this.logger.debug("Authentication failed: password does not match stored value");
                throw new BadCredentialsException(this.messages.getMessage("AbstractUserDetailsAuthenticationProvider.badCredentials", "Bad credentials"));
            }
        }
    }
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章