OAuth 2与Spring RestTemplate使用刷新令牌进行登录

4

我的目标

我有一个Java客户端应用程序(JavaFX + Spring-boot混合应用程序),您可以在此处查看它:https://github.com/FAForever/downlords-faf-client。目前,如果用户希望保持登录状态,我们会存储用户名/密码,显然这是一个非常糟糕的想法。现在,我想存储刷新令牌,然后使用该令牌登录用户。

当前状态 请参见此处

ResourceOwnerPasswordResourceDetails details = new ResourceOwnerPasswordResourceDetails();
details.setClientId(apiProperties.getClientId());
details.setClientSecret(apiProperties.getClientSecret());
details.setClientAuthenticationScheme(AuthenticationScheme.header);
details.setAccessTokenUri(apiProperties.getBaseUrl() + OAUTH_TOKEN_PATH);
details.setUsername(username);
details.setPassword(password);

OAuth2RestTemplate restTemplate = new OAuth2RestTemplate(details);

restOperations = templateBuilder
    // Base URL can be changed in login window
    .rootUri(apiProperties.getBaseUrl())
    .configure(restTemplate);

目前我所发现的

我发现restTemplate.getAccessToken().getRefreshToken()将为我提供所需保存的刷新令牌,以便稍后保持用户登录状态。

我无法理解的部分

我找不到一种只使用刷新令牌创建OAuth2RestTemplate的方法。这是否可能?有人能够指点我正确的方向吗?也许给我链接一些文章阅读?这个是正确的阅读位置吗?


1
你使用的是哪个版本的Spring?问这个问题是因为在Spring 5中,推荐使用WebClient类而不是rest template。虽然我不能告诉你如何使用它。 - Ole V.V.
1
谢谢,很有趣,也许我会转换。是的,该项目使用Spring 5。 - Alex
1
“我有一个使用Spring Boot和Spring的Java客户端应用程序”是什么意思?这是一个JavaFX应用程序还是JavaFX + Spring Boot混合应用程序? - Shekhar Rai
JavaFX + Spring-boot 混合应用程序 - Alex
我们发现WebClient并不是我们想要的,反应式编程可能意味着我们需要重构很多东西。 - Alex
1个回答

8
我不认为使用OAuth2RestTemplate可以实现此目的,但您可以自己重新实现所需的部分。我想分享一个示例,演示如何使用OAuth密码登录到Microsoft的OAuth2版本(Azure活动目录)。它缺少从现有刷新令牌获取新令牌的部分,但我在需要添加它的地方添加了一条注释。
模仿OAuthRestTemplates行为的简单方法是创建一个自定义的ClientHttpRequestInterceptor,将令牌获取委托给专用的Spring服务组件,然后将其附加到您的RestTemplate中:
@RequiredArgsConstructor
@Slf4j
public class OAuthTokenInterceptor implements ClientHttpRequestInterceptor {
  private final TokenService tokenService;

  @NotNull
  @Override
  public ClientHttpResponse intercept(HttpRequest request, byte[] body,
                                      ClientHttpRequestExecution execution) throws IOException {
    request.getHeaders().add("Authorization", "Bearer " + tokenService.getRefreshedToken().getValue());
    return execution.execute(request, body);
  }
}

这个拦截器可以添加到您的主要 RestTemplate 中:

List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(globalOAuthTokenInterceptor);
restTemplate.setInterceptors(interceptors);

拦截器使用的令牌服务将令牌保存在缓存中,当请求到来时检查令牌是否过期,如果需要则查询一个新的令牌。
@Service
@Slf4j
public class TokenService {
  private final TokenServiceProperties tokenServiceProperties;
  private final RestTemplate simpleRestTemplate;
  private OAuth2AccessToken tokenCache;

  public TokenService(TokenServiceProperties tokenServiceProperties) {
    this.tokenServiceProperties = tokenServiceProperties;

    simpleRestTemplate = new RestTemplateBuilder().
        build();
  }

  public OAuth2AccessToken getRefreshedToken() {
    if (tokenCache == null || tokenCache.isExpired()) {
      log.debug("Token expired, fetching new token");
      tokenCache = refreshOAuthToken();
    } else {
      log.debug("Token still valid for {} seconds", tokenCache.getExpiresIn());
    }

    return tokenCache;
  }

  public OAuth2AccessToken loginWithCredentials(String username, String password) {
    HttpHeaders headers = new HttpHeaders();
    headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED);
    headers.setAccept(Collections.singletonList(MediaType.APPLICATION_JSON_UTF8));

    MultiValueMap<String, String> map = new LinkedMultiValueMap<>();
    map.add("grant_type", "password");
    map.add("resource", tokenServiceProperties.getAadB2bResource());
    map.add("client_id", tokenServiceProperties.getAadB2bClientId());
    map.add("username", username);
    map.add("password", password);

    HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers);

    return simpleRestTemplate.postForObject(
        tokenServiceProperties.getAadB2bUrl(),
        request,
        OAuth2AccessToken.class
    );
  }

  private OAuth2AccessToken refreshOAuthToken() {
    return loginWithRefreshToken(tokenCache.getRefreshToken().getValue());
  }

  public OAuth2AccessToken loginWithRefreshToken(String refreshToken) {
    // add code for fetching OAuth2 token from refresh token here
    return null;
  }
}

在这个代码示例中,您需要使用用户名和密码登录一次,之后所有进一步的登录都将使用刷新令牌。如果您想直接使用刷新令牌,则可以使用公共方法,否则它将在内部完成。 由于登录代码专门编写用于登录Microsoft AAD,因此您应该重新检查MultiValueMap参数。
TokenServiceProperties很简单:
@Data
public class TokenServiceProperties {
    private String aadB2bUrl;
    private String aadB2bClientId;
    private String aadB2bResource;
}

如有必要,请进行适当的修改。

整个解决方案存在一个小缺陷:通常您可以通过依赖注入获取一个 RestTemplate,但现在您需要第二个 RestTemplate(“简单”),以获取 OAuth 令牌。在此示例中,我们在 TokenService 的构造函数中创建它。但是,这通常不是好的编程风格,因为它会使单元测试等变得更加困难。您也可以考虑使用限定符 bean 或在 TokenService 中使用更基本的 http 客户端。

另一个重要的事情要注意:我在这里使用了 spring-security-oauth2 包。如果您没有在项目中配置 Spring Security,则会触发 Spring Security 自动配置,这可能是不希望的 - 您可以通过排除不需要的包来解决这个问题,例如在 gradle 中:

implementation("org.springframework.security.oauth:spring-security-oauth2") {
    because "We only want the OAuth2AccessToken interface + implementations without activating Spring Security"
    exclude group: "org.springframework.security", module: "spring-security-web"
    exclude group: "org.springframework.security", module: "spring-security-config"
    exclude group: "org.springframework.security", module: "spring-security-core"
}

如果您想关注我们在这个问题上的进展,请查看上面的链接:https://github.com/FAForever/downlords-faf-client/issues/1533 - Alex
谢谢您的回答。它真的对我很有帮助。 - iroiroys

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接