OAuth2.0原理分析
授權伺服器
@EnableAuthorizationServer解析
我們都知道 一個授權認證伺服器最最核心的就是 @EnableAuthorizationServer , 那麼 @EnableAuthorizationServer 主要做了什麼呢? 我們看下 @EnableAuthorizationServer 源碼:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({AuthorizationServerEndpointsConfiguration.class, AuthorizationServerSecurityConfiguration.class})
public @interface EnableAuthorizationServer {
}
我們可以看到其源碼内部導入了 AuthorizationServerEndpointsConfiguration 和 AuthorizationServerSecurityConfiguration 這2個配置類。 接下來我們分别看下這2個配置類具體做了什麼。
AuthorizationServerEndpointsConfiguration
@Configuration
@Import(TokenKeyEndpointRegistrar.class)
public class AuthorizationServerEndpointsConfiguration {
// 省略 其他相關配置代碼
....
// 1、 AuthorizationEndpoint 建立
@Bean
public AuthorizationEndpoint authorizationEndpoint() throws Exception {
AuthorizationEndpoint authorizationEndpoint = new AuthorizationEndpoint();
FrameworkEndpointHandlerMapping mapping = getEndpointsConfigurer().getFrameworkEndpointHandlerMapping();
authorizationEndpoint.setUserApprovalPage(extractPath(mapping, "/oauth/confirm_access"));
authorizationEndpoint.setProviderExceptionHandler(exceptionTranslator());
authorizationEndpoint.setErrorPage(extractPath(mapping, "/oauth/error"));
authorizationEndpoint.setTokenGranter(tokenGranter());
authorizationEndpoint.setClientDetailsService(clientDetailsService);
authorizationEndpoint.setAuthorizationCodeServices(authorizationCodeServices());
authorizationEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
authorizationEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
authorizationEndpoint.setUserApprovalHandler(userApprovalHandler());
authorizationEndpoint.setRedirectResolver(redirectResolver());
return authorizationEndpoint;
}
// 2、 TokenEndpoint 建立
@Bean
public TokenEndpoint tokenEndpoint() throws Exception {
TokenEndpoint tokenEndpoint = new TokenEndpoint();
tokenEndpoint.setClientDetailsService(clientDetailsService);
tokenEndpoint.setProviderExceptionHandler(exceptionTranslator());
tokenEndpoint.setTokenGranter(tokenGranter());
tokenEndpoint.setOAuth2RequestFactory(oauth2RequestFactory());
tokenEndpoint.setOAuth2RequestValidator(oauth2RequestValidator());
tokenEndpoint.setAllowedRequestMethods(allowedTokenEndpointRequestMethods());
return tokenEndpoint;
}
// 省略 其他相關配置代碼
....
通過源碼我們可以很明确的知道:
- AuthorizationEndpoint 用于服務授權請求。預設位址:/oauth/authorize。
- TokenEndpoint 用于服務通路令牌的請求。預設位址:/oauth/token。
AuthorizationServerSecurityConfiguration
- ClientDetailsService : 内部僅有 loadClientByClientId 方法。從方法名我們就可知其是通過 clientId 來擷取 Client 資訊, 官方提供 JdbcClientDetailsService、InMemoryClientDetailsService 2個實作類,我們也可以像UserDetailsService 一樣編寫自己的實作類。
- UserDetailsService : 内部僅有 loadUserByUsername 方法。這個類不用我再介紹了吧。不清楚得同學可以看下我之前得文章。
- ClientDetailsUserDetailsService : UserDetailsService子類,内部維護了 ClientDetailsService 。其 loadUserByUsername 方法重寫後調用ClientDetailsService.loadClientByClientId()。
- ClientCredentialsTokenEndpointFilter** 作用與 UserNamePasswordAuthenticationFilter 類似,通過攔截 /oauth/token 位址,擷取到 clientId 和 clientSecret 資訊并建立 UsernamePasswordAuthenticationToken 作為 AuthenticationManager.authenticate() 參數 調用認證過程。整個認證過程唯一最大得差別在于 DaoAuthenticationProvider.retrieveUser() 擷取認證使用者資訊時調用的是 ClientDetailsUserDetailsService,根據前面講述的其内部其實是調用ClientDetailsService 擷取到用戶端資訊。
@EnableResourceServer
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ResourceServerConfiguration.class})
public @interface EnableResourceServer {
}
從源碼中我們可以看到其導入了 ResourceServerConfiguration 配置類,這個配置類最核心的配置是 應用了 ResourceServerSecurityConfigurer ,我這邊貼出 ResourceServerSecurityConfigurer 源碼 最核心的配置代碼如下:
public void configure(HttpSecurity http) throws Exception {
AuthenticationManager oauthAuthenticationManager = this.oauthAuthenticationManager(http);
this.resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
this.resourcesServerFilter.setAuthenticationEntryPoint(this.authenticationEntryPoint);
this.resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);
if (this.eventPublisher != null) {
this.resourcesServerFilter.setAuthenticationEventPublisher(this.eventPublisher);
}
if (this.tokenExtractor != null) {
this.resourcesServerFilter.setTokenExtractor(this.tokenExtractor);
}
this.resourcesServerFilter = (OAuth2AuthenticationProcessingFilter)this.postProcess(this.resourcesServerFilter);
this.resourcesServerFilter.setStateless(this.stateless);
((HttpSecurity)http.authorizeRequests().expressionHandler(this.expressionHandler).and()).addFilterBefore(this.resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class).exceptionHandling().accessDeniedHandler(this.accessDeniedHandler).authenticationEntryPoint(this.authenticationEntryPoint);
}
private AuthenticationManager oauthAuthenticationManager(HttpSecurity http) {
OAuth2AuthenticationManager oauthAuthenticationManager = new OAuth2AuthenticationManager();
if (this.authenticationManager != null) {
if (!(this.authenticationManager instanceof OAuth2AuthenticationManager)) {
return this.authenticationManager;
}
oauthAuthenticationManager = (OAuth2AuthenticationManager)this.authenticationManager;
}
oauthAuthenticationManager.setResourceId(this.resourceId);
oauthAuthenticationManager.setTokenServices(this.resourceTokenServices(http));
oauthAuthenticationManager.setClientDetailsService(this.clientDetails());
return oauthAuthenticationManager;
}
源碼中最核心的 就是 官方文檔中介紹的 OAuth2AuthenticationProcessingFilter 過濾器, 其配置分3步:
- 1、 建立 OAuth2AuthenticationProcessingFilter 過濾器 對象
- 2、 建立 OAuth2AuthenticationManager 對象 對将其作為參數設定到 OAuth2AuthenticationProcessingFilter 中
- 3、 将 OAuth2AuthenticationProcessingFilter 過濾器添加到過濾器鍊上
AuthorizationEndpoint生成授權碼
@RequestMapping(value = "/oauth/authorize")
public ModelAndView authorize(Map<String, Object> model, @RequestParam Map<String, String> parameters,
SessionStatus sessionStatus, Principal principal) {
// 1、 通過 OAuth2RequestFactory 從 參數中擷取資訊建立 AuthorizationRequest 授權請求對象
AuthorizationRequest authorizationRequest = getOAuth2RequestFactory().createAuthorizationRequest(parameters);
Set<String> responseTypes = authorizationRequest.getResponseTypes();
if (!responseTypes.contains("token") && !responseTypes.contains("code")) {
throw new UnsupportedResponseTypeException("Unsupported response types: " + responseTypes);
}
if (authorizationRequest.getClientId() == null) {
throw new InvalidClientException("A client id must be provided");
}
try {
// 2、 判斷 principal 是否 已授權 : /oauth/authorize 設定為無權限通路 ,是以要判斷,如果 判斷失敗則抛出 InsufficientAuthenticationException (AuthenticationException 子類),其異常會被 ExceptionTranslationFilter 處理 ,最終跳轉到 登入頁面,這也是為什麼我們第一次去請求擷取 授權碼時會跳轉到登陸界面的原因
if (!(principal instanceof Authentication) || !((Authentication) principal).isAuthenticated()) {
throw new InsufficientAuthenticationException(
"User must be authenticated with Spring Security before authorization can be completed.");
}
// 3、 通過 ClientDetailsService.loadClientByClientId() 擷取到 ClientDetails 用戶端資訊
ClientDetails client = getClientDetailsService().loadClientByClientId(authorizationRequest.getClientId());
// 4、 擷取參數中的回調位址并且與系統配置的回調位址對比
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);
// 5、 驗證 scope
oauth2RequestValidator.validateScope(authorizationRequest, client);
// 6、 檢測該用戶端是否設定自動 授權(即 我們配置用戶端時配置的 autoApprove(true) )
authorizationRequest = userApprovalHandler.checkForPreApproval(authorizationRequest,
(Authentication) principal);
boolean approved = userApprovalHandler.isApproved(authorizationRequest, (Authentication) principal);
authorizationRequest.setApproved(approved);
if (authorizationRequest.isApproved()) {
if (responseTypes.contains("token")) {
return getImplicitGrantResponse(authorizationRequest);
}
if (responseTypes.contains("code")) {
// 7 調用 getAuthorizationCodeResponse() 方法生成code碼并回調到設定的回調位址
return new ModelAndView(getAuthorizationCodeResponse(authorizationRequest,
(Authentication) principal));
}
}
model.put(AUTHORIZATION_REQUEST_ATTR_NAME, authorizationRequest);
model.put(ORIGINAL_AUTHORIZATION_REQUEST_ATTR_NAME, unmodifiableMap(authorizationRequest));
return getUserApprovalPageResponse(model, authorizationRequest, (Authentication) principal);
}
catch (RuntimeException e) {
sessionStatus.setComplete();
throw e;
}
}
- 1、 通過 OAuth2RequestFactory 從 參數中擷取資訊建立 AuthorizationRequest 授權請求對象
- 2、 判斷 principal 是否 已授權 : /oauth/authorize 設定為無權限通路 ,是以要判斷,如果 判斷失敗則抛出 InsufficientAuthenticationException (AuthenticationException 子類),其異常會被 ExceptionTranslationFilter 處理 ,最終跳轉到 登入頁面,這也是為什麼我們第一次去請求擷取 授權碼時會跳轉到登陸界面的原因
- 3、 通過 ClientDetailsService.loadClientByClientId() 擷取到 ClientDetails 用戶端資訊
- 4、 擷取參數中的回調位址并且與系統配置的回調位址(步驟3擷取到的client資訊)對比
- 5、 與步驟4一樣 驗證 scope
- 6、 檢測該用戶端是否設定自動 授權(即 我們配置用戶端時配置的 autoApprove(true))
- 7、 由于我們設定 autoApprove(true) 則 調用 getAuthorizationCodeResponse() 方法生成code碼并回調到設定的回調位址
- 8、 真實生成Code 的方法時 generateCode(AuthorizationRequest authorizationRequest, Authentication authentication) 方法: 其内部是authorizationCodeServices.createAuthorizationCode()方法生成code的
TokenEndpoint 生成token
@RequestMapping(value = "/oauth/token", method=RequestMethod.POST)
public ResponseEntity<OAuth2AccessToken> postAccessToken(Principal principal, @RequestParam
Map<String, String> parameters) throws HttpRequestMethodNotSupportedException {
// 1、 驗證 使用者資訊 (正常情況下會經過 ClientCredentialsTokenEndpointFilter 過濾器認證後擷取到使用者資訊 )
if (!(principal instanceof Authentication)) {
throw new InsufficientAuthenticationException(
"There is no client authentication. Try adding an appropriate authentication filter.");
}
// 2、 通過 ClientDetailsService().loadClientByClientId() 擷取系統配置用戶端資訊
String clientId = getClientId(principal);
ClientDetails authenticatedClient = getClientDetailsService().loadClientByClientId(clientId);
// 3、 通過用戶端資訊生成 TokenRequest 對象
TokenRequest tokenRequest = getOAuth2RequestFactory().createTokenRequest(parameters, authenticatedClient);
......
// 4、 調用 TokenGranter.grant()方法生成 OAuth2AccessToken 對象(即token)
OAuth2AccessToken token = getTokenGranter().grant(tokenRequest.getGrantType(), tokenRequest);
if (token == null) {
throw new UnsupportedGrantTypeException("Unsupported grant type: " + tokenRequest.getGrantType());
}
// 5、 傳回token
return getResponse(token);
}
- 1、 驗證 使用者資訊 (正常情況下會經過 ClientCredentialsTokenEndpointFilter 過濾器認證後擷取到使用者資訊 )
- 2、 通過 ClientDetailsService().loadClientByClientId() 擷取系統配置的用戶端資訊
- 3、 通過用戶端資訊生成 TokenRequest 對象
- 4、 将步驟3擷取到的 TokenRequest 作為TokenGranter.grant() 方法參照 生成 OAuth2AccessToken 對象(即token)
- 5、 傳回 token
TokenGranter
[外鍊圖檔轉存失敗,源站可能有防盜鍊機制,建議将圖檔儲存下來直接上傳(img-i0qaFief-1611489790891)(C:\Users\鄭瑤\Desktop\T\imag\鑒權\oauth\1610419689(1)].png)
TokenGranter的設計思路是使用CompositeTokenGranter管理一個List清單,每一種grantType對應一個具體的真正授權者,CompositeTokenGranter 内部就是在循環調用五種TokenGranter實作類的 grant方法,而granter内部則是通過grantType來區分是否是各自的授權類型。
五種類型分别是:
- ResourceOwnerPasswordTokenGranter ==> password密碼模式
- AuthorizationCodeTokenGranter ==> authorization_code授權碼模式
- ClientCredentialsTokenGranter ==> client_credentials用戶端模式
- ImplicitTokenGranter ==> implicit簡化模式
- RefreshTokenGranter ==>refresh_token 重新整理token專用
OAuth2AccessToken
@JsonSerialize(
using = OAuth2AccessTokenJackson1Serializer.class
)
@JsonDeserialize(
using = OAuth2AccessTokenJackson1Deserializer.class
)
@com.fasterxml.jackson.databind.annotation.JsonSerialize(
using = OAuth2AccessTokenJackson2Serializer.class
)
@com.fasterxml.jackson.databind.annotation.JsonDeserialize(
using = OAuth2AccessTokenJackson2Deserializer.class
)
public interface OAuth2AccessToken {
String BEARER_TYPE = "Bearer";
String OAUTH2_TYPE = "OAuth2";
String ACCESS_TOKEN = "access_token";
String TOKEN_TYPE = "token_type";
String EXPIRES_IN = "expires_in";
String REFRESH_TOKEN = "refresh_token";
String SCOPE = "scope";
}
AuthorizationServerTokenServices
public interface AuthorizationServerTokenServices {
//建立
OAuth2AccessToken createAccessToken(OAuth2Authentication var1) throws AuthenticationException;
//重新整理
OAuth2AccessToken refreshAccessToken(String var1, TokenRequest var2) throws AuthenticationException;
//擷取
OAuth2AccessToken getAccessToken(OAuth2Authentication var1);
}
流程
資源伺服器
@EnableResourceServer
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Import({ResourceServerConfiguration.class})
public @interface EnableResourceServer {
}
ResourceServerConfiguration
protected void configure(HttpSecurity http) throws Exception {
ResourceServerSecurityConfigurer resources = new ResourceServerSecurityConfigurer();
ResourceServerTokenServices services = this.resolveTokenServices();
if (services != null) {
resources.tokenServices(services);
} else if (this.tokenStore != null) {
resources.tokenStore(this.tokenStore);
} else if (this.endpoints != null) {
resources.tokenStore(this.endpoints.getEndpointsConfigurer().getTokenStore());
}
if (this.eventPublisher != null) {
resources.eventPublisher(this.eventPublisher);
}
Iterator var4 = this.configurers.iterator();
ResourceServerConfigurer configurer;
while(var4.hasNext()) {
configurer = (ResourceServerConfigurer)var4.next();
configurer.configure(resources);
}
((HttpSecurity)((HttpSecurity)http.authenticationProvider(new AnonymousAuthenticationProvider("default")).exceptionHandling().accessDeniedHandler(resources.getAccessDeniedHandler()).and()).sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS).and()).csrf().disable();
http.apply(resources);
if (this.endpoints != null) {
http.requestMatcher(new ResourceServerConfiguration.NotOAuthRequestMatcher(this.endpoints.oauth2EndpointHandlerMapping()));
}
var4 = this.configurers.iterator();
while(var4.hasNext()) {
configurer = (ResourceServerConfigurer)var4.next();
configurer.configure(http);
}
if (this.configurers.isEmpty()) {
((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated();
}
}
ResourceServerSecurityConfigurer
public void configure(HttpSecurity http) throws Exception {
AuthenticationManager oauthAuthenticationManager = this.oauthAuthenticationManager(http);
//建立OAuth2核心過濾器
this.resourcesServerFilter = new OAuth2AuthenticationProcessingFilter();
this.resourcesServerFilter.setAuthenticationEntryPoint(this.authenticationEntryPoint);
//設定OAuth2的身份認證處理器,沒有交給spring管理(避免影響非普通的認證流程)
this.resourcesServerFilter.setAuthenticationManager(oauthAuthenticationManager);
if (this.eventPublisher != null) {
this.resourcesServerFilter.setAuthenticationEventPublisher(this.eventPublisher);
}
if (this.tokenExtractor != null) {
//設定TokenExtractor預設的實作BearerTokenExtractor
this.resourcesServerFilter.setTokenExtractor(this.tokenExtractor);
}
this.resourcesServerFilter = (OAuth2AuthenticationProcessingFilter)this.postProcess(this.resourcesServerFilter);
this.resourcesServerFilter.setStateless(this.stateless);
// @formatter:off
((HttpSecurity)http.authorizeRequests().expressionHandler(this.expressionHandler).and()).addFilterBefore(this.resourcesServerFilter, AbstractPreAuthenticatedProcessingFilter.class).exceptionHandling().accessDeniedHandler(this.accessDeniedHandler).authenticationEntryPoint(this.authenticationEntryPoint);
}
OAuth2AuthenticationProcessingFilter
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 {
//從請求中取出身份資訊,即access_token,封裝到 PreAuthenticatedAuthenticationToken
Authentication authentication = this.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(this.authenticationDetailsSource.buildDetails(request));
}
//認證身份
Authentication authResult = this.authenticationManager.authenticate(authentication);
if (debug) {
logger.debug("Authentication success: " + authResult);
}
this.eventPublisher.publishAuthenticationSuccess(authResult);
//将身份資訊綁定到SecurityContextHolder中
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);
}
整個filter步驟最核心的是下面2個:
- 1、 調用 tokenExtractor.extract() 方法從請求中解析出token資訊并存放到 authentication 的 principal 字段 中
- 2、 調用 authenticationManager.authenticate() 認證過程: 注意此時的 authenticationManager 是 OAuth2AuthenticationManager
在解析@EnableResourceServer 時我們講過 OAuth2AuthenticationManager 與 OAuth2AuthenticationProcessingFilter 的關系,這裡不再重述,我們直接看下 OAuth2AuthenticationManager 的 authenticate() 方法實作:
public Authentication authenticate(Authentication authentication) throws AuthenticationException {
if (authentication == null) {
throw new InvalidTokenException("Invalid token (token not found)");
}
// 1、 從 authentication 中擷取 token
String token = (String) authentication.getPrincipal();
// 2、 調用 tokenServices.loadAuthentication() 方法 通過 token 參數擷取到 OAuth2Authentication 對象 ,這裡的tokenServices 就是我們資源伺服器配置的。
OAuth2Authentication auth = tokenServices.loadAuthentication(token);
if (auth == null) {
throw new InvalidTokenException("Invalid token: " + token);
}
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、 檢測用戶端資訊,由于我們采用授權伺服器和資源伺服器分離的設計,是以這個檢測方法實際沒有檢測
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());
}
}
// 4、 設定認證成功辨別并傳回
auth.setDetails(authentication.getDetails());
auth.setAuthenticated(true);
return auth;
}
整個 認證邏輯分4步:
- 1、 從 authentication 中擷取 token
- 2、 調用 tokenServices.loadAuthentication() 方法 通過 token 參數擷取到 OAuth2Authentication 對象 ,這裡的tokenServices 就是我們資源伺服器配置的。
- 3、 檢測用戶端資訊,由于我們采用授權伺服器和資源伺服器分離的設計,是以這個檢測方法實際沒有檢測
- 4、 設定認證成功辨別并傳回 ,注意傳回的是 OAuth2Authentication (Authentication 子類)。