有了这个信息,服务端就可以得知目前是哪个用户账户在请求API调用,并且它是属于哪个角色,剩下的工作就是基于这个角色信息来决定是否允许当前用户访问当前的API。很显然,这里需要一种合理的设计,而且至少需要满足以下两个需求:
授权机制的实现应该能够被后端多个服务所重用,以便解决“每个服务要各自管理自己的授权逻辑”这一弊端
API控制器不应该自己实现授权部分的代码,可以通过扩展中间件并结合C# Attribute的方式完成
在这里我们就不深入讨论如何去设计这样一套权限认证系统了,今后有机会再介绍吧。
注:Ocelot可以支持多种Claims的转换形式,这里介绍的AddHeadersToRequest只是其中的一种,更多方式可以参考:https://ocelot.readthedocs.io/en/latest/features/claimstransformation.html
Ocelot API网关授权通过Ocelot网关授权,有两种比较常用的方式,一种是在配置文件中,针对不同的downstream配置,设置其RouteClaimsRequirement配置,以便指定哪些用户角色能够被允许访问所请求的API资源。比如:
{ "ReRoutes": [ { "DownstreamPathTemplate": "/weatherforecast", "DownstreamScheme": "http", "DownstreamHostAndPorts": [ { "Host": "localhost", "Port": 5000 } ], "UpstreamPathTemplate": "/api/weather", "UpstreamHttpMethod": [ "Get" ], "AuthenticationOptions": { "AuthenticationProviderKey": "AuthKey", "AllowedScopes": [] }, "RouteClaimsRequirement": { "Role": "admin" } } ] }上面高亮部分的代码指定了只有admin角色的用户才能访问/weatherforecast API,这里的“Role”就是Claim的名称,而“admin”就是Claim的值。如果我们在此处将Role设置为superadmin,那么前端页面就无法正常访问API,而是获得403 Forbidden的状态码:
注意:理论上讲,此处的“Role”原本应该是使用标准的Role Claim的名称,即原本应该是:
但由于ASP.NET Core框架在处理JSON配置文件时存在特殊性,使得上述标准的Role Claim的名称无法被正确解析,因此,也就无法在RouteClaimsRequirement中正常使用。目前的解决方案就是用户认证后,在Access Token中带入一个自定义的Role Claim(在这里我使用最简单的名字“Role”作为这个自定义的Claim的名称,这也是为什么上面的JSON配置例子中,使用的是“Role”,而不是“”),而要做到这一点,就要修改两个地方。
首先,在IdentityService的Config.cs文件中,增加一个自定义的User Claim:
public static IEnumerable<ApiResource> GetApiResources() => new[] { new ApiResource("api.weather", "Weather API") { Scopes = { new Scope("api.weather.full_access", "Full access to Weather API") }, UserClaims = { ClaimTypes.NameIdentifier, ClaimTypes.Name, ClaimTypes.Email, ClaimTypes.Role, "Role" } } };然后,在注册新用户的API中,当用户注册信息包含Role时,将“Role” Claim也添加到数据库中:
[HttpPost] [Route("api/[controller]/register-account")] public async Task<IActionResult> RegisterAccount([FromBody] RegisterUserRequestViewModel model) { if (!ModelState.IsValid) { return BadRequest(ModelState); } var user = new AppUser { UserName = model.UserName, DisplayName = model.DisplayName, Email = model.Email }; var result = await _userManager.CreateAsync(user, model.Password); if (!result.Succeeded) return BadRequest(result.Errors); await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.NameIdentifier, user.UserName)); await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Name, user.DisplayName)); await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Email, user.Email)); if (model.RoleNames?.Count > 0) { var validRoleNames = new List<string>(); foreach (var roleName in model.RoleNames) { var trimmedRoleName = roleName.Trim(); if (await _roleManager.RoleExistsAsync(trimmedRoleName)) { validRoleNames.Add(trimmedRoleName); await _userManager.AddToRoleAsync(user, trimmedRoleName); } } await _userManager.AddClaimAsync(user, new Claim(ClaimTypes.Role, string.Join(',', validRoleNames))); await _userManager.AddClaimAsync(user, new Claim("Role", string.Join(',', validRoleNames))); } return Ok(new RegisterUserResponseViewModel(user)); }