書接上回,我們將會正式開始介紹IdentityServer4。
IdentityServer4是實現了OAuth2.0+OpenId Connect兩種協議的優秀第三方庫,屬於.net生態中的優秀成員。可以很容易集成至ASP.NET Core,頒發token。
使用Id4
基本步驟如下:
**1.**在
Startup.Configure
方法中調用
app.UseIdentityServer();
添加中間件,把Id4添加至http請求處理管道,這使得Id4可以爲OpenID Connect和OAuth2協議描述的端點(如/connect/token
)請求提供服務。
**2.**在
Startup.ConfigureServices
中註冊IdentityServer4
services.AddIdentityServer(options=>
{
...
});
**3.**配置Identity Server
-
Identity資源表示提供給客戶端進行用戶識別的信息(聲明)。聲明可能包括用戶名稱、電子郵件地址等。
API資源表示用戶可通過訪問令牌訪問的受保護數據或功能。API 資源的一個示例是要求授權的 Web API(或 API集合)。
用於簽名的憑據(credentials)
用戶可能會請求訪問的Identity資源和API資源
會請求獲取token的客戶端
用戶信息的存儲機制,如ASP.NET Core Identity或者其他機制
當你指明Id4使用的客戶端和資源,可以將IEnumerable<T>
傳遞給接受內存中的客戶端或資源存儲的方法,如果在更復雜的場景,可以通過依賴注入的方式提供客戶端和資源提供程序類型。
IdentityServer4 使用自定義 IClientStore 類型提供的內存中資源和客戶端的示例配置:
public IServiceProvider ConfigureServices(IServiceCollection services)
{
//...
services.AddSingleton<IClientStore, CustomClientStore>();
services.AddIdentityServer()
.AddSigningCredential("CN=sts")
.AddInMemoryApiResources(MyApiResourceProvider.GetAllResources())
.AddAspNetIdentity<ApplicationUser>();
//...
}
經過上面的概述,我們先易後難,將從最簡單的客戶端憑證開始實戰搭建IdentityServer4項目並以此保護api資源,首先客戶端憑證屬於OAuth2.0的一種授權方式。
主要是向IdentityServer發送post請求
token?grant_type=client_credentials& client_id=CLIENT_ID& client_secret=CLIENT_SECRET
,獲取access-token,以此來訪問api資源。在IdentityServer4中,增加了
Scope
參數,表明了客戶端的訪問權限
1.安裝Id4模板
dotnet new -i IdentityServer4.Templates
AdminUI
:測試,生產環境需要交錢,商業軟件ASP.NET Core Identity
:結合ASP.NET Core IndentityEmpty
:空模板Entity Frame Store
:使用ef數據持久化身份認證信息In-Memory Stores and Test Users
:添加內存中的用戶認證信息,和測試用戶Quickstart UI (UI assets only)
:UI
2.創建ASP.NET Core應用,搭載Id4
2.1 創建項目
使用IdentityServer4的空模板創建應用
md quickstart
cd quickstart
md src
cd src
#空模板 項目
dotnet new is4empty -n IdentityServer
The template "IdentityServer4 Empty" was created successfully.
項目添加解決方案
cd ..
dotnet new sln -n QuickStart
dotnet sln add .\IdentityServer\IdentityServer.csproj
2.2 修改launchSettings.json
測試環境,使用http,刪掉IIS相關的配置
{
"profiles": {
"SelfHost": {
"commandName": "Project",
"launchBrowser": true,
"launchUrl": ".well-known/openid-configuration",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
},
"applicationUrl": "http://localhost:5001/"
}
}
}
2.3 定義一個api scope
上篇與前文都介紹過,scope
代表資源所有者在被保護資源那裏的一些權限,可以把被保護資源分爲不同的scope
,具體的粒度由開發自定義。
模板中ApiScope爲空,在Config.cs增加
public static IEnumerable<ApiScope> ApiScopes =>
new ApiScope[]
{
//新增
new ApiScope("api1", "My API")
};
第二個參數,是
displayname
2.4 定義一個客戶端
要讓我們的IdentityServer給客戶端頒發token,就要讓客戶端在IdentityServer註冊。
客戶端,模板中的客戶端與scope一樣爲空,在Config.cs增加客戶端,代碼如下:
public static IEnumerable<Client> Clients =>
new Client[]
{
new Client
{
ClientId = "client app",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.ClientCredentials,
// secret for authentication
ClientSecrets =
{
new Secret("secret-123456".Sha256())
},
// scopes that client has access to
AllowedScopes = { "api1" }
}
};
“我們之後會增加這個定義的客戶端,這個客戶端將會訪問
”AllowedScopes
指定的api scope。
注意:在此場景下,客戶端跟用戶是沒有交互的,身份認證是通過IdentityServer的客戶密鑰。
官方描述:你可以把ClientId和ClientSecret看作應用程序本身的登錄名和密碼。它向身份服務器表明您的應用程序的身份(我是xx應用程序,想訪問服務器)。
2.5 註冊IdentityServer
註釋模板代碼Startup.ConfigureServices()
所有代碼,增加代碼:加載定義的資源和客戶端,代碼如下:
public void ConfigureServices(IServiceCollection services)
{
var builder = services.AddIdentityServer()
.AddInMemoryApiScopes(Config.ApiScopes)
.AddInMemoryClients(Config.Clients);
// omitted for brevity
}
配置完成。運行並瀏覽器訪問http://localhost:5001/.well-known/openid-configuration
,就能看到discovery document
.
它是IdentityServer中的標準端點
客戶端和APIs會使用它下載必要的配置數據,容後再表
在第一次啓動時,IdentityServer將創建一個開發者簽名密鑰,它是一個名爲tempkey.rsa的文件。您不必將該文件簽入源代碼版本控制,如果不存在該文件,它將被重新創建。
3.創建webapi
限制開始創建我們需要保護的api資源
3.1 新建項目
dotnet new webapi -n webapi
cd ..
dotnet sln add .\webapi\webapi.csproj
3.2 修改launchSettings.json
{
"profiles": {
"Api": {
"commandName": "Project",
"launchBrowser": true,
"applicationUrl": "http://localhost:6001",
"environmentVariables": {
"ASPNETCORE_ENVIRONMENT": "Development"
}
}
}
}
3.3 新增api
IdentityController.cs
[Route("api/[controller]")]
[ApiController]
public class IdentityController : ControllerBase
{
[HttpGet]
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}
3.4 引入nuget包
Microsoft.AspNetCore.Authentication.JwtBearer
這個包是當收到請求時,對授權頭中JWT的具體身份認證
dotnet add .\webapi\webapi.csproj package Microsoft.AspNetCore.Authentication.JwtBearer
3.5 註冊服務和添加中間件
最後一步是將身份認證服務添加到依賴注入中,並將身份認證中間件添加到管道中。以便:
驗證傳入的token,確保token來自可信的頒佈者(服務器)
驗證這個token在這個api中使用是有效的(也就是受衆)
看代碼:
{
public void ConfigureServices(IServiceCollection services)
{
services.AddControllers();
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
//token頒發者
options.Authority = "http://localhost:5001";
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false
};
options.RequireHttpsMetadata = false;
});
services.AddAuthorization(options =>
{
options.AddPolicy("ApiScope", policy =>
{
policy.RequireAuthenticatedUser();
//scope
policy.RequireClaim("scope", "api1");
});
});
}
public void Configure(IApplicationBuilder app)
{
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseEndpoints(endpoints =>
{
endpoints.MapControllers().RequireAuthorization("ApiScope");
});
}
}
AddAuthentication
:增加認證服務到依賴注入,註冊Bearer
作爲默認scheme
AddAuthorization
:增加授權服務到依賴注入,驗證token
中是否存在scope
,這裏使用的是ASP.NET Core授權策略系統“
這裏實質是驗證jwt中的payload的scope
”RequireHttpsMetadata
用於測試目的;將此參數設置爲 false,可在你沒有證書的環境中進行測試。在實際部署中,JWT 持有者令牌應始終只能通過 HTTPS 傳遞。UseAuthentication
:添加認證中間件,以便對host的每次調用自動執行身份認證,此中間件準備就緒後,會自動從授權標頭中提取 JWT 令牌。然後對其進行反序列化、驗證,,並將其存儲爲用戶信息,稍後供 MVC 操作或授權篩選器引用。“
JWT 持有者身份驗證中間件還可以支持更高級的方案,例如頒發機構authority 不可用時使用本地證書驗證令牌。對於此情景,可以在
”JwtBearerOptions
對象中指定TokenValidationParameters
對象。UseAuthorization
:添加授權中間件,以確保我們的api不會被匿名客戶端訪問RequireAuthorization("ApiScope")
:全局執行授權策略“
除了全局以外,還可以針對多有的api端點,或者特定的controller,action,根據實際的業務場景靈活變化吧
”
訪問:http://localhost:6001/identity
,返回狀態碼401,這是api要求憑證,所以現在api是被IdentityServer保護着
4.創建客戶端
最後一步,創建一個由IdentityServer管理的客戶端,並通過客戶端請求access-token,然後訪問api
4.1 新建項目
dotnet new console -n Client
dotnet sln add .\Client\Client.csproj
4.2 引入nuget包
需要引入IdentityModel
包,一個客戶端類,以請求disconvery endpoint
cd .\Client\
dotnet add package IdentityModel
4.3 編碼-請求Idisconvery endpoint
只需要知道IdentityServer的基礎地址,實際的各類端點地址就可以從元數據中讀取:
class Program
{
static async Task Main(string[] args)
{
// discover endpoints from metadata
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5001");
if (disco.IsError)
{
Console.WriteLine(disco.Error);
return;
}
}
}
4.4 編碼-請求access token
這一步,使用從discovery document
中獲取的信息,向IdentityServer
請求一個訪問api1的token:
// request token
var tokenResponse = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest
{
Address = disco.TokenEndpoint,
//在IdentityServer註冊的id與secret
ClientId = "client app",
ClientSecret = "secret-123456",
Scope = "api1"
});
if (tokenResponse.IsError)
{
Console.WriteLine(tokenResponse.Error);
return;
}
Console.WriteLine(tokenResponse.Json);
注意看,這裏的ClientId,ClientSecret
必須與2.4中定義客戶端保持一致,Scope
也要與AllowedScopes
保持一致。
4.5 編碼-調用api
在這一步,使用擴展方法SetBearerToken
,這個方法主要組裝http請求:授權頭+access token,並以此請求訪問api資源:
// call api
var apiClient = new HttpClient();
apiClient.SetBearerToken(tokenResponse.AccessToken);
var response = await apiClient.GetAsync("http://localhost:6001/api/identity");
if (!response.IsSuccessStatusCode)
{
Console.WriteLine(response.StatusCode);
}
else
{
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(JArray.Parse(content));
}
5.測試
啓動IdentityServer
cd .\IdentityServer\
dotnet run
啓動webapi
cd .\webapi\
dotnet run
用vs啓動client
獲取access-token,我們通過http://jwt.calebb.net/解析
這也是api返回的Claims
“身份認證的中間對JWT進行了身份認證後,會把解析到的Claims組裝進HttpContext,以供下一個中間件(如授權中間件)調用
”
接下來我們就去觸發不同的錯誤去了解IdentityServer是如何工作的,我選擇其中幾個比較有意義的測試:
5.1 使用一個無效客戶端id或者密鑰請求token
沒被註冊的客戶端,訪問時,所以是invalid_client
類比場景:去辦理門禁卡,物業沒找到你這個業主信息,辦個鬼呀
5.2 在請求token時指定無效的scope
請求token,指定的scope,在indentityserver中並不存在,所以是invalid_scope
類比場景:去辦理門禁卡,小區一共10棟,你去辦11棟,辦個鬼呀
5.3 請求api時,不傳入toekn
不傳入token,那麼webapi就沒收到token,所以返回Unauthorized
未授權
類比場景:進入小區,沒有門禁,肯定不讓你進
5.4 修改API對scope的驗證要求
被保護的資源webapi中配置plicy.RequireClaim("scope","api2");
而客戶端指定的scope是api1
客戶端是有access-token,具有進入系統憑證,但是,只允許scope爲api2的訪問,傳入的時api1,當然就返回Forbidden
類比場景:小區進入後,進入單元樓,明明是3棟2單元的樓宇,但是你的門禁只能針對3棟1單元,當然也不會刷開2單元的大門
參考鏈接
https://identityserver4.readthedocs.io/en/latest/quickstarts/1_client_credentials.html#source-code
長按二維碼關注 點外賣,先領券