Browse Source

认证服务器,有关修改密码登出获取用户信息

master
qdjjx 5 years ago
parent
commit
a544fbd8b5

+ 451
- 0
实践/后端/项目/18.IdentityServer4中的修改密码、登出等.md View File

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

BIN
实践/后端/项目/imgs/identityserver.png View File


Loading…
Cancel
Save