Asp.Net Core 中IdentityServer4 授權中心之應用實戰
阿新 • • 發佈:2020-03-11
## 一、前言
查閱了大多數相關資料,查閱到的IdentityServer4 的相關文章大多是比較簡單並且多是翻譯官網的文件編寫的,我這裡在
Asp.Net Core 中IdentityServer4 的應用分析中會以一個電商系統架構升級過程中普遍會遇到的場景進行實戰性講述分析,同時最後會把我的實戰性的程式碼放到github 上,敬請大家關注!
這裡就直接開始擼程式碼,概念性東西就已經不概述了,想要了解概念推薦大家檢視我之前的文章和官方文件:
- [Asp.Net Core IdentityServer4 中的基本概念](https://www.cnblogs.com/jlion/p/12437441.html)
- [IdentityServer4 官方文件](https://identityserver4.readthedocs.io/en/latest/)
## 二、應用實戰
### 2.1 模擬場景
最初小團隊的電商系統場景如下圖:
![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200310110017008-1660735876.png)
這張架構圖缺點:
- 釋出頻繁,釋出影響整個電商系統
- 很難做到敏捷開發
- 維護性可能會存在一定的弊端,主要看內部架構情況。
大多數小電商團隊對於多客戶端登入授權來說可能已經實現了Oauth 2.0 的身份授權驗證,但是是和電商業務整合在一個閘道器裡面,這樣不是很好的方式;由於公司業務橫向擴大,產品經理調研了代理商業務,最終讓技術開發代理商業務系統。架構師出於後續發展的各方面考慮,把代理商業務單獨建立了一個獨立的閘道器,並且把授權服務一併給獨立出來,調整後的電商系統架構圖如下:
![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200310110050365-906620357.png)
身份授權從業務系統中拆分出來後,有了如下的優勢:
- 授權服務不受業務的影響,如果業務閘道器宕機了,那至少不會影響代理商閘道器的業務授權系統的使用
- 授權服務一旦建立,一般就很難進行升級,除非特殊情況。
- 在敏捷開發中,業務系統可能釋出頻繁,電商業務系統可能每天都是在頻繁升級更新,這樣也不至於影響了授權系統服務導致代理商業務受到影響
代理商業務引入進來後,同時又增加了秒殺活動,發現成交量大大增大,支付訂單集中在某一時刻翻了十幾倍,這時候整個電商業務API閘道器已經扛不住了,負載了幾臺可能也有點吃力;開發人員經過跟架構師一起討論,得出了扛不住的原因:主要是秒殺活動高併發的支付,以至於整個電商業務系統受到影響,故準備把支付系統從業務系統中拆分出成獨立的支付閘道器,並做了一定的負載,成功解決了以上問題,這時候整個電商系統架構圖就演變成如下:
![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200310111100212-1434441217.png)
支付閘道器服務抽離後的優勢:
- 支付閘道器服務更新不會太頻繁,可以減少整個系統的因為釋出導致的一系列問題,增強穩定性
- 支付系統出現宕機不影響整個電商系統的使用,使用者還可以瀏覽商品等等其他操作,技術和運維人員也比較好排查定位問題所在;提升使用者體驗,同時提升排查問題的效率。
`授權中心`:單獨一個服務閘道器,訪問`支付業務閘道器`、`電商業務閘道器`及`代理商業務閘道器`都需要先通過`授權中心`獲得授權拿到訪問令牌`AccessToken` 才能正常的訪問這些閘道器,這樣授權模組就不會受任何的業務影響,同時各個業務閘道器也不需要寫同樣的授權業務的程式碼;`業務閘道器`僅僅只需關注本身的業務即可,`授權中心`僅僅只需要關注維護授權;經過這樣升級改造後整個系統維護性得到很大的提高,相關的業務也可以針對具體情況進行選擇性的擴容。
上面的電商閘道器演變架構圖中我這裡沒有畫出具體的請求流向,偷了個賴,這裡還是先把OAuth2.0 的授權大體的流程圖單獨貼出來:
![](https://img-blog.csdnimg.cn/20190708144314592.png?x-oss-process=image/watermark,type_ZmFuZ3poZW5naGVpdGk,shadow_10,text_aHR0cHM6Ly9ibG9nLmNzZG4ubmV0L3dxeTI0OA==,size_16,color_FFFFFF,t_70)
由於`授權閘道器服務`之前單獨抽離出來了,這次把支付業務閘道器拆分出來就也比較順利,一下子就完成了電商系統的架構升級。今天這篇文章的目的架構升級也就完成了,想要深入後續電商系統架構升級的同學可以關注後續給大家帶來的微服務的相關教程,到時繼續以這個例子來進行微服務架構上的演變升級,敬請大家關注。好了下面我們來回歸該升級的和核心主題`授權閘道器服務` `IdentityServer4` 的應用。
### 2.2 IdentityServer4 密碼授權模式
#### 授權閘道器服務
##### 靜態記憶體配置方式
> 定義資源
分資源分為身份資源(`Identity resources`)和API資源(`API resources`)。
我們先建立Jlion.NetCore.Identity.Service 閘道器服務專案,在閘道器服務中新增受保護的`API資源`,建立`OAuthMemoryData` 類程式碼如下:
```
///
/// Api資源 靜態方式定義
///
///
public static IEnumerable GetApiResources()
{
return new List
{
new ApiResource(OAuthConfig.UserApi.ApiName,OAuthConfig.UserApi.ApiName),
};
}
```
> 定義客戶端Client
接下來`OAuthMemoryData` 類中定義一個客戶端應用程式的`Client`,我們將使用它來訪問我們的API資原始碼如下:
```
public static IEnumerable GetClients()
{
return new List
{
new Client()
{
ClientId =OAuthConfig.UserApi.ClientId,
AllowedGrantTypes = new List()
{
GrantTypes.ResourceOwnerPassword.FirstOrDefault(),//Resource Owner Password模式
},
ClientSecrets = {new Secret(OAuthConfig.UserApi.Secret.Sha256()) },
AllowedScopes= {OAuthConfig.UserApi.ApiName},
AccessTokenLifetime = OAuthConfig.ExpireIn,
},
};
}
```
- `AllowedGrantTypes` :配置授權型別,可以配置多個授權型別
- `ClientSecrets`:客戶端加密方式
- `AllowedScopes`:配置授權範圍,這裡指定哪些API 受此方式保護
- `AccessTokenLifetime`:配置Token 失效時間
- `GrantTypes`:授權型別,這裡使用的是密碼模式`ResourceOwnerPassword`
程式碼中可以看到有一個`OAuthConfig` 類,這個類是我單獨建的,是用於統一管理,方便維護,程式碼如下:
```
public class OAuthConfig
{
///
/// 過期秒數
///
public const int ExpireIn = 36000;
///
/// 使用者Api相關
///
public static class UserApi
{
public static string ApiName = "user_api";
public static string ClientId = "user_clientid";
public static string Secret = "user_secret";
}
}
```
如果後續架構升級,添加了其他的閘道器服務,則只需要在這裡新增所需要保護的API 資源,也可以通過讀取資料庫方式讀取受保護的Api資源。
接下來`OAuthMemoryData` 類新增測試使用者,程式碼如下:
```
///
/// 測試的賬號和密碼
///
///
public static List GetTestUsers()
{
return new List
{
new TestUser()
{
SubjectId = "1",
Username = "test",
Password = "123456"
}
};
}
```
上面受保護的資源,和客戶端以及測試賬號都已經建立好了,現在需要把IdentityServer4 註冊到DI中:
`Startup` 中的`ConfigureServices` 程式碼如下:
```
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
#region 記憶體方式
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryApiResources(OAuthMemoryData.GetApiResources())
.AddInMemoryClients(OAuthMemoryData.GetClients())
.AddTestUsers(OAuthMemoryData.GetTestUsers());
#endregion
}
```
程式碼解讀:
- `AddDeveloperSigningCredential`:新增證書加密方式,執行該方法,會先判斷tempkey.rsa證書檔案是否存在,如果不存在的話,就建立一個新的tempkey.rsa證書檔案,如果存在的話,就使用此證書檔案。
- `AddInMemoryApiResources`:把受保護的Api資源新增到記憶體中
- `AddInMemoryClients` :客戶端配置新增到記憶體中
- `AddTestUsers` :測試的使用者新增進來
最後通過`UseIdentityServer()`需要把IdentityServer4 中介軟體新增到Http管道中,程式碼如下:
```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseIdentityServer();
app.UseRouting();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
```
好了,現在`授權閘道器服務`程式碼已經完成,現在直接通過命令列方式啟動,命令列啟動如下,我指定5000埠,如下圖:
![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200309145420622-518750766.png)
#### 電商使用者閘道器Api專案
現在我來新建一個WebApi 大的使用者閘道器服務專案,取名為`Jlion.NetCore.Identity.UserApiService`,新建後會預設有一個天氣預報的api介面,程式碼如下:
```
[ApiController]
[Route("[controller]")]
public class WeatherForecastController : ControllerBase
{
private static readonly string[] Summaries = new[]
{
"Freezing", "Bracing", "Chilly", "Cool", "Mild", "Warm", "Balmy", "Hot", "Sweltering", "Scorching"
};
private readonly ILogger _logger;
public WeatherForecastController(ILogger logger)
{
_logger = logger;
}
[HttpGet]
public IEnumerable Get()
{
var rng = new Random();
return Enumerable.Range(1, 5).Select(index => new WeatherForecast
{
Date = DateTime.Now.AddDays(index),
TemperatureC = rng.Next(-20, 55),
Summary = Summaries[rng.Next(Summaries.Length)]
})
.ToArray();
}
}
```
接下來在`Startup` 類中新增授權閘道器服務的配置到DI中,程式碼如下:
```
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthorization();
services.AddAuthentication("Bearer")
.AddIdentityServerAuthentication(options =>
{
options.Authority = "http://localhost:5000"; //配置Identityserver的授權地址
options.RequireHttpsMetadata = false; //不需要https
options.ApiName = OAuthConfig.UserApi.ApiName; //api的name,需要和config的名稱相同
});
}
```
這裡的`options.ApiName` 需要和閘道器服務中的Api 資源配置中的ApiName 一致
接下來需要把授權和認證中介軟體分別註冊到Http 管道中,程式碼如下:
```
public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers();
});
}
```
現在授權服務閘道器啟用已經完成,只需要在需要保護的`Controller` 中新增 `Authorize` 過濾器即可,現在我也通過命令列把需要保護的閘道器服務啟動,如圖:
![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200309171909609-1631064921.png)
現在我通過postman 工具來單獨訪問 使用者閘道器服務API,不攜帶任何資訊的情況下,如圖:
![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200309172307873-328739614.png)
從訪問結果可以看出返回`401 Unauthorized` 未授權。
我們接下來再來訪問授權服務閘道器,如圖:
![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200309172500470-189757185.png)
請求閘道器服務中body中攜帶了使用者名稱及密碼等相關資訊,這是返回了`access_token` 及有效期等相關資訊,我們再拿`access_token` 來繼續上面的操作,訪問使用者業務閘道器的介面,如圖:
![](https://img2020.cnblogs.com/blog/824291/202003/824291-20200309172710633-1291755074.png)
訪問結果中已經返回了我們所需要的介面資料,大家目前已經對密碼模式的使用有了一定的瞭解,但是這時候可能會有人問我,我生產環境中可能需要通過資料庫的方式進行使用者資訊的判斷,以及客戶端授權方式需要更加靈活的配置,可通過後臺來配置ClientId以及授權方式等,那應該怎麼辦呢?下面我再來給大家帶來生存環境中的實現方式。
##### 資料庫匹配驗證方式
我們需要通過使用者名稱和密碼到資料庫中驗證方式則需要實現`IResourceOwnerPasswordValidator` 介面,並實現`ValidateAsync` 驗證方法,簡單的程式碼如下:
```
public class ResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
{
public async Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
{
try
{
var userName = context.UserName;
var password = context.Password;
//驗證使用者,這麼可以到資料庫裡面驗證使用者名稱和密碼是否正確
var claimList = await ValidateUserAsync(userName, password);
// 驗證賬號
context.Result = new GrantValidationResult
(
subject: userName,
authenticationMethod: "custom",
claims: claimList.ToArray()
);
}
catch (Exception ex)
{
//驗證異常結果
context.Result = new GrantValidationResult()
{
IsError = true,
Error = ex.Message
};
}
}
#region Private Method
///
/// 驗證使用者
///
///
///
///
private async Task
- > ValidateUserAsync(string loginName, string password)
{
//TODO 這裡可以通過使用者名稱和密碼到資料庫中去驗證是否存在,
// 以及角色相關資訊,我這裡還是使用記憶體中已經存在的使用者和密碼
var user = OAuthMemoryData.GetTestUsers();
if (user == null)
throw new Exception("登入失敗,使用者名稱和密碼不正確");
return new List