(十二)Spring Security默認實現

  1. 更多的配置 (重寫configure方法)實現的自定義認證和受權(重寫用戶名密碼認證和模仿eladmin的token認證)和自定義受權
  2. Spring Security在SpringBoot中的自動裝配原理
  3. SecurityContext

ps:自定義受權,包含註解式開發和java-configure開發java

參考spring

Spring Security用戶認證和權限控制(默認實現)緩存

Spring Security用戶認證和權限控制(自定義實現)服務器

認證和鑑權

認證和鑑權分別對應authentication/authorizationSpring SecurityShiro都涉及到了這兩個詞ide

通俗點說spring-boot

  • 認證就是登陸的過程。服務器根據你傳遞的用戶名,密碼或者其餘形式的參數查詢是否存在這個用戶,若是存在說明認證成功post

  • 鑑權就是判斷你是否由權力訪問資源的流程。你想看某個視頻,只有vip用戶才能看,可是你當前登陸的帳戶不是vip,因此你不能看。判斷能不能看這個視頻(是否有權限訪問指定的資源)的過程就是鑑權ui

Spring Security的過濾器鏈

Spring Security集成了Spring MVCSpring Security經過建立一系列Filter(過濾器)來實現認證和受權this

集成Spring Security

  1. 導入Maven依賴加密

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-security</artifactId>
    </dependency>
  2. 建立WebSecurityConfigurerAdapter的子類並添加@EnableWebSecurity註解

    @EnableWebSecurity
    public class WebSecurityConfig extends WebSecurityConfigurerAdapter {
    }

至此,啓動項目後,Spring Security爲程序添加了許多過濾器用於認證和鑑權

Spring Security中內置的過濾器

Spring Security中有許多內置的過濾器(這裏只簡單介紹兩個)

在嘗試自定義認證和受權流程以前應該先掌握內置的過濾器是如何工做的

其中我認爲比較重要的內置過濾器是UsernamePasswordAuthenticationFilterFilterSecurityInterceptor(它是過濾器,不是攔截器)

Authentication

在瞭解過濾器以前先了解一下Authentication

此類是「認證主體」,這個類的包含了用戶是否定證成功,用戶的信息等信息

public interface Authentication extends Principal, Serializable {

    // 獲取權限
	Collection<? extends GrantedAuthority> getAuthorities();

	Object getCredentials();

	Object getDetails();

	Object getPrincipal();
	
    // 返回認證主體是否已經被認證過
	boolean isAuthenticated();

    // 這隻認證主體是否成功經過認證
	void setAuthenticated(boolean isAuthenticated) throws IllegalArgumentException;
}

UsernamePasswordAuthenticationFilter

這個過濾器提供用戶名,密碼方式認證服務

這個過濾器攔截了/login請求並校驗用戶名,密碼。若是認證成功(用戶名,密碼都對了)就建立一個Authentication對象;認證失敗就拋AuthenticationException

  1. 默認只攔截 POST方式的/login請求,若是不符合直接執行下面的過濾器(chain.doFilter(request, response);
  2. 獲取請求中的用戶名和密碼,並建立一個Authentication對象(實際類型是UsernamePasswordAhtienticationToken),這個對象是沒有被認證的「認證主體」
  3. 使用成員變量AuthenticationManagerauthenticate()方法進行認證
  4. AuthenticationManager遍歷持有的AuthenticationProvider對象,使用能認證此AhthenticationAuthenticationProvider進行認證
  5. AbstractUserDetailsAuthenticationProvider先使用緩存獲取user對象,若是獲取不到就用持有的UserDetialsServicePasswordEncoderDB中獲取User對象並加密密碼再封裝起來
  6. 拿到user對象後校驗對象是否過時,密碼是否正確等。沒問題後建立一個新的AuthenticationToken做爲已經被認證的「認證主體」
  7. AuthenticationToken放到SecurityContext中(SecurityContext之後再說)

上述流程中重要的部分是

  • 過濾器自己和其認證邏輯
  • AuthenticationManager對象。它持有各類AuthenticationProvider,能夠完成對不一樣類型Authentication的認證
  • AuthenticationProvider持有的UserDetialsServicePasswordEncoder。他們的做用是建立UserDetials對象爲之後建立被認證的Authentication作鋪墊
/* AbstractAuthenticationProcessingFilter 是UsernamePasswordAuthenticationFilter的父類 */
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;
    }
    Authentication authResult;
    try {
        // 認證流程在這裏,若是認證失敗會拋異常
        authResult = attemptAuthentication(request, response);
        if (authResult == null) {
            return;
        }
    }
    catch (AuthenticationException failed) {
        return;
    }

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

    // 將Authentication放進SecurityContext中
    successfulAuthentication(request, response, chain, authResult);

}

/* UsernamePasswordAuthenticationFilter */
public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException {
    // 若是請求不是POST方式就拋異常
    if (postOnly && !request.getMethod().equals("POST")) {
        throw new AuthenticationServiceException(
            "Authentication method not supported: " + request.getMethod());
    }

    String username = obtainUsername(request);
    String password = obtainPassword(request);

    // 建立一個未認證的AuthenticationToken
    UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(
        username, password);
	// 調用AuthenticationManager進行認證
    return this.getAuthenticationManager().authenticate(authRequest);
}

/* ProviderManager */
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
    Class<? extends Authentication> toTest = authentication.getClass();

    Authentication result = null;

    // 遍歷持有的AuthenticationProvider,若是有AuthenticationProvider能認證Authentication就認證
    for (AuthenticationProvider provider : getProviders()) {
        // AuthenticationProvider是否能認證當前類型的Authentication
        if (!provider.supports(toTest)) {
            continue;
        }
        try {
            // 認證
            result = provider.authenticate(authentication);
            if (result != null) {
                break;
            }
        } catch (AuthenticationException e) {
        }
	}
    if (result != null) {
        return result;
    }
}

/* AbstractUserDetailsAuthenticationProvider */
public Authentication authenticate(Authentication authentication) throws AuthenticationException {

    String username = (authentication.getPrincipal() == null) ? "NONE_PROVIDED"
        : authentication.getName();

    // 嘗試從緩存中獲取User對象
    boolean cacheWasUsed = true;
    UserDetails user = this.userCache.getUserFromCache(username);

    // 緩存中獲取不到就從新獲取並緩存起來
    if (user == null) {
        cacheWasUsed = false;

        try {
            // 調用UserDetailsService獲取user對象並使用PasswordEncoder加密密碼
            user = retrieveUser(username, (UsernamePasswordAuthenticationToken) authentication);
        }
        catch (UsernameNotFoundException notFound) {
        }
    }

    try {
        // 前置檢查
        preAuthenticationChecks.check(user);
        // 檢查密碼是否正確
        additionalAuthenticationChecks(user,
                                       (UsernamePasswordAuthenticationToken) authentication);
    }
    catch (AuthenticationException exception) {
    }

    // 後置檢查。先後置檢查都在檢查user是否已通過期
    postAuthenticationChecks.check(user);

    // 將user緩存起來
    if (!cacheWasUsed) {
        this.userCache.putUserInCache(user);
    }

    Object principalToReturn = user;

	// 建立一個被認證的Authentication(能執行到這裏還沒拋異常說明user認證成功了)
    return createSuccessAuthentication(principalToReturn, authentication, user);
}

FilterSecurityInterceptor

這個過濾器被放在過濾器鏈的最後面,控制鑑權流程,用於檢查這次請求是否有權限訪問資源,若是不能就拋AccessException

  1. SecurityContext中獲取Authentication對象,若是沒有認證就認證一下(調用AuthenticationManager認證)
  2. 獲取當前請求資源所須要的權限
  3. 調用AccessDecisionManager持有的投票器進行投票,根據投票結果斷定請求攜帶的Authentication是否有權限訪問資源。斷定標準根據AccessDecisionManager不一樣的實現類決定。例如一票否決,一票經過,多數服從少數等標準

上述流程中重要的部分是

  • 過濾器自己
  • AccessDecisionManager和投票器

ps:一般鑑權流程交給這個過濾器就足夠了,無需建立新的鑑權過濾器

AnonymousAuthenticationFilter

匿名的「權限主體」過濾器

  1. 若是你沒有通過認證就訪問被保護起來的資源,這個過濾器會檢測到SecurityContext中沒有Authentication對象,併爲其建立一個AnonymousAuthenticationToken類型的Authentication並放進SecurityContext
  2. 匿名的「認證主體」只有ROLE_ANONYMOUS權限。在訪問被保護起來的資源時會被FilterSecurityInterceptor斷定爲沒有權限訪問資源的
相關文章
相關標籤/搜索