我的博客地址:blog.sqdyy.cnhtml
Spring Security OAuth2是一個基於OAuth2
封裝的一個類庫,它提供了構建Authorization Server
、Resource Server
和Client
三種Spring
應用程序角色所須要的功能。Spring Security OAuth
須要與Spring Framework(Spring MVC)
和Spring Security
提供的功能協同工做,在使用Spring Security OAuth
構建Authorization Server
、Resource Server
和Client
的狀況下,Spring Security OAuth2
的總體架構圖以下:java
UserAgent
訪問client
,在受權容許訪問受權端點的狀況下,OAuth2RestTemplate
會建立OAuth2
認證的REST
請求,指示UserAgent
重定向到Authorization Server
的受權端點AuthorizationEndpoint
。UserAgent
訪問Authorization Server
的受權端點的authorize
方法,當未註冊受權時,受權端點將須要受權的界面/oauth/confirm_access
顯示給資源擁有者,資源擁有者受權後會經過 AuthorizationServerTokenServices
生成受權碼或訪問令牌,生成的令牌最終會經過userAgent
重定向傳遞給客戶端。OAuth2RestTemplate
拿到受權碼後建立請求訪問受權服務器TokenEndpoint
令牌端點,令牌端點經過調用AuthorizationServerTokenServices
來驗證客戶端提供的受權碼進行受權,並頒發訪問令牌響應給客戶端。OAuth2RestTemplate
在請求頭中加入從受權服務器獲取的訪問令牌來訪問資源服務器,資源服務器經過OAuth2AuthenticationManager
調用ResourceServerTokenServices
驗證訪問令牌和與訪問令牌關聯的驗證信息。訪問令牌驗證成功後,返回客戶端請求對應的資源。上面大體講解了
Spring Security OAuth2
三種應用角色的執行流程,下面咱們將逐個剖析這三種角色的架構和源碼來加深理解。git
受權服務器主要提供了資源擁有者的認證服務,客戶端經過受權服務器向資源擁有者獲取受權,而後獲取受權服務器頒發的令牌。在這個認證流程中,涉及到兩個重要端點,一個是受權端點AuthorizationEndpoint
,另外一個是令牌端點TokenEndpoint
。下面將經過源碼分析這兩個端點的內部運行流程。github
首先讓咱們來看下訪問受權端點AuthorizationEndpoint
的執行流程:spring
UserAgent
會訪問受權服務器的AuthorizationEndpoint
(受權端點)的URI:/oauth/authorize
,調用的是authorize
方法,主要用於判斷用戶是否已經受權,若是受權頒發新的authorization_code,不然跳轉到用戶受權頁面。authorize
它會先調用ClientDetailsService
獲取客戶端詳情信息,並驗證請求參數。authorize
方法再將請求參數傳遞給UserApprovalHandler
用來檢測客戶端是否已經註冊了scope
受權。approved
爲false
,將會向資源擁有者顯示請求受權的界面/oauth/confirm_access
。/oauth/authorize
,這次請求參數會增長一個user_oauth_approval
,所以會調用另外一個映射方法approveOrDeny
。approveOrDeny
會調用userApprovalHandler.updateAfterApproval
根據用戶是否受權,來決定是否更新authorizationRequest
對象中的approved
屬性。userApprovalHandler
的默認實現類是ApprovalStoreUserApprovalHandler
,其內部是經過ApprovalStore
的addApprovals
來註冊受權信息的。當沒有攜帶請求參數user_oauth_approval
時,會訪問authorize
方法,執行流程和上面1-5步對應,若是用戶已經受權則頒發新的authorization_code
,不然跳轉到用戶受權頁面:安全
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters, SessionStatus sessionStatus, Principal principal) {
// 根據請求參數封裝 認證請求對象 ----> AuthorizationRequest
// Pull out the authorization request first, using the OAuth2RequestFactory.
// All further logic should query off of the authorization request instead of referring back to the parameters map.
// The contents of the parameters map will be stored without change in the AuthorizationRequest object once it is created.
AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
// 獲取請求參數中的response_type類型,並進行條件檢驗:response_type只支持token和code,即令牌和受權碼
Set<String> responseTypes = authorizationRequest.getResponseTypes();
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
}
// 請求參數必須攜帶客戶端ID
if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
}
try {
// 在使用Spring Security OAuth2受權完成以前,必須先完成Spring Security對用戶進行的身份驗證
if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorization can be completed.");
}
// 獲取客戶端詳情
ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
// 得到重定向URL,它能夠來自請求參數,也能夠來自客戶端詳情,總之你須要將它存儲在受權請求中
// The resolved redirect URI is either the redirect_uri from the parameters or the one from clientDetails.
// Either way we need to store it on the AuthorizationRequest.
String redirectUriParameter = authorizationRequest.getRequestParameters().get(OAuth2Utils.REDIRECT_URI);
String resolvedRedirect = redirectResolver.resolveRedirect(redirectUriParameter, client);
if (!StringUtils.hasText(resolvedRedirect)) {
throw new RedirectMismatchException(
"A redirectUri must be either supplied or preconfigured in the ClientDetails");
}
authorizationRequest.setRedirectUri(resolvedRedirect);
// 根據客戶端詳情來校驗請求參數中的scope
// We intentionally only validate the parameters requested by the client (ignoring any data that may have been added to the request by the manager).
oauth2RequestValidator.validateScope(authorizationRequest, client);
// 此處檢測請求的用戶是否已經被受權,或者有配置默認受權的權限;若已經有accessToke存在或者被配置默認受權的權限則返回含有受權的對象
// 用到userApprovalHandler ----> ApprovalStoreUserApprovalHandler
// Some systems may allow for approval decisions to be remembered or approved by default.
// Check for such logic here, and set the approved flag on the authorization request accordingly.
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
(Authentication) principal);
// TODO: is this call necessary?
// 若是authorizationRequest.approved爲true,則將跳過Approval頁面。
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
// 已受權 直接返回對應的視圖,返回的視圖中包含新生成的authorization_code(固定長度的隨機字符串)值
// Validation is all done, so we can check for auto approval...
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}
// Place auth request into the model so that it is stored in the sessionfor approveOrDeny to use.
// That way we make sure that auth request comes from the session,
// so any auth request parameters passed to approveOrDeny will be ignored and retrieved from the session.
model.put("authorizationRequest", authorizationRequest);
// 未受權 跳轉到受權界面,讓用戶選擇是否受權
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
}
catch (RuntimeException e) {
sessionStatus.setComplete();
throw e;
}
}
複製代碼
用戶經過受權頁面確認是否受權,並攜帶請求參數user_oauth_approval
訪問受權端點,會執行approveOrDeny
方法,執行流程對應上面6-7步:服務器
@RequestMapping(value = "/oauth/authorize", method = RequestMethod.POST, params = OAuth2Utils.USER_OAUTH_APPROVAL)
public View approveOrDeny(@RequestParam Map<String, String> approvalParameters, Map<String, ?> model, SessionStatus sessionStatus, Principal principal) {
// 在使用Spring Security OAuth2受權完成以前,必須先完成Spring Security對用戶進行的身份驗證
if (!(principal instanceof Authentication)) {
sessionStatus.setComplete();
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorizing an access token.");
}
// 獲取請求參數
AuthorizationRequest authorizationRequest = (AuthorizationRequest) model.get("authorizationRequest");
if (authorizationRequest == null) {
sessionStatus.setComplete();
throw new InvalidRequestException("Cannot approve uninitialized authorization request.");
}
try {
// 獲取請求參數中的response_type類型
Set<String> responseTypes = authorizationRequest.getResponseTypes();
// 設置Approval的參數
authorizationRequest.setApprovalParameters(approvalParameters);
// 根據用戶是否受權,來決定是否更新authorizationRequest對象中的approved屬性。
authorizationRequest = userApprovalHandler.updateAfterApproval(authorizationRequest,
(Authentication) principal);
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
// 須要攜帶重定向URI
if (authorizationRequest.getRedirectUri() == null) {
sessionStatus.setComplete();
throw new InvalidRequestException("Cannot approve request when no redirect URI is provided.");
}
// 用戶拒絕受權,響應錯誤信息到客戶端的重定向URL上
if (!authorizationRequest.isApproved()) {
return new RedirectView(getUnsuccessfulRedirect(authorizationRequest,
new UserDeniedAuthorizationException("User denied access"), responseTypes.contains("token")),
false, true, false);
}
// 簡化模式,直接頒發訪問令牌
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest).getView();
}
// 受權碼模式,生成受權碼存儲並返回給客戶端
return getAuthorizationCodeResponse(authorizationRequest, (Authentication) principal);
}
finally {
sessionStatus.setComplete();
}
}
複製代碼
接下來咱們看下令牌端點TokenEndpoint
的執行流程:session
userAgent
經過訪問受權服務器令牌端點TokenEndpoint的URI:/oauth/token
,調用的是postAccessToken
方法,主要用於爲客戶端生成Token
。postAccessToken
首先會調用ClientDetailsService
獲取客戶端詳情信息並驗證請求參數。Token
。AbstractTokenGranter
抽象類,它的成員AuthorizationServerTokenServices
能夠用來建立、刷新、獲取token
。AuthorizationServerTokenServices
默認實現類只有DefaultTokenServices
,經過它的createAccessToken
方法能夠看到token
是如何建立的。token
的類是TokenStore
,程序根據TokenStore
接口的不一樣實現來生產和存儲token
。下面列出TokenEndpoint
的URI:/oauth/token
的源碼分析:架構
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
// 在使用Spring Security OAuth2受權完成以前,必須先完成Spring Security對用戶進行的身份驗證
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
// 經過客戶端Id獲取客戶端詳情
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
// 根據請求參數封裝 認證請求對象 ----> AuthorizationRequest
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
if (clientId != null && !clientId.equals("")) {
// Only validate the client details if a client authenticated during this
// request.
if (!clientId.equals(tokenRequest.getClientId())) {
// double check to make sure that the client ID in the token request is the same as that in the
// authenticated client
throw new InvalidClientException("Given client ID does not match authenticated client");
}
}
if (authenticatedClient != null) {
// 根據客戶端詳情來校驗請求參數中的scope,防止客戶端越權獲取更多權限
oAuth2RequestValidator.validateScope(tokenRequest, authenticatedClient);
}
// 沒有指定受權模式
if (!StringUtils.hasText(tokenRequest.getGrantType())) {
throw new InvalidRequestException("Missing grant type");
}
// 訪問此端點不該該是簡化模式
if (tokenRequest.getGrantType().equals("implicit")) {
throw new InvalidGrantException("Implicit grant type not supported from token endpoint");
}
// 若是grant_type=authoraztion_code,則清空scope
if (isAuthCodeRequest(parameters)) {
// The scope was requested or determined during the authorization step
if (!tokenRequest.getScope().isEmpty()) {
logger.debug("Clearing scope of incoming token request");
tokenRequest.setScope(Collections.<String> emptySet());
}
}
// 若是grant_type=refresh_token,設置刷新令牌的scope
if (isRefreshTokenRequest(parameters)) {
// A refresh token has its own default scopes, so we should ignore any added by the factory here.
tokenRequest.setScope(OAuth2Utils.parseParameterList(parameters.get(OAuth2Utils.SCOPE)));
}
// 爲客戶端生成token
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
return getResponse(token);
}
複製代碼
令牌端點最關鍵的就是如何生產token
,不一樣的受權模式都會基於AbstractTokenGranter
接口作不一樣實現,AbstractTokenGranter
會委託AuthorizationServerTokenServices
來建立、刷新、獲取token
。AuthorizationServerTokenServices
的默認實現只有DefaultTokenServices
,簡單抽取它的createAccessToken
方法源碼便可看到:app
// 生成accessToken和RefreshToken
@Transactional
public OAuth2AccessToken createAccessToken(OAuth2Authentication authentication) throws AuthenticationException {
// 首先嚐試獲取當前存在的Token
OAuth2AccessToken existingAccessToken = tokenStore.getAccessToken(authentication);
OAuth2RefreshToken refreshToken = null;
// 若是現有的訪問令牌accessToken不爲空且沒有失效,則保存現有訪問令牌, 若是失效則從新存儲新的訪問令牌
if (existingAccessToken != null) {
if (existingAccessToken.isExpired()) {
if (existingAccessToken.getRefreshToken() != null) {
refreshToken = existingAccessToken.getRefreshToken();
// The token store could remove the refresh token when the
// access token is removed, but we want to
// be sure...
tokenStore.removeRefreshToken(refreshToken);
}
tokenStore.removeAccessToken(existingAccessToken);
}
else {
// Re-store the access token in case the authentication has changed
tokenStore.storeAccessToken(existingAccessToken, authentication);
return existingAccessToken;
}
}
// 若是沒有刷新令牌則建立刷新令牌,若是刷新令牌過時,從新建立刷新令牌。
// Only create a new refresh token if there wasn't an existing one associated with an expired access token.
// Clients might be holding existing refresh tokens, so we re-use it in the case that the old access token expired.
if (refreshToken == null) {
refreshToken = createRefreshToken(authentication);
}
// But the refresh token itself might need to be re-issued if it has expired.
else if (refreshToken instanceof ExpiringOAuth2RefreshToken) {
ExpiringOAuth2RefreshToken expiring = (ExpiringOAuth2RefreshToken) refreshToken;
if (System.currentTimeMillis() > expiring.getExpiration().getTime()) {
refreshToken = createRefreshToken(authentication);
}
}
// 生成新的訪問令牌並儲存,保存刷新令牌
OAuth2AccessToken accessToken = createAccessToken(authentication, refreshToken);
tokenStore.storeAccessToken(accessToken, authentication);
// In case it was modified
refreshToken = accessToken.getRefreshToken();
if (refreshToken != null) {
tokenStore.storeRefreshToken(refreshToken, authentication);
}
return accessToken;
}
複製代碼
資源服務器主要用於處理客戶端對受保護資源的訪問請求並返回相應。資源服務器會驗證客戶端的訪問令牌是否有效,並獲取與訪問令牌關聯的認證信息。獲取認證信息後,驗證訪問令牌是否在容許的scope
內,驗證完成後的處理行爲能夠相似於普通應用程序來實現。下面是資源服務器的運行流程:
OAuth2AuthenticationProcessingFilter
,這個攔截器的做用是從請求中提取訪問令牌,而後從令牌中提取認證信息Authentication
並將其存放到上下文中。OAuth2AuthenticationProcessingFilter
攔截器中會調用AuthenticationManager的authenticate
方法提取認證信息。OAuth2AuthenticationProcessingFilter
攔截器若是發生認證錯誤時,將委託AuthenticationEntryPoint
作出錯誤響應,默認實現類是OAuth2AuthenticationEntryPoint
。OAuth2AuthenticationProcessingFilter
執行完成後進入下一個安全過濾器ExceptionTranslationFilter
。ExceptionTranslationFilter
過濾器用來處理在系統認證受權過程當中拋出的異常,攔截器若是發生異常,將委託AccessDeniedHandler
作出錯誤響應,默認實現類是OAuth2AccessDeniedHandler
。資源服務器咱們要關心的是它如何驗證客戶端的訪問令牌是否有效,因此咱們從一開始的OAuth2AuthenticationProcessingFilter
源碼入手,這個攔截器的做用是從請求中提取訪問令牌,而後從令牌中提取認證信息Authentication
並將其存放到上下文中:
public void doFilter(ServletRequest req, ServletResponse res, FilterChain chain) throws IOException, ServletException {
boolean debug = logger.isDebugEnabled();
HttpServletRequest request = (HttpServletRequest)req;
HttpServletResponse response = (HttpServletResponse)res;
try {
// 從請求中提取token,而後再提取token中的認證信息Authorization
Authentication authentication = this.tokenExtractor.extract(request);
if (authentication == null) {
if (this.stateless && this.isAuthenticated()) {
if (debug) {
logger.debug("Clearing security context.");
}
SecurityContextHolder.clearContext();
}
if (debug) {
logger.debug("No token in request, will continue chain.");
}
} else {
request.setAttribute(OAuth2AuthenticationDetails.ACCESS_TOKEN_VALUE, authentication.getPrincipal());
if (authentication instanceof AbstractAuthenticationToken) {
AbstractAuthenticationToken needsDetails = (AbstractAuthenticationToken)authentication;
needsDetails.setDetails(this.authenticationDetailsSource.buildDetails(request));
}
//獲取token攜帶的認證信息Authentication並進行驗證,而後存到spring security的上下文,以供後續使用
Authentication authResult = this.authenticationManager.authenticate(authentication);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
this.eventPublisher.publishAuthenticationSuccess(authResult);
SecurityContextHolder.getContext().setAuthentication(authResult);
}
} catch (OAuth2Exception var9) {
SecurityContextHolder.clearContext();
if (debug) {
logger.debug("Authentication request failed: " + var9);
}
this.eventPublisher.publishAuthenticationFailure(new BadCredentialsException(var9.getMessage(), var9), new PreAuthenticatedAuthenticationToken("access-token", "N/A"));
this.authenticationEntryPoint.commence(request, response, new InsufficientAuthenticationException(var9.getMessage(), var9));
return;
}
chain.doFilter(request, response);
}
複製代碼
上面代碼提到Oauth2AuthenticationManager
會獲取token
攜帶的認證信息進行認證,經過源碼能夠了解到它主要作了3步工做:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
String token = (String) authentication.getPrincipal();
// 1.經過token獲取OAuuth2Authentication對象
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
}
// 2.驗證資源服務的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 + ")");
}
// 3.驗證客戶端的訪問範圍(scope)
checkClientDetails(auth);
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());
}
}
auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
return auth;
}
複製代碼
驗證經過後,通過ExceptionTranslationFilter
過濾器,便可訪問資源。
Spring security OAuth2
客戶端控制着OAuth 2.0
保護的其它服務器的資源的訪問權限。配置包括創建相關受保護資源與有權限訪問資源的用戶之間的鏈接。客戶端也須要實現存儲用戶的受權代碼和訪問令牌的功能。
客戶端代碼結構不是特別複雜,這裏接觸架構圖的描述,有興趣能夠本身按着下面介紹的流程研究源碼:
UserAgent
調用客戶端的Controller
,在這以前會通過OAuth2ClientContextFilter
過濾器,它主要用來捕獲第5步可能發生的UserRedirectRequiredException
,以便重定向到受權服務器從新受權。RestOperations->OAuth2RestOperations
接口的實現類OAuth2RestTemplate
。它主要提供訪問受權服務器或資源服務器的RestAPI
。OAuth2RestTemplate
的成員OAuth2ClientContext
接口實現類爲DefaultOAuth2ClientContext
。它會校驗訪問令牌是否有效,有效則執行第6步訪問資源服務器。AccessTokenProvider
來獲取訪問令牌。AccessTokenProvider
根據定義的資源詳情信息和受權類型獲取訪問令牌,若是獲取不到,拋出UserRedirectRequiredException
。文中架構圖和部份內容參考自TERASOLUNA服務器框架(5.x)開發指南,轉載請註明來源。