[6] OAuth2AuthenticationProcessingFilter之Bearer Token驗證流程

OAuth2AuthenticationProcessingFilter

簡介

授權認證服務通過認證後會返回Access Token,該token可用於請求資源服務(業務系統)的接口。我們需要把特定的信息放到請求頭中,例如在請求頭中寫入Authorization: Bearer !xBYUEBY0N3o234N,Authorization爲key,Bearer !xBYUEBY0N3o234N爲value,!xBYUEBY0N3o234N是Access Token。請求經過OAuth2AuthenticationProcessingFilter後,過濾器中的認證管理器會調用配置的授權認證服務的check token接扣校驗token是否有效,token有效則可以繼續訪問了。需要注意的是,對於資源認證服務Spring Security會把這個過濾器加入到過濾鏈裏,但授權認證認證服務卻不能。

源碼分析

步驟1

筆者的Spring Cloud項目分ums[用戶管理]服務以及auth[授權認證]服務,ums是一個業務系統,ums藉助授權認證認證服務需要配置client-id,client-secret,token-info-uri等,token-info-uri即auth[授權認證]服務校驗token的url,配置如下:

security:
  oauth2:
    client:
      client-id: carp
      client-secret: carp
    resource:
      id: carp-ums
      token-info-uri: http://carp-auth:8002/oauth2/token/check
      loadBalanced: true

步驟2

我們doFilter()方法中的代碼進行分析,tokenExtractor.extract(request)從請求中取Bearer Token幷包裝成Authentication,然後調用authenticationManager.authenticate()向授權認證服務發起請求,對token進行認證。
認證成功後,發送認證成功事件,並把身份認證信息寫入上下文,代碼如下:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException,
        ServletException {

    final boolean debug = logger.isDebugEnabled();
    final HttpServletRequest request = (HttpServletRequest) req;
    final HttpServletResponse response = (HttpServletResponse) res;

    try {
        //此處代碼邏輯很簡單
        //Header中取出Authorization中的access token 幷包裝成Authentication,Authentication的principal就是token
        Authentication authentication = tokenExtractor.extract(request);
        
        //authentication不存在,清空上下文
        if (authentication == null) {
            if (stateless && isAuthenticated()) {
                SecurityContextHolder.clearContext();
            }
        }
        else {
            //在request屬性中寫入OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE:xxxxx xxxx是token
            request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
            //對於Bearer Token而言,authentication是PreAuthenticatedAuthenticationToken的實例,所以會執行到if分支裏
            if (authentication instanceof AbstractAuthenticationToken) {
                AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
                //buildDetails其實就是創建一個OAuth2AuthenticationDetails並放入remoteAddress、sessionId、tokenType、tokenValue信息
                needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
            }
            //調用RemoteTokenServices向授權認證服務發起請求,進行token校驗。
            Authentication authResult = authenticationManager.authenticate(authentication);
            //發佈一個身份認證成功的事件
            eventPublisher.publishAuthenticationSuccess(authResult);
            //上下文中寫入身份認證信息
            SecurityContextHolder.getContext().setAuthentication(authResult);

        }
    }
    catch (OAuth2Exception failed) {
        //清空上下文
        SecurityContextHolder.clearContext();
        //發佈一個身份認證失敗的事件
        eventPublisher.publishAuthenticationFailure(new BadCredentialsException(failed.getMessage(), failed),
                new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
        //進行錯誤處理
        authenticationEntryPoint.commence(request, response,
                new InsufficientAuthenticationException(failed.getMessage(), failed));

        return;
    }

    chain.doFilter(request, response);
}

authenticationDetailsSource**.**buildDetails()最終會執行到下面OAuth2AuthenticationDetails的構造方法,代碼邏輯也十分簡單,其實就是取出request中的相關信息,進行了一次包裝,如下:

public OAuth2AuthenticationDetails(HttpServletRequest request) {
    this.tokenValue = (String) request.getAttribute(ACCESS_TOKEN_VALUE);
    this.tokenType = (String) request.getAttribute(ACCESS_TOKEN_TYPE);
    this.remoteAddress = request.getRemoteAddr();

    HttpSession session = request.getSession(false);
    this.sessionId = (session != null) ? session.getId() : null;
    StringBuilder builder = new StringBuilder();
    if (remoteAddress!=null) {
        builder.append("remoteAddress=").append(remoteAddress);
    }
    if (builder.length()>1) {
        builder.append(", ");
    }
    if (sessionId!=null) {
        builder.append("sessionId=<SESSION>");
        if (builder.length()>1) {
            builder.append(", ");
        }
    }
    if (tokenType!=null) {
        builder.append("tokenType=").append(this.tokenType);
    }
    if (tokenValue!=null) {
        builder.append("tokenValue=<TOKEN>");
    }
    this.display = builder.toString();
}

authenticationManager是OAuth2AuthenticationManager的實例,其中tokenServices是RemoteTokenServices的實例,如果授權認證服務checkToken返回的數據結構體被自定過,與Spring Security原來返回的不一致,則需要對RemoteTokenServices進行改造,並替換默認實例。因爲RemoteTokenServices就是發送一個http請求,解析返回結果,進行數據包裝。當然,Spring Security只是一個授權認證框架,如果我們使用的Dubbo Rpc,也可以進行改造,注入Dubbo Rpc接口實現ResourceServerTokenServices接口即可。有些變量命名比較抽象,截圖以及的OAuth2AuthenticationManager代碼如下:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {

    if (authentication == null) {
        throw new InvalidTokenException("Invalid token (token not found)");
    }
    String token = (String) authentication.getPrincipal();
    //默認是調用RemoteTokenService向授權認證服務的checkToken接口發起http Token驗證請求
    //驗證通過後包裝成OAuth2Authentication
    OAuth2Authentication auth = tokenServices.loadAuthentication(token);
    //token無效
    if (auth == null) {
        throw new InvalidTokenException("Invalid token: " + token);
    }
	//判斷從接口獲取的授權信息中的資源id,是否包含業務系統配置的資源id
    Collection<String> resourceIds = auth.getOAuth2Request().getResourceIds();
    if (resourceId != null && resourceIds != null && !resourceIds.isEmpty() && !resourceIds.contains(resourceId)) {
        throw new OAuth2AccessDeniedException("Invalid token does not contain resource id (" + resourceId + ")");
    }
	//校驗客戶端id
    checkClientDetails(auth);
	//進行details拷貝
    if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
        OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
        // Guard against a cached copy of the same details
        if (!details.equals(auth.getDetails())) {
            // Preserve the authentication details from the one loaded by token services
            details.setDecodedDetails(auth.getDetails());
        }
    }
    //將details寫入到身份認證信息中
    auth.setDetails(authentication.getDetails());
    //認賬狀態設置爲已認證
    auth.setAuthenticated(true);
    return auth;

}

private void checkClientDetails(OAuth2Authentication auth) {
    if (clientDetailsService != null) {
        ClientDetails client;
        try {
            //調用clientDetailsService獲取客戶端詳情
            client = clientDetailsService.loadClientByClientId(auth.getOAuth2Request().getClientId());
        }
        catch (ClientRegistrationException e) {
            throw new OAuth2AccessDeniedException("Invalid token contains invalid client id");
        }
        //判斷客戶端配置的scope 是否包含http request的中scope參數值
        Set<String> allowed = client.getScope();
        for (String scope : auth.getOAuth2Request().getScope()) {
            if (!allowed.contains(scope)) {
                throw new OAuth2AccessDeniedException(
                        "Invalid token contains disallowed scope (" + scope + ") for this client");
            }
        }
    }
}

image.png

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章