上一篇文章中我們介紹了獲取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的整個內部流程就算是分析結束了。本系列的文章只能算是揭示一個大概的流程,重點還是介紹相關設計+介面,想要瞭解更多的細節,需要自己去翻看原始碼,研究各個實現類。在分析原始碼過程中總結出的一點經驗,與君共勉:
-
先掌握宏觀,如研究UML類圖,搞清楚關聯
-
分析頂級介面,設計是面向介面的,不重要的部分,具體實現類甚至都可以忽略
-
學會對比,如ResourceServer和AuthenticationServer是一種對稱的設計,整個框架內部的類非常多,但分門別類的記憶,會加深記憶。如ResourceServerTokenServices ,AuthenticationServerTokenServices就一定是作用相關,但所屬領域不同的兩個介面
-
熟悉設計樣式,spring中涉及了大量的設計樣式,在框架的設計中也是遵循著設計樣式的規範,如以Adapter結尾,便是運用了配接器樣式;以Factory結尾,便是運用了配接器樣式;Template結尾,便是運用了模板方法樣式;Builder結尾,便是運用了建造者樣式…
-
一點自己的理解:對原始碼的理解和靈感,這一切都建立自身的編碼經驗之上,自己遵循規範便能更好的理解別人同樣遵守規範的程式碼。相對的,閱讀好的原始碼,也能幫助我們自身提升編碼規範。