接上一篇:IdentityServer4 實現OAuth2.0四種模式之客戶端模式,這一篇講IdentityServer4 使用密碼模式保護API訪問。
一,IdentityServer配置
1,添加用戶
要用到用戶名稱密碼當然得添加用戶,在IdentityServer項目的Config類中的新增一個方法,GetUsers。返回一個TestUser的集合。
public static List<TestUser> GetUsers() {
return new List<TestUser>()
{
new TestUser()
{
//用戶名
Username="apiUser",
//密碼
Password="apiUserPassword",
//用戶Id
SubjectId="0"
}
};
}
添加好用戶還需要要將用戶注冊到IdentityServer4,修改IdentityServer項目的Startup類ConfigureServices方法
public void ConfigureServices(IServiceCollection services)
{
services.AddMvc();
//添加IdentityServer
var builder = services.AddIdentityServer()
//身份信息授權資源
.AddInMemoryIdentityResources(Config.GetIdentityResources())
//API訪問授權資源
.AddInMemoryApiResources(Config.GetApis())
//客戶端
.AddInMemoryClients(Config.GetClients())
//添加用戶
.AddTestUsers(Config.GetUsers());
if (Environment.IsDevelopment())
{
builder.AddDeveloperSigningCredential();
}
else
{
throw new Exception("need to configure key material");
}
}
2,添加客戶端
添加一個客戶端用於用戶名和密碼模式的訪問。客戶端(Client)定義里有一個AllowedGrantTypes的屬性,這個屬性決定了Client可以被那種模式被訪問,GrantTypes.ClientCredentials為客戶端憑證模式,GrantTypes.ResourceOwnerPassword為用戶名密碼模式。上一節添加的Client是客戶端憑證模式,所以還需要添加一個Client用於支持用戶名密碼模式。
public static IEnumerable<Client> GetClients()
{
return new Client[] {
new Client()
{
//客戶端Id
ClientId="apiClientCd",
//客戶端密碼
ClientSecrets={new Secret("apiSecret".Sha256()) },
//客戶端授權類型,ClientCredentials:客戶端憑證方式
AllowedGrantTypes=GrantTypes.ClientCredentials,
//允許訪問的資源
AllowedScopes={
"secretapi"
}
},
new Client()
{
//客戶端Id
ClientId="apiClientPassword",
//客戶端密碼
ClientSecrets={new Secret("apiSecret".Sha256()) },
//客戶端授權類型,ClientCredentials:客戶端憑證方式
AllowedGrantTypes=GrantTypes.ResourceOwnerPassword,
//允許訪問的資源
AllowedScopes={
"secretapi"
}
}
};
}
二,保用密碼模式訪問受保護的Api
1,使用IdentityMvc項目訪問受保護的Api
修改GetData控制器,使其支持密碼模式訪問
public async Task<IActionResult> GetData(string type)
{
type = type ?? "client";
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
if (disco.IsError)
return new JsonResult(new { err=disco.Error});
TokenResponse token = null;
switch (type)
{
case "client":
token = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest()
{
//獲取Token的地址
Address = disco.TokenEndpoint,
//客戶端Id
ClientId = "apiClientCd",
//客戶端密碼
ClientSecret = "apiSecret",
//要訪問的api資源
Scope = "secretapi"
});
break;
case "password":
token = await client.RequestPasswordTokenAsync(new PasswordTokenRequest()
{
//獲取Token的地址
Address = disco.TokenEndpoint,
//客戶端Id
ClientId = "apiClientPassword",
//客戶端密碼
ClientSecret = "apiSecret",
//要訪問的api資源
Scope = "secretapi",
UserName = "apiUser",
Password = "apiUserPassword"
});
break;
}
if (token.IsError)
return new JsonResult(new { err = token.Error });
client.SetBearerToken(token.AccessToken);
string data = await client.GetStringAsync("https://localhost:5001/api/identity");
JArray json = JArray.Parse(data);
return new JsonResult(json);
}
運行三個項目后訪問:https://localhost:5002/home/getdata?type=password
2,使用原生HTTP請求訪問受保護的Api
獲取access_token

獲取到Token后,訪問受保護的API和通過客戶端模式一樣。

三,密碼模式與客戶端憑證模式的區別
到目前為止,昨們還沒有搞清這兩個模式有什么區別,如果僅僅是為了能訪問這個API,那加不加用戶名和密碼有什么區別呢。昨們對比下這兩種模式取得Token后訪問api返回的數據,可以發現用戶名密碼模式返回的Claim的數量要多一些。Claim是什么呢,簡爾言之,是請求方附帶在Token中的一些信息。但客戶端模式不涉及到用戶信息,所以返回的Claim數量會少一些。在IdentityServer4中,TestUser有一個Claims屬性,允許自已添加Claim,有一個ClaimTypes枚舉列出了可以直接添加的Claim。添加一個ClaimTypes.Role試試。
IdentityServer.Config.GetUsers
public static List<TestUser> GetUsers() {
return new List<TestUser>()
{
new TestUser()
{
//用戶名
Username="apiUser",
//密碼
Password="apiUserPassword",
//用戶Id
SubjectId="0",
Claims=new List<Claim>(){
new Claim(ClaimTypes.Role,"admin")
}
}
};
}
這時如果啟動兩個項目,采用用戶密碼和密碼模式獲取Token訪問Api,返回的值依然是沒有role:admin的Claim的。這時又要用到ApiResouce,ApiResouce的構造函數有一個重載支持傳進一個Claim集合,用於允許該Api資源可以攜帶那些Claim。
IdentityServer.Config.GetApis
public static IEnumerable<ApiResource> GetApis()
{
return new ApiResource[] {
//secretapi:標識名稱,Secret Api:顯示名稱,可以自定義
new ApiResource("secretapi","Secret Api",new List<string>(){ ClaimTypes.Role})
};
}
現在可以啟動項目測試一下,可以發現已經可以返回role這個claim了。

Role(角色)這個Claim很有用,可以用來做簡單的權限管理。
首先修改下被保護Api的,使其支持Role驗證
IdentityApi.Controllers.IdentityController.GetUserClaims
[HttpGet]
[Route("api/identity")]
[Microsoft.AspNetCore.Authorization.Authorize(Roles ="admin")]
public object GetUserClaims()
{
return User.Claims.Select(r => new { r.Type, r.Value });
}
然后在IdentityServer端添加一個來賓角色用戶
IdentityServer.Config.GetUsers
public static List<TestUser> GetUsers() {
return new List<TestUser>()
{
new TestUser()
{
//用戶名
Username="apiUser",
//密碼
Password="apiUserPassword",
//用戶Id
SubjectId="0",
Claims=new List<Claim>(){
new Claim(ClaimTypes.Role,"admin")
}
},
new TestUser()
{
//用戶名
Username="apiUserGuest",
//密碼
Password="apiUserPassword",
//用戶Id
SubjectId="1",
Claims=new List<Claim>(){
new Claim(ClaimTypes.Role,"guest")
}
}
};
}
再回到IdentityMvc項目,修改下獲取數據的測試接口GetData,把用戶名和密碼參數化,方便調試
IdentityMvc.HomeContoller.GetData
public async Task<IActionResult> GetData(string type,string userName,string password)
{
type = type ?? "client";
var client = new HttpClient();
var disco = await client.GetDiscoveryDocumentAsync("http://localhost:5000");
if (disco.IsError)
return new JsonResult(new { err=disco.Error});
TokenResponse token = null;
switch (type)
{
case "client":
token = await client.RequestClientCredentialsTokenAsync(new ClientCredentialsTokenRequest()
{
//獲取Token的地址
Address = disco.TokenEndpoint,
//客戶端Id
ClientId = "apiClientCd",
//客戶端密碼
ClientSecret = "apiSecret",
//要訪問的api資源
Scope = "secretapi"
});
break;
case "password":
token = await client.RequestPasswordTokenAsync(new PasswordTokenRequest()
{
//獲取Token的地址
Address = disco.TokenEndpoint,
//客戶端Id
ClientId = "apiClientPassword",
//客戶端密碼
ClientSecret = "apiSecret",
//要訪問的api資源
Scope = "secretapi",
UserName =userName,
Password = password
});
break;
}
if (token.IsError)
return new JsonResult(new { err = token.Error });
client.SetBearerToken(token.AccessToken);
string data = await client.GetStringAsync("https://localhost:5001/api/identity");
JArray json = JArray.Parse(data);
return new JsonResult(json);
}
分別用apiUser和apiUserGuest訪問,用apiUserGuest訪問時請求被拒絕
https://localhost:5002/home/getdata?type=password&userName=apiUserGuest&password=apiUserPassword

上邊是添加ClaimTypes枚舉里定義好的Claim,但如果要定義的Claim不在Claim枚舉里應該怎么辦呢,比如我想所有用戶都有一個項目編號,要添加一個名為prog的Claim。
先在ApiResouce里允許攜帶名為prog.Claim
IdentityServer.Config.GetApis
public static IEnumerable<ApiResource> GetApis()
{
return new ApiResource[] {
//secretapi:標識名稱,Secret Api:顯示名稱,可以自定義
new ApiResource("secretapi","Secret Api",new List<string>(){ ClaimTypes.Role,ClaimTypes.Name,"prog"})
};
}
在用戶定義的Claims屬性里添加prog信息
IdentityServer.Config.GetUsers
public static List<TestUser> GetUsers() {
return new List<TestUser>()
{
new TestUser()
{
//用戶名
Username="apiUser",
//密碼
Password="apiUserPassword",
//用戶Id
SubjectId="0",
Claims=new List<Claim>(){
new Claim(ClaimTypes.Role,"admin"),
new Claim("prog","正式項目"),
}
},
new TestUser()
{
//用戶名
Username="apiUserGuest",
//密碼
Password="apiUserPassword",
//用戶Id
SubjectId="1",
Claims=new List<Claim>(){
new Claim(ClaimTypes.Role,"guest"),
new Claim("prog","測試項目"),
}
}
};
}
使用apiUser訪問
https://localhost:5002/home/getdata?type=password&userName=apiUser&password=apiUserPassword

密碼模式需要知道用戶的密碼,那能不能用戶自己從identityServer登錄,不把密碼給到第三方呢?,下一篇講的隱藏模式就解決了這個問題。
