【问题标题】:Setting OAuth2 token for RestTemplate in an app that uses both @ResourceServer and @EnableOauth2Sso在同时使用 @ResourceServer 和 @EnableOauth2Sso 的应用程序中为 RestTemplate 设置 OAuth2 令牌
【发布时间】:2017-06-11 12:29:04
【问题描述】:

在我当前的项目中,我有一个应用程序,它有一个小图形片段,用户使用 SSO 进行身份验证,还有一部分是纯 API,用户使用 Authorization 标头进行身份验证。

例如:

  • 使用 SSO 访问 /ping-other-service
  • /api/ping-other-service 使用不记名令牌访问

作为所有云原生应用,我们的应用程序使用 JWT 令牌 (UAA) 与使用相同 SSO 提供程序的其他服务通信,所以我想我们会使用 OAuth2RestTemplate,因为 according to the documentation 它可以神奇地插入身份验证凭据。它对所有使用 SSO 进行身份验证的端点执行此操作。但是,当我们使用通过承载令牌进行身份验证的端点时,它不会填充其余模板。

我的理解 from the documentation@EnableOAuth2Client 只会从 SSO 登录中提取令牌,而不是 auth 标头?

我看到了什么

失败的请求及其作用:

  1. curl -H "Authorization: Bearer <token>" http://localhost/api/ping-other-service
  2. 内部使用 restTemplate 调用 http://some-other-service/ping 响应 401

成功的请求及其作用:

  1. http://localhost/ping-other-service
  2. 内部使用 restTemplate 调用 http://some-other-service/ping 响应 200

我们是如何解决这个问题的

为了解决这个问题,我最终创建了以下怪物,如果无法从授权标头中获得令牌,它将从 OAuth2ClientContext 中提取令牌。

    @PostMapping(path = "/ping-other-service")
    public ResponseEntity ping(@PathVariable String caseId, HttpServletRequest request, RestTemplate restTemplate) {
        try {
            restTemplate.postForEntity(adapterUrl + "/webhook/ping", getRequest(request), Map.class);
        } catch (HttpClientErrorException e) {
            e.printStackTrace();
            return new ResponseEntity(HttpStatus.SERVICE_UNAVAILABLE);
        }

        return new ResponseEntity(HttpStatus.OK);
    }

    private HttpEntity<?> getRequest(HttpServletRequest request) {
        HttpHeaders headers = new HttpHeaders();
        headers.set("Authorization", "Bearer " + getRequestToken(request));
        return new HttpEntity<>(null, headers);
    }

    private String getRequestToken(HttpServletRequest request) {
        Authentication token = new BearerTokenExtractor().extract(request);
        if (token != null) {
            return (String) token.getPrincipal();
        } else {
            OAuth2AccessToken accessToken = oAuth2ClientContext.getAccessToken();

            if (accessToken != null) {
                return accessToken.getValue();
            }
        }

        throw new ResourceNotFound("No valid access token found");
    }

【问题讨论】:

    标签: spring spring-boot spring-security


    【解决方案1】:

    /api/** 资源中有一个传入的令牌,但是由于您使用的是 JWT,资源服务器可以在不调用身份验证服务器的情况下进行身份验证,所以没有 OAuth2RestTemplate 只是坐等您重新在令牌中继中使用上下文(如果您使用的是UserInfoTokenServices,将会有一个)。您可以很容易地创建一个,并将传入的令牌从SecurityContext 中提取出来。示例:

      @Autowired
      private OAuth2ProtectedResourceDetails resource;
    
      private OAuth2RestTemplate tokenRelayTemplate(Principal principal) {
        OAuth2Authentication authentication = (OAuth2Authentication) principal;
        OAuth2AuthenticationDetails details = (OAuth2AuthenticationDetails) authentication.getDetails();
        details.getTokenValue();
        OAuth2ClientContext context = new DefaultOAuth2ClientContext(new DefaultOAuth2AccessToken(details.getTokenValue()));
        return new OAuth2RestTemplate(resource, context);
      }
    

    如果需要,您可以将该方法转换为@Bean(在@Scope("request") 中)并使用@Qualifier 注入模板。

    Spring Cloud Security 中有一些自动配置和实用程序类可以帮助这种模式,例如:https://github.com/spring-cloud/spring-cloud-security/blob/master/spring-cloud-security/src/main/java/org/springframework/cloud/security/oauth2/client/AccessTokenContextRelay.java

    【讨论】:

    • 谢谢戴夫,我明天上班试试! :) 这对于restTemplate 是否可以无缝工作,或者我需要为/api 路径定义一个restTemplate 并为其他路径定义一个? (另一个要求是任何获得登录会话的人也可以在没有令牌的情况下调用 API 并依赖会话(除了其余模板之外,会话已配置并正常工作))
    • 如果您删除 principal 参数并从当前安全上下文中获取身份验证,则该方法可以“bean-ified”。所以“OAuth2Authentication authentication = (OAuth2Authentication) SecurityContextHolder.getContext().getAuthentication()”
    【解决方案2】:

    我在开发 Spring 资源服务器时遇到了这个问题,我需要将 OAuth2 令牌从请求传递到 restTemplate 以调用下游资源服务器。两个资源服务器都使用相同的身份验证服务器,我发现 Dave 的链接很有帮助,但我不得不深入了解如何实现这一点。我最终找到了文档here,结果证明实现非常简单。我使用的是@EnableOAuth2Client,所以我必须使用注入的OAuth2ClientContext 创建restTemplate bean 并创建适当的资源详细信息。就我而言,它是ClientCredentialsResourceDetails。感谢 Dave 所做的所有出色工作!

    @Bean 
    public OAuth2RestOperations restTemplate (OAuth2ClientContext context) {
        ClientCredentialsResourceDetails details = new ClientCredentialsResourceDetails();
        // Configure the details here
        return new OAuth2RestTemplate(details, context)
    }
    

    【讨论】:

      【解决方案3】:

      @戴夫·赛尔 我的 UAA 服务也是一个 oauth2 客户端,它需要中继来自 Zuul 的 JWT 令牌。配置oauth2客户端时如下方式

      @Configuration
      @EnableOAuth2Client
      @RibbonClient(name = "downstream")
      public class OAuthClientConfiguration {
      
          @Bean
          public OAuth2RestTemplate restTemplate(OAuth2ProtectedResourceDetails resource, OAuth2ClientContext context) {
              return new OAuth2RestTemplate(resource, context);
          }
      }
      

      我确实收到了来自下游服务的 401 响应,因为我的访问令牌的有效期很短,并且 AccessTokenContextRelay 不会更新传入的访问令牌(Zuul 确实通过刷新令牌更新过期的访问令牌)。

      OAuth2RestTemplate#getAccessToken 永远不会获取新的访问令牌,因为AccessTokenContextRelay 存储的访问令牌上的isExpired 丢弃了有效性和刷新令牌信息。

      如何解决?

      【讨论】:

        猜你喜欢
        • 2020-01-04
        • 2018-12-14
        • 1970-01-01
        • 2016-09-19
        • 2019-03-14
        • 2014-03-09
        • 2019-03-03
        • 2018-09-30
        • 2020-01-26
        相关资源
        最近更新 更多