.NET Core 3.1 WebApi 项目 + NTLM 认证

.NET Core 3.1 WebApi project + NTLM Authentication

我正在尝试迁移使用此类东西的旧 OWIN 自托管 WebApi

    var listener = (HttpListener)appBuilder.Properties["System.Net.HttpListener"];
    listener.AuthenticationSchemes = AuthenticationSchemes.IntegratedWindowsAuthentication;

到新的 .NET Core 3.1 项目。我读过 Auth

这是我的项目文件的样子

  <Project Sdk="Microsoft.NET.Sdk.Web">
    <PropertyGroup>
      <TargetFramework>netcoreapp3.1</TargetFramework>
    </PropertyGroup>
    <ItemGroup>
      <PackageReference Include="Microsoft.AspNetCore.Authentication.Negotiate" Version="3.1.2" />
      <PackageReference Include="Microsoft.AspNetCore.Mvc.NewtonsoftJson" Version="3.1.2" />
      <PackageReference Include="Newtonsoft.Json" Version="12.0.3" />
      <PackageReference Include="System.Text.Json" Version="4.7.0" />
    </ItemGroup>
  </Project>

这就是我的 launchsettings.json 的样子

{
  "iisSettings": {
    "windowsAuthentication": true,
    "anonymousAuthentication": false,
    "iisExpress": {
      "applicationUrl": "http://localhost:9600",
      "sslPort": 0
    }
  },
  "$schema": "http://json.schemastore.org/launchsettings.json",
  "profiles": {
    "IIS Express": {
      "commandName": "IISExpress",
      "launchBrowser": true,
      "launchUrl": "api/audit",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "use64Bit": true
    },
    "Audit.Core": {
      "commandName": "Project",
      "launchBrowser": true,
      "launchUrl": "api/audit",
      "environmentVariables": {
        "ASPNETCORE_ENVIRONMENT": "Development"
      },
      "applicationUrl": "http://localhost:9600"
    },
    "Azure Dev Spaces": {
      "commandName": "AzureDevSpaces",
      "launchBrowser": true
    }
  }
}

这就是我的 Startup.cs 的样子

    using Microsoft.AspNetCore.Authentication.Negotiate;
    using Microsoft.AspNetCore.Builder;
    using Microsoft.AspNetCore.Hosting;
    using Microsoft.AspNetCore.Identity;
    using Microsoft.AspNetCore.Server.HttpSys;
    using Microsoft.AspNetCore.Server.IISIntegration;
    using Microsoft.Extensions.Configuration;
    using Microsoft.Extensions.DependencyInjection;
    using Microsoft.Extensions.Hosting;
    using NLog;

    namespace xxxx
    {
        public class Startup
        {
            public Startup(IConfiguration configuration)
            {
                Configuration = configuration;
            }

            public IConfiguration Configuration { get; }

            // This method gets called by the runtime. Use this method to add services to the container.
            public void ConfigureServices(IServiceCollection services)
            {
                services.AddAuthentication(HttpSysDefaults.AuthenticationScheme);
                services.AddControllers().AddNewtonsoftJson();
            }

            // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
            public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
            {

                if (env.IsDevelopment())
                {
                    app.UseDeveloperExceptionPage();
                }

                app.UseRouting();

                app.UseAuthentication();
                app.UseAuthorization();

                app.UseEndpoints(endpoints =>
                {
                    endpoints.MapControllers();
                });
            }
        }
    }

这是 Program.cs

public class Program
{
    public static void Main(string[] args)
    {
        CreateHostBuilder(args).Build().Run();
    }

    public static IHostBuilder CreateHostBuilder(string[] args) =>
        Host.CreateDefaultBuilder(args)
            .ConfigureWebHostDefaults(webBuilder =>
            {
                webBuilder.UseStartup<Startup>()
                .UseHttpSys(options =>
                {
                    options.Authentication.Schemes = AuthenticationSchemes.NTLM;
                    options.EnableResponseCaching = false;
                    options.Authentication.AllowAnonymous = false;
                });
            })
            .ConfigureWebHost(config =>
            {
                 config.UseUrls("http://*:9600");
            });
}

我有一个自定义过滤器,我需要从中获取当前用户的身份并验证它是否具有与之关联的某个 ActiveDirectory 组。基本用法是这样的

    [ApiController]
    [Route("api/some")]
    [ControllerExceptionFilter]
    public class SomeController : ControllerBase
    {
        [HttpPost("add")]
        [ActiveDirectoryAuthorize("SomeGroup")]
        public IActionResult Add([FromBody]SomeEvent s)
        {
            var user = this.HttpContext.User.Identity;
            return Ok("cool");
        }
    }

过滤器看起来像这样

    //
    public class ActiveDirectoryAuthorizeAttribute : TypeFilterAttribute
    {
        public ActiveDirectoryAuthorizeAttribute(string groupMembership) : base(typeof(ActiveDirectoryAuthorizeFilter))
        {
            Arguments = new object[] { groupMembership };
        }
    }

    public class ActiveDirectoryAuthorizeFilter : IAuthorizationFilter
    {
        private string _groupMembership;
        public ActiveDirectoryAuthorizeFilter(string groupMembership)
        {
            _groupMembership = groupMembership;
        }

        public void OnAuthorization(AuthorizationFilterContext context)
        {
            try
            {
                Authenticate(context);
            }
            catch (InvalidWindowsUserException ex)
            {
                HandleUnauthorizedRequest(context);
            }

            catch (Exception ex)
            {
                HandleInternalServerError(context);
            }
        }

        protected void HandleUnauthorizedRequest(AuthorizationFilterContext context)
        {
            context.HttpContext.Response.Headers.Add("WWW-Authenticate", "NTLM");
            context.Result = new ContentResult
            {
                Content = "Unauthorized",
                StatusCode = (int)HttpStatusCode.Unauthorized
            };
        }

        private void HandleInternalServerError(AuthorizationFilterContext context)
        {
            context.HttpContext.Response.Headers.Add("WWW-Authenticate", "NTLM");
            context.Result = new ContentResult
            {
                Content = "Internal Server Error",
                StatusCode = (int)HttpStatusCode.InternalServerError
            };
        }

        private void Authenticate(AuthorizationFilterContext context)
        {
            var identity = context?.HttpContext?.User?.Identity;

            if (identity == null)
            {
                throw new InvalidWindowsUserException("Access denied");
            }

            EnsureAdmin(identity);
        }

        private void EnsureAdmin(IIdentity identity)
        {
            ......
        }
    }

所有这些准备就绪后,我可以启动 POSTMAN 并使用 BAD 密码发出 NTLM 请求,我得到一个 401。这是预期的

然后我编辑 POSTMAN 请求以输入正确的密码,我得到了这个,在那里我得到了 200 并从上面显示的控制器得到了 "cool" 响应

到目前为止一切都按预期工作。但是,如果我随后更改当前工作的 POSTMAN (200 ok) 请求以再次使用 BAD NTLM 密码。我期待看到 401,但当前用户只是在我的自定义过滤器中显示为仍被授权

我实际上得到了 200 OK

此行为不同于旧的基于 OWIN 的 WebApi。哪个确实识别了以下序列

  1. NTLM 密码错误,401 未经授权
  2. NTLM 密码正确,200 OK
  3. NTLM 密码错误,401 未经授权

我还需要在什么地方设置其他东西吗?有人对此有任何线索吗?

我遇到过同样的情况,我觉得是 Postman 的问题,比如 'cached' 好的密码,即使你改错了也一直在发送。

如果退出 re-enter Postman,并使用错误的密码重复上次请求,您将得到 'correct' 401 Unauthorized,即:

 1. NTLM bad password              -> 401 Unauthorized - correct
 2. NTLM good password             -> 200 OK           - correct
 3. NTLM bad password              -> 200 OK           - WRONG, as if Postman cached the good password
 4. Exit Postman - Re-enter Postman
 5. NTLM bad password (same as 3.) -> 401 Unauthorized - correct

也许它与 NTLM 认证方案有关,这意味着 'challenge',即在幕后有一个 'negotiation',有几个 Postman 透明处理的 HTTP 调用,但有些东西改密码会出错

编辑:值得一提的是,Postman 中的 NTLM 身份验证功能目前处于测试阶段