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

IdentityServer4:如何基于白名单严格校验returnUrl参数?

IdentityServer4 v4.1.2 returnUrl校验问题及解决方案

背景

我们在旧系统中使用IdentityServer4(v4.1.2)实现OAuth认证,需严格校验returnUrl参数以防范开放重定向漏洞,具体安全需求:

  • 基于白名单实现校验,仅允许重定向至授权域名及路径。
  • 用短码或标识符替代returnUrl中的直接URL,映射至预定义的已校验URL。

问题

我希望复用IdentityServer4内置的returnUrl校验机制(如GetAuthorizationContextAsync()),避免完全自定义方案,但IdentityServer4无法解析我的回调URL。其内部通过ParseAsync()处理,依赖IsLocalUrl()做校验,而IsLocalUrl()对以下URL返回false:

https://localhost:44382/api/account/callback?ReturnUrl=http%3A%2F%2Flocalhost%3A3000%2Fauth-callback

关键配置

IdentityServer服务注册

services.AddIdentity<ApplicationUser, ApplicationRole>().AddEntityFrameworkStores<ApplicationDbContext>();
services.AddIdentityServer(o => o.IssuerUri = azureADConfiguration.Authority)
    .AddSigningCredential(signingCredentials)
    .AddConfigurationStore(options =>
    {
        options.DefaultSchema = "Identity";
        options.ConfigureDbContext = builder =>
            builder.UseSqlServer(connectionString, optionsBuilder =>
                optionsBuilder.MigrationsAssembly(typeof(ApplicationDbContext).Namespace));
    })
    .AddOperationalStore(options =>
    {
        options.DefaultSchema = "Identity";
        options.ConfigureDbContext = builder =>
            builder.UseSqlServer(connectionString,
                sql => sql.MigrationsAssembly(typeof(ApplicationDbContext).Namespace));
        options.EnableTokenCleanup = true;
        options.TokenCleanupInterval = 30;
    })
    .AddAspNetIdentity<ApplicationUser>();

包版本

<PackageVersion Include="IdentityServer4" Version="4.1.2" />
<PackageVersion Include="IdentityServer4.AccessTokenValidation" Version="3.0.1" />
<PackageVersion Include="IdentityServer4.AspNetIdentity" Version="4.1.2" />
<PackageVersion Include="IdentityServer4.EntityFramework" Version="4.1.2" />
<PackageVersion Include="IdentityServer4.EntityFramework.Storage" Version="4.1.2" />
<PackageVersion Include="IdentityServer4.Storage" Version="4.1.2" />
<PackageVersion Include="Cnblogs.IdentityServer4.EntityFramework.Storage" Version="4.2.1" />

Account控制器代码

[HttpGet("Login")]
[AllowAnonymous]
public Task<IActionResult> Login(string returnUrl) => ExternalLogin("oidc", returnUrl);

[HttpPost]
[HttpGet]
[AllowAnonymous]
public Task<IActionResult> ExternalLogin(string provider, string returnUrl = null)
{
    // 请求重定向到外部登录提供者
    var redirectUrl = Url.Action("ExternalLoginCallback", "Account", new { ReturnUrl = returnUrl });
    var properties = _signInManager.ConfigureExternalAuthenticationProperties(provider, redirectUrl);
    return Task.FromResult<IActionResult>(Challenge(properties, provider));
}

[HttpGet("Callback")]
[AllowAnonymous]
public async Task<IActionResult> ExternalLoginCallback(string returnUrl = null, string remoteError = null)
{
    //await _interaction.GetAuthorizationContextAsync(returnUrl);
    
    if (returnUrl == null)
    {
        throw new SecurityException("returnUrl未提供");
    }

    if (remoteError != null)
    {
        throw new SecurityException(remoteError);
    }

    var info = await _signInManager.GetExternalLoginInfoAsync();
    if (info == null)
    {
        throw new SecurityException("未找到外部登录信息");
    }

    var email = info.Principal.FindFirstValue("preferred_username");
    if (string.IsNullOrEmpty(email))
    {
        throw new SecurityException("未找到用户邮箱");
    }

    var userFound = await _userManager.FindByEmailAsync(email);
    if (userFound == null)
    {
        throw new SecurityException("系统中未找到该用户");
    }

    if (!userFound.IsActive)
        throw new SecurityException("该用户已被禁用");

    // 后续逻辑省略...
}

解决方案

1. 自定义returnUrl校验逻辑,兼容白名单域名

IdentityServer4默认的IsLocalUrl仅认可相对路径或同域名绝对路径,我们可以自定义校验方法结合白名单:

private bool IsValidReturnUrl(string returnUrl)
{
    // 白名单域名列表,按需添加生产环境域名
    var allowedDomains = new List<string> 
    { 
        "localhost:3000", 
        "your-production-domain.com" 
    };

    // 先检查是否为本地路径
    if (Url.IsLocalUrl(returnUrl))
    {
        return true;
    }

    // 解析绝对URL,校验域名是否在白名单内
    if (Uri.TryCreate(returnUrl, UriKind.Absolute, out var uri))
    {
        return allowedDomains.Contains(uri.Authority);
    }

    return false;
}

在ExternalLoginCallback中替换原有校验逻辑:

if (!IsValidReturnUrl(returnUrl))
{
    throw new SecurityException("非法的returnUrl");
}

2. 正确复用IdentityServer4的InteractionService

GetAuthorizationContextAsync()仅处理IdentityServer自身的授权上下文(如connect/authorize端点的回调),你的当前用法不符合预期。正确流程:

  • 前端直接请求IdentityServer的connect/authorize端点,携带预配置在Client的AllowedRedirectUris白名单中的redirect_uri。
  • IdentityServer会自动校验redirect_uri的合法性,无需手动处理。
  • 登录回调完成用户认证后,调用GetAuthorizationContextAsync()获取授权上下文,再重定向到合法的redirect_uri。

3. 实现短码映射方案

满足“用短码替代直接URL”的需求,可通过数据库存储映射关系:

  1. 创建映射实体与表:
public class ReturnUrlMapping
{
    public string ShortCode { get; set; }
    public string ValidUrl { get; set; }
    public bool IsActive { get; set; }
}
  1. 登录时接收短码,替换为真实URL:
[HttpGet("Login")]
[AllowAnonymous]
public async Task<IActionResult> Login(string returnCode)
{
    var mapping = await _dbContext.ReturnUrlMappings.FirstOrDefaultAsync(m => m.ShortCode == returnCode && m.IsActive);
    if (mapping == null)
    {
        throw new SecurityException("无效的returnCode");
    }
    return await ExternalLogin("oidc", mapping.ValidUrl);
}
  1. 在ExternalLoginCallback中校验真实URL是否在白名单内,再执行重定向。

内容的提问来源于stack exchange,提问作者nop

相关产品推荐
方舟 Agent Plan

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

最近更新时间:2026.06.12 22:00:13