回顧
上一篇介紹了IdentityServer4客戶端授權的方式,今天來看看IdentityServer4的基於密碼驗證的方式,與客戶端驗證相比,主要是配置文件調整一下,讓我們來看一下
配置修改
public static class Config
{
public static List<TestUser> GetUsers()
{
return new List<TestUser>
{
new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password"
},
new TestUser
{
SubjectId = "2",
Username = "bob",
Password = "password"
}
};
}
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new IdentityResource[]
{
new IdentityResources.OpenId()
};
}
public static IEnumerable<ApiResource> GetApis()
{
return new List<ApiResource>
{
new ApiResource("api1", "My API")
};
}
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "client",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.ClientCredentials,
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
// scopes that client has access to
AllowedScopes = { "api1" }
},
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api1" }
}
};
}
}
通過上面的代碼,與客戶端授權方式相比,多了兩個東西,一個是GetClients()方法中增加了一個Client,授權方式為資源擁有者密碼的模式,另一個是增加了一個方法GetUsers(),真實場景中TestUser一般使用Asp.NetCore.Identity的用戶,這里暫時使用TestUser來測試,IdentityServer4不是用戶管理系統,它是授權框架(發放令牌的)
注冊用戶
在Startup中,把TestUser也添加上
public void ConfigureServices(IServiceCollection services)
{
services.AddIdentityServer()
.AddDeveloperSigningCredential()
.AddInMemoryClients(Config.GetClients())
.AddInMemoryApiResources(Config.GetApis())
.AddInMemoryIdentityResources(Config.GetIdentityResources())
.AddTestUsers(Config.GetUsers());
}
新建測試Api項目
可以選擇刪除IIS的設置,與前一篇文章的操作一致,並修改端口號為5001(Api資源服務地址)
並新增一個控制器IdentityController,繼承自ControllerBase
[Route("identity")]
[Authorize]
public class IdentityController : ControllerBase
{
public IActionResult Get()
{
return new JsonResult(from c in User.Claims select new { c.Type, c.Value });
}
}
注意到IdentityController控制器類上有個特性[Authorize],這個代表這個控制器需要驗證OK后才能訪問,如果沒有[Authorize]就說明訪問不需要授權,在Get方法中這么寫是為了獲取到用戶的身份信息,也就是Bearer Token中所包含的用戶信息,當然也可以從Cookie中獲取,我們使用Bearer Token的方式,以便照顧有移動客戶端的場景
配置Api項目
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
services.AddMvcCore()
.AddAuthorization()
.AddJsonFormatters();
///這里使用5000端口的授權服務端來驗證
services.AddAuthentication("Bearer")
.AddJwtBearer("Bearer", options =>
{
options.Authority = "http://localhost:5000";
options.RequireHttpsMetadata = false;
options.Audience = "api1";
});
}
public void Configure(IApplicationBuilder app)
{
///這句別忘了,啟用驗證
app.UseAuthentication();
app.UseMvc();
}
}
啟動
解決方案右鍵選擇屬性菜單並打開,設置啟動方式,選擇多個啟動項目,啟動,讓兩個服務都運行起來
驗證測試
打開Postman,填入參數,提交,獲取Token
拿到Token后,到5001地址去執行http://localhost:5001/identity,選擇Bearer Token,把剛才獲取到的Token填入,並點擊紅色按鈕
點擊Send 按鈕后,Api執行成功
如果不填寫Token,或者故意將Token填錯將返回401,未授權錯誤
打開網址(https://jwt.io/),把Token復制進去,解析一下看看,與客戶端授權方式相比,多了一個Sub
進一步思考
IdentityServer4應該有可以獲取到用戶信息的端口,我們從之前的發現端點里也能猜到一些,那我們拿着剛剛獲取到的Token去這個端點獲取下試試看
那我們就用Postman測試下,填寫http://localhost:5000/connect/userinfo,使用Get
從上圖可以看出,報403了,被拒絕訪問了,可能哪里出了問題,經過一番搜索,授權服務端Config里GetClients()方法里ro.client這個用戶,有個AllowedScopes = { "api1"},這里的權限可能不足
如下所示
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api1"}
}
調整后,允許的權限如下
AllowedScopes = { "api1",IdentityServerConstants.StandardScopes.OpenId}
...期間重復的步驟省略,再次獲取一次看看
這次OK了,看到一個sub,這個也貌似對應着TestUser里的SubjectId,這個是用戶的唯一編號
new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password",
},
但是如果我想返回更多的用戶信息怎么辦呢,比如返回用戶的電話號碼,Email,以及自定義的類似組織等信息,應該如何處理呢,那我們給用戶增加些身份(Claim)信息
public static List<TestUser> GetUsers()
{
return new List<TestUser>
{
new TestUser
{
SubjectId = "1",
Username = "alice",
Password = "password",
Claims = new Claim[]
{
new Claim(JwtClaimTypes.NickName,"Sarco"),
new Claim(JwtClaimTypes.GivenName,"SarcoTest"),
new Claim(JwtClaimTypes.PhoneNumber,"186221085730"),
new Claim("org_code","3210")
}
},
new TestUser
{
SubjectId = "2",
Username = "bob",
Password = "password"
}
};
}
我們增加了四項身份信息,NickName,GivenName,PhoneNumber和OrgCode,其中第四項是自定義的,
同時修改下Client的AllowedScopes
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
AllowedScopes = { "api1",IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile},
}
再次獲取Token
會發現報錯了,上面scope我沒有填寫,和之前的一樣,但是報錯了,如果填寫上,如api 或者 openid,可以成功,那看看不填有沒有辦法呢,經過一番研究,Config里GetIdentityResources()方法增加一項new IdentityResources.Profile()就可以了。
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile()
};
}
再次獲取下Token,並且根據Token再次獲取用戶信息
會發現,現在nickname和given_name有了,但是phone_number和org_code還是沒有
報錯原因:GetIdentityResources()方法添加后,只是說現在授權資源里包含Profile了,但是在GetClients()方法里的AllowedScopes里並不包含,所以報錯
那么phone_number和org_code怎么沒有出現呢?
我們看下Profile的范圍,根據說明,它包含終端用戶的默認身份信息有name,family_name,given_name,family_name,middle_name,nick_name,preferred_username,profile,picture,website,gender,birthdate,zoneinfo,locate and updated_at,也就是可以這么說,這么多的身份信息都屬於Profile這個組里面,因為之前的用戶信息里,我只添加了四項
new Claim(JwtClaimTypes.NickName,"Sarco"),
new Claim(JwtClaimTypes.GivenName,"SarcoTest"),
new Claim(JwtClaimTypes.PhoneNumber,"186221085730"),
new Claim("org_code","3210")
其中NickName和GivenName是屬於Profile組的,所以,當客戶的AllowedScopes里包含IdentityServerConstants.StandardScopes.Profile時,nick_name和given_name會顯示出來,而PhoneNumber不屬於Profile里,所以不會返回顯示,自定義的組織信息肯定也無法獲取到,還記得我前面說的
經過一番研究,Config里GetIdentityResources()方法增加一項new IdentityResources.Profile()就可以了,這是為什么呢?
記得之前獲取Token的時候,Scope不填寫的時候會報錯,而填api或者openid就不會報錯,是因為如果不填寫,授權服務端就會從CliendId為"ro.client"的客戶端擁有的Scope里全找一次,而我們一開始AllowedScopes里面包含了
IdentityServerConstants.StandardScopes.Profile,但是在GetIdentityResources()方法里沒有添加new IdentityResources.Profile(),所以執行到獲取Scope為IdentityServerConstants.StandardScopes.Profile時,由於找不到這項的IdentityResources,所以失敗了,但是獲取Token的時候填寫api或者openid時,精確查找,由於AllowScopes和GetIdentityResources()都有,所以可以成功,這里可以這么說,AllowedScopes里的項必須在GetIdentityResources()或者GetApis()里里面要有
理解了這一點后,那我們調整下代碼讓自定義的org_code返回
public static IEnumerable<IdentityResource> GetIdentityResources()
{
return new IdentityResource[]
{
new IdentityResources.OpenId(),
new IdentityResources.Profile(),
new IdentityResources.Phone(),
new IdentityResource("org","組織代碼",new string[]{"org_code" })
};
}
public static IEnumerable<Client> GetClients()
{
return new List<Client>
{
new Client
{
ClientId = "client",
// no interactive user, use the clientid/secret for authentication
AllowedGrantTypes = GrantTypes.ClientCredentials,
// secret for authentication
ClientSecrets =
{
new Secret("secret".Sha256())
},
// scopes that client has access to
AllowedScopes = { "api1" }
},
// resource owner password grant client
new Client
{
ClientId = "ro.client",
AllowedGrantTypes = GrantTypes.ResourceOwnerPassword,
ClientSecrets =
{
new Secret("secret".Sha256())
},
///org代表GetIdentityResources()里的自定義new IdentityResource("org","組織代碼",new string[]{"org_code" }),而org_code代表用戶Claims里的new Claim("org_code","3210")
AllowedScopes = { "api1",IdentityServerConstants.StandardScopes.OpenId,
IdentityServerConstants.StandardScopes.Profile,"org"},
}
};
}
調整后,再用postman獲取Token,然后從http://localhost:5000/connect/userinfo里獲取下用戶信息
順利的獲取到了phone_number,org_code等信息
總結
IdentityResource與TestUser中的Claims的關系
TestUser中的多項Claim可以成為一個IdentityResource的一個項,也就是可以創建一個IdentityResource,包含User中的一個和多個Claim信息,類似於教師證里面包含工號和職位等多個相關信息
Client的AllowedScopes與IdentityResource以及ApiResource的關系
Client的AllowedScopes中的項必須在IdentityResource或者ApiResource中能找到,否則也會報錯,ApiResource代表Api資源,IdentityResource項代表能訪問用戶身份信息包含哪些信息