鼎鼎知识库
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

18.IdentityServer4中的修改密码、登出等.md 14KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451
  1. # 了解IdentityServer4的原理
  2. 首先在请求管道里有一个有关IdentityServer4的中间件,通过这个中间件来设施所有有关IdentityServer4的逻辑。
  3. ```
  4. app.UseIdentityServer();
  5. ```
  6. 在DI容器里通过`services.AddDbContext<IdentityDbContext>()`有了上下文,通过`services.AddIdentity<IdentityUser, IdentityRole>`有了Identity,通过`services.AddIdentityServer()`有了IdentityServer4。其中的关系用图表示就是:
  7. ![](./imgs/identityserver.png)
  8. IdentityServer4掌管着所有的用户。
  9. ```
  10. public static IENumerable<TestUser> GetUsers()
  11. {
  12. return enw List<TestUser>{
  13. new TestUser{
  14. SubjectId="1",
  15. Username = "",
  16. Passowrd=""
  17. Claims = {
  18. new Claim(JwtClaimTypes.Name,""),
  19. new Claim(JwtClaimTypes.GivenName,""),
  20. new Claim(JwtClaimTypes.FamilyName,""),
  21. new Claim(JwtClaimTypes.Email, ""),
  22. new Claim(JwtClaimTypes.EmailVerified, "true", ClaimValueTypes.Boolean),
  23. new Claim(JwtClaimTypes.WebSite, ""),
  24. new Claim(JwtClaimTypes.Address,@"", IdentityServer4.IdentityServerConstancts.ClaimValueTypes.Json),
  25. new Claim(JwtClaimTypes.Role, GlobalSetting.Temp_Role_Manager),
  26. new Claim("GroupId","1")
  27. }
  28. }
  29. };
  30. }
  31. ```
  32. 以上`GroupId`是如何加上的呢?是通过`IProfileService`这个接口加上的。
  33. ```
  34. public class ProfielService : IProfileService
  35. {
  36. private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory;
  37. private readonly UserManager<ApplicationUser> _userManager;
  38. private readonly IHostingEnvironment _hostingEnvironment;
  39. //构造函数略
  40. public async Task GetProfileDataAsync(ProfileDataRequest context)
  41. {
  42. if(_hostingEnvironment.IsDevelopment())
  43. {
  44. context.IssuedClaims.AddRange(context.Subject.Claims);
  45. }
  46. else
  47. {
  48. //获取用户的SubjectId
  49. var sub = context.Subject.GetSubjectId();
  50. //获取用户
  51. var user = await _userManager.FindByIdAsync(sub);
  52. //获取用户的ClaimsPrincipal
  53. var claims = await _claimsFactory.CreateAsync(user);
  54. //获取用户的所有claim
  55. var claims = principal.Claims.ToList();
  56. claims = claims.Where(claim => context.RequestedClaimTypes.Contains(claim.Type)).ToList();
  57. claims.Add(new Claim("GroupId", user.GroupId));
  58. context.IssuedClaims = claims;
  59. }
  60. }
  61. public async Task IsActiveAsync(IsActiveContext context)
  62. {
  63. if(!_hostingEnvironment.IsDevelopment())
  64. {
  65. var sub = context.Subject.GetSubjectId();
  66. var user = await _userManager.FindByIdAsync(sub);
  67. context.IsActive = user != null;
  68. }
  69. }
  70. }
  71. ```
  72. 以上,在上下文中有一个类型为`ClaimsPrincipal`的`Subject`属性,从`ClaimsPricipal`的`GetSubjectId`方法可以获取string类型的编号,把这个编号交给`UserManager`就获取到用户,把用户交给`IUserClaimsPrincipalFactory`获取`ClaimsPrincipal`,在其中包含所有的`Claim`。也就是:
  73. - 从上下文获取SubjectId
  74. - 从`UserManager`获取User
  75. - 从`IUserClaimsPrincipalFactory`获取`ClaimsPrincipal`
  76. - 从`ClaimsPrincipal`获取`Claims`
  77. IdentityServer4管理着所有的`IdentityResource`
  78. ```
  79. public static IEnumerable<IdentityResource> GetIdentityResources()
  80. {
  81. return new List<IdentityResource>{
  82. new IdentityResource.OpenId(),
  83. new IdentityResource.Profile(),
  84. new IdentityResource.Address(),
  85. new IdentityResource.Phone(),
  86. new IdentityResource.Email(),
  87. new IdentityResource("roles","角色",new List<string>{JwtClaimTypes.Role})
  88. };
  89. }
  90. ```
  91. IdentityServer4管理着所有的`ApiResource`
  92. ```
  93. public static IEnumerable<ApiResource> GetApiResources()
  94. {
  95. return new List<ApiResource>{
  96. new ApiResource("for_moble","")
  97. };
  98. }
  99. ```
  100. IdentityServer4管理着所有的`Client`
  101. ```
  102. public static IEnumerable<Client> GetClients()
  103. {
  104. return new List<Client>{
  105. new Client{
  106. ClientId = "",
  107. ClientSecrets = new List<Secret>{},
  108. AllowedGrantTYpes = GrantTypes.ResourceOwnerPassowrd,
  109. AllowedScopes = new List<string>{
  110. "for_mobile",
  111. IdentityServerConstants.StandardScopes.OpenId,
  112. IdentityServerConstants.StandardScopes.Profile,
  113. IdentityServerConstants.StandardScopes.Address,
  114. IdentityServerConstants.StandardScopes.Email,
  115. IdentityServerConstants.StandardScopes.Phone,
  116. "roles" //这里和IdentityResource中的对应
  117. }
  118. }
  119. };
  120. }
  121. ```
  122. 种子数据是如何加上的呢?
  123. ```
  124. var host = CreateWebHostBuilder(args).Build();
  125. using(var scope = host.Services.CreateScope())
  126. {
  127. var services = scope.ServiceProvider;
  128. var userManager = services.GetRequestService<UserManager<ApplicationUser>>();
  129. var roleManager = services.GetRequiredServce<RoleManager<ApplicationRole>>();
  130. SeedData.EnsureSeedData(servcies, userManager, roleManager).Wait();
  131. }
  132. host.Run();
  133. ```
  134. 接着往下走
  135. ```
  136. public static async Task EnsureSeedData(IServiceProvider serviceProvider, UserManager<ApplicationUser> userManager, RoleManager<ApplicationRole> roleManager)
  137. {
  138. //先保证所有的上下文数据库完成了迁移
  139. serviceProvider.GetRequestSerivce<ApplicationDbContext>().Database.Migrate();
  140. serviceProvider.GetRequredService<ConfigurationDbContext>().Database.Migrate();
  141. serviceProvider.GetRequriedService<PersistedGrantDbContext>().Database.Migrate();
  142. //确认和ConfigurationDbContext相关的几张表
  143. if(!context.Clients.Any())
  144. {
  145. }
  146. if(!context.IdentityResources.Any())
  147. {
  148. }
  149. if(!context.ApiResources.Any())
  150. {
  151. }
  152. //种子化用户数据
  153. if(!serviceProvider.GetRequredService<ApplicationDbContext>.Users.Any)
  154. {
  155. }
  156. }
  157. ```
  158. 最后客户端需要配置认证服务器。
  159. ```
  160. services.AddAuthentication();
  161. app.UseAuthentication();
  162. ```
  163. # 看一个例子
  164. > 在stackoverflow上提到了这样一个问题:有一台IdentityServer4的验证服务器,有多个Client, 当在某个Client的API中重置密码产生新的token,这个token在其它client就不生效了。
  165. 更新密码产生新的token的写法:
  166. ```
  167. var token = await _userManager.GeneratePasswordResetTokenAsync(appUser);
  168. ```
  169. IdentityServer4的配置:
  170. ```
  171. var migrationAssembly = typeof(Startup).GetTypeInfo().Assembly.GetName().Name;
  172. services
  173. .AddIdentity<ApplicationUser, IdentityRole>(options => {
  174. options.Lockout.AllowedForNewUsers = true;
  175. options.Lockout.DefaultLockoutTimeSpan = new System.TimeSpan(12,0,0);
  176. options.Lockout.MaxFailedAccessAttempts = int.Parse(Configuration["MaxFailedAttempts"]);
  177. })
  178. .AddEntityFrameworkStores<ApplicationDbContext>()
  179. .AddDefaultTOkenProviders();
  180. var builder = services.AddIdentityServer(options => {
  181. options.Events.RaiseErrorEvents = true;
  182. options.Events.RaiseInformationEvents = true;
  183. options.Events.RaiseFailureEvents = true;
  184. options.Events.RaiseSuccessEvents = ture;
  185. options.Authentication.CookieSlidingExpiration = ture;
  186. })
  187. .AddAspNetIdentity<ApplicationUser>()
  188. .AddConfigurationStore(options => {
  189. options.ConfigureDbContext = b =>
  190. b.UseSqlServer(connectionString, sql => sql.MirationsAssembly(migrationAssembly));
  191. options.DefaultSchemma = Globals.Some;
  192. })
  193. .AddOperationalStore(options => {
  194. options.ConfigureDbContext = b =>
  195. b.UseSqlServer(connectionString, sql => sql.MigrationAssembly(migrationAssembly));
  196. options.DefaultSchema = "";
  197. opitions.EnalbeTokenCleanup = true;
  198. options.TokenCleanupInterval = 30;
  199. })
  200. .AddProfileServce<CustomProfileService>()
  201. .AddSigninCredentialFromConfig();
  202. ```
  203. 在一个客户端的API的`Startup.cs`中
  204. ```
  205. services.AddTransient<IUserStore<ApplicationUser>, UserStore<ApplicaitonUser, IdentityRole, ApplicationDBContext>>();
  206. services.AddTransient<IRoleStore<IdentityRole>, RoleStore<IdentityRole, ApplicatioDbContext>>();
  207. services.AddTransient<IPasswordHasher<ApplicationUser>, PasswordHasher<ApplicationUser>>();
  208. services.AddTransient<ILookupNormalizer, UpperInvariantLookupNomalizer>();
  209. servces.AddTransient<IdentityErrorDescriber>();
  210. var identityBuilder = new IdentityBuilder(typeof(Applicationuser), typeof(IdentityRole), services);
  211. identityBuilder.AddTokenProvider("Default", typeof(DataProtectorTokenProvider<ApplicationUser>));
  212. services.AddTransient<UserManager<ApplicationUser>>();
  213. ```
  214. 在底下的回复中,让所有客户端的API和IdentityServer4 实例使用同一个ASP.NET Core Data Protection。使用Redis缓存作为分布式缓存,让所有的客户端API和IdentityServer4在创建token的时候使用同样的key。在所有的`Startup.cs`中:
  215. ```
  216. services.AddSession();
  217. services.Configure<RedisConfiguration>(Configuration.GetSection("redis"));//配置类全局公用
  218. services.AddDistributedRedisCache(options => {
  219. options.Configuration = Configuration.GetValue<string>("redis:host");
  220. });//配置redis
  221. var redis = Connectionmultiplexer.Connect(Configuration.GetValue<string>("redis:host"));
  222. services.AddDataProtection()
  223. .PersisteKeysToRedis(redis, "DataProtection-Keys")
  224. .SetApplicationName();
  225. services.AddTransient<ICacheService, CacheService>();
  226. ```
  227. 在客户端API的请求管道中
  228. ```
  229. app.UseAuthentication();
  230. app.UseSession();
  231. ```
  232. 在IdentityServer4的请求管道中
  233. ```
  234. app.UseIdentityServer();
  235. app.UseSession();
  236. ```
  237. 也就是说在生成token的时候和DataProtection有关,让所有的客户端API和IdentityServer4使用同样的key是这里的解决思路。
  238. # 尝试
  239. 在API的项目首先实现`IdentityUser`接口。
  240. ```
  241. using Microsoft.AspNetCore.Identity.EntityFrameworkCore;
  242. public class ApplicationUser : IdentityUser
  243. {
  244. public bool IsAmin{get;set;}
  245. public string DataEventRecourdsRole{get;set;}
  246. public string SecuredFilesRole{get;set;}
  247. public DateTime AccountExpires{get;set;}
  248. }
  249. ```
  250. 需要把新创建的`IdentityUser`放到DI容器中,并且配有上下文。
  251. ```
  252. services.AddDbContext<ApplicationDbContext>(options => options.UseSqllite(Configuration.GetConnectionString("")));
  253. services.AddIdentity<ApplicationUser, IdentityRole>()
  254. .AddEntityFrameworkStores<ApplicationDbContext>()
  255. .AddDefaultTokenProviders();
  256. ```
  257. 在web中创建用户
  258. ```
  259. private readonly UserManager<ApplicationUser> _userManager;
  260. private readonly SignInManager<ApplicationUser> _signInManager;
  261. [HttpPost]
  262. [AllowAnonymous]
  263. [ValidateAntiForgeryToken]
  264. public async Task<IActioinResult> Register(RegisterViewModel model, string returnUrl)
  265. {
  266. ViewData["ReturnUrl"] = returnUrl;
  267. if(ModelState.IsValid)
  268. {
  269. var dataEventsRole = "dataEventRecords.user";
  270. var secureFilesRole = "securedFiles.user";
  271. if(model.IsAdmin)
  272. {
  273. dataEventsRole = "dataEventRecourds.admin";
  274. securedFilesRole = "securedFiles.admin";
  275. }
  276. var user = new ApplicationUser{
  277. UserName = model.Email,
  278. Email = model.Email,
  279. IsAdmin = model.IsAdmin,
  280. DataEventRecordsRole = dataEventsRole,
  281. SecuredFilesRole = securedFilesRole,
  282. AccountExpres = DateTime.UtcNow.AddDays(7.0)
  283. };
  284. var result = await _userManager.CreateAsync(user, model.Password);
  285. if(result.Succeeded)
  286. {
  287. await _signInManager.SignInAsync(user, isPersistent:false);
  288. _logger.LogInformation();
  289. return RedirectToLocal(returnUrl);
  290. }
  291. AddErrors(result);
  292. }
  293. return View(model);
  294. }
  295. ```
  296. 在`ApplicationUser`中添加的属性如果要开放出去给到其它的客户端,需要通过`IProfileServce`。
  297. ```
  298. public class IdentityWithAdditionalClaimsProfileService : IProfileService
  299. {
  300. private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory;
  301. private readonly UserManager<ApplicationUser> _userManager;
  302. public async Task GetPfoileDataAsync(ProfileDataRequestContext context)
  303. {
  304. var sub = context.Subject.GetSubjectId();
  305. var user = await _userManager.FindByIdAsync(sub);
  306. var principal = await _claimsFactory.CreateAsync(user);
  307. var claims = principal.Claims.ToList();
  308. claims = claims.Where(claim => context.RequestedClaimTypes.Containes(claim.Type)).ToList();
  309. claims.Add(new Claim(JwtClaimTypes.GivenName, user.UserName));
  310. if(user.IsAdmin)
  311. {
  312. claims.Add(new Claim(JwtClimTypes.Role, "admin"));
  313. }
  314. else
  315. {
  316. claims.Add(new Claim(JwtClaimTypes.Role,"user"));
  317. }
  318. if(user.DataEventRecordsRole == "dataEventRecords.admin")
  319. {
  320. claims.Add(new Cliam(JwtClaimTypes.Role, "dataEventRecords.admin"));
  321. cliams.Add(new Claim(JwtClaimTypes.Role, "dataEventRecords.user"));
  322. claims.Add(new Claim(JwtClaimTypes.Role, "dataEventRecords"));
  323. claims.Add(new Claim(JwtClaimTypes.Scoe, "dataEventRecords"));
  324. }
  325. else
  326. {
  327. }
  328. claims.Add(new Claim(IdentityServerConstants.StandardScopes.Email, user.Email));
  329. context.IssuedClaims = claims;
  330. }
  331. }
  332. ```
  333. 以上在`ApplicationUser`中的属性值被加到了claims中,在`Startup`中也可以加适当的policy.
  334. ```
  335. services.AddAuthorization(options => {
  336. options.AddPolicy("dataEventRecordsAdmin", policyAdmin => {
  337. policyAdmin.RequireClaim("role","dataEventRecords.admin")
  338. })
  339. })
  340. ```
  341. 最后Policy被用到控制器中。
  342. ```
  343. [Authorize("policyname")]
  344. public class SomeController : Controller
  345. ```
  346. 接下来提供一个接口给外界调用。
  347. [Authorize]
  348. [Produces("application/json")]
  349. [Route(api/UserManagement)]
  350. public class UserManagementController : Controller
  351. {
  352. private readonly ApplicationDbContext _context;
  353. public IActionResult Get()
  354. {
  355. var users = _context.Users.ToList();
  356. var result = new List<UserDto>();
  357. return Ok(result);
  358. }
  359. public void Put(string id, [FromBody]UserDto userDto)
  360. {
  361. var user = _context.Users.First(t=>t.Id == id);
  362. user.IsAdmin = userDto.IsAdmin;
  363. if(userDto.IsActive)
  364. {
  365. if(user.AccountExpres < DateTime.UtcNow)
  366. {
  367. user.AccountExpres = DateTime.UtcNow.AddDays(7.0);
  368. }
  369. }
  370. else
  371. {
  372. user.AccountExpires = new DateTime();
  373. }
  374. _context.Users.Update(user);
  375. _context.SaveChanges();
  376. }
  377. }