# 了解IdentityServer4的原理 首先在请求管道里有一个有关IdentityServer4的中间件,通过这个中间件来设施所有有关IdentityServer4的逻辑。 ``` app.UseIdentityServer(); ``` 在DI容器里通过`services.AddDbContext()`有了上下文,通过`services.AddIdentity`有了Identity,通过`services.AddIdentityServer()`有了IdentityServer4。其中的关系用图表示就是: ![](./imgs/identityserver.png) IdentityServer4掌管着所有的用户。 ``` public static IENumerable GetUsers() { return enw List{ new TestUser{ SubjectId="1", Username = "", Passowrd="" Claims = { new Claim(JwtClaimTypes.Name,""), new Claim(JwtClaimTypes.GivenName,""), new Claim(JwtClaimTypes.FamilyName,""), new Claim(JwtClaimTypes.Email, ""), new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean), new Claim(JwtClaimTypes.WebSite, ""), new Claim(JwtClaimTypes.Address,@"", IdentityServer4.IdentityServerConstancts.ClaimValueTypes.Json), new Claim(JwtClaimTypes.Role, GlobalSetting.Temp_Role_Manager), new Claim("GroupId","1") } } }; } ``` 以上`GroupId`是如何加上的呢?是通过`IProfileService`这个接口加上的。 ``` public class ProfielService : IProfileService { private readonly IUserClaimsPrincipalFactory _claimsFactory; private readonly UserManager _userManager; private readonly IHostingEnvironment _hostingEnvironment; //构造函数略 public async Task GetProfileDataAsync(ProfileDataRequest context) { if(_hostingEnvironment.IsDevelopment()) { context.IssuedClaims.AddRange(context.Subject.Claims); } else { //获取用户的SubjectId var sub = context.Subject.GetSubjectId(); //获取用户 var user = await _userManager.FindByIdAsync(sub); //获取用户的ClaimsPrincipal var claims = await _claimsFactory.CreateAsync(user); //获取用户的所有claim var claims = principal.Claims.ToList(); claims = claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList(); claims.Add(new Claim("GroupId", user.GroupId)); context.IssuedClaims = claims; } } public async Task IsActiveAsync(IsActiveContext context) { if(!_hostingEnvironment.IsDevelopment()) { var sub = context.Subject.GetSubjectId(); var user = await _userManager.FindByIdAsync(sub); context.IsActive = user != null; } } } ``` 以上,在上下文中有一个类型为`ClaimsPrincipal`的`Subject`属性,从`ClaimsPricipal`的`GetSubjectId`方法可以获取string类型的编号,把这个编号交给`UserManager`就获取到用户,把用户交给`IUserClaimsPrincipalFactory`获取`ClaimsPrincipal`,在其中包含所有的`Claim`。也就是: - 从上下文获取SubjectId - 从`UserManager`获取User - 从`IUserClaimsPrincipalFactory`获取`ClaimsPrincipal` - 从`ClaimsPrincipal`获取`Claims` IdentityServer4管理着所有的`IdentityResource` ``` public static IEnumerable GetIdentityResources() { return new List{ new IdentityResource.OpenId(), new IdentityResource.Profile(), new IdentityResource.Address(), new IdentityResource.Phone(), new IdentityResource.Email(), new IdentityResource("roles","角色",new List{JwtClaimTypes.Role}) }; } ``` IdentityServer4管理着所有的`ApiResource` ``` public static IEnumerable GetApiResources() { return new List{ new ApiResource("for_moble","") }; } ``` IdentityServer4管理着所有的`Client` ``` public static IEnumerable GetClients() { return new List{ new Client{ ClientId = "", ClientSecrets = new List{}, AllowedGrantTYpes = GrantTypes.ResourceOwnerPassowrd, AllowedScopes = new List{ "for_mobile", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstants.StandardScopes.Profile, IdentityServerConstants.StandardScopes.Address, IdentityServerConstants.StandardScopes.Email, IdentityServerConstants.StandardScopes.Phone, "roles" //这里和IdentityResource中的对应 } } }; } ``` 种子数据是如何加上的呢? ``` var host = CreateWebHostBuilder(args).Build(); using(var scope = host.Services.CreateScope()) { var services = scope.ServiceProvider; var userManager = services.GetRequestService>(); var roleManager = services.GetRequiredServce>(); SeedData.EnsureSeedData(servcies, userManager, roleManager).Wait(); } host.Run(); ``` 接着往下走 ``` public static async Task EnsureSeedData(IServiceProvider serviceProvider, UserManager userManager, RoleManager roleManager) { //先保证所有的上下文数据库完成了迁移 serviceProvider.GetRequestSerivce().Database.Migrate(); serviceProvider.GetRequredService().Database.Migrate(); serviceProvider.GetRequriedService().Database.Migrate(); //确认和ConfigurationDbContext相关的几张表 if(!context.Clients.Any()) { } if(!context.IdentityResources.Any()) { } if(!context.ApiResources.Any()) { } //种子化用户数据 if(!serviceProvider.GetRequredService.Users.Any) { } } ``` 最后客户端需要配置认证服务器。 ``` services.AddAuthentication(); app.UseAuthentication(); ``` # 看一个例子 > 在stackoverflow上提到了这样一个问题:有一台IdentityServer4的验证服务器,有多个Client, 当在某个Client的API中重置密码产生新的token,这个token在其它client就不生效了。 更新密码产生新的token的写法: ``` var token = await _userManager.GeneratePasswordResetTokenAsync(appUser); ``` IdentityServer4的配置: ``` var migrationAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name; services .AddIdentity(options => { options.Lockout.AllowedForNewUsers = true; options.Lockout.DefaultLockoutTimeSpan = new System.TimeSpan(12,0,0); options.Lockout.MaxFailedAccessAttempts = int.Parse(Configuration["MaxFailedAttempts"]); }) .AddEntityFrameworkStores() .AddDefaultTOkenProviders(); var builder = services.AddIdentityServer(options => { options.Events.RaiseErrorEvents = true; options.Events.RaiseInformationEvents = true; options.Events.RaiseFailureEvents = true; options.Events.RaiseSuccessEvents = ture; options.Authentication.CookieSlidingExpiration = ture; }) .AddAspNetIdentity() .AddConfigurationStore(options => { options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MirationsAssembly(migrationAssembly)); options.DefaultSchemma = Globals.Some; }) .AddOperationalStore(options => { options.ConfigureDbContext = b => b.UseSqlServer(connectionString, sql => sql.MigrationAssembly(migrationAssembly)); options.DefaultSchema = ""; opitions.EnalbeTokenCleanup = true; options.TokenCleanupInterval = 30; }) .AddProfileServce() .AddSigninCredentialFromConfig(); ``` 在一个客户端的API的`Startup.cs`中 ``` services.AddTransient, UserStore>(); services.AddTransient, RoleStore>(); services.AddTransient, PasswordHasher>(); services.AddTransient(); servces.AddTransient(); var identityBuilder = new IdentityBuilder(typeof(Applicationuser), typeof(IdentityRole), services); identityBuilder.AddTokenProvider("Default", typeof(DataProtectorTokenProvider)); services.AddTransient>(); ``` 在底下的回复中,让所有客户端的API和IdentityServer4 实例使用同一个ASP.NET Core Data Protection。使用Redis缓存作为分布式缓存,让所有的客户端API和IdentityServer4在创建token的时候使用同样的key。在所有的`Startup.cs`中: ``` services.AddSession(); services.Configure(Configuration.GetSection("redis"));//配置类全局公用 services.AddDistributedRedisCache(options => { options.Configuration = Configuration.GetValue("redis:host"); });//配置redis var redis = Connectionmultiplexer.Connect(Configuration.GetValue("redis:host")); services.AddDataProtection() .PersisteKeysToRedis(redis, "DataProtection-Keys") .SetApplicationName(); services.AddTransient(); ``` 在客户端API的请求管道中 ``` app.UseAuthentication(); app.UseSession(); ``` 在IdentityServer4的请求管道中 ``` app.UseIdentityServer(); app.UseSession(); ``` 也就是说在生成token的时候和DataProtection有关,让所有的客户端API和IdentityServer4使用同样的key是这里的解决思路。 # 尝试 在API的项目首先实现`IdentityUser`接口。 ``` using Microsoft.AspNetCore.Identity.EntityFrameworkCore; public class ApplicationUser : IdentityUser { public bool IsAmin{get;set;} public string DataEventRecourdsRole{get;set;} public string SecuredFilesRole{get;set;} public DateTime AccountExpires{get;set;} } ``` 需要把新创建的`IdentityUser`放到DI容器中,并且配有上下文。 ``` services.AddDbContext(options => options.UseSqllite(Configuration.GetConnectionString(""))); services.AddIdentity() .AddEntityFrameworkStores() .AddDefaultTokenProviders(); ``` 在web中创建用户 ``` private readonly UserManager _userManager; private readonly SignInManager _signInManager; [HttpPost] [AllowAnonymous] [ValidateAntiForgeryToken] public async Task Register(RegisterViewModel model, string returnUrl) { ViewData["ReturnUrl"] = returnUrl; if(ModelState.IsValid) { var dataEventsRole = "dataEventRecords.user"; var secureFilesRole = "securedFiles.user"; if(model.IsAdmin) { dataEventsRole = "dataEventRecourds.admin"; securedFilesRole = "securedFiles.admin"; } var user = new ApplicationUser{ UserName = model.Email, Email = model.Email, IsAdmin = model.IsAdmin, DataEventRecordsRole = dataEventsRole, SecuredFilesRole = securedFilesRole, AccountExpres = DateTime.UtcNow.AddDays(7.0) }; var result = await _userManager.CreateAsync(user, model.Password); if(result.Succeeded) { await _signInManager.SignInAsync(user, isPersistent:false); _logger.LogInformation(); return RedirectToLocal(returnUrl); } AddErrors(result); } return View(model); } ``` 在`ApplicationUser`中添加的属性如果要开放出去给到其它的客户端,需要通过`IProfileServce`。 ``` public class IdentityWithAdditionalClaimsProfileService : IProfileService { private readonly IUserClaimsPrincipalFactory _claimsFactory; private readonly UserManager _userManager; public async Task GetPfoileDataAsync(ProfileDataRequestContext context) { var sub = context.Subject.GetSubjectId(); var user = await _userManager.FindByIdAsync(sub); var principal = await _claimsFactory.CreateAsync(user); var claims = principal.Claims.ToList(); claims = claims.Where(claim => context.RequestedClaimTypes.Containes(claim.Type)).ToList(); claims.Add(new Claim(JwtClaimTypes.GivenName, user.UserName)); if(user.IsAdmin) { claims.Add(new Claim(JwtClimTypes.Role, "admin")); } else { claims.Add(new Claim(JwtClaimTypes.Role,"user")); } if(user.DataEventRecordsRole == "dataEventRecords.admin") { claims.Add(new Cliam(JwtClaimTypes.Role, "dataEventRecords.admin")); cliams.Add(new Claim(JwtClaimTypes.Role, "dataEventRecords.user")); claims.Add(new Claim(JwtClaimTypes.Role, "dataEventRecords")); claims.Add(new Claim(JwtClaimTypes.Scoe, "dataEventRecords")); } else { } claims.Add(new Claim(IdentityServerConstants.StandardScopes.Email, user.Email)); context.IssuedClaims = claims; } } ``` 以上在`ApplicationUser`中的属性值被加到了claims中,在`Startup`中也可以加适当的policy. ``` services.AddAuthorization(options => { options.AddPolicy("dataEventRecordsAdmin", policyAdmin => { policyAdmin.RequireClaim("role","dataEventRecords.admin") }) }) ``` 最后Policy被用到控制器中。 ``` [Authorize("policyname")] public class SomeController : Controller ``` 接下来提供一个接口给外界调用。 [Authorize] [Produces("application/json")] [Route(api/UserManagement)] public class UserManagementController : Controller { private readonly ApplicationDbContext _context; public IActionResult Get() { var users = _context.Users.ToList(); var result = new List(); return Ok(result); } public void Put(string id, [FromBody]UserDto userDto) { var user = _context.Users.First(t=>t.Id == id); user.IsAdmin = userDto.IsAdmin; if(userDto.IsActive) { if(user.AccountExpres < DateTime.UtcNow) { user.AccountExpres = DateTime.UtcNow.AddDays(7.0); } } else { user.AccountExpires = new DateTime(); } _context.Users.Update(user); _context.SaveChanges(); } }