在IdentityServer4 中扩展身份以管理 ASP.NET Core 中的用户
本文介绍如何扩展标识并将其与标识服务器 4 一起使用,以实现特定于应用程序的要求。该应用程序允许用户注册,并可以访问该应用程序7天。在此之后,用户将无法登录。任何管理员都可以使用自定义用户管理 API 激活或停用用户。额外的属性将添加到标识用户模型中以支持此操作。标识是使用 EFCore 和 SQLite 保留的。SPA 应用程序是使用角度、Web 包 4 和类型脚本 2 实现的。
代码: https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow
历史:
2019-09-20: 更新 ASP.NET 核心 3.0, 角度 8.2.6
2018-06-22: 更新 ASP.NET 核心 2.1, 角度 6.0.6, ASP.NET 核心标识 2.1
完整历史记录:
https://github.com/damienbod/AspNet5IdentityServerAngularImplicitFlow#history
本系列中的其他文章:
- ASP.NET 核心中的身份服务器4 的授权策略和数据保护
- 角度开放 ID 将隐式流与身份服务器连接4
- 在不使用 URL 或 Cookie 中的访问令牌的情况下下载有角度的安全文件
- 使用身份服务器4 和 OpenID 连接隐式流的完整服务器注销
- 身份服务器4、网页 API 和角度在单个项目中
- 在身份服务器4 中扩展身份以管理 ASP.NET 核心中的用户
- 在 Angular 中为 OpenID 连接隐式流实现静默令牌续订
- OpenID 连接会话管理使用角度应用程序和身份服务器4
更新标识
更新标识非常简单。该包提供由应用程序用户实现的标识用户类。可以向此类添加任何额外的必需属性。这需要 Microsoft.AspNetCore.Identity.EntityFrameworkCore 包,该包作为 NuGet 包包含在项目中。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
|
using System;using Microsoft.AspNetCore.Identity.EntityFrameworkCore;namespace IdentityServerWithAspNetIdentity.Models{ public class ApplicationUser : IdentityUser { public bool IsAdmin { get; set; } public string DataEventRecordsRole { get; set; } public string SecuredFilesRole { get; set; } public DateTime AccountExpires { get; set; } }} |
需要将标识添加到应用程序中。这是在配置服务方法的启动类中使用添加标识扩展完成的。SQLite 用于保存数据。然后,使用 SQLite 的应用程序数据库上下文将用作标识的存储。
|
1
2
3
4
5
6
|
services.AddDbContext<ApplicationDbContext>(options => options.UseSqlite(Configuration.GetConnectionString("DefaultConnection")));services.AddIdentity<ApplicationUser, IdentityRole>().AddEntityFrameworkStores<ApplicationDbContext>().AddDefaultTokenProviders(); |
从 SQLite 数据库的应用程序设置中读取配置。使用启动构造函数中的配置生成器读取配置。
|
1
2
3
4
|
"ConnectionStrings": { "DefaultConnection": "Data Source=C:\\git\\damienbod\\AspNet5IdentityServerAngularImplicitFlow\\src\\ResourceWithIdentityServerWithClient\\usersdatabase.sqlite" }, |
然后,使用 EFCore 迁移创建标识存储。
|
1
2
3
|
dotnet ef migrations add testMigrationdotnet ef database update |
标识中的新属性以三种方式使用;创建新用户时,为用户创建令牌并使用策略在资源上验证令牌时。
使用标识创建新用户
标识应用程序用户是在帐户控制器的 Register 方法中创建的。可以根据需要使用已添加到应用程序用户中的新扩展属性。在此示例中,新用户将有权访问 7 天。如果用户可以设置自定义属性,则需要扩展注册视图模型和相应的视图。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
|
[HttpPost][AllowAnonymous][ValidateAntiForgeryToken]public async Task<IActionResult> Register(RegisterViewModel model, string returnUrl = null){ ViewData["ReturnUrl"] = returnUrl; if (ModelState.IsValid) { var dataEventsRole = "dataEventRecords.user"; var securedFilesRole = "securedFiles.user"; if (model.IsAdmin) { dataEventsRole = "dataEventRecords.admin"; securedFilesRole = "securedFiles.admin"; } var user = new ApplicationUser { UserName = model.Email, Email = model.Email, IsAdmin = model.IsAdmin, DataEventRecordsRole = dataEventsRole, SecuredFilesRole = securedFilesRole, AccountExpires = DateTime.UtcNow.AddDays(7.0) }; var result = await _userManager.CreateAsync(user, model.Password); if (result.Succeeded) { // For more information on how to enable account confirmation and password reset please visit http://go.microsoft.com/fwlink/?LinkID=532713 // Send an email with this link //var code = await _userManager.GenerateEmailConfirmationTokenAsync(user); //var callbackUrl = Url.Action("ConfirmEmail", "Account", new { userId = user.Id, code = code }, protocol: HttpContext.Request.Scheme); //await _emailSender.SendEmailAsync(model.Email, "Confirm your account", // $"Please confirm your account by clicking this link: <a href='{callbackUrl}'>link</a>"); await _signInManager.SignInAsync(user, isPersistent: false); _logger.LogInformation(3, "User created a new account with password."); return RedirectToLocal(returnUrl); } AddErrors(result); } // If we got this far, something failed, redisplay form return View(model);} |
使用身份在身份服务器4 中创建令牌
需要将标识属性添加到声明中,以便客户端 SPA 或其所在的任何客户端都可以使用这些属性。在标识服务 4 中,“I配置文件服务”接口用于此目的。每个自定义应用程序用户属性都根据需要添加为声明。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
|
using System;using System.Collections.Generic;using System.Linq;using System.Security.Claims;using System.Threading.Tasks;using IdentityModel;using IdentityServer4.Extensions;using IdentityServer4.Models;using IdentityServer4.Services;using IdentityServerWithAspNetIdentity.Models;using Microsoft.AspNetCore.Identity;namespace IdentityServerWithAspNetIdentitySqlite{ using IdentityServer4; public class IdentityWithAdditionalClaimsProfileService : IProfileService { private readonly IUserClaimsPrincipalFactory<ApplicationUser> _claimsFactory; private readonly UserManager<ApplicationUser> _userManager; public IdentityWithAdditionalClaimsProfileService(UserManager<ApplicationUser> userManager, IUserClaimsPrincipalFactory<ApplicationUser> claimsFactory) { _userManager = userManager; _claimsFactory = claimsFactory; } public async Task GetProfileDataAsync(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.Contains(claim.Type)).ToList(); claims.Add(new Claim(JwtClaimTypes.GivenName, user.UserName)); if (user.IsAdmin) { claims.Add(new Claim(JwtClaimTypes.Role, "admin")); } else { claims.Add(new Claim(JwtClaimTypes.Role, "user")); } if (user.DataEventRecordsRole == "dataEventRecords.admin") { claims.Add(new Claim(JwtClaimTypes.Role, "dataEventRecords.admin")); claims.Add(new Claim(JwtClaimTypes.Role, "dataEventRecords.user")); claims.Add(new Claim(JwtClaimTypes.Role, "dataEventRecords")); claims.Add(new Claim(JwtClaimTypes.Scope, "dataEventRecords")); } else { claims.Add(new Claim(JwtClaimTypes.Role, "dataEventRecords.user")); claims.Add(new Claim(JwtClaimTypes.Role, "dataEventRecords")); claims.Add(new Claim(JwtClaimTypes.Scope, "dataEventRecords")); } if (user.SecuredFilesRole == "securedFiles.admin") { claims.Add(new Claim(JwtClaimTypes.Role, "securedFiles.admin")); claims.Add(new Claim(JwtClaimTypes.Role, "securedFiles.user")); claims.Add(new Claim(JwtClaimTypes.Role, "securedFiles")); claims.Add(new Claim(JwtClaimTypes.Scope, "securedFiles")); } else { claims.Add(new Claim(JwtClaimTypes.Role, "securedFiles.user")); claims.Add(new Claim(JwtClaimTypes.Role, "securedFiles")); claims.Add(new Claim(JwtClaimTypes.Scope, "securedFiles")); } claims.Add(new Claim(IdentityServerConstants.StandardScopes.Email, user.Email)); context.IssuedClaims = claims; } public async Task IsActiveAsync(IsActiveContext context) { var sub = context.Subject.GetSubjectId(); var user = await _userManager.FindByIdAsync(sub); context.IsActive = user != null; } }} |
使用标识属性验证令牌
属性用于定义登录用户是否具有管理员角色。这是使用 IProfile 服务中的管理员声明添加到令牌中的。现在,这可以通过定义策略并在控制器中验证策略来使用。这些策略将添加到“配置服务”方法的“启动”类中。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
|
services.AddAuthorization(options =>{ options.AddPolicy("dataEventRecordsAdmin", policyAdmin => { policyAdmin.RequireClaim("role", "dataEventRecords.admin"); }); options.AddPolicy("admin", policyAdmin => { policyAdmin.RequireClaim("role", "admin"); }); options.AddPolicy("dataEventRecordsUser", policyUser => { policyUser.RequireClaim("role", "dataEventRecords.user"); });}); |
然后,可以使用“授权”属性在 MVC 控制器中使用该策略。管理策略在用户管理控制器中使用。
|
1
2
3
4
5
|
[Authorize("admin")][Produces("application/json")][Route("api/UserManagement")]public class UserManagementController : Controller{ |
现在,用户可以是管理员用户并在 7 天后过期,应用程序需要一个 UI 来管理这一点。此 UI 在《角度 2》SPA 中实现。UI 需要用户管理 API 来获取所有用户并更新用户。标识 EFCore ApplicationDbContext 上下文直接在控制器中使用以简化操作,但通常这会与控制器分开,或者如果您有很多用户,则需要使用筛选的结果列表来支持某种类型的搜索逻辑。我喜欢在MVC控制器中没有逻辑。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
|
using System;using System.Collections.Generic;using System.Linq;using IdentityServerWithAspNetIdentity.Data;using Microsoft.AspNetCore.Authorization;using Microsoft.AspNetCore.Mvc;using ResourceWithIdentityServerWithClient.Model;namespace ResourceWithIdentityServerWithClient.Controllers{ [Authorize("admin")] [Produces("application/json")] [Route("api/UserManagement")] public class UserManagementController : Controller { private readonly ApplicationDbContext _context; public UserManagementController(ApplicationDbContext context) { _context = context; } [HttpGet] public IActionResult Get() { var users = _context.Users.ToList(); var result = new List<UserDto>(); foreach(var applicationUser in users) { var user = new UserDto { Id = applicationUser.Id, Name = applicationUser.Email, IsAdmin = applicationUser.IsAdmin, IsActive = applicationUser.AccountExpires > DateTime.UtcNow }; result.Add(user); } return Ok(result); } [HttpPut("{id}")] 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.AccountExpires < DateTime.UtcNow) { user.AccountExpires = DateTime.UtcNow.AddDays(7.0); } } else { // deactivate user user.AccountExpires = new DateTime(); } _context.Users.Update(user); _context.SaveChanges(); } }} |
角度用户管理组件
角度SPA是使用带有类型脚本的Webpack 4构建的。请参阅 https://github.com/damienbod/Angular2WebpackVisualStudio,了解如何使用 ASP.NET 核心设置角度 Webpack 4 应用。
角度应用需要服务才能访问核心 MVC 服务 ASP.NET。这是在用户管理服务中实现的,然后需要将其添加到 app.module 中。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
|
import { Injectable } from '@angular/core';import { HttpClient, HttpHeaders } from '@angular/common/http';import { Observable } from 'rxjs';import { Configuration } from '../app.constants';import { OidcSecurityService } from '../auth/services/oidc.security.service';import { User } from './models/User';@Injectable()export class UserManagementService { private actionUrl: string; private headers: HttpHeaders = new HttpHeaders(); constructor(private _http: HttpClient, configuration: Configuration, private _securityService: OidcSecurityService) { this.actionUrl = `${configuration.Server}/api/UserManagement`; } private setHeaders() { this.headers = new HttpHeaders(); this.headers = this.headers.set('Content-Type', 'application/json'); this.headers = this.headers.set('Accept', 'application/json'); const token = this._securityService.getToken(); if (token !== '') { const tokenValue = 'Bearer ' + token; this.headers = this.headers.append('Authorization', tokenValue); } } public GetAll = (): Observable<User[]> => { this.setHeaders(); return this._http.get<User[]>(this.actionUrl, { headers: this.headers }); } public Update = (id: string, itemToUpdate: User): Observable<any> => { this.setHeaders(); return this._http.put( this.actionUrl + id, JSON.stringify(itemToUpdate), { headers: this.headers } ); }} |
用户管理组件使用该服务并显示所有用户,并提供一种更新每个用户的方法。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
|
import { Component, OnInit, OnDestroy } from '@angular/core';import { Subscription } from 'rxjs'import { OidcSecurityService } from '../auth/services/oidc.security.service';import { UserManagementService } from '../user-management/UserManagementService';import { User } from './models/User';@Component({ selector: 'app-user-management', templateUrl: 'user-management.component.html'})export class UserManagementComponent implements OnInit, OnDestroy { isAuthorizedSubscription: Subscription | undefined; isAuthorized = false; public message: string; public Users: User[] = []; constructor( private _userManagementService: UserManagementService, public oidcSecurityService: OidcSecurityService, ) { this.message = 'user-management'; } ngOnInit() { this.isAuthorizedSubscription = this.oidcSecurityService.getIsAuthorized().subscribe( (isAuthorized: boolean) => { this.isAuthorized = isAuthorized; this.getData() }); } ngOnDestroy(): void { if (this.isAuthorizedSubscription) { this.isAuthorizedSubscription.unsubscribe(); } } private getData() { this._userManagementService .GetAll() .subscribe(data => this.Users = data, error => this.oidcSecurityService.handleError(error), () => console.log('User Management Get all completed')); } public Update(user: User) { this._userManagementService.Update(user.id, user) .subscribe((() => console.log('subscribed')), error => this.oidcSecurityService.handleError(error), () => console.log('update request sent!')); }} |
“用户管理组件”模板使用“用户”数据进行显示、更新等。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
|
<div class="col-md-12" *ngIf="securityService.IsAuthorized()"> <div class="panel panel-default"> <div class="panel-heading"> <h3 class="panel-title">{{message}}</h3> </div> <div class="panel-body" *ngIf="Users"> <table class="table"> <thead> <tr> <th>Name</th> <th>IsAdmin</th> <th>IsActive</th> <th></th> </tr> </thead> <tbody> <tr style="height:20px;" *ngFor="let user of Users"> <td>{{user.name}}</td> <td> <input type="checkbox" [(ngModel)]="user.isAdmin" class="form-control" style="box-shadow:none" /> </td> <td> <input type="checkbox" [(ngModel)]="user.isActive" class="form-control" style="box-shadow:none" /> </td> <td> <button (click)="Update(user)" class="form-control">Update</button> </td> </tr> </tbody> </table> </div> </div></div> |
需要将用户管理组件和服务添加到模块中。
|
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
|
import { NgModule } from '@angular/core';import { FormsModule } from '@angular/forms';import { BrowserModule } from '@angular/platform-browser';import { AppComponent } from './app.component';import { Configuration } from './app.constants';import { routing } from './app.routes';import { HttpClientModule } from '@angular/common/http';import { ForbiddenComponent } from './forbidden/forbidden.component';import { HomeComponent } from './home/home.component';import { UnauthorizedComponent } from './unauthorized/unauthorized.component';import { UserManagementComponent } from './user-management/user-management.component';import { DataEventRecordsModule } from './dataeventrecords/dataeventrecords.module';import { NavigationComponent } from './navigation/navigation.component';import { HasAdminRoleAuthenticationGuard } from './guards/hasAdminRoleAuthenticationGuard';import { HasAdminRoleCanLoadGuard } from './guards/hasAdminRoleCanLoadGuard';import { UserManagementService } from './user-management/UserManagementService';import { AuthModule } from './auth/modules/auth.module';import { OidcSecurityService } from './auth/services/oidc.security.service';import { AuthWellKnownEndpoints } from './auth/models/auth.well-known-endpoints';import { OpenIdConfiguration } from './auth/models/auth.configuration';@NgModule({ imports: [ BrowserModule, FormsModule, routing, HttpClientModule, DataEventRecordsModule, AuthModule.forRoot(), ], declarations: [ AppComponent, ForbiddenComponent, HomeComponent, UnauthorizedComponent, UserManagementComponent, NavigationComponent, ], providers: [ OidcSecurityService, UserManagementService, Configuration, HasAdminRoleAuthenticationGuard, HasAdminRoleCanLoadGuard ], bootstrap: [AppComponent],})export class AppModule { constructor( public oidcSecurityService: OidcSecurityService ) { const config: OpenIdConfiguration = { client_id: 'singleapp', response_type: 'id_token token', scope: 'dataEventRecords openid', start_checksession: false, silent_renew: true, post_login_route: '/dataeventrecords', forbidden_route: '/Forbidden', unauthorized_route: '/Unauthorized', log_console_warning_active: true, log_console_debug_active: true, max_id_token_iat_offset_allowed_in_seconds: 10 }; const authWellKnownEndpoints: AuthWellKnownEndpoints = { }; this.oidcSecurityService.setupModule(config, authWellKnownEndpoints); }} |
现在,可以通过角度 UI 管理标识用户。

链接
https://github.com/IdentityServer/IdentityServer4
http://docs.identityserver.io/en/dev/
https://github.com/IdentityServer/IdentityServer4.Samples
https://docs.asp.net/en/latest/security/authentication/identity.html
https://github.com/IdentityServer/IdentityServer4/issues/349
https://damienbod.com/2016/06/12/asp-net-core-angular2-with-webpack-and-visual-studio/
浙公网安备 33010602011771号