我没有找到很好的方法来解决这个问题 - 为了支持两种授权方案,不得不复制API很痛苦。
我一直在研究使用反向代理的想法,它看起来对此是一个很好的解决方案。
- 用户登录网站(使用cookie httpOnly进行会话)
- 网站使用防伪令牌
- SPA向网站服务器发送请求,并在标头中包括防伪令牌:https://app.mydomain.com/api/secureResource
- 网站服务器验证防伪令牌(CSRF)
- 网站服务器确定请求是针对API的,并应将其发送到反向代理
- 网站服务器获取用户访问API的访问令牌
- 反向代理将请求转发到API:https://api.mydomain.com/api/secureResource
请注意,防伪令牌(#2,#4)非常重要,否则您可能会暴露API以进行CSRF攻击。
示例(.NET Core 2.1 MVC with IdentityServer4):
为了获得此的工作示例,我从IdentityServer4快速入门切换到混合流并添加API访问开始。 这设置了我所追求的情况,在此情况下,MVC应用程序使用cookie并可以向身份服务器请求access_token以调用API。
我使用Microsoft.AspNetCore.Proxy作为反向代理并修改了快速入门。
MVC Startup.ConfigureServices:
services.AddAntiforgery()
services.TryAddSingleton<IHttpContextAccessor, HttpContextAccessor>()
MVC启动.Configure:
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 (Marius Schulz):
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 和中间件配置还很陌生,代码看起来可能不够优美。我仅对此进行了有限的测试,并且只使用 GET 请求访问了 API,没有使用 SSL(https)。
如预期的那样,如果 AJAX 请求中的防伪标记被删除,请求将失败。如果用户未登录(经过身份验证),请求也会失败。
像往常一样,每个项目都是独特的,因此始终要验证是否满足安全要求。请查看此答案上留下的任何评论以了解可能引发的任何潜在安全问题。
另外,我认为一旦所有常用浏览器(即老版本浏览器被淘汰)都支持子资源完整性 (SRI) 和内容安全策略 (CSP),就应该重新评估使用本地存储来存储 API 令牌的情况,这将减少令牌存储的复杂性。现在应该使用 SRI 和 CSP 来帮助减少支持浏览器的攻击面。