## @EnableResourceServer與@EnableAuthorizationServer
還記得我們在第一節中就介紹過了OAuth2的兩個核心概念,資源服務器與身份認證服務器。我們對兩個注解進行配置的同時,到底觸發了內部的什么相關配置呢?
上一篇文章重點介紹的其實是與身份認證相關的流程,即如果獲取token,而本節要分析的攜帶token訪問受限資源,自然便是與@EnableResourceServer相關的資源服務器配置了。
我們注意到其相關配置類是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。

回顧我們第一篇文章的配置,壓根沒有出現過這個OAuth2AuthenticationManager,并且它脫離了我們熟悉的認證流程(第二篇文章中的認證管理器UML圖是一張經典的spring security結構類圖),它直接重寫了容器的頂級身份認證接口,內部維護了一個ClientDetailService和ResourceServerTokenServices,這兩個核心類在 從零開始的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 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 header.
*/
protected String extractHeaderToken(HttpServletRequest request) {
Enumeration<String> headers = request.getHeaders("Authorization");
while (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結尾,便是運用了建造者模式…
* 一點自己的理解:對源碼的理解和靈感,這一切都建立自身的編碼經驗之上,自己遵循規范便能更好的理解別人同樣遵守規范的代碼。相對的,閱讀好的源碼,也能幫助我們自身提升編碼規范。
- java
- 設計模式
- 設計模式總覽
- 設計原則
- 工廠方法模式
- 抽象工廠模式
- 單例模式
- 建造者模式
- 原型模式
- 適配器模式
- 裝飾者模式
- 代理模式
- 外觀模式
- 橋接模式
- 組合模式
- 享元模式
- 策略模式
- 模板方法模式
- 觀察者模式
- 迭代子模式
- 責任鏈模式
- 命令模式
- 備忘錄模式
- 狀態模式
- 訪問者模式
- 中介者模式
- 解釋器模式
- 附錄
- JVM相關
- JVM內存結構
- Java虛擬機的內存組成以及堆內存介紹
- Java堆和棧
- 附錄-數據結構的堆棧和內存分配的堆區棧區的區別
- Java內存之Java 堆
- Java內存之虛擬機和內存區域概述
- Java 內存之方法區和運行時常量池
- Java 內存之直接內存(堆外內存)
- JAVA內存模型
- Java內存模型介紹
- 內存模型如何解決緩存一致性問題
- 深入理解Java內存模型——基礎
- 深入理解Java內存模型——重排序
- 深入理解Java內存模型——順序一致性
- 深入理解Java內存模型——volatile
- 深入理解Java內存模型——鎖
- 深入理解Java內存模型——final
- 深入理解Java內存模型——總結
- 內存可見性
- JAVA對象模型
- JVM內存結構 VS Java內存模型 VS Java對象模型
- Java的對象模型
- Java的對象頭
- HotSpot虛擬機
- HotSpot虛擬機對象探秘
- 深入分析Java的編譯原理
- Java虛擬機的鎖優化技術
- 對象和數組并不是都在堆上分配內存的
- 垃圾回收
- JVM內存管理及垃圾回收
- JVM 垃圾回收器工作原理及使用實例介紹
- JVM內存回收理論與實現(對象存活的判定)
- JVM參數及調優
- CMS GC日志分析
- JVM實用參數(一)JVM類型以及編譯器模式
- JVM實用參數(二)參數分類和即時(JIT)編譯器診斷
- JVM實用參數(三)打印所有XX參數及值
- JVM實用參數(四)內存調優
- JVM實用參數(五)新生代垃圾回收
- JVM實用參數(六) 吞吐量收集器
- JVM實用參數(七)CMS收集器
- JVM實用參數(八)GC日志
- Java性能調優原則
- JVM 優化經驗總結
- 面試題整理
- 面試題1
- java日志規約
- Spring安全
- OAtuth2.0簡介
- Spring Session 簡介(一)
- Spring Session 簡介(二)
- Spring Session 簡介(三)
- Spring Security 簡介(一)
- Spring Security 簡介(二)
- Spring Security 簡介(三)
- Spring Security 簡介(四)
- Spring Security 簡介(五)
- Spring Security Oauth2 (一)
- Spring Security Oauth2 (二)
- Spring Security Oauth2 (三)
- SpringBoot
- Shiro
- Shiro和Spring Security對比
- Shiro簡介
- Session、Cookie和Cache
- Web Socket
- Spring WebFlux