Oauth 2 spring RestTemplate 使用刷新令牌登录

Ale*_*lex 2 java spring javafx oauth-2.0 spring-security-oauth2

我想要达到的目标

所以我有一个 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);
Run Code Online (Sandbox Code Playgroud)

到目前为止我发现了什么

我发现这restTemplate.getAccessToken().getRefreshToken()会给我我想要保存的刷新令牌,以便让用户保持登录状态。

什么我想不通

我找不到仅使用刷新令牌创建 OAuth2RestTemplate 的方法。这甚至可能吗?有人可以指出我正确的方向吗?也许链接我一些文章阅读?是这个读取正确的地方?

Bru*_*000 5

我不认为这是可能的OAuth2RestTemplate,但是您可以自己重新实现所需的部分。我想与您分享一个示例,用于 OAuth 密码登录到 Microsoft 的 OAuth2(Azure Active Directory)风格。它确实错过了从现有刷新令牌中获取新令牌的部分,但我在需要添加它的地方添加了一条评论。

模仿 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);
  }
}
Run Code Online (Sandbox Code Playgroud)

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

List<ClientHttpRequestInterceptor> interceptors = new ArrayList<>();
interceptors.add(globalOAuthTokenInterceptor);
restTemplate.setInterceptors(interceptors);
Run Code Online (Sandbox Code Playgroud)

拦截器中使用的令牌服务将令牌保存在缓存中,并根据请求检查令牌是否过期,并在需要时查询新令牌。

@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;
  }
}
Run Code Online (Sandbox Code Playgroud)

在此代码示例中,您将使用用户名和密码登录一次,之后所有进一步登录都将使用刷新令牌。如果你想直接使用刷新令牌,就使用公共方法,否则会在内部完成。由于登录代码是专门为登录 Microsoft AAD 而编写的,因此您应该重新检查 MultiValueMap 参数。

TokenServiceProperties 很简单:

@Data
public class TokenServiceProperties {
    private String aadB2bUrl;
    private String aadB2bClientId;
    private String aadB2bResource;
}
Run Code Online (Sandbox Code Playgroud)

如果需要,调整它们。

整个解决方案有一个小缺点:您现在需要第二个(“简单”的)来获取 OAuth 令牌,而不是通常通过依赖注入获取的 RestTemplate。在这个例子中,我们在 TokenService 的构造函数中创建它。然而,这通常是不好的风格,因为它使单元测试等变得更加困难。您还可以考虑在 TokenService 中使用合格的 bean 或使用更基本的 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"
}
Run Code Online (Sandbox Code Playgroud)