【问题标题】:Different authentication schema (Windows, Bearer) for each route每个路由的不同身份验证架构(Windows、Bearer)
【发布时间】:2018-10-26 16:40:31
【问题描述】:

我需要将使用 Windows 身份验证的单点登录添加到我的 Intranet Angular Web 应用程序(托管在 IIS 上),该应用程序使用 JWT Bearer 令牌进行身份验证。控制器使用[Authorize] 属性进行保护,并且 JWT Bearer 令牌身份验证正在工作。所有的控制器都暴露在api/ 路由下。

这个想法是在sso/ 路由下发布一个新的SsoController,它应该使用Windows 身份验证进行保护,并公开一个WindowsLogin 操作,该操作为应用程序返回一个有效的不记名令牌。

当我使用 ASP.net Web Forms 时,它非常简单,您只需在 web.config/system.webServer 部分启用 Windows 身份验证,在 system.web 部分禁用它在应用程序范围内,然后在下面再次启用它<location path="sso"> 标签。这样,ASP.net 仅为 sso 路由下的请求生成 NTLM/Negotiate 质询。

我几乎可以正常工作 - SsoController 获取 Windows 用户名并创建 JWT 令牌就好了,但管道仍在为 all HTTP 401 生成 WWW-Authenticate: NTLMWWW-Authenticate: Negotiate 标头响应,而不仅仅是sso 路由下的响应。

我如何告诉管道我只希望对所有 api/ 请求进行匿名或不记名身份验证?

提前感谢您的帮助。

程序.cs

public static IWebHostBuilder CreateWebHostBuilder(string[] args) =>
  WebHost.CreateDefaultBuilder(args)
    .UseStartup<Startup>()
    .UseIISIntegration();

Startup.cs

public void ConfigureServices(IServiceCollection services)
{
    // Set up data directory
    services.AddDbContext<AuthContext>(options =>
        options.UseSqlServer(Configuration.GetConnectionString("AuthContext")));

    services.AddAuthentication(IISDefaults.AuthenticationScheme);
    services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
        .AddJwtBearer(options =>
        {
            options.TokenValidationParameters = new TokenValidationParameters
            {
                ValidateIssuer = true,
                ValidateAudience = true,
                ValidateLifetime = true,
                ValidateIssuerSigningKey = true,

                ValidIssuer = "AngularWebApp.Web",
                ValidAudience = "AngularWebApp.Web.Client",
                IssuerSigningKey = _signingKey,
                ClockSkew = TimeSpan.Zero   //the default for this setting is 5 minutes
            };
            options.Events = new JwtBearerEvents
            {
                OnAuthenticationFailed = context =>
                {
                    if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
                    {
                        context.Response.Headers.Add("Token-Expired", "true");
                    }
                    return Task.CompletedTask;
                }
            };
        });

    services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_1);

    // In production, the Angular files will be served from this directory
    services.AddSpaStaticFiles(configuration =>
    {
        configuration.RootPath = "ClientApp/dist";
    });
}

public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
    if (env.IsDevelopment())
    {
        app.UseDeveloperExceptionPage();
    }
    else
    {
        app.UseExceptionHandler("/Error");
        app.UseHsts();
    }

    app.UseHttpsRedirection();
    app.UseStaticFiles();
    app.UseSpaStaticFiles();
    app.UseAuthentication();

    app.UseWhen(context => context.Request.Path.StartsWithSegments("/sso"),
        builder => builder.UseMiddleware<WindowsAuthMiddleware>());

    app.UseMvc(routes =>
    {
        routes.MapRoute(
            name: "default",
            template: "{controller}/{action=Index}/{id?}");
    });

    app.UseSpa(spa =>
    {
        // To learn more about options for serving an Angular SPA from ASP.NET Core,
        // see https://go.microsoft.com/fwlink/?linkid=864501

        spa.Options.SourcePath = "ClientApp";

        if (env.IsDevelopment())
        {
            spa.UseAngularCliServer(npmScript: "start");
        }
    });
}

WindowsAuthMiddleware.cs

public class WindowsAuthMiddleware
{
    private readonly RequestDelegate next;

    public WindowsAuthMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        if (!context.User.Identity.IsAuthenticated)
        {
            await context.ChallengeAsync(IISDefaults.AuthenticationScheme);
            return;
        }

        await next(context);
    }
}

web.config

<system.webServer>
  <aspNetCore processPath="%LAUNCHER_PATH%" arguments="%LAUNCHER_ARGS%" stdoutLogEnabled="false" stdoutLogFile=".\logs\stdout" forwardWindowsAuthToken="true"/>
  <security>
    <authentication>
      <anonymousAuthentication enabled="true" />
      <windowsAuthentication enabled="true" />
    </authentication>
  </security>
</system.webServer>

【问题讨论】:

标签: iis asp.net-core windows-authentication asp.net-core-2.1


【解决方案1】:

所以,我在过去几天里调查了这个问题,我得到了一个可行的解决方案——如果有点老套的话。

事实证明,主要问题是 IIS 将为应用程序发送的所有 401 响应处理 Windows 身份验证协商。只要您在 IIS(或system.webServer 部分)中启用 Windows 身份验证,就会在较低级别完成此操作,但我无法找到绕过此行为的方法。实际上,我使用经典的 Web 表单应用程序进行了测试,它的工作原理相同 - 我从未注意到这一点的原因是经典的表单身份验证很少生成 401 响应,而是使用重定向 (30x) 将用户带到登录页面。

这给了我一个想法:我可以将另一个中间件添加到管道中,将授权基础架构生成的 401 响应重写为另一个很少使用的 HTTP 代码,并在我的客户端 Angular 应用程序中检测到它以使其表现为 401(通过刷新访问令牌或拒绝路由器导航等)。我使用了 HTTP 错误 418“我是茶壶”,因为它是现有但未使用的代码。代码如下:

替换Http401StatusCodeMiddleware.cs

public class ReplaceHttp401StatusCodeMiddleware
{
    private readonly RequestDelegate next;

    public ReplaceHttp401StatusCodeMiddleware(RequestDelegate next)
    {
        this.next = next;
    }

    public async Task Invoke(HttpContext context)
    {
        await next(context);

        if (context.Response.StatusCode == 401)
        {
            // Replace all 401 responses, except the ones under the /sso paths
            // which will let IIS trigger the Windows Authentication mechanisms
            if (!context.Request.Path.StartsWithSegments("/sso"))
            {
                context.Response.StatusCode = 418;
                context.Response.Headers["X-Original-HTTP-Status-Code"] = "401";
            }
        }
    }
}

Startup.cs

public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
{
    ...

    // Enable the SSO login using Windows Authentication
    app.UseWhen(
            context => context.Request.Path.StartsWithSegments("/sso"),
            builder => builder.UseMiddleware<WindowsAuthMiddleware>());
    app.UseMiddleware<ReplaceHttp401StatusCodeMiddleware>();

    ...
}

中间件还会在响应中注入原始状态码以供进一步参考。

我还将 Mickaël Derriey 的建议应用到我的代码中以使用授权策略,因为它使控制器更清洁,但解决方案没有必要工作。

【讨论】:

    【解决方案2】:

    欢迎来到 StackOverflow!这是你在这里遇到的一个有趣的问题。 首先,我声明我没有测试此答案中的任何内容。

    使用授权策略驱动身份验证来源

    我喜欢您创建的 WindowsAuthMiddleware 背后的想法,以及如果 URL 以 /sso 开头,它是如何有条件地插入管道的。

    MVC 与授权系统集成,并提供与授权策略相同的功能。结果是一样的,并且让您不必编写低级代码。

    您可以在ConfigureServices 方法中定义授权策略。在你的情况下,如果我没记错的话,有两个政策:

    • 所有对/sso的请求都应该通过Windows认证;和
    • 所有其他请求都应使用 JWT 进行身份验证
    services.AddAuthorization(options =>
    {
        options.AddPolicy("Windows", new AuthorizationPolicyBuilder()
            .AddAuthenticationSchemes(IISDefaults.AuthenticationScheme)
            .RequireAuthenticatedUser()
            .Build());
    
        options.AddPolicy("JWT", new AuthorizationPolicyBuilder()
            .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme)
            .RequireAuthenticatedUser()
            .Build());
    });
    

    然后,您可以在用于装饰控制器和/或操作的 [Authorize] 属性中按名称引用这些策略。

    [Authorize("Windows")]
    public class SsoController : Controller
    {
        // Actions
    }
    
    [Authorize("JWT")]
    public class ApiController : Controller
    {
        // Actions
    }
    

    这样做意味着 Windows 身份验证处理程序将不会针对 /api 请求运行,因此响应不应包含 WWW-Authenticate: NTLM 和 WWW-Authenticate: Negotiate 标头。

    取消所有请求的自动认证

    当您将身份验证方案作为AddAuthentication 的参数传递时,这意味着身份验证中间件将尝试针对该方案对每个请求进行身份验证。

    当您有一个身份验证方案时,这很有用,但在这种情况下,您可以考虑将其删除,因为即使对/sso 的请求,JWT 处理程序也会分析令牌请求。

    两次调用AddAuthentication

    你应该只给AddAuthentication打一个电话:

    • 第一个将 IIS 身份验证方案设置为默认值,因此处理程序应在每个请求上运行;
    • 第二次调用会覆盖该设置并将 JWT 方案设置为默认方案

    告诉我你的情况!

    【讨论】:

    • 谢谢 Mickaël,我很快就会研究政策机制。现在我解决了 Windows 身份验证问题(请参阅下面的回复),是时候了解如何处理角色和各种权限了,我认为策略将对此有所帮助!
    • 关于上述问题的快速问题 - 您如何使用此模式处理 jwt 的必要参数?我正在网上查看是否有 .AddJwtBearer 的替代品(它存在于 AddAuthentication 但在 AddAuthorization 中没有位置),我没有想出任何东西。如果我尝试在没有任何描述符的情况下运行它,我会在随后的 API 调用中收到 500 个服务器错误。
    猜你喜欢
    • 1970-01-01
    • 2012-08-24
    • 1970-01-01
    • 2016-09-05
    • 2020-01-16
    • 2016-09-29
    • 1970-01-01
    • 2021-03-13
    • 1970-01-01
    相关资源
    最近更新 更多