实现权限访问控制列表
This commit is contained in:
1 parent
5a170f3024
commit
258781bdc5
17 files changed
+557
-7
No files matched your search
@@ -0,0 +1,10 @@
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace HelloShop.ServiceDefaults.Authorization;
|
||||
|
||||
public interface IPermissionChecker
|
||||
{
|
||||
Task<bool> IsGrantedAsync(string name, string? resourceType = null, string? resourceId = null);
|
||||
|
||||
Task<bool> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name, string? resourceType = null, string? resourceId = null);
|
||||
}
|
||||
@@ -0,0 +1,49 @@
|
||||
using HelloShop.ServiceDefaults.Constants;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using System.Security.Claims;
|
||||
|
||||
namespace HelloShop.ServiceDefaults.Authorization;
|
||||
|
||||
public abstract class PermissionChecker(IHttpContextAccessor httpContextAccessor, IDistributedCache distributedCache) : IPermissionChecker
|
||||
{
|
||||
protected HttpContext HttpContext { get; init; } = httpContextAccessor.HttpContext ?? throw new InvalidOperationException();
|
||||
|
||||
public async Task<bool> IsGrantedAsync(string name, string? resourceType = null, string? resourceId = null) => await IsGrantedAsync(HttpContext.User, name, resourceType, resourceId);
|
||||
|
||||
public async Task<bool> IsGrantedAsync(ClaimsPrincipal claimsPrincipal, string name, string? resourceType = null, string? resourceId = null)
|
||||
{
|
||||
var roleIds = claimsPrincipal.FindAll(CustomClaimTypes.RoleIdentifier).Select(c => Convert.ToInt32(c.Value)).ToArray();
|
||||
|
||||
foreach (var roleId in roleIds)
|
||||
{
|
||||
var cacheKey = PermissionGrantCacheItem.CreateCacheKey(roleId, name, resourceType, resourceId);
|
||||
|
||||
if (distributedCache.TryGetValue(cacheKey, out PermissionGrantCacheItem? cacheItem) && cacheItem != null)
|
||||
{
|
||||
if (cacheItem.IsGranted)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
bool isGranted = await IsGrantedAsync(roleId, name, resourceType, resourceId);
|
||||
|
||||
await distributedCache.SetObjectAsync(cacheKey, new PermissionGrantCacheItem(isGranted), new DistributedCacheEntryOptions
|
||||
{
|
||||
SlidingExpiration = TimeSpan.FromMinutes(10)
|
||||
});
|
||||
|
||||
if (isGranted)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
public abstract Task<bool> IsGrantedAsync(int roleId, string name, string? resourceType = null, string? resourceId = null);
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
namespace HelloShop.ServiceDefaults.Authorization;
|
||||
|
||||
public class PermissionGrantCacheItem(bool isGranted = false)
|
||||
{
|
||||
public bool IsGranted { get; set; } = isGranted;
|
||||
|
||||
public static string CreateCacheKey(int roleId, string name, string? resourceType = null, string? resourceId = null)
|
||||
{
|
||||
string cacheKey = $"acl:role:{roleId}:{name}";
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(resourceType))
|
||||
{
|
||||
cacheKey += $":{resourceType}";
|
||||
}
|
||||
|
||||
if (!string.IsNullOrWhiteSpace(resourceId))
|
||||
{
|
||||
cacheKey += $":{resourceId}";
|
||||
}
|
||||
|
||||
return cacheKey;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,12 @@
|
||||
namespace HelloShop.ServiceDefaults.Authorization;
|
||||
|
||||
public class PermissionGrantedResponse
|
||||
{
|
||||
public required string Name { get; set; }
|
||||
|
||||
public string? ResourceType { get; set; }
|
||||
|
||||
public string? ResourceId { get; set; }
|
||||
|
||||
public bool IsGranted { get; set; }
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
|
||||
using Microsoft.AspNetCore.Authentication;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.WebUtilities;
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using Microsoft.Extensions.Options;
|
||||
using System.Net.Http.Headers;
|
||||
using System.Net.Http.Json;
|
||||
|
||||
namespace HelloShop.ServiceDefaults.Authorization;
|
||||
|
||||
public class RemotePermissionChecker(IHttpContextAccessor httpContextAccessor, IDistributedCache distributedCache, IHttpClientFactory httpClientFactory, IOptions<RemotePermissionCheckerOptions> options) : PermissionChecker(httpContextAccessor, distributedCache)
|
||||
{
|
||||
private readonly RemotePermissionCheckerOptions _remotePermissionCheckerOptions = options.Value;
|
||||
|
||||
public override async Task<bool> IsGrantedAsync(int roleId, string name, string? resourceType = null, string? resourceId = null)
|
||||
{
|
||||
string? accessToken = await HttpContext.GetTokenAsync("access_token");
|
||||
|
||||
HttpClient httpClient = httpClientFactory.CreateClient();
|
||||
|
||||
httpClient.DefaultRequestHeaders.Authorization = new AuthenticationHeaderValue("Bearer", accessToken);
|
||||
|
||||
httpClient.BaseAddress = new Uri(_remotePermissionCheckerOptions.ApiEndpoint);
|
||||
|
||||
Dictionary<string, string?> parameters = new()
|
||||
{
|
||||
{ nameof(roleId), roleId.ToString() },
|
||||
{ nameof(name), name },
|
||||
{ nameof(resourceType) , resourceType },
|
||||
{ nameof(resourceId), resourceId }
|
||||
};
|
||||
|
||||
string queryString = QueryHelpers.AddQueryString(string.Empty, parameters);
|
||||
|
||||
var permissionGrants = httpClient.GetFromJsonAsAsyncEnumerable<PermissionGrantedResponse>(queryString);
|
||||
|
||||
await foreach (var permissionGrant in permissionGrants)
|
||||
{
|
||||
if (permissionGrant != null && permissionGrant.IsGranted)
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,6 @@
|
||||
namespace HelloShop.ServiceDefaults.Authorization;
|
||||
|
||||
public class RemotePermissionCheckerOptions
|
||||
{
|
||||
public string ApiEndpoint { get; set; } = default!;
|
||||
}
|
||||
@@ -0,0 +1,174 @@
|
||||
using Microsoft.Extensions.Caching.Distributed;
|
||||
using System.Text.Json;
|
||||
|
||||
namespace Microsoft.Extensions.Caching.Distributed;
|
||||
|
||||
public static class CustomDistributedCacheExtensions
|
||||
{
|
||||
/// <summary>
|
||||
/// Sets a string in the specified cache with the specified key.
|
||||
/// </summary>
|
||||
/// <param name="cache">The cache in which to store the data.</param>
|
||||
/// <param name="key">The key to store the data in.</param>
|
||||
/// <param name="value">The data to store in the cache.</param>
|
||||
/// <exception cref="System.ArgumentNullException">Thrown when <paramref name="key"/> or <paramref name="value"/> is null.</exception>
|
||||
public static void SetObject<TItem>(this IDistributedCache cache, string key, TItem value)
|
||||
{
|
||||
var bytes = JsonSerializer.SerializeToUtf8Bytes(value);
|
||||
cache.Set(key, bytes);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Sets a string in the specified cache with the specified key.
|
||||
/// </summary>
|
||||
/// <param name="cache">The cache in which to store the data.</param>
|
||||
/// <param name="key">The key to store the data in.</param>
|
||||
/// <param name="value">The data to store in the cache.</param>
|
||||
/// <param name="options">The cache options for the entry.</param>
|
||||
/// <exception cref="System.ArgumentNullException">Thrown when <paramref name="key"/> or <paramref name="value"/> is null.</exception>
|
||||
public static void SetObject<TItem>(this IDistributedCache cache, string key, TItem value, DistributedCacheEntryOptions options)
|
||||
{
|
||||
var bytes = JsonSerializer.SerializeToUtf8Bytes(value);
|
||||
cache.Set(key, bytes, options);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously sets a string in the specified cache with the specified key.
|
||||
/// </summary>
|
||||
/// <param name="cache">The cache in which to store the data.</param>
|
||||
/// <param name="key">The key to store the data in.</param>
|
||||
/// <param name="value">The data to store in the cache.</param>
|
||||
/// <param name="token">Optional. A <see cref="CancellationToken" /> to cancel the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous set operation.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">Thrown when <paramref name="key"/> or <paramref name="value"/> is null.</exception>
|
||||
public static Task SetObjectAsync<TItem>(this IDistributedCache cache, string key, TItem value, CancellationToken token = default)
|
||||
{
|
||||
var bytes = JsonSerializer.SerializeToUtf8Bytes(value);
|
||||
return cache.SetAsync(key, bytes, new DistributedCacheEntryOptions(), token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously sets a string in the specified cache with the specified key.
|
||||
/// </summary>
|
||||
/// <param name="cache">The cache in which to store the data.</param>
|
||||
/// <param name="key">The key to store the data in.</param>
|
||||
/// <param name="value">The data to store in the cache.</param>
|
||||
/// <param name="options">The cache options for the entry.</param>
|
||||
/// <param name="token">Optional. A <see cref="CancellationToken" /> to cancel the operation.</param>
|
||||
/// <returns>A task that represents the asynchronous set operation.</returns>
|
||||
/// <exception cref="System.ArgumentNullException">Thrown when <paramref name="key"/> or <paramref name="value"/> is null.</exception>
|
||||
public static Task SetObjectAsync<TItem>(this IDistributedCache cache, string key, TItem value, DistributedCacheEntryOptions options, CancellationToken token = default)
|
||||
{
|
||||
var bytes = JsonSerializer.SerializeToUtf8Bytes(value);
|
||||
return cache.SetAsync(key, bytes, options, token);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets a string from the specified cache with the specified key.
|
||||
/// </summary>
|
||||
/// <param name="cache">The cache in which to store the data.</param>
|
||||
/// <param name="key">The key to get the stored data for.</param>
|
||||
/// <returns>The T value from the stored cache key.</returns>
|
||||
public static TItem? GetObject<TItem>(this IDistributedCache cache, string key)
|
||||
{
|
||||
byte[]? data = cache.Get(key);
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<TItem>(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously gets a string from the specified cache with the specified key.
|
||||
/// </summary>
|
||||
/// <param name="cache">The cache in which to store the data.</param>
|
||||
/// <param name="key">The key to get the stored data for.</param>
|
||||
/// <param name="token">Optional. A <see cref="CancellationToken" /> to cancel the operation.</param>
|
||||
/// <returns>A task that gets the T value from the stored cache key.</returns>
|
||||
public static async Task<TItem?> GetObjectAsync<TItem>(this IDistributedCache cache, string key, CancellationToken token = default)
|
||||
{
|
||||
byte[]? data = await cache.GetAsync(key, token).ConfigureAwait(false);
|
||||
|
||||
if (data == null)
|
||||
{
|
||||
return default;
|
||||
}
|
||||
|
||||
return JsonSerializer.Deserialize<TItem>(data);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Try to get the value associated with the given key.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the object to get.</typeparam>
|
||||
/// <param name="cache">The <see cref="IDistributedCache"/> instance this method extends.</param>
|
||||
/// <param name="key">The key of the value to get.</param>
|
||||
/// <param name="value">The value associated with the given key.</param>
|
||||
/// <returns><c>true</c> if the key was found. <c>false</c> otherwise.</returns>
|
||||
public static bool TryGetValue<TItem>(this IDistributedCache cache, string key, out TItem? value)
|
||||
{
|
||||
var data = cache.Get(key);
|
||||
|
||||
value = default;
|
||||
|
||||
try
|
||||
{
|
||||
value = JsonSerializer.Deserialize<TItem>(data);
|
||||
}
|
||||
catch
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Gets the value associated with this key if it exists, or generates a new entry using the provided key and a value from the given factory if the key is not found.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the object to get.</typeparam>
|
||||
/// <param name="cache">The <see cref="IDistributedCache"/> instance this method extends.</param>
|
||||
/// <param name="key">The key of the entry to look for or create.</param>
|
||||
/// <param name="factory">The factory that creates the value associated with this key if the key does not exist in the cache.</param>
|
||||
/// <param name="createOptions">The options to be applied to the <see cref="ICacheEntry"/> if the key does not exist in the cache.</param>
|
||||
/// <returns>The value associated with this key.</returns>
|
||||
public static TItem? GetOrCreate<TItem>(this IDistributedCache cache, string key, Func<DistributedCacheEntryOptions, TItem> factory, DistributedCacheEntryOptions? createOptions = null)
|
||||
{
|
||||
if (!cache.TryGetValue(key, out object? result))
|
||||
{
|
||||
createOptions ??= new DistributedCacheEntryOptions();
|
||||
|
||||
result = factory(createOptions);
|
||||
|
||||
cache.SetObject(key, result, createOptions);
|
||||
}
|
||||
|
||||
return (TItem?)result;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// Asynchronously gets the value associated with this key if it exists, or generates a new entry using the provided key and a value from the given factory if the key is not found.
|
||||
/// </summary>
|
||||
/// <typeparam name="TItem">The type of the object to get.</typeparam>
|
||||
/// <param name="cache">The <see cref="IDistributedCache"/> instance this method extends.</param>
|
||||
/// <param name="key">The key of the entry to look for or create.</param>
|
||||
/// <param name="factory">The factory task that creates the value associated with this key if the key does not exist in the cache.</param>
|
||||
/// <param name="createOptions">The options to be applied to the <see cref="ICacheEntry"/> if the key does not exist in the cache.</param>
|
||||
/// <returns>The task object representing the asynchronous operation.</returns>
|
||||
public static async Task<TItem?> GetOrCreateAsync<TItem>(this IDistributedCache cache, string key, Func<DistributedCacheEntryOptions, Task<TItem>> factory, DistributedCacheEntryOptions? createOptions = null)
|
||||
{
|
||||
if (!cache.TryGetValue(key, out object? result))
|
||||
{
|
||||
createOptions ??= new DistributedCacheEntryOptions();
|
||||
|
||||
result = await factory(createOptions).ConfigureAwait(false);
|
||||
|
||||
await cache.SetObjectAsync(key, result, createOptions);
|
||||
}
|
||||
|
||||
return (TItem?)result;
|
||||
}
|
||||
}
|
||||
@@ -1,4 +1,5 @@
|
||||
using HelloShop.ServiceDefaults.Permissions;
|
||||
using HelloShop.ServiceDefaults.Authorization;
|
||||
using HelloShop.ServiceDefaults.Permissions;
|
||||
using Microsoft.AspNetCore.Builder;
|
||||
using Microsoft.AspNetCore.Http;
|
||||
using Microsoft.AspNetCore.Routing;
|
||||
@@ -21,7 +22,7 @@ public static class PermissionExtensions
|
||||
|
||||
return services;
|
||||
}
|
||||
|
||||
|
||||
public static IEndpointRouteBuilder MapPermissionDefinitions(this IEndpointRouteBuilder endpoints, params string[] tags)
|
||||
{
|
||||
var routeGroup = endpoints.MapGroup(string.Empty);
|
||||
@@ -62,4 +63,13 @@ public static class PermissionExtensions
|
||||
|
||||
return routeGroup;
|
||||
}
|
||||
|
||||
public static IServiceCollection AddRemotePermissionChecker(this IServiceCollection services, Action<RemotePermissionCheckerOptions> configureOptions)
|
||||
{
|
||||
services.Configure(configureOptions);
|
||||
|
||||
services.AddTransient<IPermissionChecker, RemotePermissionChecker>();
|
||||
|
||||
return services;
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user