新文章 网摘 文章 随笔 日记

在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

本系列中的其他文章:

更新标识

更新标识非常简单。该包提供由应用程序用户实现的标识用户类。可以向此类添加任何额外的必需属性。这需要 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 testMigration
 
dotnet 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 = {
            stsServer: 'https://localhost:44363',
            redirect_url: 'https://localhost:44363',
            client_id: 'singleapp',
            response_type: 'id_token token',
            scope: 'dataEventRecords openid',
            post_logout_redirect_uri: 'https://localhost:44363/Unauthorized',
            start_checksession: false,
            silent_renew: true,
            silent_renew_url: 'https://localhost:44363/silent-renew.html',
            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 = {
            issuer: 'https://localhost:44363',
            authorization_endpoint: 'https://localhost:44363/connect/authorize',
            token_endpoint: 'https://localhost:44363/connect/token',
            userinfo_endpoint: 'https://localhost:44363/connect/userinfo',
            end_session_endpoint: 'https://localhost:44363/connect/endsession',
            check_session_iframe: 'https://localhost:44363/connect/checksession',
            revocation_endpoint: 'https://localhost:44363/connect/revocation',
            introspection_endpoint: 'https://localhost:44363/connect/introspect'
        };
 
        this.oidcSecurityService.setupModule(config, authWellKnownEndpoints);
    }
}

现在,可以通过角度 UI 管理标识用户。

extendingidentity_01

链接

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/

 

来源:在身份服务器4 中扩展身份以管理 ASP.NET 核心|中的用户软件工程 (damienbod.com)

posted @ 2022-10-13 09:25  岭南春  阅读(114)  评论(0)    收藏  举报