鼎鼎知识库
Вы не можете выбрать более 25 тем Темы должны начинаться с буквы или цифры, могут содержать дефисы(-) и должны содержать не более 35 символов.

19IdentityServer4给API自定义claim.md 6.9KB

5 лет назад
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249
  1. IdentityServer4有自己的验证逻辑,需要将需要的Claim放入验证结果,然后向API传递。实现`IResourceOwernerPasswordValidator`这个接口就可以。
  2. #在In-Memory中添加Claim
  3. 添加用户
  4. ```
  5. new TestUser{
  6. SubjectId="",
  7. Username="",
  8. Password="",
  9. Claims = new List<Claim>{new Claim(JwtClaimTypes.Role, "superadmin")}
  10. },
  11. new TestUser{
  12. SubjectId = "",
  13. Username="",
  14. Password="",
  15. Claims = new List<Claim>{new Claim(JwtClaimTypes.Role, "admin")}
  16. }
  17. ```
  18. 此时查看`HttpContext.User.Claims`却没有刚才Type是role的claim。为什么呢?
  19. --因为在验证服务器上管理的`ApiResource`中没有把自定义的claim设置进去。如何设置呢?
  20. ```
  21. public static IEnumerable<ApiResource> GetApiResources()
  22. {
  23. return new List<ApiResource>{
  24. new ApiResource("","",new List<string>(){JwtClaimTypes.Role})
  25. };
  26. }
  27. ```
  28. 为什么在`ApiResource`的构造函数把claim能传递出去呢? 再看`ApiResource`的源代码。
  29. ```
  30. public ApiResouce(string name, string displayName, IEnumerable<string> claimTypes)
  31. {
  32. if(name.IsMissing()) throw new ArgumentNullException(nameof(name));
  33. Name = name;
  34. DisplayName = displayName;
  35. //也就是IdentityServer4管理的ApiResource最终会以Scope的形式放在claim集合中
  36. Scopes.Add(new Scope(name, displayName));
  37. if(!claimTypes.IsNullOrEmpty())
  38. {
  39. foreach(var type in claimTypes)
  40. {
  41. UserClaim.Add(type);
  42. }
  43. }
  44. }
  45. ```
  46. 原来给`ApiResource`中的自定义claim给到了这里的`UserClaims`属性,一看这个属性就使用用来存放用户有关的所有claim,肯定是一个集合。
  47. ```
  48. public ICollection<string> UserClaims{get;set;} = new HashSet<string>();
  49. ```
  50. 在API控制器方法中定义不同的控制器方法给不同的角色。
  51. ```
  52. [Route("[controller]")]
  53. public class IdentityController : ControllerBase
  54. {
  55. [Authorize(Roles = "superadmin")]
  56. [HttpGet]
  57. public IActionResult Get()
  58. {
  59. return new JsonResult(from c in HttpContext.User.Claims select new {c.Type, c.Value});
  60. }
  61. [Authorize(Roles = "admin")]
  62. [Route("id")]
  63. [HttpGet]
  64. public string Get(int id)
  65. {
  66. return id.ToString();
  67. }
  68. }
  69. ```
  70. 在一个控制器客户端使用superadmin角色访问第二个控制器方法,即需要admin角色的控制器方法。
  71. ```
  72. response = await client.GetAsync("http://localhost:5001/identity/1");
  73. if(!response.IsSuccessStatusCode)
  74. {
  75. Console.WriteLine(response.StatusCode);
  76. Console.WriteLine("没有权限访问");
  77. }
  78. else
  79. {
  80. var content = response.Content.ReadAsStringAsync().Result;
  81. Console.WriteLine(content);
  82. }
  83. ```
  84. # 在生产环境下添加claim
  85. 需要实现`IResourceOwnerPasswordValidator`接口。
  86. ```
  87. public class CustomResourceOwnerPasswordValidator : IResourceOwnerPasswordValidator
  88. {
  89. private readonly TestUserStore _users;
  90. private readonly ISystemClock _clock;
  91. public CustomResourceOwnerPasswordValidator(TestUserStore users, ISystemClock clock)
  92. {
  93. _users = users;
  94. _clock = clock;
  95. }
  96. public Task ValidateAsync(ResourceOwnerPasswordValidationContext context)
  97. {
  98. if(_users.ValidateCredentials(context.UserName, context.Password))//首先还是要验证用户名和密码
  99. {
  100. var user= _users.FindByUsername(context.UserName);
  101. context.Result = new GrantValidationResult(user.SubjectId ?? throw new ArgumentException(), OidcConstants.AuthenticationMethods.Password, _clock.UtcNow.UtcDateTime, user.claims);//这里把所有的claim返回假如验证逻辑
  102. }
  103. else
  104. {
  105. context.Result = new GrantValidationResult(TokenRequestErrors.InvalidGrant, "");
  106. }
  107. return Task.CompletedTask;
  108. }
  109. }
  110. ```
  111. 需要在DI容器中配置。
  112. ```
  113. public class Startup
  114. {
  115. public void ConfigureServices(IServiceCollection services)
  116. {
  117. services.AddIdentityServer()
  118. .AddDeveloperSigningCredential()
  119. .AddInMemoryApiResources(Config.GetApiResources())
  120. .AddInMemoryClients(Config.GetClients())
  121. .AddTestUsers(Config.GetUsers())
  122. .AddResourceOwnerValidator<CustomResourceOwnerPasswordValidator>();
  123. }
  124. }
  125. ```
  126. 以上已有用户的claim信息已经放到了验证逻辑中。接下来还需要`IProfileService`接口的帮忙。
  127. ```
  128. public class CustomProfileService : IProfileService
  129. {
  130. protected readonly ILogger Logger;
  131. protected readoly TestUserStore Users;
  132. public CustomProfileService(TestUserStore users, ILogger<TestUserProfilerService> logger)
  133. {
  134. Users = users;
  135. Logger = logger;
  136. }
  137. //这里的方法在创建令牌期间会被调用
  138. public virutal Task GetProfileDataAsync(ProfileDataRequestContext context)
  139. {
  140. context.LogProfileRequest(Logger);
  141. if(context.ReqeustedClaimTypes.Any())
  142. {
  143. var user = Users.FindBySubjectId(context.Subject.GetSubjectId());
  144. if(user != null)
  145. {
  146. //这里只将用户请求的claim加入到context.IssuedClaims集合中去
  147. context.AddRequestedClaims(user.Claims);
  148. }
  149. }
  150. context.LogIssuedClaims(Logger);
  151. return Task.CompletedTask;
  152. }
  153. }
  154. ```
  155. 以上只返回客户端请求的claims,但是如果用以下写法会把所有的claims都返回给客户端。
  156. ```
  157. context.IssuedClaims.AddRange(user.Claims);
  158. ```
  159. `IProfileService`也需要放到容器中。
  160. ```
  161. servces.AddProfileService<CustomProfileService>();
  162. ```
  163. 以上所有的claim返回就少了控制,不见得是好事。
  164. 如果想控制发出的claims,一种方式是通过身份资源。身份资源会放到token的Scope参数中。
  165. ```
  166. public static IEnumerable<IdentiyResource> GetIdentityResourceResources()
  167. {
  168. var customProfile = new IdentityResource{
  169. "custom.profile","",new []{"role"}
  170. };
  171. return new List<IdentityResource>{
  172. new IdentityResource.OpenId(),
  173. new IdentityResource.Profile(),
  174. customProfile
  175. }
  176. }
  177. ```
  178. 需要注意的是IdentityResource需要和Client中的AllowdScopes配合起来用。
  179. ```
  180. new Client
  181. {
  182. ClientId="",
  183. AllowdGrantTypes = GrantTypes.ResourceOwnerPassword,
  184. ClientSecrets = {
  185. new Secret()
  186. },
  187. //包括APIresource, IdentityResource
  188. AllowdScopes = {"api1", IdentityServerConstants.StandardScopes.OpenId, IdentityServerConstatns.StandardScopes.Profile, "custom.profile"}
  189. }
  190. ```
  191. 另外在Client资源里有一个Claims属性,这里的设置会被直接添加到token中去。
  192. ```
  193. new Client
  194. {
  195. Claims = new List<Claim>{
  196. new Claim(JwtCliamTypes.Role, "admin");
  197. }
  198. }
  199. ```
  200. 放在这里的claim会在token中以`client_role`出现,但在使用的时候还是`Authorize(Roles="Admin")`.
  201. > 总结时刻
  202. 为API提供自定义claim,可以尝试如下切入点:
  203. - 实现`IResourceOwnerPasswordValidator`接口
  204. - 实现`IProfileService`接口
  205. - `IdentiyResource`和`Client`的`AllowedScopes`配合
  206. - `IdentiyResource`和`Client`的`Claims`配合