我还没有找到很多关于执行此操作的好方法的信息 - 不得不复制 API 只是为了支持 2 种授权方案是一件痛苦的事情。
我一直在研究使用反向代理的想法,在我看来这是一个很好的解决方案。
- 用户登录网站(使用 cookie httpOnly 进行会话)
- 网站使用防伪令牌
- SPA 向网站服务器发送请求,并在标头中包含防伪令牌:https://app.mydomain.com/api/secureResource
- 网站服务器验证防伪令牌 (CSRF)
- 网站服务器确定请求是针对 API 的,应将其发送到反向代理
- 网站服务器获取 API 的用户访问令牌
- 反向代理将请求转发到 API:https://api.mydomain.com/api/secureResource
请注意,防伪令牌(#2、#4)至关重要,否则您可能会将您的 API 暴露给 CSRF 攻击。
示例(带有 IdentityServer4 的 .NET Core 2.1 MVC):
为了获得一个工作示例,我从 IdentityServer4 快速入门 Switching to Hybrid Flow and adding API Access back 开始。这设置了 MVC 应用程序使用 cookie 并可以从身份服务器请求 access_token 以调用 API 的场景。
我用Microsoft.AspNetCore.Proxy做反向代理,修改了快速入门。
MVC 启动.ConfigureServices:
services.AddAntiforgery();
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>();
MVC 启动.配置:
app.MapWhen(IsApiRequest, builder =>
{
builder.UseAntiforgeryTokens();
var messageHandler = new BearerTokenRequestHandler(builder.ApplicationServices);
var proxyOptions = new ProxyOptions
{
Scheme = "https",
Host = "api.mydomain.com",
Port = "443",
BackChannelMessageHandler = messageHandler
};
builder.RunProxy(proxyOptions);
});
private static bool IsApiRequest(HttpContext httpContext)
{
return httpContext.Request.Path.Value.StartsWith(@"/api/", StringComparison.OrdinalIgnoreCase);
}
ValidateAntiForgeryToken(马吕斯·舒尔茨):
public class ValidateAntiForgeryTokenMiddleware
{
private readonly RequestDelegate next;
private readonly IAntiforgery antiforgery;
public ValidateAntiForgeryTokenMiddleware(RequestDelegate next, IAntiforgery antiforgery)
{
this.next = next;
this.antiforgery = antiforgery;
}
public async Task Invoke(HttpContext context)
{
await antiforgery.ValidateRequestAsync(context);
await next(context);
}
}
public static class ApplicationBuilderExtensions
{
public static IApplicationBuilder UseAntiforgeryTokens(this IApplicationBuilder app)
{
return app.UseMiddleware<ValidateAntiForgeryTokenMiddleware>();
}
}
BearerTokenRequestHandler:
public class BearerTokenRequestHandler : DelegatingHandler
{
private readonly IServiceProvider serviceProvider;
public BearerTokenRequestHandler(IServiceProvider serviceProvider, HttpMessageHandler innerHandler = null)
{
this.serviceProvider = serviceProvider;
InnerHandler = innerHandler ?? new HttpClientHandler();
}
protected override async Task<HttpResponseMessage> SendAsync(HttpRequestMessage request, CancellationToken cancellationToken)
{
var httpContextAccessor = serviceProvider.GetService<IHttpContextAccessor>();
var accessToken = await httpContextAccessor.HttpContext.GetTokenAsync("access_token");
request.Headers.Authorization =new AuthenticationHeaderValue("Bearer", accessToken);
var result = await base.SendAsync(request, cancellationToken);
return result;
}
}
_Layout.cshtml:
@Html.AntiForgeryToken()
然后,您可以使用您的 SPA 框架提出请求。为了验证我刚刚做了一个简单的 AJAX 请求:
<a onclick="sendSecureAjaxRequest()">Do Secure AJAX Request</a>
<div id="ajax-content"></div>
<script language="javascript">
function sendSecureAjaxRequest(path) {
var myRequest = new XMLHttpRequest();
myRequest.open('GET', '/api/secureResource');
myRequest.setRequestHeader("RequestVerificationToken",
document.getElementsByName('__RequestVerificationToken')[0].value);
myRequest.onreadystatechange = function () {
if (myRequest.readyState === XMLHttpRequest.DONE) {
if (myRequest.status === 200) {
document.getElementById('ajax-content').innerHTML = myRequest.responseText;
} else {
alert('There was an error processing the AJAX request: ' + myRequest.status);
}
}
};
myRequest.send();
};
</script>
这是一个概念验证测试,所以你的里程可能非常好,而且我对 .NET Core 和中间件配置还很陌生,所以它可能看起来更漂亮。我对此进行了有限的测试,只向 API 发出了 GET 请求,没有使用 SSL (https)。
正如预期的那样,如果从 AJAX 请求中删除了防伪令牌,则会失败。如果用户尚未登录(经过身份验证),则请求失败。
与往常一样,每个项目都是独一无二的,因此请务必验证您的安全要求是否得到满足。请查看此答案中留下的任何 cmets,以了解有人可能提出的任何潜在安全问题。
另一方面,我认为一旦子资源完整性 (SRI) 和内容安全策略 (CSP) 在所有常用浏览器上可用(即旧版浏览器被逐步淘汰),就应该重新评估本地存储以存储 API令牌存储的复杂性。 现在应该使用 SRI 和 CSP 来帮助减少支持浏览器的攻击面。