前言
久違了各位,之前錄制過IdentityServer4的基礎視頻(https://space.bilibili.com/319652230/#/),有興趣了解的童鞋可以看一下,只不過未發表成博客。我們使用IdentityServer4結和ASP.NET Identity來進行用戶的認證和授權管理,在實際項目中我們都會繼承ASP.NET Core Identity中IdentityUser類即用戶實體,並添加我們自定義的擴展屬性,在客戶端(Clients)中我們只能拿到用戶Id,但是若我們要獲取用戶中其他重要的屬性,此時相對於IdentityServer4而言則需要自定義聲明,那么在IdentityServer4中如何添加自定義聲明並在客戶端中能正確獲取到呢?本文詳細講解一下,對於IdentityServer4我也是初學者,僅僅止於知道和使用而已,若有錯誤的地方,還請大佬指正。
IdentityServer4添加自定義聲明(方式一)
首先我們繼承自IdentityUser類並添加額外的屬性比如部門Id,如下:
public class OnLineBookIdentityUser : IdentityUser { public string DepartmentId { get; set; } }
接下來我們需要將部門Id通過IdentityServer4添加到聲明中,在IdentityServer4中添加自定義聲明我們需要實現IProfileService接口,該接口有如下兩個方法。
我們實現上述兩個方法,在第一個方法中參數也就是用戶基本信息上下文中拿到用戶Id即Subject,然后我們定義用戶中的部門Id屬性為idr,並將用戶中的部門Id屬性映射到聲明的idr中,最終實現如下:
public class ProfileService : IProfileService { protected UserManager<OnLineBookIdentityUser> _userManager; public ProfileService(UserManager<OnLineBookIdentityUser> userManager) { _userManager = userManager; } public Task GetProfileDataAsync(ProfileDataRequestContext context) { var user = _userManager.GetUserAsync(context.Subject).Result; var claims = new List<Claim> { new Claim("idr", user.DepartmentId), }; context.IssuedClaims.AddRange(claims); return Task.FromResult(0); } public Task IsActiveAsync(IsActiveContext context) { var user = _userManager.GetUserAsync(context.Subject).Result; context.IsActive = true; return Task.FromResult(0); } }
接下來在注入IdentityServer4時,添加我們自定義實現的ProfileService,更多基礎請參考IdentityServer4官網以及我所錄制的IdentityServer4基礎視頻。
//注入IdentityServer4使用AspNetIdentity services.AddIdentityServer(options => { options.Authentication.CookieLifetime = TimeSpan.FromMinutes(1); }) .AddDeveloperSigningCredential() .AddAspNetIdentity<OnLineBookIdentityUser>() .... .AddProfileService<ProfileService>();
曉晨姐姐在他發表的博客文章(http://www.cnblogs.com/stulzq/p/8726002.html)中說必須還要實現IResourceOwnerPasswordValidator接口,那么在客戶端獲取到自定義聲明應該是通過調用接口的方式獲取用戶自定義聲明(不知是否理解正確或者說通過客戶端中User中Principal獲取到呢?)這里我們並未實現上述IResourceOwnerPasswordValidator接口,我們看看在客戶端是否能拿到上述我們聲明的idr呢?在客戶端我們定義如下控制器,訪問需要進行授權,並獲取我們添加的自定義聲明idr,如下:
[Route("[controller]"), Authorize] public class OrderController : Controller { private readonly IOrderService _orderService; public OrderController(IOrderService orderService) { _orderService = orderService; } [HttpGet("index")] public IActionResult Index() { var idr = User.FindFirst("idr")?.Value; return View(nameof(Index), idr); } }
並在上述index視圖中我們答應自定義聲明idr所對應的部門的值,如下:
@model string @if (Model is null) { <h1>idr is null</h1> } else { <h1>@Model.ToString()</h1> }
接下來我們通過動態gif來演示下,注意如下視頻http://localhost:5000/為IdentityServer4認證、授權服務器端。而http://localhost:5003/為客戶端。
上述我們可以看到在登錄之后重定向到客戶端,我們拿到ack即AccessToken里面有自定義聲明idr,但是當我們訪問Order/Index並未獲取到idr,這是為何,這是因為我們通過如下即ClaimPrincipal去獲取idr時,實際上是獲取的id_token里面的用戶信息,而不是AccessToken,而id_token我們看到沒有idr,所以才出現沒有獲取到的情況。
var idr = User.FindFirst("idr")?.Value;
為了解決這個問題,我們可以在通過IdentityServer4創建的Clients表中所對應的調用客戶端中的如下列設置為True即可。
接下來我們再來演示一下,此時我們將看到解析通過id_token將返回idr,並能在客戶端讀取到idr。
IdentityServer4添加自定義聲明(方式二)
除了上述方式通過實現ProfileService接口外,我們還可以通過實現自定義UserClaimsPrincipalFactory工廠類來實現,復寫CreateAsync方法來創建自定義聲明如下:
public class CustomizeUserClaimsFactory<TRole> : UserClaimsPrincipalFactory<OnLineBookIdentityUser, TRole> where TRole : class { public CustomizeUserClaimsFactory(UserManager<OnLineBookIdentityUser> userManager, RoleManager<TRole> roleManager, IOptions<IdentityOptions> optionsAccessor) : base(userManager, roleManager, optionsAccessor) { } public async override Task<ClaimsPrincipal> CreateAsync(OnLineBookIdentityUser user) { var cliamsPrincipal = await base.CreateAsync(user); var identity = cliamsPrincipal.Identities.First(); if (!identity.HasClaim(x => x.Type == "idr")) { identity.AddClaim(new Claim("idr", user.DepartmentId)); } return cliamsPrincipal; } }
然后通過創建擴展方法將上述自定義用戶聲明工廠進行注入,如下:
public static IdentityBuilder AddCustomizeUserClaimsPrincipalFactory(this IdentityBuilder builder) { var interfaceType = typeof(IUserClaimsPrincipalFactory<>); interfaceType = interfaceType.MakeGenericType(builder.UserType); var classType = typeof(CustomizeUserClaimsFactory<>); classType = classType.MakeGenericType(builder.RoleType); builder.Services.AddScoped(interfaceType, classType); return builder; }
最后在注入Identity時,添加上述自定義聲明工廠,如下:
services.AddIdentity<OnLineBookIdentityUser, IdentityRole>() .AddEntityFrameworkStores<OnLineBookDbContext>() .AddDefaultTokenProviders() .AddCustomizeUserClaimsPrincipalFactory();
總結
本節內容需要有一定IdentityServer4基礎,如若不太了解請參考官方文檔,同時針對如上在客戶端如何獲取自定義聲明,重點在於在對應客戶端表中設置AlwaysIncludeInIdToken為True才好使,並未去深究設置該列為True所產生的副作用,感謝閱讀,若有不同見解,望留下您的評論。