接上一篇: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登錄,不把密碼給到第三方呢?,下一篇講的隱藏模式就解決了這個問題。