首页 > 解决方案 > 解决方案 Spring Backend OAuth2 Client 用于 Web 应用程序和本地(移动)应用程序

问题描述

在过去的几天里,我一直在尝试弄清楚如何使 OAuth2 在具有 OAuth2 客户端的本机应用程序上工作,该客户端由一个单独的前端应用程序和一个 Spring 后端组成。好消息!我想出了一种方法,让它既可以作为 Web 应用程序(在浏览器上)也可以在本机(移动)应用程序上运行。在这里,我想分享我的发现,并就可能的改进征求任何建议。

Spring 开箱即用的地方

Spring Oauth2 适用于 Web 应用程序。我们添加依赖项<artifactId>spring-security-oauth2-autoconfigure</artifactId>。我们添加注解@EnableOAuth2Client。此外,我们添加配置。对于详细的教程,我想向您推荐本教程

挑战开始出现的地方

Spring 使用会话 cookie (JSESSIONID) 来建立会话,该会话使用 Set-Cookie 标头发送到前端。在移动应用程序中,此 Set-Cookie 标头不会在后续请求中发送回后端。这意味着在移动应用程序上,后端将每个请求视为一个新会话。为了解决这个问题,我实现了会话标头而不是 cookie。可以读取此标头,因此可以将其添加到后续请求中。

    @Bean
    public HttpSessionIdResolver httpSessionIdResolver() {
        return HeaderHttpSessionIdResolver.xAuthToken();
    }

然而,这只解决了部分问题。前端发出一个请求,window.location.href这使得无法添加自定义标头(无法使用 REST 调用,因为它无法将调用者重定向到授权服务器登录页面,因为浏览器阻止了这一点)。浏览器会自动将 cookie 添加到使用window.location.href. 这就是为什么它适用于浏览器,但不适用于移动应用程序。因此,我们需要修改 Spring 的 OAuth2 流程,以便能够接收 REST 调用,而不是使用window.location.href.

Spring中的OAuth2 Client流程

在 Oauth2 进程之后,前端对后端进行两次调用:

  1. 使用window.location.href调用重定向到授权服务器(例如 Facebook、Google 或您自己的授权服务器)。
  2. 使用代码和状态查询参数发出 REST GET 请求以检索访问令牌。

但是,如果 Spring 无法识别会话(例如在手机上),它会创建一个新的 OAuth2ClientContext 类,因此在第二次调用时会引发错误:InvalidRequestException("Possible CSRF detected - state parameter was required but no state could be found");AuthorizationCodeAccessTokenProvider.class. 它抛出此错误的原因preservedState是请求中的属性为空。这篇文章对@Nico de wit 的回答很好地解释了这一点。

我创建了 Spring OAuth2 流程的视觉效果,其中显示了“会话中存在上下文?”框。一旦您从登录授权服务器中检索到授权代码,这就是出错的地方。这是因为在 getParametersForToken 框中进一步检查了preservedState,然后它为空,因为它来自新的 OAuth2ClientContext 对象(而不是在将第一次调用重定向到授权服务器页面时使用的同一对象)。

在此处输入图像描述

解决方案

我通过扩展解决了这个问题OAuth2ClientContextFilter.class。如果尚未检索到授权代码,则此类负责将用户重定向到授权服务器登录页面。自定义类现在不是重定向,而是发回 200 和正文中的 url,前端需要重定向到该 url。此外,前端现在可以进行 REST 调用,而不是使用window.location.href重定向。这看起来像:

    @Override
    public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException,
                                                                                                                    ServletException {
        HttpServletRequest request = (HttpServletRequest)servletRequest;
        HttpServletResponse response = (HttpServletResponse)servletResponse;
        request.setAttribute(CURRENT_URI, this.calculateCurrentUri(request));

        try {
            chain.doFilter(servletRequest, servletResponse);
        } catch (IOException var9) {
            throw var9;
        } catch (Exception var10) {
            Throwable[] causeChain = this.throwableAnalyzer.determineCauseChain(var10);
            UserRedirectRequiredException redirect = (UserRedirectRequiredException)this.throwableAnalyzer.getFirstThrowableOfType(UserRedirectRequiredException.class, causeChain);
            if (redirect == null) {
                if (var10 instanceof ServletException) {
                    throw (ServletException)var10;
                }

                if (var10 instanceof RuntimeException) {
                    throw (RuntimeException)var10;
                }

                throw new NestedServletException("Unhandled exception", var10);
            }

            // The original code redirects the caller to the authorization page
            // this.redirectUser(redirect, request, response);

            // Instead we create the redirect Url from the Exception and add it to the body
            String redirectUrl = createRedirectUrl(redirect);
            response.setStatus(200);
            response.getWriter().write(redirectUrlToJson(redirectUrl));
        }
    }

createRedirectUrl 包含一些构建 Url 的逻辑:

    private String createRedirectUrl(UserRedirectRequiredException e) {
        String redirectUri = e.getRedirectUri();
        UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(redirectUri);
        Map<String, String> requestParams = e.getRequestParams();
        Iterator it = requestParams.entrySet().iterator();

        while (it.hasNext()) {
            Map.Entry<String, String> param = (Map.Entry)it.next();
            builder.queryParam(param.getKey(), param.getValue());
        }

        if (e.getStateKey() != null) {
            builder.queryParam("state", e.getStateKey());
        }

        return builder.build().encode().toUriString();
    }

我希望通过在 Web 和移动应用程序上使用 Spring 实现 OAuth2 来帮助其他人。随时提供反馈!

问候,

巴特

标签: springspring-bootmobilespring-securityoauth-2.0

解决方案


推荐阅读