【问题标题】:How to implement AD Group based authorization globally in asp.net core 2.x web application?如何在 asp.net core 2.x Web 应用程序中全局实现基于 AD 组的授权?
【发布时间】:2018-10-11 18:30:09
【问题描述】:

我想知道是否有人可以为我指出一个方向或一个示例,其中包含完整的代码让我得到一个整体的想法?

谢谢。

更新: 我在 Startup.cs 中只有以下代码,并确保在 launchSettings.json 中 windowsAutication 为 true。

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddMvc(config =>
    {
        var policy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         //.RequireRole(@"Departmental - Information Technology - Development")   // Works
                         .RequireRole(@"*IT.Center of Excellence.Digital Workplace")              // Error
                         .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    });

}

我想我已启用身份验证并尝试授权指定 AD 组中的用户在全局级别访问应用程序。

如果我使用注释的 RequireRole 它可以工作,但使用未注释的 RequireRole 它会给我这个错误: Win32Exception: 主域和信任域之间的信任关系失败。

堆栈的顶行显示: System.Security.Principal.NTAccount.TranslateToSids(IdentityReferenceCollection sourceAccounts, out bool someFailed)

知道为什么吗?

我对以上更新的理解

RequireRole 中指定的组名似乎是电子邮件分发列表而不是安全组。如果我使用其他一些 AD 组,它可以工作,但会出现这个新错误:

InvalidOperationException: 没有指定 authenticationScheme,也没有找到 DefaultForbidScheme。

如果我在 Startup.cs 的 ConfigureServices 中添加 IIS 默认 authenticationScheme

services.AddAuthentication(IISDefaults.AuthenticationScheme);

它给了我一个 HTTP 403 页面:该网站拒绝显示此网页

所以这是最后的代码:

public void ConfigureServices(IServiceCollection services)
{
    services.AddMvc();

    services.AddAuthentication(IISDefaults.AuthenticationScheme);

    services.AddMvc(config =>
    {
        var policy = new AuthorizationPolicyBuilder()
                         .RequireAuthenticatedUser()
                         .RequireRole(@"Departmental - Information Technology - Development") // AD security group
                         .Build();
        config.Filters.Add(new AuthorizeFilter(policy));
    });

}

如果我理解错误,请纠正我。谢谢。

【问题讨论】:

标签: asp.net-core authorization asp.net-core-2.0 asp.net-authorization asp.net-core-mvc-2.0


【解决方案1】:

选项 1:Windows 身份验证

您可以为 Intranet 应用程序打开 Windows 身份验证。阅读文档here。您可以通过this 之类的操作来检查用户是否属于某个角色/组。

在此之前,您可以通过在命令提示符下执行gpresult /R 来检查您的计算机加入的组信息。请参阅this post 了解更多信息。

User.IsInRole("xxxx")  // this should return True for any group listed up there

如果您不需要获取任何与 Windows 相关的信息,则无需将当前主体转换为 Windows 主体。

如果你想得到所有组的列表,你仍然需要查询你的AD。

警告:

与选项 2 方法相比,有时我看到在计算机上使用 gpresult /R 的结果中没有显示某些组。这就是为什么有时当您执行User.IsInRole() 时它会返回错误。我仍然不知道为什么会这样。

选项 2:使用 AD 查找的表单身份验证

Windows 身份验证仅提供有关用户和 AD 组的少量信息。有时这就够了,但大多数时候还不够。

您还可以使用常规表单身份验证并与下面的 AD 对话并发出 cookie。这样,虽然用户需要使用他们的 Windows 凭据和密码登录您的应用,但您可以完全控制 AD 信息。

您不想手动编写所有内容。幸运的是,有一个库 Novell.Directory.Ldap.NETStandard 可以提供帮助。你可以在 NuGet 中找到它。

定义您需要从 AD 获得什么的接口,以及登录协议:

namespace DL.SO.Services.Core
{
    public interface IAppUser
    {
        string Username { get; }
        string DisplayName { get; }
        string Email { get; }
        string[] Roles { get; }
    }

    public interface IAuthenticationService
    {
        IAppUser Login(string username, string password);
    }
}

AppUser 实现:

using DL.SO.Services.Core;

namespace DL.SO.Services.Security.Ldap.Entities
{
    public class AppUser : IAppUser
    {
        public string Username { get; set; }
        public string DisplayName { get; set; }
        public string Email { get; set; }
        public string[] Roles { get; set; }
    }
}

用于从 appsettings.json 映射值的 Ldap 配置对象:

namespace DL.SO.Services.Security.Ldap
{
    public class LdapConfig
    {
        public string Url { get; set; }
        public string BindDn { get; set; }
        public string Username { get; set; }
        public string Password { get; set; }
        public string SearchBase { get; set; }
        public string SearchFilter { get; set; }
    }
}

LdapAuthenticationService 实现:

using Microsoft.Extensions.Options;
using Novell.Directory.Ldap;
using System;
using System.Linq;
using System.Text.RegularExpressions;
using DL.SO.Services.Core;
using DL.SO.Services.Security.Ldap.Entities;

namespace DL.SO.Services.Security.Ldap
{
    public class LdapAuthenticationService : IAuthenticationService
    {
        private const string MemberOfAttribute = "memberOf";
        private const string DisplayNameAttribute = "displayName";
        private const string SAMAccountNameAttribute = "sAMAccountName";
        private const string MailAttribute = "mail";

        private readonly LdapConfig _config;
        private readonly LdapConnection _connection;

        public LdapAuthenticationService(IOptions<LdapConfig> configAccessor)
        {
            _config = configAccessor.Value;
            _connection = new LdapConnection();
        }

        public IAppUser Login(string username, string password)
        {
            _connection.Connect(_config.Url, LdapConnection.DEFAULT_PORT);
            _connection.Bind(_config.Username, _config.Password);

            var searchFilter = String.Format(_config.SearchFilter, username);
            var result = _connection.Search(
                _config.SearchBase,
                LdapConnection.SCOPE_SUB, 
                searchFilter,
                new[] { 
                    MemberOfAttribute, 
                    DisplayNameAttribute, 
                    SAMAccountNameAttribute, 
                    MailAttribute 
                }, 
                false
            );

            try
            {
                var user = result.next();
                if (user != null)
                {
                    _connection.Bind(user.DN, password);
                    if (_connection.Bound)
                    {
                        var accountNameAttr = user.getAttribute(SAMAccountNameAttribute);
                        if (accountNameAttr == null)
                        {
                            throw new Exception("Your account is missing the account name.");
                        }

                        var displayNameAttr = user.getAttribute(DisplayNameAttribute);
                        if (displayNameAttr == null)
                        {
                            throw new Exception("Your account is missing the display name.");
                        }

                        var emailAttr = user.getAttribute(MailAttribute);
                        if (emailAttr == null)
                        {
                            throw new Exception("Your account is missing an email.");
                        }

                        var memberAttr = user.getAttribute(MemberOfAttribute);
                        if (memberAttr == null)
                        {
                            throw new Exception("Your account is missing roles.");
                        }

                        return new AppUser
                        {
                            DisplayName = displayNameAttr.StringValue,
                            Username = accountNameAttr.StringValue,
                            Email = emailAttr.StringValue,
                            Roles = memberAttr.StringValueArray
                                .Select(x => GetGroup(x))
                                .Where(x => x != null)
                                .Distinct()
                                .ToArray()
                        };
                    }
                }
            }
            finally
            {
                _connection.Disconnect();
            }

            return null;
        }

        private string GetGroup(string value)
        {
            Match match = Regex.Match(value, "^CN=([^,]*)");
            if (!match.Success)
            {
                return null;
            }

            return match.Groups[1].Value;
        }
    }
}

appsettings.json 中的配置(只是一个例子):

{
    "ldap": {
       "url": "[YOUR_COMPANY].loc",
       "bindDn": "CN=Users,DC=[YOUR_COMPANY],DC=loc",
       "username": "[YOUR_COMPANY_ADMIN]",
       "password": "xxx",
       "searchBase": "DC=[YOUR_COMPANY],DC=loc",
       "searchFilter": "(&(objectClass=user)(objectClass=person)(sAMAccountName={0}))"
    },
    "cookies": {
        "cookieName": "cookie-name-you-want-for-your-app",
        "loginPath": "/account/login",
        "logoutPath": "/account/logout",
        "accessDeniedPath": "/account/accessDenied",
        "returnUrlParameter": "returnUrl"
    }
}

为应用设置身份验证(也可以是授权):

namespace DL.SO.Web.UI
{
    public class Startup
    {
        private readonly IHostingEnvironment _currentEnvironment;
        public IConfiguration Configuration { get; private set; }

        public Startup(IConfiguration configuration, IHostingEnvironment env)
        {
            _currentEnvironment = env;
            Configuration = configuration;
        }

        public void ConfigureServices(IServiceCollection services)
        { 
            // Authentication service
            services.Configure<LdapConfig>(this.Configuration.GetSection("ldap"));
            services.AddScoped<IAuthenticationService, LdapAuthenticationService>();

            // MVC
            services.AddMvc(config =>
            {
                // Requiring authenticated users on the site globally
                var policy = new AuthorizationPolicyBuilder()
                    .RequireAuthenticatedUser()

                    // You can chain more requirements here
                    // .RequireRole(...) OR
                    // .RequireClaim(...) OR
                    // .Requirements.Add(...)         

                    .Build();
                config.Filters.Add(new AuthorizeFilter(policy));
            });

            services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();

            // Authentication
            var cookiesConfig = this.Configuration.GetSection("cookies")
                .Get<CookiesConfig>();
            services.AddAuthentication(
                CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(options =>
                {
                    options.Cookie.Name = cookiesConfig.CookieName;
                    options.LoginPath = cookiesConfig.LoginPath;
                    options.LogoutPath = cookiesConfig.LogoutPath;
                    options.AccessDeniedPath = cookiesConfig.AccessDeniedPath;
                    options.ReturnUrlParameter = cookiesConfig.ReturnUrlParameter;
                });

            // Setup more authorization policies as an example.
            // You can use them to protected more strict areas. Otherwise
            // you don't need them.
            services.AddAuthorization(options =>
            {
                options.AddPolicy("AdminOnly", 
                    policy => policy.RequireClaim(ClaimTypes.Role, "[ADMIN_ROLE_OF_YOUR_COMPANY]"));

                // More on Microsoft documentation
                // https://docs.microsoft.com/en-us/aspnet/core/security/authorization/policies?view=aspnetcore-2.1
            });
        }

        public void Configure(IApplicationBuilder app)
        {
            app.UseAuthentication();
            app.UseMvc(...);
        }  
    }
}

如何使用身份验证服务对用户进行身份验证:

namespace DL.SO.Web.UI.Controllers
{
    public class AccountController : Controller
    {
        private readonly IAuthenticationService _authService;

        public AccountController(IAuthenticationService authService)
        {
            _authService = authService;
        }

        [AllowAnonymous]
        [HttpPost]
        public async Task<IActionResult> Login(LoginViewModel model)
        {
            if (ModelState.IsValid)
            {
                try
                {
                    var user = _authService.Login(model.Username, model.Password);

                    // If the user is authenticated, store its claims to cookie
                    if (user != null)
                    {
                        var userClaims = new List<Claim>
                        {
                            new Claim(ClaimTypes.Name, user.Username),
                            new Claim(CustomClaimTypes.DisplayName, user.DisplayName),
                            new Claim(ClaimTypes.Email, user.Email)
                        };

                        // Roles
                        foreach (var role in user.Roles)
                        {
                            userClaims.Add(new Claim(ClaimTypes.Role, role));
                        }

                        var principal = new ClaimsPrincipal(
                            new ClaimsIdentity(userClaims, _authService.GetType().Name)
                        );

                        await HttpContext.SignInAsync(                            
                          CookieAuthenticationDefaults.AuthenticationScheme, 
                            principal,
                            new AuthenticationProperties
                            {
                                IsPersistent = model.RememberMe
                            }
                        );

                        return Redirect(Url.IsLocalUrl(model.ReturnUrl)
                            ? model.ReturnUrl
                            : "/");
                    }

                    ModelState.AddModelError("", @"Your username or password
                        is incorrect. Please try again.");
                }
                catch (Exception ex)
                {
                    ModelState.AddModelError("", ex.Message);
                }
            }
            return View(model);
        }
    }
}

如何读取存储在声明中的信息:

public class TopNavbarViewComponent : ViewComponent
{
    private readonly IHttpContextAccessor _httpContextAccessor;

    public TopNavbarViewComponent(IHttpContextAccessor httpContextAccessor)
    {
        _httpContextAccessor = httpContextAccessor;
    }

    public async Task<IViewComponentResult> InvokeAsync()
    {
        string loggedInUsername = _httpContextAccessor.HttpContext.User.Identity.Name;

        string loggedInUserDisplayName = _httpContextAccessor.HttpContext.User.GetDisplayName();

       ...
       return View(vm);
    }
}

ClaimsPrincipal 的扩展方法:

namespace DL.SO.Framework.Mvc.Extensions
{
    public static class ClaimsPrincipalExtensions
    {
        public static Claim GetClaim(this ClaimsPrincipal user, string claimType)
        {
            return user.Claims
                .SingleOrDefault(c => c.Type == claimType);
        }

        public static string GetDisplayName(this ClaimsPrincipal user)
        {
            var claim = GetClaim(user, CustomClaimTypes.DisplayName);

            return claim?.Value;
        }

        public static string GetEmail(this ClaimsPrincipal user)
        {
            var claim = GetClaim(user, ClaimTypes.Email);

            return claim?.Value;
        }
    }
}

如何使用策略授权:

namespace DL.SO.Web.UI.Areas.Admin.Controllers
{
    [Area("admin")]
    [Authorize(Policy = "AdminOnly")]
    public abstract class AdminControllerBase : Controller {}
}

奖金

您可以下载AD Explorer from Microsoft,以便可视化您的公司广告。

哎呀。我本来打算先给出一些东西作为开始,但我最终写了一篇很长的帖子。

【讨论】:

  • 谢谢大卫。因此,据我了解,您的实现需要每次用户尝试访问应用程序时在 AD 中执行搜索/查找,并且自定义代码将在全局级别进行检查,对吗?
  • @XiaoHan:只有当用户没有被认证的时候?如果用户已通过身份验证,则声明已存储在 cookie 中。现在我明白了,如果 AD 管理员更改用户的信息,它们将不会被刷新。因此,最好将 cookie 的过期时间设置为合理的短时间,以便应用程序要求用户尽快重新进行身份验证并更新声明。
  • 感谢您的回复。我还没有走那么远。仍在探索处理内部应用程序的 AD 组的常见方案有哪些选项。我猜身份验证已经得到处理,因为它是内部的,并且 .net 核心具有内置功能。正确的?如果我错了,请纠正我,为了归档我的目标,我需要编写自定义代码来查找用户(例如 [域名]\smith)是否在 AD 组(例如 [域名]\dev 组),对吗?到目前为止,我在 ConfigureServices 中只有 AuthorizationPolicyBuilder 和 AddAuthorization 部分。
  • 查看我在帖子上的更新。是的,如果您打开 Windows 身份验证。如果您想使用常规表单身份验证,则否。稍后,您需要自己编写身份验证部分。
  • 我正在尝试选项 1,但是在顶部的原始帖子中更新了代码,我发现如果使用使用 gpresult /r 命令返回的任何 AD 组,它会给我一个新错误:InvalidOperationException:没有指定 authenticationScheme,也没有找到 DefaultForbidScheme。我猜前面的错误是由于组必须是为电子邮件分发引起的。所以为了解决这个新错误,我添加了 services.AddAuthentication(IISDefaults.AuthenticationScheme);在 ConfigureServices 中,现在我得到一个带有 HTTP 403 消息的页面网站拒绝显示此网页,猜猜这就是我想要的。
猜你喜欢
  • 2020-11-16
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
  • 2020-10-19
  • 2018-06-27
  • 2021-04-26
  • 1970-01-01
相关资源
最近更新 更多