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");
}
}
}
}