Spring Oauth2 Client,自动刷新过期的access_token [英] Spring Oauth2 Client, automatically refresh expired access_token

查看:272
本文介绍了Spring Oauth2 Client,自动刷新过期的access_token的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

让我解释一下我的用例.

Let me explain my use case.

我需要一个 spring boot oauth2 客户端应用程序(不是资源服务器,因为我们已经有一个单独的资源服务器).我还有以下要求:

I need to have a spring boot oauth2 client application (not a resource server As we already have a separate resource server). Also I have following requirements:

  1. 对于资源服务器的每个外出请求,我们需要发送 id_token.(通过自定义 resttemplate 完成).

  1. For each out going request to resource server, we need to send id_token. (Done by customizing resttemplate).

对于任何请求,无论是否调用资源服务器,如果访问令牌过期,我的应用程序必须自动刷新它(无需任何用户干预,如任何弹出或重定向.).

For any request, no matter if it invokes resource server or not, If access token is expired my application must refresh it automatically (without any user intervention like any popup or redirection.).

如果 refresh_token 也已过期,则用户必须注销.

If refresh_token is also expired, user must be logged out.

问题:

对于第 2 点和第 3 点,我花了很多时间阅读文档和代码以及 Stack Overflow,但未能找到解决方案(或不理解).所以我决定把我在许多博客和文档中找到的所有部分放在一起,并提出我的解决方案.以下是我对第 2 点的解决方案.

For point 2 and 3, I have spent many hours reading documents and code and Stack Overflow but was not able to find the solution (or did not understand). So I decided to put all pieces together which I found on many blogs and documents, and come up with my solution. Below is my solution for point 2.

  1. 我们可以看看下面的代码并建议这种方法是否有任何问题吗?

  1. Can we please have a look to below code and suggest if there could be any problem with this approach?

  1. 如何解决第 3 点 我正在考虑扩展第 2 点的解决方案,但不确定我需要编写什么代码,有人可以指导我吗?

/**
 * 
 * @author agam
 *
 */
@Component
public class ExpiredTokenFilter extends OncePerRequestFilter {

    private static final Logger log = LoggerFactory.getLogger(ExpiredTokenFilter.class);

    private Duration accessTokenExpiresSkew = Duration.ofMillis(1000);

    private Clock clock = Clock.systemUTC();

    @Autowired
    private OAuth2AuthorizedClientService oAuth2AuthorizedClientService;

    @Autowired
    CustomOidcUserService userService;

    private DefaultRefreshTokenTokenResponseClient accessTokenResponseClient;

    private JwtDecoderFactory<ClientRegistration> jwtDecoderFactory;

    private static final String INVALID_ID_TOKEN_ERROR_CODE = "invalid_id_token";

    public ExpiredTokenFilter() {
        super();
        this.accessTokenResponseClient = new DefaultRefreshTokenTokenResponseClient();
        this.jwtDecoderFactory = new OidcIdTokenDecoderFactory();
    }

    @Override
    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain)
            throws ServletException, IOException {
        log.debug("my custom filter called ");
        /**
         * check if authentication is done.
         */
        if (null != SecurityContextHolder.getContext().getAuthentication()) {
            OAuth2AuthenticationToken currentUser = (OAuth2AuthenticationToken) SecurityContextHolder.getContext()
                    .getAuthentication();
            OAuth2AuthorizedClient authorizedClient = this.oAuth2AuthorizedClientService
                    .loadAuthorizedClient(currentUser.getAuthorizedClientRegistrationId(), currentUser.getName());
            /**
             * Check if token existing token is expired.
             */
            if (isExpired(authorizedClient.getAccessToken())) {

                /*
                 * do something to get new access token
                 */
                log.debug(
                        "=========================== Token Expired !! going to refresh ================================================");
                ClientRegistration clientRegistration = authorizedClient.getClientRegistration();
                /*
                 * Call Auth server token endpoint to refresh token. 
                 */
                OAuth2RefreshTokenGrantRequest refreshTokenGrantRequest = new OAuth2RefreshTokenGrantRequest(
                        clientRegistration, authorizedClient.getAccessToken(), authorizedClient.getRefreshToken());
                OAuth2AccessTokenResponse accessTokenResponse = this.accessTokenResponseClient
                        .getTokenResponse(refreshTokenGrantRequest);
                /*
                 * Convert id_token to OidcToken.
                 */
                OidcIdToken idToken = createOidcToken(clientRegistration, accessTokenResponse);
                /*
                 * Since I have already implemented a custom OidcUserService, reuse existing
                 * code to get new user. 
                 */
                OidcUser oidcUser = this.userService.loadUser(new OidcUserRequest(clientRegistration,
                        accessTokenResponse.getAccessToken(), idToken, accessTokenResponse.getAdditionalParameters()));

                log.debug(
                        "=========================== Token Refresh Done !! ================================================");
                /*
                 * Print old and new id_token, just in case.
                 */
                DefaultOidcUser user = (DefaultOidcUser) currentUser.getPrincipal();
                log.debug("new id token is " + oidcUser.getIdToken().getTokenValue());
                log.debug("old id token was " + user.getIdToken().getTokenValue());
                /*
                 * Create new authentication(OAuth2AuthenticationToken).
                 */
                OAuth2AuthenticationToken updatedUser = new OAuth2AuthenticationToken(oidcUser,
                        oidcUser.getAuthorities(), currentUser.getAuthorizedClientRegistrationId());
                /*
                 * Update access_token and refresh_token by saving new authorized client.
                 */
                OAuth2AuthorizedClient updatedAuthorizedClient = new OAuth2AuthorizedClient(clientRegistration,
                        currentUser.getName(), accessTokenResponse.getAccessToken(),
                        accessTokenResponse.getRefreshToken());
                this.oAuth2AuthorizedClientService.saveAuthorizedClient(updatedAuthorizedClient, updatedUser);
                /*
                 * Set new authentication in SecurityContextHolder.
                 */
                SecurityContextHolder.getContext().setAuthentication(updatedUser);
            }

        }
        filterChain.doFilter(request, response);
    }

    private Boolean isExpired(OAuth2AccessToken oAuth2AccessToken) {
        Instant now = this.clock.instant();
        Instant expiresAt = oAuth2AccessToken.getExpiresAt();
        return now.isAfter(expiresAt.minus(this.accessTokenExpiresSkew));
    }

    private OidcIdToken createOidcToken(ClientRegistration clientRegistration,
            OAuth2AccessTokenResponse accessTokenResponse) {
        JwtDecoder jwtDecoder = this.jwtDecoderFactory.createDecoder(clientRegistration);
        Jwt jwt;
        try {
            jwt = jwtDecoder
                    .decode((String) accessTokenResponse.getAdditionalParameters().get(OidcParameterNames.ID_TOKEN));
        } catch (JwtException ex) {
            OAuth2Error invalidIdTokenError = new OAuth2Error(INVALID_ID_TOKEN_ERROR_CODE, ex.getMessage(), null);
            throw new OAuth2AuthenticationException(invalidIdTokenError, invalidIdTokenError.toString(), ex);
        }
        OidcIdToken idToken = new OidcIdToken(jwt.getTokenValue(), jwt.getIssuedAt(), jwt.getExpiresAt(),
                jwt.getClaims());
        return idToken;
    }
}

我愿意接受任何改进我的代码的建议.谢谢.

I am open for any suggestion to improve my code. Thanks.

推荐答案

没有足够的细节来完全理解您的用例.了解一下就好了:

There are not enough details to understand your use-case fully. It would be great to understand:

  • Spring 安全性正在围绕 OAuth2 快速发展,请考虑提及您正在使用的版本.我的回答假设 5.2+
  • 您是在 servlet(用户以某种方式登录)还是非 servlet(如 @Scheduled 方法)环境中
  • Spring security is rapidly evolving around OAuth2, consider mentioning the version you are using. My answer assumes 5.2+
  • Are you in servlet (user logged in somehow) or non-servlet (like @Scheduled method) environment

根据有限的信息和我有限的知识,我有以下提示:

From the limited information and my limited knowledge I have following hints:

  • 考虑使用 WebClient 而不是 RestTemplate,这是他们未来的方向.它是被动的,但不要害怕.它可以用于阻塞"环境也是如此,您不会使用它的全部潜力,但您仍然可以从它对 OAuth2 的更好支持中受益
  • WebClient 本身有一个 ServletOAuth2AuthorizedClientExchangeFilterFunction,它几乎可以完成您想要实现的功能
  • 在创建 ServletOAuth2AuthorizedClientExchangeFilterFunction 时,您传入 AuthorizedClientServiceOAuth2AuthorizedClientManager 这是关于如何(重新)验证客户端的策略.
  • Consider using WebClient instead of RestTemplate, this is they way to go for the future. It is reactive but don't be scared. It can be used in "blocking" environment as well, you will not use it's full potential but you can still benefit from its better support for OAuth2
  • WebClient itself has a ServletOAuth2AuthorizedClientExchangeFilterFunction which does pretty much what you are trying to achieve
  • When creating ServletOAuth2AuthorizedClientExchangeFilterFunction you pass in AuthorizedClientServiceOAuth2AuthorizedClientManager which is a strategy on how to (re)authenticate client.

示例配置可能如下所示:

Sample configuration may look as follows:

@Bean
public WebClient webClient(ClientRegistrationRepository clientRegistrationRepository, OAuth2AuthorizedClientService authorizedClientService) {

    AuthorizedClientServiceOAuth2AuthorizedClientManager manager = new AuthorizedClientServiceOAuth2AuthorizedClientManager(clientRegistrationRepository, authorizedClientService);
    manager.setAuthorizedClientProvider(new DelegatingOAuth2AuthorizedClientProvider(
            new RefreshTokenOAuth2AuthorizedClientProvider(),
            new ClientCredentialsOAuth2AuthorizedClientProvider()));

    ServletOAuth2AuthorizedClientExchangeFilterFunction oauth2 = new ServletOAuth2AuthorizedClientExchangeFilterFunction(manager);

    oauth2.setDefaultClientRegistrationId("your-client-registratioin-id");

    return WebClient.builder()
            .filter(oauth2)
            .apply(oauth2.oauth2Configuration())
            .build();
}

并将其用作:

@Autowire
private final WebClient webClient;

...

webClient.get()
    .uri("http://localhost:8081/api/message")
            .retrieve()
            .bodyToMono(String.class)
            .map(string -> "Retrieved using password grant: " + string)
            .subscribe(log::info);

希望这有助于朝着正确的方向前进!玩得开心

Hope this helps to move in the right direction! Have fun

这篇关于Spring Oauth2 Client,自动刷新过期的access_token的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持IT屋!

查看全文
登录 关闭
扫码关注1秒登录
发送“验证码”获取 | 15天全站免登陆