🍉Spring Authorization Server (4) 客户端、资源服务、授权服务 源码加流程细讲 再也不绕路

3,130 阅读11分钟

客户端

什么是客户端?

常见的浏览器,App 等这些都是客户端。

在 Spring Authorization Server 中 什么是客户端呢?

demo中的demo-client就是客户端;从代码层面理解引入了 spring-boot-starter-oauth2-client 依赖的就是客户端,demo-authorizationserver 中也引入了 spring-boot-starter-oauth2-client ,说明 demo-authorizationserver 也是客户端。
demo-client中、demo-authorizationserver中都引入 spring-boot-starter-oauth2-client依赖,猜猜看他们的作用应该也是一致的。

授权服务

什么是授权服务?

通俗点讲也是就授权客户端的服务。
例如:掘金使用微信登录微信扫码授权登录,这时微信的服务也就是授权服务 掘金相对时客户端。再例如:Gitee 使用 QQ登录时 ,QQ授权登录 ,此时QQ服务就是授权服务Gitee相对就是客户端;所以客户端和授权服务是相对的,不同场景下所对应的角色就不一样了

在 Spring Authorization Server 中什么是授权服务呢?

demo中的demo-authorizationserver就是授权服务;从代码层面理解引入了 spring-security-oauth2-authorization-server 依赖的就是授权服务。

资源服务

在 Spring Authorization Server 中什么是资源服务?

demo中的messages-resource就是资源服务;从代码层面理解引入了 spring-boot-starter-oauth2-resource-server 依赖的就是资源服务。

我们从 demo-client 入手看

日志很重要,别忽略,仔细看👇

访问 http://127.0.0.1:8080/index,看看demo-client的输出的日志。 image.png demo-client 日志输出

o.s.security.web.FilterChainProxy        : Securing GET /index
o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous >SecurityContext
o.s.s.w.s.HttpSessionRequestCache        : Saved request http://127.0.0.1:8080/index?continue >to session
o.s.s.web.DefaultRedirectStrategy        : Redirecting to >http://127.0.0.1:8080/oauth2/authorization/messaging-client-oidc
o.s.security.web.FilterChainProxy        : Securing GET /oauth2/authorization/messaging->client-oidc
o.s.s.web.DefaultRedirectStrategy        : Redirecting to >http://192.168.56.1:9000/oauth2/authorize?response_type=code&client_id=messaging->client&scope=openid%20profile&state=_c0sC->fsLjR6ZTi37U65_J5Z5JCnpoSyfoFa5A2llo4%3D&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/>messaging-client-oidc&nonce=t02L8qUOiibfrKdZuukArlhoymJBFRFDual0kSTKI7w

Redirecting to http://127.0.0.1:8080/oauth2/authorization/messaging-client-oidc 这个/oauth2/authorization一看应该是 spring security 内置的api,对应的 OAuth2AuthorizationRequestRedirectFilter

public class OAuth2AuthorizationRequestRedirectFilter extends OncePerRequestFilter {
      > /oauth2/authorization 处理的  //The default base {@code URI} used for authorization requests.
       public static final String DEFAULT_AUTHORIZATION_REQUEST_BASE_URI = >"/oauth2/authorization";          
       private OAuth2AuthorizationRequestResolver authorizationRequestResolver;
       @Override
       protected void doFilterInternal(HttpServletRequest request, HttpServletResponse >response, FilterChain filterChain) throws ServletException, IOException {
       try {
              //核心处理就是这了
              OAuth2AuthorizationRequest authorizationRequest = >this.authorizationRequestResolver.resolve(request); //
              if (authorizationRequest != null) {
                  this.sendRedirectForAuthorization(request, response, authorizationRequest);
                  return;
              }
          }
          catch (Exception ex) {
              this.unsuccessfulRedirectForAuthorization(request, response, ex);
              return;
          }
      }
    ...        
}

this.authorizationRequestResolver.resolve(request) 中的 authorizationRequestResolver 对应的默认实现 DefaultOAuth2AuthorizationRequestResolver Class

public final class DefaultOAuth2AuthorizationRequestResolver implements >OAuth2AuthorizationRequestResolver {
    @Override
    public OAuth2AuthorizationRequest resolve(HttpServletRequest request) {
        //这个就是从 /oauth2/authorization/messaging-client-oidc  获取 registrationId 也就是 messaging-client-oidc
        String registrationId = resolveRegistrationId(request);
        if (registrationId == null) {
           return null;
        }
        String redirectUriAction = getAction(request, "login");
        return resolve(request, registrationId, redirectUriAction);
     }
     
    private String resolveRegistrationId(HttpServletRequest request) {
       if (this.authorizationRequestMatcher.matches(request)) {
          return this.authorizationRequestMatcher.matcher(request).getVariables()
                .get(REGISTRATION_ID_URI_VARIABLE_NAME);
       }
       return null;
    }
   
    private OAuth2AuthorizationRequest resolve(HttpServletRequest request, String registrationId,
          String redirectUriAction) {
       if (registrationId == null) {
          return null;
       }
       ClientRegistration clientRegistration = this.clientRegistrationRepository.findByRegistrationId(registrationId);
       if (clientRegistration == null) {
           throw new InvalidClientRegistrationIdException("Invalid Client Registration with Id: " + registrationId);
        }
       OAuth2AuthorizationRequest.Builder builder = getBuilder(clientRegistration);
       String redirectUriStr = expandRedirectUri(request, clientRegistration, redirectUriAction);
       // @formatter:off
        builder.clientId(clientRegistration.getClientId())
            .authorizationUri(clientRegistration.getProviderDetails().getAuthorizationUri())
            .redirectUri(redirectUriStr)
            .scopes(clientRegistration.getScopes())
            .state(DEFAULT_STATE_GENERATOR.generateKey());
        // @formatter:on
        this.authorizationRequestCustomizer.accept(builder);
        return builder.build();
    }
}

String registrationId = resolveRegistrationId(request); 这个是从 URL 上获取到 registrationId ,然后通过 registrationId 找到 yml中 registrationId 对应的所有配置,
然后build重定向URL 👉
Redirecting to http://192.168.56.1:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid%20profile&state=dFUSOiG3BXavKSPFCHfK-ER7LbcAyOnHkzWcsNG9w_4%3D&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc&nonce=XMptCr0NC1yRO1yNJ6v6VdtuNTC09Di_wPm_LWb7Ph0
这个重定向URL 看到有点眼熟 是吧,就是之前演示授权码模式 获取授权码的请求URL
以上日志最后一行就是这样得来的。

重定向到了 demo-authorizationserver(授权服务器)接着看

客户端的重定向到授权服务的URL http://192.168.56.1:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid%20profile&state=dFUSOiG3BXavKSPFCHfK-ER7LbcAyOnHkzWcsNG9w_4%3D&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc&nonce=XMptCr0NC1yRO1yNJ6v6VdtuNTC09Di_wPm_LWb7Ph0
看看demo-authorizationserver的日志输出。

o.s.security.web.FilterChainProxy        : Securing GET /oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid%20profile&state=u2Gws4H0Sk34mm8gQGRvx3Ulx6ukazugneZEbaCeBj8%3D&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc&nonce=XG92BigeimIMXkDZnQwKmJY1E2_pGGzeAeeqwwq87mU
o.s.s.w.a.AnonymousAuthenticationFilter  : Set SecurityContextHolder to anonymous SecurityContext
o.s.s.w.s.HttpSessionEventPublisher      : Publishing event: org.springframework.security.web.session.HttpSessionCreatedEvent[source=io.undertow.servlet.spec.HttpSessionImpl@6670b8f6]
o.s.s.w.s.HttpSessionRequestCache        : Saved request http://192.168.56.1:9000/oauth2/authorize?response_type=code&client_id=messaging-client&scope=openid%20profile&state=u2Gws4H0Sk34mm8gQGRvx3Ulx6ukazugneZEbaCeBj8%3D&redirect_uri=http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc&nonce=XG92BigeimIMXkDZnQwKmJY1E2_pGGzeAeeqwwq87mU&continue to session
s.w.a.DelegatingAuthenticationEntryPoint : Trying to match using MediaTypeRequestMatcher [contentNegotiationStrategy=org.springframework.web.accept.HeaderContentNegotiationStrategy@2c929396, matchingMediaTypes=[text/html], useEquals=false, ignoredMediaTypes=[]]
s.w.a.DelegatingAuthenticationEntryPoint : Match found! Executing org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint@1441274e
o.s.s.web.DefaultRedirectStrategy        : Redirecting to http://192.168.56.1:9000/login

Securing GET /oauth2/authorize? 对应是 OAuth2AuthorizationEndpointFilter Filter

public final class OAuth2AuthorizationEndpointFilter extends OncePerRequestFilter {
        //The default endpoint {@code URI} for authorization requests.
        private static final String DEFAULT_AUTHORIZATION_ENDPOINT_URI = "/oauth2/authorize";
        ...省略
        @Override
        protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, >FilterChain filterChain)
                throws ServletException, IOException {
            if (!this.authorizationEndpointMatcher.matches(request)) {
                filterChain.doFilter(request, response);
                return;
            }
            try {
                Authentication authentication = this.authenticationConverter.convert(request);
                if (authentication instanceof AbstractAuthenticationToken) {
                    ((AbstractAuthenticationToken) authentication)
                            .setDetails(this.authenticationDetailsSource.buildDetails(request));
                }
                Authentication authenticationResult = >this.authenticationManager.authenticate(authentication);
                if (!authenticationResult.isAuthenticated()) {
                    // If the Principal (Resource Owner) is not authenticated then
                    // pass through the chain with the expectation that the authentication process
                    // will commence via AuthenticationEntryPoint
                    filterChain.doFilter(request, response);
                    return;
                }
                if (authenticationResult instanceof OAuth2AuthorizationConsentAuthenticationToken) {
                    if (this.logger.isTraceEnabled()) {
                        this.logger.trace("Authorization consent is required");
                    }
                    sendAuthorizationConsent(request, response,
                            (OAuth2AuthorizationCodeRequestAuthenticationToken) authentication,
                            (OAuth2AuthorizationConsentAuthenticationToken) authenticationResult);
                    return;
                }
       
                this.sessionAuthenticationStrategy.onAuthentication(
                        authenticationResult, request, response);
       
                this.authenticationSuccessHandler.onAuthenticationSuccess(
                        request, response, authenticationResult);
       
            } catch (OAuth2AuthenticationException ex) {
                if (this.logger.isTraceEnabled()) {
                    this.logger.trace(LogMessage.format("Authorization request failed: %s", >ex.getError()), ex);
                }
                this.authenticationFailureHandler.onAuthenticationFailure(request, response, ex);
            }
        }
        ...省略
}

OAuth2AuthorizationEndpointFilter 中关键的2行代码
1.Authentication authentication = this.authenticationConverter.convert(request)
authenticationConverter 此时对应的 OAuth2AuthorizationCodeRequestAuthenticationConverter

public final class OAuth2AuthorizationCodeRequestAuthenticationConverter implements >AuthenticationConverter {
  private static final Authentication ANONYMOUS_AUTHENTICATION = new >AnonymousAuthenticationToken(
        "anonymous", "anonymousUser", AuthorityUtils.createAuthorityList("ROLE_ANONYMOUS"));
  private static final RequestMatcher OIDC_REQUEST_MATCHER = createOidcRequestMatcher();

  @Override
  public Authentication convert(HttpServletRequest request) {
     ...
 
   return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationUri, clientId, principal,
     redirectUri, state, scopes, additionalParameters);
     
  }

authentication 返回的 OAuth2AuthorizationCodeRequestAuthenticationToken 对应是 OAuth2AuthorizationCodeRequestAuthenticationProvider 处理。
2.Authentication authenticationResult = this.authenticationManager.authenticate(authentication)

public final class OAuth2AuthorizationCodeRequestAuthenticationProvider implements AuthenticationProvider {

  @Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  	OAuth2AuthorizationCodeRequestAuthenticationToken authorizationCodeRequestAuthentication =
  			(OAuth2AuthorizationCodeRequestAuthenticationToken) 
  			authentication;
           //...省略了部分代码			
  	 //生成授权码		
  	 OAuth2AuthorizationCode authorizationCode = this.authorizationCodeGenerator.generate(tokenContext);			
    
  	 OAuth2Authorization authorization = authorizationBuilder(registeredClient, principal, authorizationRequest)
  			.authorizedScopes(authorizationRequest.getScopes())
  			.token(authorizationCode)
  			.build();
  	 //保存了授权请求		
  	 this.authorizationService.save(authorization);
  	 //返回新的 OAuth2AuthorizationCodeRequestAuthenticationToken
  	 return new OAuth2AuthorizationCodeRequestAuthenticationToken(authorizationRequest.getAuthorizationUri(),
  			registeredClient.getClientId(), principal, authorizationCode, redirectUri,
  			authorizationRequest.getState(), authorizationRequest.getScopes());
  	}		
}

demo-authorizationserver 授权服务器登录、授权成功后重定向 客户端的日志输出

o.s.s.web.DefaultRedirectStrategy  : Redirecting to http://127.0.0.1:8080/login/oauth2/code/messaging-client-oidc?code=sV9eoLu4NYu7vjSzcstwC8lSrZXts_1LFUPeBVtfbg6KuIUlBBah6MOU5Z9z8DAopR10p2ocJ1eVf1za3F0-DqJLfz0OZ3jhRqgWbp0q_lyDX16LGv8wL23pNGRc-XBP&state=cfWVM_UkJ8209MOuS5tiGaPlCucbOjP8XaAiDiAZ338%3D

重定向到了 demo-client(客户端)接着看

回调到客户端的URL/login/oauth2/code/messaging-client-oidc对应的是OAuth2LoginAuthenticationFilter Filter 继承了AbstractAuthenticationProcessingFilter,这个URL 也是一个模板 对应/login/oauth2/code/*

AbstractAuthenticationProcessingFilter

public abstract class AbstractAuthenticationProcessingFilter extends GenericFilterBean implements ApplicationEventPublisherAware, MessageSourceAware {

private void doFilter(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
  		throws IOException, ServletException {
  	if (!requiresAuthentication(request, response)) {
  		chain.doFilter(request, response);
  		return;
  	}
  	try {
  		Authentication authenticationResult = attemptAuthentication(request, response);
  		if (authenticationResult == null) {
  			// return immediately as subclass has indicated that it hasn't completed
  			return;
  		}
  		this.sessionStrategy.onAuthentication(authenticationResult, request, response);
  		// Authentication success
  		if (this.continueChainBeforeSuccessfulAuthentication) {
  			chain.doFilter(request, response);
  		}
  		successfulAuthentication(request, response, chain, authenticationResult);
  	}
  	catch (InternalAuthenticationServiceException failed) {
  		this.logger.error("An internal error occurred while trying to authenticate the user.", failed);
  		unsuccessfulAuthentication(request, response, failed);
  	}
  	catch (AuthenticationException ex) {
  		// Authentication failed
  		unsuccessfulAuthentication(request, response, ex);
  	}
  }
}

OAuth2LoginAuthenticationFilter

public class OAuth2LoginAuthenticationFilter extends AbstractAuthenticationProcessingFilter {
   public static final String DEFAULT_FILTER_PROCESSES_URI = "/login/oauth2/code/*";	
    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response)
  			throws AuthenticationException {
          //...省略
          OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest);
          
          OAuth2AuthenticationToken oauth2Authentication = this.authenticationResultConverter.convert(authenticationResult);	
          this.authorizedClientRepository.saveAuthorizedClient(authorizedClient, oauth2Authentication, request, response);
          return oauth2Authentication;
   }
}
OAuth2LoginAuthenticationToken authenticationResult = (OAuth2LoginAuthenticationToken) this.getAuthenticationManager().authenticate(authenticationRequest); 对应的就是 `OidcAuthorizationCodeAuthenticationProvider`

```java
public class OidcAuthorizationCodeAuthenticationProvider implements AuthenticationProvider {
      @Override
	     public Authentication authenticate(Authentication authentication) throws AuthenticationException {
		OAuth2LoginAuthenticationToken authorizationCodeAuthentication = (OAuth2LoginAuthenticationToken) authentication;
           //...省略
		OAuth2AccessTokenResponse accessTokenResponse = getResponse(authorizationCodeAuthentication);
          //...省略
   
         OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest(clientRegistration,
     accessTokenResponse.getAccessToken(), idToken, additionalParameters));
         //...省略
	}
	
  //继续跟 OAuth2AccessTokenResponse accessTokenResponse = getResponse(authorizationCodeAuthentication);	
  private OAuth2AccessTokenResponse getResponse(OAuth2LoginAuthenticationToken authorizationCodeAuthentication) {
  	try {
  	return this.accessTokenResponseClient.getTokenResponse(new OAuth2AuthorizationCodeGrantRequest(authorizationCodeAuthentication.getClientRegistration(),
  						authorizationCodeAuthentication.getAuthorizationExchange()));
  	}
  	catch (OAuth2AuthorizationException ex) {
  		OAuth2Error oauth2Error = ex.getError();
  		throw new OAuth2AuthenticationException(oauth2Error, oauth2Error.toString(), ex);
  	}
  }
}

this.accessTokenResponseClient.getTokenResponse() 对应是 DefaultAuthorizationCodeTokenResponseClient,这个里面封装了request请求去请求授权服务器

public final class DefaultAuthorizationCodeTokenResponseClient implements OAuth2AccessTokenResponseClient<OAuth2AuthorizationCodeGrantRequest> {	
  @Override
  public OAuth2AccessTokenResponse getTokenResponse(
  		OAuth2AuthorizationCodeGrantRequest authorizationCodeGrantRequest) {
  	Assert.notNull(authorizationCodeGrantRequest, "authorizationCodeGrantRequest cannot be null");
  	RequestEntity<?> request = this.requestEntityConverter.convert(authorizationCodeGrantRequest);
  	ResponseEntity<OAuth2AccessTokenResponse> response = getResponse(request);
  
  	return response.getBody();
  }
  
  private ResponseEntity<OAuth2AccessTokenResponse> getResponse(RequestEntity<?> request) {
  	try {
  		return this.restOperations.exchange(request, OAuth2AccessTokenResponse.class);
  	}
  	catch (RestClientException ex) {
  		OAuth2Error oauth2Error = new OAuth2Error(INVALID_TOKEN_RESPONSE_ERROR_CODE,
  				"An error occurred while attempting to retrieve the OAuth 2.0 Access Token Response: "
  						+ ex.getMessage(),
  				null);
  		throw new OAuth2AuthorizationException(oauth2Error, ex);
  	}
  }	
}		

/oauth2/token 就是获取token请求,看以上的请求参数 就是授权码模式的请求参数 👍
image.png

又回到 demo-authorizationserver(授权服务)

接着看 /oauth2/token

/oauth2/token 对应的 OAuth2TokenEndpointFilter

public final class OAuth2TokenEndpointFilter extends OncePerRequestFilter {
       	//The default endpoint {@code URI} for access token requests
       	private static final String DEFAULT_TOKEN_ENDPOINT_URI = "/oauth2/token";
       		@Override
       	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
       			throws ServletException, IOException {
       
       		       //核心代码差不多就这2行
       			Authentication authorizationGrantAuthentication = this.authenticationConverter.convert(request);
   
       			OAuth2AccessTokenAuthenticationToken accessTokenAuthentication =
       					(OAuth2AccessTokenAuthenticationToken) this.authenticationManager.authenticate(authorizationGrantAuthentication);      		
       	}
}

Authentication authentication = this.authenticationConverter.convert(request);
授权码获取token时 authenticationConverter 对应OAuth2AuthorizationCodeAuthenticationConverter验证参数 将对应参数封装再 OAuth2AuthorizationCodeAuthenticationToken返回

Authentication authenticationResult = this.authenticationManager.authenticate(authentication); 对应的 OAuth2AuthorizationCodeAuthenticationProvider 里面对参数认证通过后 生成 access_token、refresh_token 等信息返回 。

又回到 demo-client(客户端)

授权服务器响应回来的数据,也就是/oauth2/token 接口响应数据,客户端有了 授权服务的token 携带这个token去访问资源服务就ok了 image.png 访问
image.png 这个请求实际上会去访问 http://127.0.0.1:8090/messages 也就是资源服务的 messages 接口

messages-resource(资源服务器)

messages-resource的日志输出

o.s.security.web.FilterChainProxy        : Securing GET /messages
o.s.web.client.RestTemplate              : HTTP GET http://192.168.56.1:9000/.well-known/openid-configuration
o.s.web.client.RestTemplate              : Accept=[application/json, application/*+json]
o.s.web.client.RestTemplate              : Response 200 OK
o.s.web.client.RestTemplate              : Reading to [java.util.Map<java.lang.String, java.lang.Object>]
o.s.web.client.RestTemplate              : HTTP GET http://192.168.56.1:9000/oauth2/jwks
o.s.web.client.RestTemplate              : Accept=[text/plain, application/json, application/*+json, */*]
o.s.web.client.RestTemplate              : Response 200 OK
o.s.web.client.RestTemplate              : Reading to [java.lang.String] as "application/json;charset=ISO-8859-1"
o.s.s.o.s.r.a.JwtAuthenticationProvider  : Authenticated token
.s.r.w.a.BearerTokenAuthenticationFilter : Set SecurityContextHolder to JwtAuthenticationToken [Principal=org.springframework.security.oauth2.jwt.Jwt@ecb83e6d, Credentials=[PROTECTED], Authenticated=true, Details=WebAuthenticationDetails [RemoteIpAddress=127.0.0.1, SessionId=null], Granted Authorities=[SCOPE_message.read, SCOPE_message.write]]
o.s.security.web.FilterChainProxy        : Secured GET /messages
o.s.web.servlet.DispatcherServlet        : GET "/messages", parameters={}	    

HTTP GET http://192.168.56.1:9000/.well-known/openid-configuration 到授权服务器 获取对应的配置 返回数据的如下

 {
    "issuer": "http://192.168.56.1:9000",
    "authorization_endpoint": "http://192.168.56.1:9000/oauth2/authorize",
    "device_authorization_endpoint": "http://192.168.56.1:9000/oauth2/device_authorization",
    "token_endpoint": "http://192.168.56.1:9000/oauth2/token",
    "token_endpoint_auth_methods_supported": [
        "client_secret_basic",
        "client_secret_post",
        "client_secret_jwt",
        "private_key_jwt"
    ],
    "jwks_uri": "http://192.168.56.1:9000/oauth2/jwks",
    "userinfo_endpoint": "http://192.168.56.1:9000/userinfo",
    "end_session_endpoint": "http://192.168.56.1:9000/connect/logout",
    "response_types_supported": [
        "code"
    ],
    "grant_types_supported": [
        "authorization_code",
        "client_credentials",
        "refresh_token",
        "urn:ietf:params:oauth:grant-type:device_code"
    ],
    "revocation_endpoint": "http://192.168.56.1:9000/oauth2/revoke",
    "revocation_endpoint_auth_methods_supported": [
        "client_secret_basic",
        "client_secret_post",
        "client_secret_jwt",
        "private_key_jwt"
    ],
    "introspection_endpoint": "http://192.168.56.1:9000/oauth2/introspect",
    "introspection_endpoint_auth_methods_supported": [
        "client_secret_basic",
        "client_secret_post",
        "client_secret_jwt",
        "private_key_jwt"
    ],
    "subject_types_supported": [
        "public"
    ],
    "id_token_signing_alg_values_supported": [
        "RS256"
    ],
    "scopes_supported": [
        "openid"
    ]
}   

HTTP GET http://192.168.56.1:9000/oauth2/jwks 对应是 NimbusJwkSetEndpointFilter

public final class NimbusJwkSetEndpointFilter extends OncePerRequestFilter {
  /**
   * The default endpoint {@code URI} for JWK Set requests.
   */
  private static final String DEFAULT_JWK_SET_ENDPOINT_URI = "/oauth2/jwks";

  @Override
  	protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
  			throws ServletException, IOException {
  		if (!this.requestMatcher.matches(request)) {
  			filterChain.doFilter(request, response);
  			return;
  		}
  		JWKSet jwkSet;
  		try {
  			jwkSet = new JWKSet(this.jwkSource.get(this.jwkSelector, null));
  		}
  		catch (Exception ex) {
  			throw new IllegalStateException("Failed to select the JWK(s) -> " + ex.getMessage(), ex);
  		}
  		response.setContentType(MediaType.APPLICATION_JSON_VALUE);
  		try (Writer writer = response.getWriter()) {
                              // toString() excludes private keys
  			writer.write(jwkSet.toString());	
  		}
  	}
  }
}    

/oauth2/jwks 返回的数据 也就是加密算法和私钥

{
   "keys": [
       {
           "kty": "RSA",
           "e": "AQAB",
           "kid": "24e8e61e-0318-45cb-bf1b-74fc67fee331",
           "n": "lJ8_64e1A_XSasigEH8k2JksMG5B1xR3DjbznmPguIVDnTY1V5dTiPPED6EuKcJjpnt88SJETcno-vq7X_WUtntVJ187TyLz1zaA2yHykFK5JFKzPMwURRzdFddZFujyQNPSD_bFGtSo326ksqXAdy4I5DhNmgvU1pKYUTk6IK2zsFC8FtPDNHRNSKe-rA2pek4SIDMhSQf07cDo26N8fbJWFvrlh6httIpGnyYpIYs1Yb7YJIf8Mr_y-XmCCvDQQJc-h0C4svu3YbGsoFn_QTsu-5ayErRvZarrQdR2RNowLzXLeje5p4l4yPG4qs56jR0mMLSkdXfVkJuvGYSwZw"
       }
   ]
}

作用就是来解密 demo-client 携带到 messages-resource 的token。

o.s.s.o.s.r.a.JwtAuthenticationProvider : Authenticated token 从日志就能看出 JwtAuthenticationProvider 这里进行的 token的验证

public final class JwtAuthenticationProvider implements AuthenticationProvider {
@Override
  public Authentication authenticate(Authentication authentication) throws AuthenticationException {
  	BearerTokenAuthenticationToken bearer = (BearerTokenAuthenticationToken) authentication;
  	Jwt jwt = getJwt(bearer);
  	AbstractAuthenticationToken token = this.jwtAuthenticationConverter.convert(jwt);
  	if (token.getDetails() == null) {
  		token.setDetails(bearer.getDetails());
  	}
  	this.logger.debug("Authenticated token");
  	return token;
  }

  private Jwt getJwt(BearerTokenAuthenticationToken bearer) {
  	try {
  		return this.jwtDecoder.decode(bearer.getToken());
  	}
  	catch (BadJwtException failed) {
  		this.logger.debug("Failed to authenticate since the JWT was invalid");
  		throw new InvalidBearerTokenException(failed.getMessage(), failed);
  	}
  	catch (JwtException failed) {
  		throw new AuthenticationServiceException(failed.getMessage(), failed);
  	}
  }
}

token 验证成功后 页面就成功访问到资源服务的 message 接口了 image.png 我们前面讲到的demo中 用postman 携带token 访问资源服务 操作过,你看 demo-clien 内部自己就携带token去了 是不是很方便。

demo-authorizationserver【授权服务】 选择gitee登录 时

选择gitee登录 时,demo-authorizationserver 输出日志 image.png

o.s.security.web.FilterChainProxy        : Securing GET /oauth2/authorization/gitee
o.s.s.web.DefaultRedirectStrategy        : Redirecting to https://gitee.com/oauth/authorize?response_type=code&client_id=29b85c97ed682910eaa4276d84a0c4532f00b962e1b9fe8552520129e65ae432&scope=emails%20user_info&state=wEC1KPetaUh7yDnCYVKvqixMld-7C2AOrSTq1sAOEac%3D&redirect_uri=http://192.168.56.1:9000/login/oauth2/code/gitee

/oauth2/authorization/gitee 也是 OAuth2AuthorizationRequestRedirectFilter 处理,这个和先前的 /oauth2/authorization/messaging-client-oidc一样,那么也会走 DefaultOAuth2AuthorizationRequestResolver 最后构建 gitee 出授权请求 进而 重定向。

因为 demo-authorizationserver 引入 spring-boot-starter-oauth2-client依赖, 所以它即是 客户端 也是授权服务。 giee登录认证、授权成功后,会回调demo-authorizationserver的 http://192.168.56.1:9000/login/oauth2/code/gitee

o.s.security.web.FilterChainProxy        : Securing GET /login/oauth2/code/gitee?code=878b7b045bacc8ce85f3c5437dfb9669d5a4584a53237ba9db79b33439a10a01&state=jUPPNYGLGSX4wdUCUIpAxCo22xBnTUDoVL_5eyyhuGI%3D

/login/oauth2/code/gitee与 之前的/login/oauth2/code/messaging-client-oidc 处理都是一样的 ,对应都是 OAuth2LoginAuthenticationFilter Filter 继承了AbstractAuthenticationProcessingFilter,这2个 filter去处理。 img_4day_3.png
OAuth2LoginAuthenticationFilter 从 回调的 URL 同样的获取到授权码 然后内部再去请求 gitee 获取到 token 然后存储到 自身 demo-authorizationserver 中,那此时 demo-authorizationserver 相对于 gitee 是客户端,并且已经获取授权,说明demo-authorizationserver 客户端也就不用登录了。

总结

spring-boot-starter-oauth2-client👇

OAuth2AuthorizationRequestRedirectFilter

  • /oauth2/authorization 处理的 Filter

DefaultOAuth2AuthorizationRequestResolver

  • 从 /oauth2/authorization/{registrationId} URL 上获取到 registrationId
  • 通过 registrationId 在yml配置中找到 对应的 client—id 的所有配置
  • 通过 client—id 构建对应 clien-id 配置的重定向URL,然后进行重定向

OAuth2LoginAuthenticationFilter

  • 处理 oauth2 授权服务回调 模板URL /login/oauth2/code/* 的Filter

OidcAuthorizationCodeAuthenticationProvider

  • 处理 oauth2 授权服务回调 的请求数据

DefaultAuthorizationCodeTokenResponseClient

  • 构建 oauth2 授权授权码模式的认证请求(获取token请求)

spring-security-oauth2-authorization-server👇

OAuth2AuthorizationEndpointFilter

  • /oauth2/authorize 处理的 Filter

OAuth2AuthorizationCodeRequestAuthenticationConverter

  • 从request 获取 验证OAuth2 授权码模式的授权请求参数,然后封装到 OAuth2AuthorizationCodeRequestAuthenticationToken 对象中返回

OAuth2AuthorizationCodeRequestAuthenticationProvider

  • 处理 OAuth2AuthorizationCodeRequestAuthenticationToken 类型的认证逻辑

OAuth2TokenEndpointFilter

  • /oauth2/token 处理的 Filter

OAuth2AuthorizationCodeAuthenticationConverter

  • 从request 获取 验证OAuth2 授权码模式的认证请求参数,然后封装到OAuth2AuthorizationCodeAuthenticationToken 对象中返回

OAuth2AuthorizationCodeAuthenticationProvider

  • 验证授权码是否有效、验证client-id 有效性、对应的scope
  • 生成 Access token 、Refresh token、ID token
  • 保存授权请求和相关 token

NimbusJwkSetEndpointFilter

  • 处理 /oauth2/jwks 的Filter 返回JWT加密算法和私钥

spring-boot-starter-oauth2-resource-server👇

JwtAuthenticationProvider 

  • 对请求中 token 进行验证

oauth2 客户端认证、授权流程图(以gitee示例)

img_4day_4.png 文章中看这个图不太清晰,到github上下载 github.com/WatermelonP… 看!!!,这个图很重要,包含了整个oauth2的整个流程(详细程度80%)