Github 倉庫原始碼地址 https://github.com/whuanles/2020-07-12
ASP.NET Core 中的策略授權
首先我們來建立一個 WebAPI 應用。
然後引入 Microsoft.AspNetCore.Authentication.JwtBearer
包。
策略
Startup 類的 ConfigureServices 方法中,新增一個策略的形式如下:
services.AddAuthorization(options =>
{
options.AddPolicy("AtLeast21", policy =>
policy.Requirements.Add(new MinimumAgeRequirement(21)));
});
這裡我們分步來說。
services.AddAuthorization 用於新增授權方式,目前只支援 AddPolicy。
ASP.NET Core 中,有基於角色、宣告、策略的三種授權形式,都是使用 AddPolicy
來新增授權處理。
其中,有兩個 API 如下:
public void AddPolicy(string name, AuthorizationPolicy policy);
public void AddPolicy(string name, Action<AuthorizationPolicyBuilder> configurePolicy);
name = "AtLeast21"
,這裡 "AtLeast21" 是策略的名稱。
policy.Requirements.Add()
用於新增一個策略的標記(儲存此策略的資料),此標記需要繼承 IAuthorizationRequirement
介面。
策略的名稱應該如何設定呢?在授權上應該如何編寫策略以及使用 Requirements.Add()
?
這裡先放一放,我們接下來再講解。
定義一個 Controller
我們來新增一個 Controller :
[ApiController]
[Route("[controller]")]
public class BookController : ControllerBase
{
private static List<string> BookContent = new List<string>();
[HttpGet("Add")]
public string AddContent(string body)
{
BookContent.Add(body);
return "success";
}
[HttpGet("Remove")]
public string RemoveContent(int n)
{
BookContent.Remove(BookContent[n]);
return "success";
}
[HttpGet("Select")]
public List<object> SelectContent()
{
List<object> obj = new List<object>();
int i = 0;
foreach (var item in BookContent)
{
int tmp = i;
i++;
obj.Add(new { Num = tmp, Body = item });
}
return obj;
}
[HttpGet("Update")]
public string UpdateContent(int n, string body)
{
BookContent[n] = body;
return "success";
}
}
功能很簡單,就是對列表內容增刪查改。
設定許可權
前面我們建立了 BookController
,具有增刪查改的功能。應該為每一個功能都應該設定一種許可權。
ASP.NET Core 中,一個許可權標記,需要繼承IAuthorizationRequirement
介面。
我們來設定五個許可權:
新增一個檔案,填寫以下程式碼。
/*
IAuthorizationRequirement 是一個空介面,具體對於授權的需求,其屬性等資訊是自定義的
這裡的繼承關係也沒有任何意義
*/
// 能夠訪問 Book 的許可權
public class BookRequirment : IAuthorizationRequirement
{
}
// 增刪查改 Book 許可權
// 可以繼承 IAuthorizationRequirement ,也可以繼承 BookRequirment
public class BookAddRequirment : BookRequirment
{
}
public class BookRemoveRequirment : BookRequirment
{
}
public class BookSelectRequirment : BookRequirment
{
}
public class BookUpdateRequirment : BookRequirment
{
}
BookRequirment 代表能夠訪問 BookController,其它四個分別代表增刪查改的許可權。
定義策略
許可權設定後,我們開始設定策略。
在 Startup 的 ConfigureServices
中,新增:
services.AddAuthorization(options =>
{
options.AddPolicy("Book", policy =>
{
policy.Requirements.Add(new BookRequirment());
});
options.AddPolicy("Book:Add", policy =>
{
policy.Requirements.Add(new BookAddRequirment());
});
options.AddPolicy("Book:Remove", policy =>
{
policy.Requirements.Add(new BookRemoveRequirment());
});
options.AddPolicy("Book:Select", policy =>
{
policy.Requirements.Add(new BookSelectRequirment());
});
options.AddPolicy("Book:Update", policy =>
{
policy.Requirements.Add(new BookUpdateRequirment());
});
});
這裡我們為每種策略只設定一種許可權,當然每種策略都可以新增多個許可權,
這裡名稱使用 :
隔開,主要是為了可讀性,讓人一看就知道是層次關係。
儲存使用者資訊
這裡為了更加簡單,就不使用資料庫了。
以下使用者資訊結構是隨便寫的。使用者-角色-角色具有的許可權。
這個許可權用什麼型別儲存都可以。只要能夠標識區分是哪個許可權就行。
/// <summary>
/// 儲存使用者資訊
/// </summary>
public static class UsersData
{
public static readonly List<User> Users = new List<User>();
static UsersData()
{
// 新增一個管理員
Users.Add(new User
{
Name = "admin",
Email = "admin@admin.com",
Role = new Role
{
Requirements = new List<Type>
{
typeof( BookRequirment),
typeof( BookAddRequirment),
typeof( BookRemoveRequirment),
typeof( BookSelectRequirment),
typeof( BookUpdateRequirment)
}
}
});
// 沒有刪除許可權
Users.Add(new User
{
Name = "作者",
Email = "wirter",
Role = new Role
{
Requirements = new List<Type>
{
typeof( BookRequirment),
typeof( BookAddRequirment),
typeof( BookRemoveRequirment),
typeof( BookSelectRequirment),
}
}
});
}
}
public class User
{
public string Name { get; set; }
public string Email { get; set; }
public Role Role { get; set; }
}
/// <summary>
/// 這裡的儲存角色的策略授權,字串數字等都行,只要能夠儲存表示就OK
/// <para>在這裡沒有任何意義,只是標識的一種方式</param>
/// </summary>
public class Role
{
public List<Type> Requirements { get; set; }
}
標記訪問許可權
定義策略完畢後,就要為 Controller 和 Action 標記訪問許可權了。
使用 [Authorize(Policy = "{string}")]
特性和屬性來設定訪問此 Controller 、 Action 所需要的許可權。
這裡我們分開設定,每個功能標記一種許可權(最小粒度應該是一個功能 ,而不是一個 API)。
[Authorize(Policy = "Book")]
[ApiController]
[Route("[controller]")]
public class BookController : ControllerBase
{
private static List<string> BookContent = new List<string>();
[Authorize(Policy = "Book:Add")]
[HttpGet("Add")]
public string AddContent(string body){}
[Authorize(Policy = "Book:Remove")]
[HttpGet("Remove")]
public string RemoveContent(int n){}
[Authorize(Policy = "Book:Select")]
[HttpGet("Select")]
public List<object> SelectContent(){}
[Authorize(Policy = "Book:Update")]
[HttpGet("Update")]
public string UpdateContent(int n, string body){}
}
認證:Token 憑據
因為使用的是 WebAPI,所以使用 Bearer Token 認證,當然使用 Cookie 等也可以。使用什麼認證方式都可以。
// 設定驗證方式為 Bearer Token
// 新增 using Microsoft.AspNetCore.Authentication.JwtBearer;
// 你也可以使用 字串 "Brearer" 代替 JwtBearerDefaults.AuthenticationScheme
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abcdABCD1234abcdABCD1234")), // 加密解密Token的金鑰
// 是否驗證釋出者
ValidateIssuer = true,
// 釋出者名稱
ValidIssuer = "server",
// 是否驗證訂閱者
// 訂閱者名稱
ValidateAudience = true,
ValidAudience = "client007",
// 是否驗證令牌有效期
ValidateLifetime = true,
// 每次頒發令牌,令牌有效時間
ClockSkew = TimeSpan.FromMinutes(120)
};
});
上面的程式碼是一個模板,可以隨便改。這裡的認證方式跟我們的策略授權沒什麼關係。
頒發登入憑據
下面這個 Action 放置到 BookController,作為登入功能。這一部分也不重要,主要是為使用者頒發憑據,以及標識使用者。使用者的 Claim 可以儲存此使用者的唯一標識。
/// <summary>
/// 使用者登入並且頒發憑據
/// </summary>
/// <param name="name"></param>
/// <returns></returns>
[AllowAnonymous]
[HttpGet("Token")]
public string Token(string name)
{
User user = UsersData.Users.FirstOrDefault(x => x.Name.Equals(name, StringComparison.OrdinalIgnoreCase));
if (user is null)
return "未找到此使用者";
// 定義使用者資訊
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, name),
new Claim(JwtRegisteredClaimNames.Email, user.Email)
};
// 和 Startup 中的配置一致
SymmetricSecurityKey key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("abcdABCD1234abcdABCD1234"));
JwtSecurityToken token = new JwtSecurityToken(
issuer: "server",
audience: "client007",
claims: claims,
notBefore: DateTime.Now,
expires: DateTime.Now.AddMinutes(30),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
);
string jwtToken = new JwtSecurityTokenHandler().WriteToken(token);
return jwtToken;
}
Configure 中補充以下兩行:
app.UseAuthentication();
app.UseAuthorization();
自定義授權
自定義授權需要繼承 IAuthorizationHandler
介面,實現此介面的類能夠決定是否對使用者的訪問進行授權。
實現程式碼如下:
/// <summary>
/// 判斷使用者是否具有許可權
/// </summary>
public class PermissionHandler : IAuthorizationHandler
{
public async Task HandleAsync(AuthorizationHandlerContext context)
{
// 當前訪問 Controller/Action 所需要的許可權(策略授權)
IAuthorizationRequirement[] pendingRequirements = context.PendingRequirements.ToArray();
// 取出使用者資訊
IEnumerable<Claim> claims = context.User?.Claims;
// 未登入或者取不到使用者資訊
if (claims is null)
{
context.Fail();
return;
}
// 取出使用者名稱
Claim userName = claims.FirstOrDefault(x => x.Type == ClaimTypes.Name);
if (userName is null)
{
context.Fail();
return;
}
// ... 省略一些檢驗過程 ...
// 獲取此使用者的資訊
User user = UsersData.Users.FirstOrDefault(x => x.Name.Equals(userName.Value, StringComparison.OrdinalIgnoreCase));
List<Type> auths = user.Role.Requirements;
// 逐個檢查
foreach (IAuthorizationRequirement requirement in pendingRequirements)
{
// 如果使用者許可權列表中沒有找到此許可權的話
if (!auths.Any(x => x == requirement.GetType()))
context.Fail();
context.Succeed(requirement);
}
await Task.CompletedTask;
}
}
過程:
- 從上下文(Context) 中獲取使用者資訊(context.User)
- 獲取此使用者所屬的角色,並獲取此角色具有的許可權
- 獲取此次請求的 Controller/Action 需要的許可權(context.PendingRequirements)
- 檢查所需要的許可權(foreach迴圈),此使用者是否都具有
最後需要將此介面、服務,註冊到容器中:
services.AddSingleton<IAuthorizationHandler, PermissionHandler>();
做完這些後,就可以測試授權了。
IAuthorizationService
前面實現了 IAuthorizationHandler 介面的類,用於自定義確定使用者是否有權訪問此 Controller/Action。
IAuthorizationService 介面用於確定授權是否成功,其定義如下:
public interface IAuthorizationService
{
Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, IEnumerable<IAuthorizationRequirement> requirements);
Task<AuthorizationResult> AuthorizeAsync(ClaimsPrincipal user, object? resource, string policyName);
}
DefaultAuthorizationService
介面實現了 IAuthorizationService
,ASP.NET Core 預設使用 DefaultAuthorizationService
來確認授權。
前面我們使用 IAuthorizationHandler
介面來自定義授權,如果再深入一層的話,就追溯到了IAuthorizationService
。
DefaultAuthorizationService
是 IAuthorizationService
的預設實現,其中有一段程式碼如下:
DefaultAuthorizationService
比較複雜,一般情況下,我們只要實現 IAuthorizationHandler
` 就夠了。
ABP 授權
前面已經介紹了 ASP.NET Core 中的策略授權,這裡介紹一下 ABP 中的授權,我們繼續利用前面已經實現的 ASP.NET Core 程式碼。
建立 ABP 應用
Nuget 安裝 Volo.Abp.AspNetCore.Mvc
、Volo.Abp.Autofac
。
建立 AppModule
類,程式碼如下:
[DependsOn(typeof(AbpAspNetCoreMvcModule))]
[DependsOn(typeof(AbpAutofacModule))]
public class AppModule : AbpModule
{
public override void OnApplicationInitialization(
ApplicationInitializationContext context)
{
var app = context.GetApplicationBuilder();
var env = context.GetEnvironment();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseConfiguredEndpoints();
}
}
在 Program 的 Host 加上 .UseServiceProviderFactory(new AutofacServiceProviderFactory())
,示例如下:
public static IHostBuilder CreateHostBuilder(string[] args) =>
Host.CreateDefaultBuilder(args)
.UseServiceProviderFactory(new AutofacServiceProviderFactory())
...
...
然後在 Startup 中的 ConfiguraServices
方法中,新增 ABP 模組, 並且設定使用 Autofac。
public void ConfigureServices(IServiceCollection services)
{
services.AddApplication<AppModule>(options=>
{
options.UseAutofac();
});
}
定義許可權
ABP 中使用 PermissionDefinitionProvider
類來定義許可權,建立一個類,其程式碼如下:
public class BookPermissionDefinitionProvider : PermissionDefinitionProvider
{
public override void Define(IPermissionDefinitionContext context)
{
var myGroup = context.AddGroup("Book");
var permission = myGroup.AddPermission("Book");
permission.AddChild("Book:Add");
permission.AddChild("Book:Remove");
permission.AddChild("Book:Select");
permission.AddChild("Book:Update");
}
}
這裡定義了一個組 Book
,定義了一個許可權 Book
了,Book
其下有四個子許可權。
刪除 Startup 中的services.AddAuthorization(options =>...
。
將剩餘的依賴注入服務程式碼移動到 AppModule 的 ConfigureServices
中。
Startup 的 Configure 改成:
app.InitializeApplication();
AbpModule 中的 Configure
改成:
var app = context.GetApplicationBuilder();
var env = context.GetEnvironment();
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Error");
}
app.UseStaticFiles();
app.UseRouting();
app.UseAuthentication();
app.UseAuthorization();
app.UseConfiguredEndpoints();
PermissionHandler 需要改成:
public class PermissionHandler : IAuthorizationHandler
{
public Task HandleAsync(AuthorizationHandlerContext context)
{
// 當前訪問 Controller/Action 所需要的許可權(策略授權)
IAuthorizationRequirement[] pendingRequirements = context.PendingRequirements.ToArray();
// 逐個檢查
foreach (IAuthorizationRequirement requirement in pendingRequirements)
{
context.Succeed(requirement);
}
return Task.CompletedTask;
}
}
刪除 UserData 檔案;BookController 需要修改一下登入和憑證。
具體細節可參考倉庫原始碼。