spring - 解决方案 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 进程之后,前端对后端进行两次调用:
- 使用
window.location.href
调用重定向到授权服务器(例如 Facebook、Google 或您自己的授权服务器)。 - 使用代码和状态查询参数发出 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 来帮助其他人。随时提供反馈!
问候,
巴特
解决方案
推荐阅读
- django - Django 2 升级丢失 filter_horizontal 功能
- regex - 如何使用正则表达式从具有已知前缀和后缀的字符串中提取子字符串?
- c# - ASP.NET Core 对 Azure 应用服务的 HTTP 请求缓慢
- reactjs - 使用 React 制作一个简单的消息板。我必须设置服务器,还是可以在本地存储状态?
- laravel - 从 MessageBag Laravel 中删除自定义错误
- traefik - 如何暴露两个 HTTPS 端口?
- vue.js - 无法安装组件:未定义模板或渲染函数。(Vue 使用插件)
- python - Python写入抓取数据的json文件
- ruby - 如何为 Timecop 修补文件和核心扩展
- git - 在 VSTS 中搜索提交时产生错误:VCErrorCommitNotFound