如何配置Identity Server Connect/Authorize内部调用的CSP
当Identity Server从https://sso.com/connect/authorize?..重定向到https://anotherhost.com/signin-oidc时,浏览器抛出CSP错误:
refused to send form data to form-action 'https://anotherhost.com/signin-oidc' because it violates the following content security policy directive : form-action https://anotherhost.com
经排查,Chrome和Safari即使在GET请求场景下,若CSP配置不正确也会阻止该表单提交,需要将https://anotherhost.com/signin-oidc加入form-action指令。但在Asp.net Core中尝试以下代码时,仅保留了新增的form-action指令,原有其他CSP策略全部丢失:
app.Use(async (context, next) => { if (context.Request.Path.StartsWithSegments("/connect/authorize")) { context.Response.Headers["Content-Security-Policy"] += "form-action 'self' https://anotherhost.com/signin-oidc"; } await next(); });
需要解决两个问题:
- 如何在
connect/authorize端点内部添加内容安全策略? - 在Asp.net Core中如何正确管理CSP配置,避免覆盖原有策略?
1. 针对connect/authorize端点添加CSP
Identity Server的/connect/authorize端点在需要重定向时,会返回一个自动提交的HTML表单。要为这个特定端点添加CSP,有两种可靠方式:
方式一:自定义IEndpointResultCreator
Identity Server使用IEndpointResultCreator生成端点响应结果,你可以自定义该服务修改authorize端点的响应头:
public class CustomEndpointResultCreator : DefaultEndpointResultCreator { public CustomEndpointResultCreator(ISystemClock clock) : base(clock) { } public override Task ExecuteAsync(EndpointResult result, HttpContext context) { if (result is AuthorizeResult authorizeResult && authorizeResult.RedirectUri.StartsWith("https://anotherhost.com/signin-oidc")) { // 获取现有CSP头,不存在则初始化 var existingCsp = context.Response.Headers.ContainsKey("Content-Security-Policy") ? context.Response.Headers["Content-Security-Policy"].ToString() : ""; // 追加form-action指令,避免覆盖原有策略 if (!existingCsp.Contains("form-action")) { existingCsp += " form-action 'self' https://anotherhost.com/signin-oidc;"; } else { // 若已有form-action,追加目标地址 existingCsp = existingCsp.Replace("form-action ", $"form-action https://anotherhost.com/signin-oidc "); } context.Response.Headers["Content-Security-Policy"] = existingCsp.Trim(); } return base.ExecuteAsync(result, context); } }
然后在Startup/Program.cs中替换默认服务:
builder.Services.AddTransient<IEndpointResultCreator, CustomEndpointResultCreator>();
方式二:精准匹配的中间件
改进原有中间件,确保正确合并而非覆盖原有CSP头:
app.Use(async (context, next) => { await next(); // 在后续中间件执行完成后处理响应,避免覆盖其他中间件设置的CSP if (context.Request.Path.StartsWithSegments("/connect/authorize") && context.Response.StatusCode == 200) { const string targetAction = "https://anotherhost.com/signin-oidc"; var cspHeader = context.Response.Headers.TryGetValue("Content-Security-Policy", out var existing) ? existing.ToString() : ""; if (string.IsNullOrEmpty(cspHeader)) { context.Response.Headers["Content-Security-Policy"] = $"form-action 'self' {targetAction};"; } else if (!cspHeader.Contains(targetAction)) { // 检查是否已有form-action指令 if (cspHeader.Contains("form-action")) { // 追加到现有form-action中 context.Response.Headers["Content-Security-Policy"] = cspHeader.Replace("form-action ", $"form-action {targetAction} "); } else { // 添加新的form-action指令 context.Response.Headers["Content-Security-Policy"] = $"{cspHeader} form-action 'self' {targetAction};"; } } } });
注意要在await next()之后处理响应,确保其他中间件设置的CSP已经存在,再进行合并。
2. Asp.net Core中正确管理CSP配置
不要手动拼接响应头,推荐使用官方的CSP配置方式,确保策略的一致性和可维护性:
方式一:使用CspBuilder(Asp.net Core 3.0+)
通过Microsoft.AspNetCore.Http.Headers提供的CSP构建器配置全局策略,再针对特定端点修改:
builder.Services.AddCsp(options => { // 配置全局默认CSP策略 options.DefaultSources(s => s.Self()); options.ScriptSources(s => s.Self().UnsafeInline()); // 其他全局策略... // 针对/connect/authorize端点追加form-action options.AddPolicy("AuthorizeCsp", policy => { policy.DefaultSources(s => s.Self()); policy.FormActions(s => s.Self().CustomSources("https://anotherhost.com/signin-oidc")); // 复制全局策略的其他配置 }); }); // 在管道中应用全局CSP,然后针对特定端点应用自定义策略 app.UseCsp(); app.UseWhen(context => context.Request.Path.StartsWithSegments("/connect/authorize"), appBuilder => { appBuilder.UseCsp("AuthorizeCsp"); });
方式二:使用中间件统一管理CSP
创建专门的CSP中间件,集中管理所有端点的CSP策略,避免分散配置:
public class CspMiddleware { private readonly RequestDelegate _next; private readonly IDictionary<string, Action<CspBuilder>> _endpointPolicies; public CspMiddleware(RequestDelegate next, IDictionary<string, Action<CspBuilder>> endpointPolicies) { _next = next; _endpointPolicies = endpointPolicies; } public async Task InvokeAsync(HttpContext context) { await _next(context); // 检查当前端点是否有自定义CSP策略 foreach (var (path, policyBuilder) in _endpointPolicies) { if (context.Request.Path.StartsWithSegments(path)) { var builder = new CspBuilder(); policyBuilder(builder); // 获取全局CSP并合并 if (context.Response.Headers.TryGetValue("Content-Security-Policy", out var globalCsp)) { var mergedPolicy = MergeCspPolicies(globalCsp.ToString(), builder.Build()); context.Response.Headers["Content-Security-Policy"] = mergedPolicy; } else { context.Response.Headers["Content-Security-Policy"] = builder.Build(); } break; } } } private string MergeCspPolicies(string globalPolicy, string endpointPolicy) { // 实现策略合并逻辑,比如合并相同指令的源,避免重复 var globalDirectives = globalPolicy.Split(';').ToDictionary(d => d.Split(' ')[0].Trim(), d => d); var endpointDirectives = endpointPolicy.Split(';').ToDictionary(d => d.Split(' ')[0].Trim(), d); foreach (var (directive, value) in endpointDirectives) { if (globalDirectives.ContainsKey(directive)) { globalDirectives[directive] = $"{globalDirectives[directive].Trim()} {value.Split(' ', 2)[1].Trim()}"; } else { globalDirectives.Add(directive, value); } } return string.Join("; ", globalDirectives.Values); } } // 注册中间件扩展方法 public static class CspMiddlewareExtensions { public static IApplicationBuilder UseCustomCsp(this IApplicationBuilder app, IDictionary<string, Action<CspBuilder>> endpointPolicies) { return app.UseMiddleware<CspMiddleware>(endpointPolicies); } } // 在Program.cs中使用 var endpointPolicies = new Dictionary<string, Action<CspBuilder>> { { "/connect/authorize", builder => { builder.FormActions(s => s.CustomSources("https://anotherhost.com/signin-oidc")); } } }; app.UseCustomCsp(endpointPolicies);
内容的提问来源于stack exchange,提问作者gaurav

