歡迎光臨
每天分享高質量文章

從零開始的Spring Security OAuth2(三)

上一篇文章中我們介紹了獲取token的流程,這一篇重點分析一下,攜帶token訪問受限資源時,內部的工作流程。

@EnableResourceServer與@EnableAuthorizationServer

還記得我們在第一節中就介紹過了OAuth2的兩個核心概念,資源伺服器與身份認證伺服器。我們對兩個註解進行配置的同時,到底觸發了內部的什麼相關配置呢?

上一篇文章重點介紹的其實是與身份認證相關的流程,即如果獲取token,而本節要分析的攜帶token訪問受限資源,自然便是與@EnableResourceServer相關的資源伺服器配置了。


作者:老徐

原文地址:http://suo.im/2pN2yr

友情提示:歡迎關註公眾號【芋道原始碼】。?關註後,拉你進【原始碼圈】微信群和【老徐】搞基嗨皮。

友情提示:歡迎關註公眾號【芋道原始碼】。?關註後,拉你進【原始碼圈】微信群和【老徐】搞基嗨皮。

友情提示:歡迎關註公眾號【芋道原始碼】。?關註後,拉你進【原始碼圈】微信群和【老徐】搞基嗨皮。


我們註意到其相關配置類是ResourceServerConfigurer,內部關聯了ResourceServerSecurityConfigurer和HttpSecurity。前者與資源安全配置相關,後者與http安全配置相關。(類名比較類似,註意區分,以Adapter結尾的是配接器,以Configurer結尾的是配置器,以Builder結尾的是建造器,他們分別代表不同的設計樣式,對設計樣式有所瞭解可以更加方便理解其設計思路)

public class ResourceServerConfigurerAdapter implements ResourceServerConfigurer {
   @Override
   public void configure(ResourceServerSecurityConfigurer resources <1> ) throws Exception {
   }

   @Override
   public void configure(HttpSecurity http) throws Exception {
       http.authorizeRequests().anyRequest().authenticated();
   }

}

<1> ResourceServerSecurityConfigurer顯然便是我們分析的重點了。

ResourceServerSecurityConfigurer(瞭解)

其核心配置如下所示:

public void configure(HttpSecurity http) throws Exception {
   AuthenticationManager oauthAuthenticationManager = oauthAuthenticationManager(http);
   resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();//<1>
   resourcesServerFilter.setAuthenticationEntryPoint(authenticationEntryPoint);
   resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);//<2>
   if (eventPublisher != null) {
       resourcesServerFilter.setAuthenticationEventPublisher(eventPublisher);
   }
   if (tokenExtractor != null) {
       resourcesServerFilter.setTokenExtractor(tokenExtractor);//<3>
   }
   resourcesServerFilter = postProcess(resourcesServerFilter);
   resourcesServerFilter.setStateless(stateless);

   // @formatter:off
   http
       .authorizeRequests().expressionHandler(expressionHandler)
   .and()
       .addFilterBefore(resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class)
       .exceptionHandling()
           .accessDeniedHandler(accessDeniedHandler)//<4>
           .authenticationEntryPoint(authenticationEntryPoint);
   // @formatter:on
}

這段是整個oauth2與HttpSecurity相關的核心配置,其中有非常多的註意點,順帶的都強調一下:

<1> 建立OAuth2AuthenticationProcessingFilter,即下一節所要介紹的OAuth2核心過濾器。

<2> 為OAuth2AuthenticationProcessingFilter提供固定的AuthenticationManager即OAuth2AuthenticationManager,它並沒有將OAuth2AuthenticationManager新增到spring的容器中,不然可能會影響spring security的普通認證流程(非oauth2請求),只有被OAuth2AuthenticationProcessingFilter攔截到的oauth2相關請求才被特殊的身份認證器處理。

<3> 設定了TokenExtractor預設的實現—-BearerTokenExtractor,這個類在下一節介紹。

<4> 相關的異常處理器,可以重寫相關實現,達到自定義異常的目的。

還記得我們在一開始的配置中配置了資源伺服器,是它觸發了相關的配置。

@Configuration
@EnableResourceServer
protected static class ResourceServerConfiguration extends ResourceServerConfigurerAdapter {}

核心過濾器 OAuth2AuthenticationProcessingFilter(掌握)

回顧一下我們之前是如何攜帶token訪問受限資源的:
http://localhost:8080/order/1?access_token=950a7cc9-5a8a-42c9-a693-40e817b1a4b0
唯一的身份憑證,便是這個access_token,攜帶它進行訪問,會進入OAuth2AuthenticationProcessingFilter之中,其核心程式碼如下:

public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain){
   final HttpServletRequest request = (HttpServletRequest) req;
   final HttpServletResponse response = (HttpServletResponse) res;

   try {
       //從請求中取出身份資訊,即access_token
       Authentication authentication = tokenExtractor.extract(request);

       if (authentication == null) {
           ...
       }
       else {
           request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
           if (authentication instanceof AbstractAuthenticationToken) {
               AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken) authentication;
               needsDetails.setDetails(authenticationDetailsSource.buildDetails(request));
           }
           //認證身份
           Authentication authResult = authenticationManager.authenticate(authentication);
           ...
           eventPublisher.publishAuthenticationSuccess(authResult);
           //將身份資訊系結到SecurityContextHolder中
           SecurityContextHolder.getContext().setAuthentication(authResult);
       }
   }
   catch (OAuth2Exception failed) {
       ...
       return;
   }

   chain.doFilter(request, response);
}

整個過濾器便是oauth2身份鑒定的關鍵,在原始碼中,對這個類有一段如下的描述

A pre-authentication filter for OAuth2 protected resources. Extracts an OAuth2 token from the incoming request and uses it to populate the Spring Security context with an {@link OAuth2Authentication} (if used in conjunction with an {@link OAuth2AuthenticationManager}). OAuth2保護資源的預先認證過濾器。如果與OAuth2AuthenticationManager結合使用,則會從到來的請求之中提取一個OAuth2 token,之後使用OAuth2Authentication來填充Spring Security背景關係。

其中涉及到了兩個關鍵的類TokenExtractor,AuthenticationManager。相信後者這個介面大家已經不陌生,但前面這個類之前還未出現在我們的視野中。

OAuth2的身份管理器–OAuth2AuthenticationManager(掌握)

在之前的OAuth2核心過濾器中出現的AuthenticationManager其實在我們意料之中,攜帶access_token必定得經過身份認證,但是在我們debug進入其中後,發現了一個出乎意料的事,AuthenticationManager的實現類並不是我們在前面文章中聊到的常用實現類ProviderManager,而是OAuth2AuthenticationManager。

圖1 新的AuthenticationManager實現類OAuth2AuthenticationManager

回顧我們第一篇文章的配置,壓根沒有出現過這個OAuth2AuthenticationManager,並且它脫離了我們熟悉的認證流程(第二篇文章中的認證管理器UML圖是一張經典的spring security結構類圖),它直接重寫了容器的頂級身份認證介面,內部維護了一個ClientDetailService和ResourceServerTokenServices,這兩個核心類在 Re:從零開始的Spring Security Oauth2(二)有分析過。在ResourceServerSecurityConfigurer的小節中我們已經知曉了它是如何被框架自動配置的,這裡要強調的是OAuth2AuthenticationManager是密切與token認證相關的,而不是與獲取token密切相關的。

其判別身份的關鍵程式碼如下:

public Authentication authenticate(Authentication authentication) throws AuthenticationException {
   ...
   String token = (String) authentication.getPrincipal();
   //最終還是藉助tokenServices根據token載入身份資訊
   OAuth2Authentication auth = tokenServices.loadAuthentication(token);
   ...

   checkClientDetails(auth);

   if (authentication.getDetails() instanceof OAuth2AuthenticationDetails) {
       OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
       ...
   }
   auth.setDetails(authentication.getDetails());
   auth.setAuthenticated(true);
   return auth;

}

說到tokenServices這個密切與token相關的介面,這裡要強調下,避免產生誤解。tokenServices分為兩類,一個是用在AuthenticationServer端,第二篇文章中介紹的

public interface AuthorizationServerTokenServices {
   //建立token
   OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException;
   //掃清token
   OAuth2AccessToken refreshAccessToken(String refreshToken, TokenRequest tokenRequest)
           throws AuthenticationException
;
   //獲取token
   OAuth2AccessToken getAccessToken(OAuth2Authentication authentication);
}

而在ResourceServer端有自己的tokenServices介面:

public interface ResourceServerTokenServices {
   //根據accessToken載入客戶端資訊
   OAuth2Authentication loadAuthentication(String accessToken) throws AuthenticationException, InvalidTokenException;

   //根據accessToken獲取完整的訪問令牌詳細資訊。
   OAuth2AccessToken readAccessToken(String accessToken);

}

具體內部如何載入,和AuthorizationServer大同小異,只是從tokenStore中取出相應身份的流程有點區別,不再詳細看實現類了。

TokenExtractor(瞭解)

這個介面只有一個實現類,而且程式碼非常簡單

public class BearerTokenExtractor implements TokenExtractor {
   private final static Log logger = LogFactory.getLog(BearerTokenExtractor.class);
   @Override
   public Authentication extract(HttpServletRequest request) {
       String tokenValue = extractToken(request);
       if (tokenValue != null) {
           PreAuthenticatedAuthenticationToken authentication = new PreAuthenticatedAuthenticationToken(tokenValue, "");
           return authentication;
       }
       return null;
   }

   protected String extractToken(HttpServletRequest request) {
       // first check the essay-header...
       String token = extractHeaderToken(request);

       // bearer type allows a request parameter as well
       if (token == null) {
           ...
           //從requestParameter中獲取token
       }

       return token;
   }

/**
    * Extract the OAuth bearer token from a essay-header.
    */

   protected String extractHeaderToken(HttpServletRequest request) {
       Enumeration essay-headers = request.getHeaders("Authorization");
       while (essay-headers.hasMoreElements()) { // typically there is only one (most servers enforce that)
           ...
           //從Header中獲取token
       }
       return null;
   }

}

它的作用在於分離出請求中包含的token。也啟示了我們可以使用多種方式攜帶token。
1 在Header中攜帶

http://localhost:8080/order/1
Header
Authentication:Bearer f732723d-af7f-41bb-bd06-2636ab2be135

2 拼接在url中作為requestParam

http://localhost:8080/order/1?access_token=f732723d-af7f-41bb-bd06-2636ab2be135

3 在form表單中攜帶

http://localhost:8080/order/1
form param:
access_token=f732723d-af7f-41bb-bd06-2636ab2be135

異常處理

OAuth2在資源伺服器端的異常處理不算特別完善,但基本夠用,如果想要重寫異常機制,可以直接替換掉相關的Handler,如許可權相關的AccessDeniedHandler。具體的配置應該在@EnableResourceServer中被改寫,這是配接器+配置器的好處。

總結

到這兒,Spring Security OAuth2的整個內部流程就算是分析結束了。本系列的文章只能算是揭示一個大概的流程,重點還是介紹相關設計+介面,想要瞭解更多的細節,需要自己去翻看原始碼,研究各個實現類。在分析原始碼過程中總結出的一點經驗,與君共勉:

  1. 先掌握宏觀,如研究UML類圖,搞清楚關聯

  2. 分析頂級介面,設計是面向介面的,不重要的部分,具體實現類甚至都可以忽略

  3. 學會對比,如ResourceServer和AuthenticationServer是一種對稱的設計,整個框架內部的類非常多,但分門別類的記憶,會加深記憶。如ResourceServerTokenServices ,AuthenticationServerTokenServices就一定是作用相關,但所屬領域不同的兩個介面

  4. 熟悉設計樣式,spring中涉及了大量的設計樣式,在框架的設計中也是遵循著設計樣式的規範,如以Adapter結尾,便是運用了配接器樣式;以Factory結尾,便是運用了配接器樣式;Template結尾,便是運用了模板方法樣式;Builder結尾,便是運用了建造者樣式…

  5. 一點自己的理解:對原始碼的理解和靈感,這一切都建立自身的編碼經驗之上,自己遵循規範便能更好的理解別人同樣遵守規範的程式碼。相對的,閱讀好的原始碼,也能幫助我們自身提升編碼規範。

贊(0)

分享創造快樂