Angular 和 Azure AD 第三部分 - 添加基于角色的安全性





5.00/5 (1投票)
为我们的 Azure AD/Angular 网站添加基于角色的安全
下载 Angular_Azure_AD.zip
引言
到目前为止,这都相当容易,我们有一个 Angular 网站,其 API 由 Azure AD 保护,而且工作量不大。最后这个步骤,支持角色,无疑是最复杂的。
背景
这是本系列的最后一篇文章。如果您想跟着我边解释边编写代码,应该从上一篇文章的源代码开始。完成后,您将能够在 Angular 网站中使用 Azure AD 实现基于角色的安全。虽然我们可以返回用户的角色并使前端只显示对用户有效的选项,但主要的保护是在 API 上,单一页面应用程序会将所有源代码发送到客户端,而我们主要确保的是敏感数据不会被未经授权的人看到。
创建角色
在 Azure AD 中,角色映射到所谓的“组”。因此,第一步是在 Azure 中创建一些组,转到 Azure AD,点击“组”并创建一个新的。我的组名为“WebsiteUser”和“WebsiteAdmin”,如果您正在按照代码进行操作。
我不确定 Office 365 组是做什么的,但您需要创建安全组。
一旦创建了一个组,就需要从详细信息中获取其对象 ID。您可以重命名您的组,但您的代码将继续工作,因为您总是通过对象 ID(而不是名称)来处理组。
从这里开始,如何将用户添加到组中就非常显而易见了。创建至少两个组并将它们添加到您的用户中。
访问角色
现在,您还需要完成最后一步。转到 Azure AD 中的应用程序并查看清单。
默认情况下,它显示“"groupMembershipClaims": null”。将其更改为“All”,现在您的令牌将包含您所属组的 ID。我的令牌,解码后(JWT 是三个 Base64 编码的字符串,网络上有解码器),如下所示。
令牌当然是以标头形式传递在我们的受保护请求中的。
后端自定义身份验证
.NET Core 建议不要编写自定义身份验证,而是编写策略。这是一个普遍正确的想法,但对于这种情况,我们还是要编写一个自定义身份验证方法。
在您的项目中添加一个名为 CustomAuth 的文件夹。
创建一个名为 'Constants' 的类。在其中,放入这个值:
public static class Constants { public const string WebsiteUser = "WebsiteUser"; public const string WebsiteAdmin = " WebsiteAdmin"; public const string Groups = "groups"; }
其中前两个值是您的组名。这些用于将组名映射到 ID,因此它们需要与名称完全匹配。
最后一个类是为映射权限创建常量。现在我们需要一个类来存储我们角色的 GUID(这将作为配置从我们的 appsettings 中获取)。
public class Groups { public Guid WebsiteUser { get; set; } public Guid WebsiteAdmin { get; set; } }
创建一个名为 GroupAuth 的类。将此代码添加到其中
public class GroupAuthRequirementAttribute : TypeFilterAttribute { public GroupAuthRequirementAttribute(string claimType, string claimValue) : base(typeof(GroupAuthFilter)) { Arguments = new object[] { new Claim(claimType, claimValue) }; } }
这是我们将要放在要保护的方法上的属性。提供两个参数意味着我们可以过滤任何我们想要的属性,尽管我们可以简化代码并始终过滤“group”,但此代码有可能以更通用的方式使用。
现在将您的组 ID 添加到 appsettings.json 中。这里的名称需要与 Groups 类中的名称匹配,我们使用这些设置来填充该类的实例。
"Groups": { "WebsiteUser": "3d134942-e73f-xxxx-xxxx-xxxxxxxx", "WebsiteAdmin": "698c5328-73f4-xxxx-xxxx-xxxxxxxx" },
现在开始编码。
public class GroupAuthFilter : IAuthorizationFilter { readonly Claim _claim; readonly IConfiguration Configuration; public GroupAuthFilter(Claim claim, IConfiguration configuration) { _claim = claim; Configuration = configuration; } }
IAuthorizationFilter 接口用于创建身份验证过滤器。配置被注入,这在 .Net Core 中都适用。Claim 是由我们的 Attribute 类创建的。这是类的基础部分,现在让我们让它做些事情。
public void OnAuthorization(AuthorizationFilterContext context) { Groups groups = Configuration.GetSection("Groups").Get<Groups>(); var property = groups.GetType().GetProperty(_claim.Value); if(property == null) { // Invalid value context.Result = new ForbidResult(); return; } Guid value = Guid.Empty; value = (Guid)property.GetValue(groups); var hasClaim = context.HttpContext.User.Claims.Any(c => c.Type == Constants.Groups && c.Value == value.ToString()); if(!hasClaim) { context.Result = new ForbidResult(); } }
逐行解释
Groups groups = Configuration.GetSection("Groups").Get<Groups>();
使用配置中的 Groups 对象的值填充我们的 Groups 类。
var property = groups.GetType().GetProperty(_claim.Value); if(property == null) { // Invalid value context.Result = new ForbidResult(); return; } Guid value = Guid.Empty; value = (Guid)property.GetValue(groups);
使用反射根据我们在属性中传入的属性名称来获取 GUID。
var hasClaim = context.HttpContext.User.Claims.Any(c => c.Type == Constants.Groups && c.Value == value.ToString()); if(!hasClaim) { context.Result = new ForbidResult(); }
HttpContext.User 对象是从我们传入的令牌填充的。我们需要搜索其声明中是否存在带有组 ID 的组声明。如果声明不存在,则请求失败。
现在是坏消息……
这不是很棒吗?简洁,简单……可惜,它不起作用。关键在于,如果您是超过 5 个组的成员,您的令牌将不再包含这些 GUID,它只会包含一个属性,通知您用户属于某个组(因此,超过 5 个)。
要解决此问题,您需要添加一个 NuGet 库,名为 Microsoft.Identity.Client。然后您需要添加此代码。
if (!hasClaim) { // This could mean the user has more than 5 groups and we need to ask Azure AD for them. var options = Configuration.GetSection("AzureAD").Get<AzureAdOptions>(); var app = ConfidentialClientApplicationBuilder.Create(options.ClientId) .WithClientSecret(options.ClientSecret) .WithAuthority(new Uri(options.Authority)) .Build(); // With client credentials flows the scopes is ALWAYS of the shape "resource/.default", as the // application permissions need to be set statically (in the portal or by PowerShell), and then granted by // a tenant administrator string[] scopes = new string[] { "https://graph.microsoft.com/.default" }; AuthenticationResult result = null; try { result = app.AcquireTokenForClient(scopes) .ExecuteAsync().Result; } catch (MsalServiceException ex) when (ex.Message.Contains("AADSTS70011")) { // Invalid scope. The scope has to be of the form "https://resourceurl/.default" // Mitigation: change the scope to be as expected context.Result = new ForbidResult(); return; } catch(Exception ex) { context.Result = new ForbidResult(); return; } var httpClient = new HttpClient(); var defaultRequestHeaders = httpClient.DefaultRequestHeaders; if (defaultRequestHeaders.Accept == null || !defaultRequestHeaders.Accept.Any(m => m.MediaType == "application/json")) { httpClient.DefaultRequestHeaders.Accept.Add(new MediaTypeWithQualityHeaderValue("application/json")); } defaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", result.AccessToken); var body = "{ \"securityEnabledOnly\" : false } "; try { var userId = context.HttpContext.User.Claims.Where(e => e.Type == "http://schemas.microsoft.com/identity/claims/objectidentifier").FirstOrDefault()?.Value; HttpResponseMessage response = httpClient.PostAsync($"https://graph.microsoft.com/v1.0/users/{userId}/getMemberGroups", new StringContent(body, Encoding.UTF8, "application/json")).Result; if (response.IsSuccessStatusCode) { string json = response.Content.ReadAsStringAsync().Result; JObject items = JsonConvert.DeserializeObject(json) as JObject; foreach (var groupId in items["value"].Children()) { if (value.ToString() == groupId.Value<string>()) { // It worked so return; return; } } } else { string json = response.Content.ReadAsStringAsync().Result; } } catch (Exception ex) { } context.Result = new ForbidResult(); }
Microsoft Graph API 允许您查询用户属于哪些组。但是,您在登录时获得的令牌不能用于 Graph API。您需要请求一个特殊的令牌才能进行这些请求。此代码的前半部分使用此新库来实现此目的。后半部分使用此新令牌对 Graph API 进行标准 HTTP 请求。我们从新令牌中提取用户 ID,并使用它来形成所需的 URL。
两个注意事项
首先,如果您的 Azure appsettings.json 中没有 ClientSecret,您需要添加一个。之前的代码不需要这个 secret,但调用 Graph API 需要。
其次,您还需要为您的 Azure AD 应用程序添加权限,以允许用户请求用户所属的组。
此 API 的文档在此处
https://docs.microsoft.com/en-us/graph/api/user-getmembergroups?view=graph-rest-1.0
您需要的权限是
Application | Group.Read.All, Directory.Read.All, Directory.ReadWrite.All |
完成后不要忘记按下“授予管理员同意”按钮。这些权限可以设置在显示的两个位置,我认为您需要的是应用程序权限(我确实必须添加它们才能开始工作)。
有两种方法可以测试这一点。首先,给自己分配超过 5 个组。其次,设置一个断点并强制二次代码每次都运行。
改进空间
此代码效果很好,但它为 Graph API 获取令牌并在每次请求时调用 API。缓存令牌或组列表会更有效。缺点是,某人可能从一个组中被移除,但在缓存过期之前仍能保留其访问权限。鉴于这是一个基于特定业务规则的复杂问题,我没有尝试回答它,但我绝对不建议在生产环境中使用此代码。
关注点
这里主要令人沮丧的一点是,Azure 应用程序不允许您指定您关心的具体组,然后始终只提供那些组 ID,并随您的令牌一起提供。能够请求 Graph API 获取您的组所花费的精力比应有的要多,并且当我在纯 .NET 中执行此操作时,工作量还会进一步增加,因为我使用的库不存在。
历史
V1.0