31、关于oauth2.0 认证服务器同时也是资源服务器(同一进程)不能使用access_token原因解析

获的了access_token怎么使用?我们历经千辛万苦,才获取到access_token,那么获取到了access_token如何使用access_token?

 

1、单独的资源服务器如何使用access_token

 

    在《30、oauth2.0认证服务器与资源服务器分离成不同服务(进程)或既是认证服务又是资源服务器》一文中,我们后面一段代码解释了核心过滤器 OAuth2AuthenticationProcessingFilter处理流程,BearerTokenExtractor获取token的解析流程:

(1)从Http的头Header中查找Authentication头且为Bearer验证

(2)如果(1)中找不到,则从RequestParam中查找,也就是url请求参数中查找

(3)如果(2)中找找不到,则从form表单中查找;

所以使用access_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

 

2、认证服务器同时也是资源服务器情况如何使用access_token

 

(1)如果客户端访问资源服务器(间接访问授权服务器,资源服务器与授权服务器分离《同时授权服务也是资源服务-提供用户权限等详细信息查询》)

    经过确认,如果是资源服务器访问异地的认证服务器,使用的方式为(资源服务器配置方式为使用token)check_token,具体示例如下:

security:  

  oauth2:

    resource:

      filter-order: 3

      id: test_resource_id

      tokenInfoUri:http://localhost:7006/oauth/check_token

      preferTokenInfo: true

    client:

      accessTokenUri:http://localhost:7006/oauth/token

      userAuthorizationUri:http://localhost:7006/oauth/authorize

      clientId: wx_takeout_client_id

      clientSecret: wx_takeout_client_secret

 

也就是说,如果访问资源服务器的资源并携带上access_token(用以上3中方式) 那么资源服务器则会将传递给资源服务器的access_token到认证服务器去验证,通过则可以访问,否则无权访问

具体流程是资源服务器带上access_token后访问check_token地址,将token转为http的头(至于如何转换我没有具体跟踪,感兴趣的自己可以跟踪一下),头中包含了Authentication授权字段。该字段值为client_id和client_secret的Basic值(与直接访问授权服务器的字段值不一样)进行校验最终使用ClientCredentialsTokenEndpointFilter认证。该过滤器会使用提取client_id然后从数据库中查找client_id的记录权限信息。验证无效则无法访问。

 

(2)如果客户端访问授权服务器(也是资源服务器)

    经过确认,授权服务器同时也是资源服务器的时候,是不需要像资源服务器那样在配置文件中配置,这样的话客户端访问的URL是不会通过check_token进行校验的,会通过BasicAuthenticationFilter进行校验,也就是必须是Http的Header中包含Authentication且对应值为Basic的用户名和密码(此时不是client_id和client_secret)加密,才能通过。否则会报401错误。

 

原因:

(1)Spring初始化时,会初始化10个过滤器链,对应过滤器链如下

 

ApplicationFilterConfig[name=characterEncodingFilter,  filterClass=org.springframework.boot.web.servlet.filter.OrderedCharacterEncodingFilter]

ApplicationFilterConfig[name=webMvcMetricsFilter,  filterClass=org.springframework.boot.actuate.metrics.web.servlet.WebMvcMetricsFilter]

ApplicationFilterConfig[name=hiddenHttpMethodFilter,  filterClass=org.springframework.boot.web.servlet.filter.OrderedHiddenHttpMethodFilter]

ApplicationFilterConfig[name=httpPutFormContentFilter,  filterClass=org.springframework.boot.web.servlet.filter.OrderedHttpPutFormContentFilter]

ApplicationFilterConfig[name=requestContextFilter,  filterClass=org.springframework.boot.web.servlet.filter.OrderedRequestContextFilter]

ApplicationFilterConfig[name=springSecurityFilterChain,  filterClass=org.springframework.boot.web.servlet.DelegatingFilterProxyRegistrationBean$1]

ApplicationFilterConfig[name=httpTraceFilter,  filterClass=org.springframework.boot.actuate.web.trace.servlet.HttpTraceFilter]

ApplicationFilterConfig[name=webStatFilter,  filterClass=com.alibaba.druid.support.http.WebStatFilter]

ApplicationFilterConfig[name=resourceUrlEncodingFilter,  filterClass=org.springframework.web.servlet.resource.ResourceUrlEncodingFilter]

ApplicationFilterConfig[name=Tomcat WebSocket (JSR356)  Filter,  filterClass=org.apache.tomcat.websocket.server.WsFilter]

 

其中第6个过滤器链就是oauth2.0的权限过滤过滤器链。我们只要关注它即可。所有过滤器的处理流程ApplicationFilterChain中进行循环调用所有处理器链:

循环跳过前5个过滤器链,直接跳转到第6个过滤器链

接下来就是调用改过滤器链的过程:

    @Override

     public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)

              throws ServletException, IOException {

          // Lazily initialize the delegate if necessary.

          Filter delegateToUse = this.delegate;

          if (delegateToUse == null) {

              synchronized (this.delegateMonitor) {

                   delegateToUse = this.delegate;

                   if (delegateToUse == null) {

                        WebApplicationContext wac = findWebApplicationContext();

                        if (wac == null) {

                             throw new IllegalStateException("No WebApplicationContext found: " +

                                      "no ContextLoaderListener or DispatcherServlet registered?");

                        }

                        delegateToUse = initDelegate(wac);

                   }

                   this.delegate = delegateToUse;

              }

          }

          // Let the delegate perform the actual doFilter operation.

          invokeDelegate(delegateToUse, request, response, filterChain);

     }

这里又使用了过滤器链代理,使用代理获取所有过滤器链的所有过滤器,然后逐个执行所有的过滤器。

    private void doFilterInternal(ServletRequest request, ServletResponse response,

              FilterChain chainthrows IOException, ServletException {

          FirewalledRequest fwRequest = firewall

                   .getFirewalledRequest((HttpServletRequest) request);

          HttpServletResponse fwResponse = firewall

                   .getFirewalledResponse((HttpServletResponse) response);

          List<Filter> filters = getFilters(fwRequest);

          if (filters == null || filters.size() == 0) {

              if (logger.isDebugEnabled()) {

                   logger.debug(UrlUtils.buildRequestUrl(fwRequest)

                             + (filters == null ? " has no matching filters"

                                      : " has an empty filter list"));

              }

              fwRequest.reset();

              chain.doFilter(fwRequestfwResponse);

              return;

          }

          VirtualFilterChain vfc = new VirtualFilterChain(fwRequestchainfilters);

          vfc.doFilter(fwRequestfwResponse);

     }

重点就是过滤器链中的代理获取过滤器列表的过程(可以理解为过滤器链中就包含了多个过滤器--就是列表):

    private List<Filter> getFilters(HttpServletRequest request) {

          for (SecurityFilterChain chain : filterChains) {

              if (chain.matches(request)) {

                   return chain.getFilters();

              }

          }

          return null;

     }

 

调试到这里发现,过滤器是根据请求参数(这里是请求路径url)动态添加到列表中的,不同的url处理的过滤器连表都不一样.

 

a. 如果是与终端请求相关(如/oauth/authorize或check_token等)则会使用过滤器列表:

    OrRequestMatcher 

    [

       requestMatchers=

        [

            Ant  [pattern='/oauth/token'], 

            Ant  [pattern='/oauth/token_key'],

            Ant  [pattern='/oauth/check_token'

        ]

    ],  

[

  org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@51131f0,  

org.springframework.security.web.context.SecurityContextPersistenceFilter@7c93a0b4,  

org.springframework.security.web.header.HeaderWriterFilter@4a2c416a,  

org.springframework.security.web.authentication.logout.LogoutFilter@cdea588,  

org.springframework.security.oauth2.provider.client.ClientCredentialsTokenEndpointFilter@5219e424,  

org.springframework.security.web.authentication.www.BasicAuthenticationFilter@2ebf71de,  

org.springframework.security.web.savedrequest.RequestCacheAwareFilter@1a0a059c,  

org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@e944bb2,  

org.springframework.security.web.authentication.AnonymousAuthenticationFilter@1756f30,  

org.springframework.security.web.session.SessionManagementFilter@fcbdef7,  

org.springframework.security.web.access.ExceptionTranslationFilter@6310bdff,  

org.springframework.security.web.access.intercept.FilterSecurityInterceptor@40cbb015

]

]

 

也即是资源服务器使用check_token访问授权服务器的时候,会使用上面的过滤器列表,其中包含了一个ClientCredentialsTokenEndpointFilter,也就是关键,这个filter会根据http头部的client_id和client_secret(具体是client_id)从数据库中查询出对应的记录。验证是否有对应权限

 

b. 如果是不与终端相关的(也即是客户端直接访问对应资源的url,不经过单独的资源服务器中转验证)

这时候的访问的url不是check_token,而直接就是客户端访问的资源的url,如(/user/principal).如果是这样:

    private List<Filter> getFilters(HttpServletRequest request) {

          for (SecurityFilterChain chain : filterChains) {

              if (chain.matches(request)) {

                   return chain.getFilters();

              }

          }

          return null;

     }

 

匹配到的过滤器列表对应值就是:

 

[  

    org.springframework.security.web.util.matcher.AnyRequestMatcher@1,         

[

org.springframework.security.web.context.request.async.WebAsyncManagerIntegrationFilter@200a66e2, org.springframework.security.web.context.SecurityContextPersistenceFilter@39ab3107, org.springframework.security.web.header.HeaderWriterFilter@95f952, org.springframework.security.web.authentication.logout.LogoutFilter@7aa398ea, org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter@2f45d0a7, org.springframework.security.web.authentication.ui.DefaultLoginPageGeneratingFilter@1ae0b1b8, org.springframework.security.web.authentication.www.BasicAuthenticationFilter@6cf0996f, org.springframework.security.web.savedrequest.RequestCacheAwareFilter@6e3b967b, org.springframework.security.web.servletapi.SecurityContextHolderAwareRequestFilter@41cbbd87, org.springframework.security.web.authentication.AnonymousAuthenticationFilter@1fab4d5d, org.springframework.security.web.session.SessionManagementFilter@4453e472, org.springframework.security.web.access.ExceptionTranslationFilter@3fa97ae, org.springframework.security.web.access.intercept.FilterSecurityInterceptor@2d52498f

]

]

 

这个时候就不会有ClientCredentialsTokenEndpointFilter而是多了一个BasicAuthenticationFilter,这个过滤器则是使用自定义的UserDetailImpl接口实现通过用户名获取用户信息。不论是资源服务器访问还是客户端直接访问授权服务器,http的header中必须包含Authentication字段,对应值都为basic加密后的值,如果是资源服务器访问则为client_id和client_secret的加密值,如果是客户端直接访问授权服务,则是用户名和密码的加密值(如admin,123456)。具体验证头过程在BasicAuthenticationFilter中:

 

    @Override

     protected void doFilterInternal(HttpServletRequest request,

              HttpServletResponse response, FilterChain chain)

                        throws IOException, ServletException {

          final boolean debug = this.logger.isDebugEnabled();

          String header = request.getHeader("Authorization");

          if (header == null || !header.startsWith("Basic ")) {

              chain.doFilter(request, response);

              return;

          }

          try {

              String[] tokens = extractAndDecodeHeader(header, request);

              assert tokens.length == 2;

              String username = tokens[0];

              if (debug) {

                   this.logger

                             .debug("Basic Authentication Authorization header found for user '"

                                      + username + "'");

              }

              if (authenticationIsRequired(username)) {

                   UsernamePasswordAuthenticationToken authRequest = new UsernamePasswordAuthenticationToken(

                             usernametokens[1]);

                   authRequest.setDetails(

                             this.authenticationDetailsSource.buildDetails(request));

                   Authentication authResult = this.authenticationManager

                             .authenticate(authRequest);

                   if (debug) {

                        this.logger.debug("Authentication success: " + authResult);

                   }

                    SecurityContextHolder.getContext().setAuthentication(authResult);

                   this.rememberMeServices.loginSuccess(requestresponseauthResult);

                   onSuccessfulAuthentication(requestresponseauthResult);

              }

          }

          catch (AuthenticationException failed) {

              SecurityContextHolder.clearContext();

              if (debug) {

                   this.logger.debug("Authentication request for failed: " + failed);

              }

              this.rememberMeServices.loginFail(requestresponse);

              onUnsuccessfulAuthentication(requestresponsefailed);

              if (this.ignoreFailure) {

                   chain.doFilter(requestresponse);

              }

              else {

                   this.authenticationEntryPoint.commence(requestresponsefailed);

              }

              return;

          }

          chain.doFilter(requestresponse);

     }

 

    以上就是提取Authentication的过程,如果没有或不是basic就无法通过验证(所以资源服务或直接访问授权服务都必须最后转为header且包含Authentication值为basic),接下来就是authenticate验证过程,如果是ClientCredentialsTokenEndpointFilter则UserDetail为ClientCredentialsService从数据库中通过client_id获取授权信息,否则通过UserDetailImpl(自己实现的接口)获取授权信息。

 

 

总结:

 

(1) 认证服务器配置了资源服务器,无法使用access_token访问对应资源,这可能是一个bug或可能本人在本文中使用方法不对(如果谁知道,麻烦通知我或给我留言,感激不尽)。

(2)资源服务器(分离模式)通过access_token访问有三种模式,bearer授权头、请求参数,form表单;

(3)访问认证服务器资源使用用户名密码(非client_id或client_secret)的basic授权头模式或通过资源服务器间接访问,访问资源服务器可以通过access_token访问。

 

快来成为我的朋友或合作伙伴,一起交流,一起进步!
QQ群:961179337
微信:lixiang6153
邮箱:lixx2048@163.com
公众号:IT技术快餐
更多资料等你来拿!

 

 

  • 0
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 3
    评论
OAuth2的access_token是用于访问受保护的资源时进行身份验证和授权的凭证。通过将access_token附加到请求头或请求参数中,客户端可以向资源服务器发送请求并获得资源的访问权限。access_token通常是使用JWT(JSON Web Token)进行签名的,以确保其安全性和完整性。然而,access_token只提供了身份验证的功能,而没有提供有关用户的其他信息。这就是为什么在OAuth2中还存在idToken原因。idToken是一个包含用户相关信息的令牌,如用户ID、姓名、电子邮件等。它通常被用于提供身份验证和用户信息的完整性,并且可以通过认证服务器的/token端点进行验证。因此,通过同时使用access_token和idToken,客户端可以获得对资源的访问权限,并获得关于用户的更详细的信息。<span class="em">1</span><span class="em">2</span><span class="em">3</span> #### 引用[.reference_title] - *1* [【笔记】浅谈OAuth2 accessToken和OIDC idToken的理解和使用场景](https://blog.csdn.net/qq_26878363/article/details/115394602)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] - *2* *3* [springsecurity-oauth2令牌 access_token验证](https://blog.csdn.net/clonetx/article/details/125395593)[target="_blank" data-report-click={"spm":"1018.2226.3001.9630","extra":{"utm_source":"vip_chatgpt_common_search_pc_result","utm_medium":"distribute.pc_search_result.none-task-cask-2~all~insert_cask~default-1-null.142^v92^chatsearchT3_1"}}] [.reference_item style="max-width: 50%"] [ .reference_list ]

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 3
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

贝壳里的沙

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值