驗證替使用者建立身分識別,授權則是用來判斷使用者能不能使用某一個功能,ASP.NET Core 提供許多的授權 Role、Claims、Policy 等,老實講 Policy 授權使用上有一點門檻,分享一下我的實際用法,也給需要的人參考
開發環境
- .NET 6
- Rider 2022.1.2
標準步驟
在 DI Container 時就固定住有哪些原則,授權就會跟著原則走
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Permission", policy =>
policy.Requirements.Add(new PermissionAuthorizationRequirement()));
});
使用 Authorization Middleware
app.UseAuthentication();
app.UseAuthorization();
加入 Policy Authorization 的 DI Container
設定 Policy Authorization 的 DI Container,
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Permission", policy =>
policy.Requirements.Add(new PermissionAuthorizationRequirement()));
});
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, PermissionAuthorizationMiddlewareResultHandler>();
建立 「Permission」 原則, 裡面有一個必要 PermissionAuthorizationRequirement 參數
[Authorize(Policy = "Permission")]
在 Endpoint 設定 [Authorize],當 PolicyName 不符合 AddAuthorization 裡面的 Policy 會噴出例外,反之,會進入 PermissionAuthorizationHandler 跟 PermissionAuthorizationMiddlewareResultHandler
[Authorize(Policy = "Permission")]
[HttpGet]
public async Task<IActionResult> Get()
{
return this.Ok("好");
}
實作 IAuthorizationRequirement
存放授權必要的參數
public class PermissionAuthorizationRequirement : IAuthorizationRequirement
{
}
實作 AuthorizationHandler<IAuthorizationRequirement>
主要的處理授權的流程放在這裡,只要控制 AuthorizationHandlerContext 的狀態即可,需要用到幾個方法、狀態:
- 檢查失敗時調用 AuthorizationHandlerContext.Fail()
- 所有的失敗原因會被集中在 AuthorizationHandlerContext.FailureReasons
- 只要有一個失敗原因 AuthorizationHandlerContext.HasFailed == true,AuthorizationHandlerContext.HasSucceeded == false
- 沒有任何的失敗原因 AuthorizationHandlerContext.HasSucceeded == true
public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionAuthorizationRequirement>
{
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
PermissionAuthorizationRequirement requirement)
{
if (context.User.Identity.IsAuthenticated == false)
{
return;
}
// ....處理流程
context.Fail(new AuthorizationFailureReason(this,
$"Invalid Permission"));
if (context.HasFailed == false)
{
context.Succeed(requirement);
}
}
}
實作 IAuthorizationMiddlewareResultHandler
授權失敗,預設會回傳 403,PermissionAuthorizationMiddlewareResultHandler 用來可以
- 自訂回應
- 強化 challenge or forbid 回應
我們會替 4 開頭的 HttpStatus,加上詳細的 Error Body
public class PermissionAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
public async Task HandleAsync(RequestDelegate next,
HttpContext context,
AuthorizationPolicy policy,
PolicyAuthorizationResult authorizeResult)
{
await next(context);
}
}
動態設定原則步驟
當我們的原則很多的時,就可以透過 IAuthorizationPolicyProvider 來處理,就不是寫死在 DI Container 裡面了,官方有給一個 範例,我也是從這裡去調整的
實作 IAuthorizationPolicyProvider
主要就是在 GetPolicyAsync 決定要使用那些 Policy
internal class PermissionAuthorizationPolicyProvider : IAuthorizationPolicyProvider
{
public DefaultAuthorizationPolicyProvider FallbackPolicyProvider { get; }
public PermissionAuthorizationPolicyProvider(IOptions<AuthorizationOptions> options)
{
FallbackPolicyProvider = new DefaultAuthorizationPolicyProvider(options);
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync() => FallbackPolicyProvider.GetDefaultPolicyAsync();
public Task<AuthorizationPolicy> GetFallbackPolicyAsync() => FallbackPolicyProvider.GetFallbackPolicyAsync();
public Task<AuthorizationPolicy> GetPolicyAsync(string policyName)
{
if (policyName.StartsWith(POLICY_PREFIX, StringComparison.OrdinalIgnoreCase))
{
var policy = new AuthorizationPolicyBuilder();
policy.AddRequirements(new PermissionAuthorizationRequirement());
return Task.FromResult(policy.Build());
}
return FallbackPolicyProvider.GetPolicyAsync(policyName);
}
}
加入 Policy Authorization 的 DI Container
原本是
builder.Services.AddAuthorization(options =>
{
options.AddPolicy("Permission", policy =>
policy.Requirements.Add(new PermissionAuthorizationRequirement()));
});
換成
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionAuthorizationPolicyProvider>();
上述的基本流程,應該是可以運作的,可以試著設定中斷點觀察,理解基本的運作之後接下來就要把授權整合進整個流程了。
實作
接下來我想要實作的授權流程如下
- 請求受保護的端點
- 通過身分驗證
- 用身分 UserId 取得授權表
- 沒有授權回傳 HttpStatus 403
- 有授權回傳 HttpStatus 200
定義授權清單
- 用常數列舉出授權
- GetValues:用反射來取出有哪些值
public class Permission
{
public class Operation
{
public const string Write = $"{nameof(Permission)}.{nameof(Operation)}:{nameof(Write)}";
public const string Read = $"{nameof(Permission)}.{nameof(Operation)}:{nameof(Read)}";
private static readonly Lazy<Dictionary<string, Type>> s_values
= new(() =>
{
return FieldTypeAssistant.GetStaticFieldName<Operation>()
.ToDictionary(p => p.Key,
p => p.Value,
StringComparer.InvariantCultureIgnoreCase);
});
public static Dictionary<string, Type> GetValues()
=> s_values.Value;
}
}
實作取得授權清單
來源可以是資料庫,這裡我用記憶體
public class PermissionAuthorizationProvider : IPermissionAuthorizationProvider
{
private readonly Dictionary<string, IEnumerable<string>> _clientPermissions =
new(StringComparer.InvariantCultureIgnoreCase)
{
{ "yao", new[] { Permission.Operation.Read, Permission.Operation.Write } },
{ "jojo", new[] { Permission.Operation.Read} }
};
public IEnumerable<string> GetPermissions(string userId)
{
if (this._clientPermissions.TryGetValue(userId, out var result) == false)
{
result = new List<string>();
}
return result;
}
}
當授權流程都定案之後,可以被抽換的就是這個點,到時候就可以根據你的測試案例來決定需要甚麼授權
實作 PermissionAuthorizationHandler
這裡就是授權的主要流程
- 用戶有沒有權限可以訪問端點,用程式來寫就是判斷 PermissionAuthorizationRequirement.PolicyName 的值有沒有在授權清單裡 (IPermissionAuthorizationProvider.GetPermissions)
- 若沒有權限,呼叫 AuthorizationHandlerContext.Fail
- 若有權限,呼叫 AuthorizationHandlerContext.Succeed
public class PermissionAuthorizationHandler : AuthorizationHandler<PermissionAuthorizationRequirement>
{
private readonly IPermissionAuthorizationProvider _authorizationProvider;
public PermissionAuthorizationHandler(IPermissionAuthorizationProvider authorizationProvider)
{
this._authorizationProvider = authorizationProvider;
}
protected override async Task HandleRequirementAsync(AuthorizationHandlerContext context,
PermissionAuthorizationRequirement requirement)
{
if (context.User.Identity.IsAuthenticated == false)
{
context.Fail(new AuthorizationFailureReason(this, $"目前請求沒有通過驗證"));
return;
}
var userId = context.User.Identity.Name;
var permissions = this._authorizationProvider.GetPermissions(userId);
if (permissions.Any(p => p.StartsWith(requirement.PolicyName, StringComparison.InvariantCultureIgnoreCase)) ==
false)
{
context.Fail(new AuthorizationFailureReason(this, $"用戶 '{userId}',沒有授權 '{requirement.PolicyName}'"));
}
if (context.HasFailed == false)
{
context.Succeed(requirement);
}
}
}
實作 PermissionAuthorizationMiddlewareResultHandler
根據 Forbidden 寫詳細的 Log,回應 HttpStatus 403 及粗略的內容
public class PermissionAuthorizationMiddlewareResultHandler : IAuthorizationMiddlewareResultHandler
{
private readonly ILogger<PermissionAuthorizationMiddlewareResultHandler> _logger;
private readonly JsonSerializerOptions _jsonSerializerOptions;
public PermissionAuthorizationMiddlewareResultHandler(
ILogger<PermissionAuthorizationMiddlewareResultHandler> logger,
JsonSerializerOptions jsonSerializerOptions)
{
this._logger = logger;
this._jsonSerializerOptions = jsonSerializerOptions;
}
public async Task HandleAsync(
RequestDelegate next,
HttpContext context,
AuthorizationPolicy policy,
PolicyAuthorizationResult authorizeResult)
{
var permissionAuthorizationRequirements = policy.Requirements.OfType<PermissionAuthorizationRequirement>();
if (authorizeResult.Forbidden
&& permissionAuthorizationRequirements.Any())
{
context.Response.StatusCode = 403;
this._logger.LogInformation("{AuthorizationFailureResults}", new
{
ErrorCode = "Invalid Authorization",
ErrorMessages = authorizeResult.AuthorizationFailure.FailureReasons
});
// 回傳前端模糊訊息
await context.Response.WriteAsJsonAsync(new
{
ErrorCode = "Invalid Authorization",
ErrorMessages = new[] { "Please contact your administrator" }
// ErrorMessages = authorizeResult.AuthorizationFailure.FailureReasons
}, this._jsonSerializerOptions);
return;
}
await next.Invoke(context);
// await next(context);
}
}
設定驗證授權 DI Container
builder.Services.AddSingleton<IBasicAuthenticationProvider, BasicAuthenticationProvider>();
builder.Services.AddBasicAuthentication(options => { });
builder.Services.AddSingleton<IAuthorizationHandler, PermissionAuthorizationHandler>();
builder.Services.AddSingleton<IAuthorizationMiddlewareResultHandler, PermissionAuthorizationMiddlewareResultHandler>();
builder.Services.AddSingleton<IAuthorizationPolicyProvider, PermissionAuthorizationPolicyProvider>();
builder.Services.AddSingleton<IPermissionAuthorizationProvider, PermissionAuthorizationProvider>();
設定 Middleware
// Configure the HTTP request pipeline.
if (app.Environment.IsDevelopment())
{
app.UseSwagger();
app.UseSwaggerUI();
}
app.UseHttpsRedirection();
app.UseAuthentication();
app.UseAuthorization();
app.MapControllers();
app.Run();
設定端點
[ApiController]
[Route("[controller]")]
public class PermissionController : ControllerBase
{
private readonly ILogger<TestController> _logger;
public PermissionController(ILogger<TestController> logger)
{
this._logger = logger;
}
[Authorize(Policy = Permission.Operation.Read)]
[HttpGet]
public async Task<IActionResult> Get()
{
return this.Ok("好");
}
}
PermissionAuthorizationMiddleware 整合測試
這裡我用 new WebApplicationFactory<Program>().WithWebHostBuilder() 來建立 TestServer 實例,其中 WithWebHostBuilder 具有覆蓋Program 的機制,這裡注入 DefaultBasicAuthenticationProvider,覆蓋原有的設定,它回傳驗證通過
private static WebApplicationFactory<Program> CreateTestServer()
{
var server = new WebApplicationFactory<Program>().WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.AddSingleton<IBasicAuthenticationProvider, DefaultBasicAuthenticationProvider>();
services.AddControllers()
.AddApplicationPart(typeof(TestController).Assembly);
});
});
return server;
}
完整代碼如下:
[TestClass]
public class PermissionAuthorizationMiddleware整合測試
{
[TestMethod]
public async Task 訪問受保護的服務_授權成功()
{
var server = CreateTestServer();
var httpClient = server.CreateClient();
var url = "permission";
var clientId = "YAO";
var clientSecret = "9527";
var request = new HttpRequestMessage(HttpMethod.Get, url)
{
Headers = { Authorization = CreateAuthenticationHeaderValue(clientId, clientSecret) }
};
var response = httpClient.SendAsync(request).Result;
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);
Assert.AreEqual(HttpStatusCode.OK, response.StatusCode);
}
[TestMethod]
public async Task 訪問受保護的服務_授權失敗()
{
var server = CreateTestServer();
var httpClient = server.CreateClient();
var url = "permission";
var clientId = "jojo";
var clientSecret = "9527";
var request = new HttpRequestMessage(HttpMethod.Get, url)
{
Headers = { Authorization = CreateAuthenticationHeaderValue(clientId, clientSecret) }
};
var response = httpClient.SendAsync(request).Result;
var content = await response.Content.ReadAsStringAsync();
Console.WriteLine(content);
Assert.AreEqual(HttpStatusCode.Forbidden, response.StatusCode);
}
private static WebApplicationFactory<Program> CreateTestServer()
{
var server = new WebApplicationFactory<Program>().WithWebHostBuilder(builder =>
{
builder.ConfigureServices(services =>
{
services.AddSingleton<IBasicAuthenticationProvider, DefaultBasicAuthenticationProvider>();
services.AddControllers()
.AddApplicationPart(typeof(TestController).Assembly);
});
});
return server;
}
private static AuthenticationHeaderValue CreateAuthenticationHeaderValue(string clientId, string clientSecret)
{
var authenticationString = $"{clientId}:{clientSecret}";
var base64Encoded = Convert.ToBase64String(Encoding.ASCII.GetBytes(authenticationString));
return new AuthenticationHeaderValue("basic", base64Encoded);
}
}
範例位置
若有謬誤,煩請告知,新手發帖請多包涵
Microsoft MVP Award 2010~2017 C# 第四季
Microsoft MVP Award 2018~2022 .NET