You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

如何配置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();
});

需要解决两个问题:

  1. 如何在connect/authorize端点内部添加内容安全策略?
  2. 在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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.06.15 05:23:15