日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 编程资源 > 编程问答 >内容正文

编程问答

Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(一)

發布時間:2023/12/4 编程问答 29 豆豆
生活随笔 收集整理的這篇文章主要介紹了 Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(一) 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

好吧,這個題目我也想了很久,不知道如何用最簡單的幾個字來概括這篇文章,原本打算取名《Angular單頁面應用基于Ocelot API網關與IdentityServer4+ASP.NET Identity實現身份認證與授權》,然而如你所見,這樣的名字實在是太長了。所以,我不得不縮寫“單頁面應用”幾個字,然后去掉ASP.NET Identity的描述,最后形成目前的標題。

不過,這也就意味著這篇文章會涵蓋很多內容和技術,我會利用這些技術來走通一個完整的流程,這個流程也代表著在微服務架構中單點登錄的一種實現模式。在此過程中,我們會使用到如下技術或框架:

  • Angular 8

  • Ocelot API Gateway

  • IdentityServer4

  • ASP.NET Identity

  • Entity Framework Core

  • SQL Server

本文假設讀者具有上述技術框架的基礎知識。由于內容比較多,我還是將這篇文章分幾個部分進行講解和討論。

場景描述

在微服務架構下的一種比較流行的設計,就是基于前后端分離,前端只做呈現和用戶操作流的管理,后端服務由API網關同一協調,以從業務層面為前端提供各種服務。大致可以用下圖表示:

在這個結構中,我沒有將Identity Service放在API Gateway后端,因為考慮到Identity Service本身并沒有承擔任何業務功能。從它所能提供的端點(Endpoint)的角度,它也需要做負載均衡、熔斷等保護,但我們暫時不討論這些內容。

流程上其實也比較簡單,在上圖的數字標識中:

  • Client向Identity Service發送認證請求,通常可以是用戶名密碼

  • 如果驗證通過,Identity Service會向Client返回認證的Token

  • Client使用Token向API Gateway發送API調用請求

  • API Gateway將Client發送過來的Token發送給Identity Service,以驗證Token的有效性

  • 如果驗證成功,Identity Service會告知API Gateway認證成功

  • API Gateway轉發Client的請求到后端API Service

  • API Service將結果返回給API Gateway

  • API Gateway將API Service返回的結果轉發到Client

  • 只是在這些步驟中,我們有很多技術選擇,比如Identity Service的實現方式、認證方式等等。接下來,我就在ASP.NET Core的基礎上使用IdentityServer4、Entity Framework Core和Ocelot來完成這一流程。在完成整個流程的演練之前,需要確保機器滿足以下條件:

    • 安裝Visual Studio 2019 Community Edition。使用Visual Studio Code也是可以的,根據自己的需要選擇

    • 安裝Visual Studio Code

    • 安裝Angular 8

    IdentityServer4結合ASP.NET Identity實現Identity Service

    創建新項目

    首先第一步就是實現Identity Service。在Visual Studio 2019 Community Edition中,新建一個ASP.NET Core Web Application,模板選擇Web Application (Model-View-Controller),然后點擊Authentication下的Change按鈕,再選擇Individual User Accounts選項,以便將ASP.NET Identity的依賴包都加入項目,并且自動完成基礎代碼的搭建。

    然后,通過NuGet添加IdentityServer4.AspNetIdentity以及IdentityServer4.EntityFramework的引用,IdentityServer4也隨之會被添加進來。接下來,在該項目的目錄下,執行以下命令安裝IdentityServer4的模板,并將IdentityServer4的GUI加入到當前項目:

    1

    2

    dotnet new -i identityserver4.templates

    dotnet new is4ui --force

    然后調整一下項目結構,將原本的Controllers目錄刪除,同時刪除Models目錄下的ErrorViewModel類,然后將Quickstart目錄重命名為Controllers,編譯代碼,代碼應該可以編譯通過,接下來就是實現我們自己的Identity。

    定制Identity Service

    為了能夠展現一個標準的應用場景,我自己定義了User和Role對象,它們分別繼承于IdentityUser和IdentityRole類:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    public class AppUser : IdentityUser

    {

    ????public string DisplayName { get; set; }

    }

    ?

    public class AppRole : IdentityRole

    {

    ????public string Description { get; set; }

    }

    當然,Data目錄下的ApplicationDbContext也要做相應調整,它應該繼承于IdentityDbContext<AppUser, AppRole, string>類,這是因為我們使用了自定義的IdentityUser和IdentityRole的實現:

    1

    2

    3

    4

    5

    6

    7

    public class ApplicationDbContext : IdentityDbContext<AppUser, AppRole, string>

    {

    ????public ApplicationDbContext(DbContextOptions<ApplicationDbContext> options)

    ????????: base(options)

    ????{

    ????}

    }

    之后修改Startup.cs里的ConfigureServices方法,通過調用AddIdentity、AddIdentityServer以及AddDbContext,將ASP.NET Identity、IdentityServer4以及存儲認證數據所使用的Entity Framework Core的依賴全部注冊進來。為了測試方便,目前我們還是使用Developer Signing Credential,對于Identity Resource、API Resource以及Clients,我們也是暫時先寫死(hard code):


    public void ConfigureServices(IServiceCollection services)

    {

    ????services.AddDbContext<ApplicationDbContext>(options =>

    ????????options.UseSqlServer(

    ????????????Configuration.GetConnectionString("DefaultConnection")));

    ????services.AddIdentity<AppUser, AppRole>()

    ????????.AddEntityFrameworkStores<ApplicationDbContext>()

    ????????.AddDefaultTokenProviders();

    ????services.AddIdentityServer().AddDeveloperSigningCredential()

    ??????.AddOperationalStore(options =>

    ??????{

    ??????????options.ConfigureDbContext = builder => builder.UseSqlServer(Configuration.GetConnectionString("DefaultConnection"),

    ??????????????sqlServerDbContextOptionsBuilder =>

    ??????????????sqlServerDbContextOptionsBuilder.MigrationsAssembly(typeof(Startup).Assembly.GetName().Name));

    ??????????options.EnableTokenCleanup = true;

    ??????????options.TokenCleanupInterval = 30; // interval in seconds

    ??????})

    ??????.AddInMemoryIdentityResources(Config.GetIdentityResources())

    ??????.AddInMemoryApiResources(Config.GetApiResources())

    ??????.AddInMemoryClients(Config.GetClients())

    ??????.AddAspNetIdentity<AppUser>();

    ?

    ????services.AddCors(options => options.AddPolicy("AllowAll", p => p.AllowAnyOrigin()

    ???????.AllowAnyMethod()

    ???????.AllowAnyHeader()));

    ?

    ????services.AddControllersWithViews();

    ????services.AddRazorPages();

    ????services.AddControllers();

    }

    然后,調整Configure方法的實現,將IdentityServer加入進來,同時配置CORS使得站點能夠被跨域訪問:


    public void Configure(IApplicationBuilder app, IWebHostEnvironment env)

    {

    ????if (env.IsDevelopment())

    ????{

    ????????app.UseDeveloperExceptionPage();

    ????????app.UseDatabaseErrorPage();

    ????}

    ????else

    ????{

    ????????app.UseExceptionHandler("/Home/Error");

    ????????app.UseHsts();

    ????}

    ?

    ????app.UseCors("AllowAll");

    ????app.UseHttpsRedirection();

    ????app.UseStaticFiles();

    ?

    ????app.UseRouting();

    ????app.UseIdentityServer();

    ?

    ????app.UseAuthentication();

    ????app.UseAuthorization();

    ?

    ????app.UseEndpoints(endpoints =>

    ????{

    ????????endpoints.MapControllerRoute(

    ????????????name: "default",

    ????????????pattern: "{controller=Home}/{action=Index}/{id?}");

    ????????endpoints.MapRazorPages();

    ????});

    }

    完成這部分代碼調整后,編譯是通不過的,因為我們還沒有定義IdentityServer4的IdentityResource、API Resource和Clients。在項目中新建一個Config類,代碼如下:


    public static class Config

    {

    ????public static IEnumerable<IdentityResource> GetIdentityResources() =>

    ????????new IdentityResource[]

    ????????{

    ????????????new IdentityResources.OpenId(),

    ????????????new IdentityResources.Email(),

    ????????????new IdentityResources.Profile()

    ????????};

    ?

    ????public static IEnumerable<ApiResource> GetApiResources() =>

    ????????new[]

    ????????{

    ????????????new ApiResource("api.weather", "Weather API")

    ????????????{

    ????????????????Scopes =

    ????????????????{

    ????????????????????new Scope("api.weather.full_access", "Full access to Weather API")

    ????????????????},

    ????????????????UserClaims =

    ????????????????{

    ????????????????????ClaimTypes.NameIdentifier,

    ????????????????????ClaimTypes.Name,

    ????????????????????ClaimTypes.Email,

    ????????????????????ClaimTypes.Role

    ????????????????}

    ????????????}

    ????????};

    ?

    ????public static IEnumerable<Client> GetClients() =>

    ????????new[]

    ????????{

    ????????????new Client

    ????????????{

    ????????????????RequireConsent = false,

    ????????????????ClientId = "angular",

    ????????????????ClientName = "Angular SPA",

    ????????????????AllowedGrantTypes = GrantTypes.Implicit,

    ????????????????AllowedScopes = { "openid", "profile", "email", "api.weather.full_access" },

    ????????????????RedirectUris = {"http://localhost:4200/auth-callback"},

    ????????????????PostLogoutRedirectUris = {"http://localhost:4200/"},

    ????????????????AllowedCorsOrigins = {"http://localhost:4200"},

    ????????????????AllowAccessTokensViaBrowser = true,

    ????????????????AccessTokenLifetime = 3600

    ????????????},

    ????????????new Client

    ????????????{

    ????????????????ClientId = "webapi",

    ????????????????AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,

    ????????????????ClientSecrets =

    ????????????????{

    ????????????????????new Secret("mysecret".Sha256())

    ????????????????},

    ????????????????AlwaysSendClientClaims = true,

    ????????????????AllowedScopes = { "api.weather.full_access" }

    ????????????}

    ????????};

    }

    大致說明一下上面的代碼。通俗地講,IdentityResource是指允許應用程序訪問用戶的哪些身份認證資源,比如,用戶的電子郵件或者其它用戶賬戶信息,在Open ID Connect規范中,這些信息會被轉換成Claims,保存在User Identity的對象里;ApiResource用來指定被IdentityServer4所保護的資源,比如這里新建了一個ApiResource,用來保護Weather API,它定義了自己的Scope和UserClaims。Scope其實是一種關聯關系,它關聯著Client與ApiResource,用來表示什么樣的Client對于什么樣的ApiResource具有怎樣的訪問權限,比如在這里,我定義了兩個Client:angular和webapi,它們對Weather API都可以訪問;UserClaims定義了當認證通過之后,IdentityServer4應該向請求方返回哪些Claim。至于Client,就比較容易理解了,它定義了客戶端能夠以哪幾種方式來向IdentityServer4提交請求。

    至此,我們的源代碼就可以編譯通過了,成功編譯之后,還需要使用Entity Framework Core所提供的命令行工具或者Powershell Cmdlet來初始化數據庫。我這里選擇使用Visual Studio 2019 Community中的Package Manager Console,在執行數據庫更新之前,確保appsettings.json文件里設置了正確的SQL Server連接字符串。當然,你也可以選擇使用其它類型的數據庫,只要對ConfigureServices方法做些相應的修改即可。在Package Manager Console中,依次執行下面的命令:

    1

    2

    3

    4

    Add-Migration ModifiedUserAndRole -Context ApplicationDbContext

    Add-Migration ModifiedUserAndRole –Context PersistedGrantDbContext

    Update-Database -Context ApplicationDbContext

    Update-Database -Context PersistedGrantDbContext

    效果如下:

    打開SQL Server Management Studio,看到數據表都已成功創建:

    由于IdentityServer4的模板所產生的代碼使用的是mock user,也就是IdentityServer4里默認的TestUser,因此,相關部分的代碼需要被替換掉,最主要的部分就是AccountController的Login方法,將該方法中的相關代碼替換為:


    if (ModelState.IsValid)

    {

    ????var user = await _userManager.FindByNameAsync(model.Username);

    ?

    ????if (user != null && await _userManager.CheckPasswordAsync(user, model.Password))

    ????{

    ????????await _events.RaiseAsync(new UserLoginSuccessEvent(user.UserName, user.Id, user.DisplayName));

    ?

    ????????// only set explicit expiration here if user chooses "remember me".

    ????????// otherwise we rely upon expiration configured in cookie middleware.

    ????????AuthenticationProperties props = null;

    ????????if (AccountOptions.AllowRememberLogin && model.RememberLogin)

    ????????{

    ????????????props = new AuthenticationProperties

    ????????????{

    ????????????????IsPersistent = true,

    ????????????????ExpiresUtc = DateTimeOffset.UtcNow.Add(AccountOptions.RememberMeLoginDuration)

    ????????????};

    ????????};

    ?

    ????????// issue authentication cookie with subject ID and username

    ????????await HttpContext.SignInAsync(user.Id, user.UserName, props);

    ?

    ????????if (context != null)

    ????????{

    ????????????if (await _clientStore.IsPkceClientAsync(context.ClientId))

    ????????????{

    ????????????????// if the client is PKCE then we assume it's native, so this change in how to

    ????????????????// return the response is for better UX for the end user.

    ????????????????return View("Redirect", new RedirectViewModel { RedirectUrl = model.ReturnUrl });

    ????????????}

    ?

    ????????????// we can trust model.ReturnUrl since GetAuthorizationContextAsync returned non-null

    ????????????return Redirect(model.ReturnUrl);

    ????????}

    ?

    ????????// request for a local page

    ????????if (Url.IsLocalUrl(model.ReturnUrl))

    ????????{

    ????????????return Redirect(model.ReturnUrl);

    ????????}

    ????????else if (string.IsNullOrEmpty(model.ReturnUrl))

    ????????{

    ????????????return Redirect("~/");

    ????????}

    ????????else

    ????????{

    ????????????// user might have clicked on a malicious link - should be logged

    ????????????throw new Exception("invalid return URL");

    ????????}

    ????}

    ?

    ????await _events.RaiseAsync(new UserLoginFailureEvent(model.Username, "invalid credentials", clientId: context?.ClientId));

    ????ModelState.AddModelError(string.Empty, AccountOptions.InvalidCredentialsErrorMessage);

    }

    這樣才能通過注入的userManager和EntityFramework Core來訪問SQL Server,以完成登錄邏輯。

    新用戶注冊API

    由IdentityServer4所提供的默認UI模板中沒有包括新用戶注冊的頁面,開發者可以根據自己的需要向Identity Service中增加View來提供注冊界面。不過為了快速演示,我打算先增加兩個API,然后使用curl來新建一些用于測試的角色(Role)和用戶(User)。下面的代碼為客戶端提供了注冊角色和注冊用戶的API:

    18

    119

    public class RegisterRoleRequestViewModel

    {

    ????[Required]

    ????public string Name { get; set; }

    ?

    ????public string Description { get; set; }

    }

    ?

    public class RegisterRoleResponseViewModel

    {

    ????public RegisterRoleResponseViewModel(AppRole role)

    ????{

    ????????Id = role.Id;

    ????????Name = role.Name;

    ????????Description = role.Description;

    ????}

    ?

    ????public string Id { get; }

    ?

    ????public string Name { get; }

    ?

    ????public string Description { get; }

    }

    ?

    public class RegisterUserRequestViewModel

    {

    ????[Required]

    ????[StringLength(50, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 2)]

    ????[Display(Name = "DisplayName")]

    ????public string DisplayName { get; set; }

    ?

    ????public string Email { get; set; }

    ?

    ????[Required]

    ????[StringLength(100, ErrorMessage = "The {0} must be at least {2} and at max {1} characters long.", MinimumLength = 6)]

    ????[DataType(DataType.Password)]

    ????[Display(Name = "Password")]

    ????public string Password { get; set; }

    ?

    ????[Required]

    ????[StringLength(20)]

    ????[Display(Name = "UserName")]

    ????public string UserName { get; set; }

    ?

    ????public List<string> RoleNames { get; set; }

    }

    ?

    public class RegisterUserResponseViewModel

    {

    ????public string Id { get; set; }

    ????public string UserName { get; set; }

    ????public string DisplayName { get; set; }

    ????public string Email { get; set; }

    ?

    ????public RegisterUserResponseViewModel(AppUser user)

    ????{

    ????????Id = user.Id;

    ????????UserName = user.UserName;

    ????????DisplayName = user.DisplayName;

    ????????Email = user.Email;

    ????}

    }

    ?

    // Controllers\Account\AccountController.cs

    [HttpPost]

    [Route("api/[controller]/register-account")]

    public async Task<IActionResult> RegisterAccount([FromBody] RegisterUserRequestViewModel model)

    {

    ????if (!ModelState.IsValid)

    ????{

    ????????return BadRequest(ModelState);

    ????}

    ?

    ????var user = new AppUser { UserName = model.UserName, DisplayName = model.DisplayName, Email = model.Email };

    ?????

    ?

    ????var result = await _userManager.CreateAsync(user, model.Password);

    ?

    ????if (!result.Succeeded) return BadRequest(result.Errors);

    ?

    ????await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.NameIdentifier, user.UserName));

    ????await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Name, user.DisplayName));

    ????await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, user.Email));

    ?

    ????if (model.RoleNames?.Count > 0)

    ????{

    ????????var validRoleNames = new List<string>();

    ????????foreach(var roleName in model.RoleNames)

    ????????{

    ????????????var trimmedRoleName = roleName.Trim();

    ????????????if (await _roleManager.RoleExistsAsync(trimmedRoleName))

    ????????????{

    ????????????????validRoleNames.Add(trimmedRoleName);

    ????????????????await _userManager.AddToRoleAsync(user, trimmedRoleName);

    ????????????}

    ????????}

    ?

    ????????await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Role, string.Join(',', validRoleNames)));

    ????}

    ?

    ????return Ok(new RegisterUserResponseViewModel(user));

    }

    ?

    // Controllers\Account\AccountController.cs

    [HttpPost]

    [Route("api/[controller]/register-role")]

    public async Task<IActionResult> RegisterRole([FromBody] RegisterRoleRequestViewModel model)

    {

    ????if (!ModelState.IsValid)

    ????{

    ????????return BadRequest(ModelState);

    ????}

    ?

    ????var appRole = new AppRole { Name = model.Name, Description = model.Description };

    ????var result = await _roleManager.CreateAsync(appRole);

    ????if (!result.Succeeded) return BadRequest(result.Errors);

    ?

    ????return Ok(new RegisterRoleResponseViewModel(appRole));

    }

    在上面的代碼中,值得關注的就是register-account API中的幾行AddClaimAsync調用,我們將一些用戶信息數據加入到User Identity的Claims中,比如,將用戶的角色信息,通過逗號分隔的字符串保存為Claim,在后續進行用戶授權的時候,會用到這些數據。

    創建一些基礎數據

    運行我們已經搭建好的Identity Service,然后使用下面的curl命令創建一些基礎數據:

    1

    2

    3

    4

    5

    6

    7

    8

    9

    curl -X POST https://localhost:7890/api/account/register-role \

    ??-d '{"name":"admin","description":"Administrator"}' \

    ??-H 'Content-Type:application/json' --insecure

    curl -X POST https://localhost:7890/api/account/register-account \

    ??-d '{"userName":"daxnet","password":"P@ssw0rd123","displayName":"Sunny Chen","email":"daxnet@163.com","roleNames":["admin"]}' \

    ??-H 'Content-Type:application/json' --insecure

    curl -X POST https://localhost:7890/api/account/register-account \

    ??-d '{"userName":"acqy","password":"P@ssw0rd123","displayName":"Qingyang Chen","email":"qychen@163.com"}' \

    ??-H 'Content-Type:application/json' --insecure

    完成這些命令后,系統中會創建一個admin的角色,并且會創建daxnet和acqy兩個用戶,daxnet具有admin角色,而acqy則沒有該角色。

    使用瀏覽器訪問https://localhost:7890,點擊主頁的鏈接進入登錄界面,用已創建的用戶名和密碼登錄,可以看到如下的界面,表示Identity Service的開發基本完成:

    小結

    一篇文章實在是寫不完,今天就暫且告一段落吧,下一講我將介紹Weather API和基于Ocelot的API網關,整合Identity Service進行身份認證。

    源代碼

    訪問以下Github地址以獲取源代碼:

    https://github.com/daxnet/identity-demo

    創作挑戰賽新人創作獎勵來咯,堅持創作打卡瓜分現金大獎

    總結

    以上是生活随笔為你收集整理的Angular SPA基于Ocelot API网关与IdentityServer4的身份认证与授权(一)的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。