Ver código fonte

为API添加自定义claim

master
qdjjx 5 anos atrás
pai
commit
76f7eaa500

+ 249
- 0
实践/后端/项目/19IdentityServer4给API自定义claim.md Ver arquivo

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

Carregando…
Cancelar
Salvar