闽公网安备 35020302035485号
UserName = admin Age = 105 Birth = 1990,4,12 Address = 堆代码 www.duidaima.comClaimTypes 静态类定义了一些标准的 type 值。如用户名 Name,国家 Country,手机号 MobilePhone,家庭电话 HomePhone 等等。你也可以自己定义一个,反正就是个字符串。
public class User
{
public string? UserName { get; set; }
public string? Password { get; set; }
/// <summary>
/// 用户等级,1-5
/// </summary>
public int Level { get; set; } = 1;
}
上面类中,Level 属性表示的是用户等级。然后,用下面的代码来产生一些用户数据。public static class UserDatas
{
internal static readonly IEnumerable<User> UserList = new User[]
{
new(){UserName="admin", Password="123456", Level=5},
new(){UserName="kitty", Password="112211", Level=3},
new(){UserName="bob",Password="215215", Level=2},
new(){UserName="billy", Password="886600", Level=1}
};
// 获取所有用户
public static IEnumerable<User> GetUsers() => UserList;
// 根据用户名和密码校对后返回的用户实体
public static User? CheckUser(string username, string passwd)
{
return UserList.FirstOrDefault(u => u.UserName!.Equals(username, StringComparison.OrdinalIgnoreCase) && u.Password == passwd);
}
}
这样的功能,对于咱们今天要说的内容,已经够用了。关于验证,这里不是重点。所以老周用最简单的方案——Cookie。builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(opt =>
{
opt.LoginPath = "/UserLog";
opt.LogoutPath = "/Logout";
opt.AccessDeniedPath = "/Denied";
opt.Cookie.Name = "ck_auth_ent";
opt.ReturnUrlParameter = "backUrl";
});
这个验证方案是结合 Session 和 Cookie 来完成的,也是Web身份验证的经典方案了。上述代码中我配置了一些选项:app.MapGet("/Denied", () => "访问被拒绝");
app.MapGet("/Logout", async (HttpContext context) =>
{
await context.SignOutAsync();
});
对于 LoginPath,我用一个 Razor Pages 来处理。@page
@using MyApp
@using Microsoft.AspNetCore.Authentication
@using Microsoft.AspNetCore.Authentication.Cookies
@using System.Security.Claims
@addTagHelper *,Microsoft.AspNetCore.Mvc.TagHelpers
<form method="post">
<style>
label{
display:inline-block;
min-width:100px;
}
</style>
<div>
<label for="userName">用户名:</label>
<input type="text" name="userName" />
</div>
<div>
<label for="passWord">密码:</label>
<input type="password" name="passWord" />
</div>
<div>
<button type="submit">登入</button>
</div>
</form>
@functions{
//[IgnoreAntiforgeryToken]
public async void OnPost(string userName, string passWord)
{
var u = UserDatas.CheckUser(userName, passWord);
if(u != null)
{
Claim[] cs = new Claim[]
{
new Claim(ClaimTypes.Name, u.UserName!),
new Claim("level", u.Level.ToString()) //注意这里,收集重要情报
};
ClaimsIdentity id = new(cs, CookieAuthenticationDefaults.AuthenticationScheme);
ClaimsPrincipal p = new(id);
await HttpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, p);
//HttpContext.Response.Redirect("/");
}
}
}
其他的各位可以不关注,重点是 OnPost 方法,首先用刚才写的 UserDatas.CheckUser 静态方法来验证用户名和密码(这个是要我们自己写代码来完成的,CookieAuthenticationHandler 可不负责这个)。用户名和密码正确后,咱们就要收集信息了。收集啥呢?这个要根据你稍后在授权时要用到什么来决定的。就拿今天的主题来讲,我们需要知道用户等级,所以要收集 Level 属性的值。这里 ClaimType 我直接用“level”,Value 就是 Level 属性的值。 var ticket = new AuthenticationTicket(signInContext.Principal!, signInContext.Properties, signInContext.Scheme.Name);
// 保存 Session
if (Options.SessionStore != null)
{
if (_sessionKey != null)
{
// Renew the ticket in cases of multiple requests see: https://github.com/dotnet/aspnetcore/issues/22135
await Options.SessionStore.RenewAsync(_sessionKey, ticket, Context, Context.RequestAborted);
}
else
{
_sessionKey = await Options.SessionStore.StoreAsync(ticket, Context, Context.RequestAborted);
}
var principal = new ClaimsPrincipal(
new ClaimsIdentity(
new[] { new Claim(SessionIdClaim, _sessionKey, ClaimValueTypes.String, Options.ClaimsIssuer) },
Options.ClaimsIssuer));
ticket = new AuthenticationTicket(principal, null, Scheme.Name);
}
// 生成加密后的 Cookie 值
var cookieValue = Options.TicketDataFormat.Protect(ticket, GetTlsTokenBinding());
// 追加 Cookie 到响应消息中
Options.CookieManager.AppendResponseCookie(
Context,
Options.Cookie.Name!,
cookieValue,
signInContext.CookieOptions);
……
好了,上面的都是周边工作,下面我们来干正事。 public class LevelAuthorizationRequirement : IAuthorizationRequirement
{
public int Level { get; private set; }
public LevelAuthorizationRequirement(int lv)
{
Level = lv;
}
}
授权处理有两个接口:public class DefaultAuthorizationEvaluator : IAuthorizationEvaluator
{
public AuthorizationResult Evaluate(AuthorizationHandlerContext context)
=> context.HasSucceeded
? AuthorizationResult.Success()
: AuthorizationResult.Failed(context.HasFailed
? AuthorizationFailure.Failed(context.FailureReasons)
: AuthorizationFailure.Failed(context.PendingRequirements));
}
所以,咱们的代码可以选择实现一个抽象类:AuthorizationHandler<TRequirement>,其中,TRequirement 需要实现 IAuthorizationRequirement 接口。这个抽象类已经满足咱们的需求了。public class LevelAuthorizationHandler : AuthorizationHandler<LevelAuthorizationRequirement>
{
// 策略名称,写成常量方便使用
public const string POLICY_NAME = "Level";
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, LevelAuthorizationRequirement requirement)
{
// 查找声明
Claim? clm = context.User.Claims.FirstOrDefault(c => c.Type == "level");
if(clm != null)
{
// 读出用户等级
int lv = int.Parse(clm.Value);
// 看看用户等级是否满足条件
if(lv >= requirement.Level)
{
// 满足,标记此阶段允许授权
context.Succeed(requirement);
}
}
return Task.CompletedTask;
}
}
在授权请求启动时,AuthorizationHandlerContext (上下文)对象会把所有 IAuthorizationRequirement 对象添加到一个哈希表中(HashSet<T>),表示一大串正等着授权处理的约束条件。当我们调用 Succeed 方法时,会把已满足要求的 IAuthorizationRequirement 传递给方法参数。在 Success 方法内部会从哈希表中删除此 IAuthorizationRequirement,以表示该条件已满足了,不必再证。public virtual void Succeed(IAuthorizationRequirement requirement)
{
_succeedCalled = true;
_pendingRequirements.Remove(requirement);
}
记得要在服务容器中注册,否则咱们写的 Handler 是不起作用的。 builder.Services.AddSingleton<IAuthorizationHandler, LevelAuthorizationHandler>();
builder.Services.AddAuthorizationBuilder().AddPolicy(LevelAuthorizationHandler.POLICY_NAME, pb =>
{
pb.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
pb.AddRequirements(new LevelAuthorizationRequirement(3));
});
策略的名称我们前面以常量的方式定义了,记得否?public class HomeController : Controller
{
[HttpGet("/")]
[Authorize(Policy = LevelAuthorizationHandler.POLICY_NAME)]
public IActionResult Index()
{
return View();
}
}
这里咱们用基于策略的授权方式,所以[Authorize]特性要指定策略名称。好,运行。本来是访问根目录 / 的,但由于验证不通过,自动跳到登录页了。注意URL上的 backUrl 参数:?backUrl=/。本来要访问 / 的,所以登录后再跳回 / 。我们选一个用户等级为 5 的登录。由于用户等级为 5,是 >=3 的存在,所以授权通过。
builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(opt =>
{
opt.LoginPath = "/UserLog";
opt.LogoutPath = "/Logout";
opt.AccessDeniedPath = "/Denied";
opt.Cookie.Name = "ck_auth_ent";
opt.ReturnUrlParameter = "backUrl";
});
现在咱们找个用户等级低于 3 的登录。登录后被拒绝访问。builder.Services.AddAuthorizationBuilder()
.AddPolicy("Level3", pb =>
{
pb.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
pb.AddRequirements(new LevelAuthorizationRequirement(3));
})
.AddPolicy("Level5", pb =>
{
pb.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
pb.AddRequirements(new LevelAuthorizationRequirement(5));
});
是的,这样确实是可行的。不过不够动态,要是我弄个策略从 Level1 到 Level10 呢,岂不要写十个?官方有个用 Age 生成授权策略的示例让老周获得了灵感——是的,咱们就是要动态生成授权策略。需要用到一个接口:IAuthorizationPolicyProvider。这个接口可以根据策略名称返回授权策略,所以,咱们可以拿它做文章。public class LevelAuthorizationPolicyProvider : IAuthorizationPolicyProvider
{
private readonly AuthorizationOptions _options;
public LevelAuthorizationPolicyProvider(IOptions<AuthorizationOptions> opt)
{
_options = opt.Value;
}
public Task<AuthorizationPolicy> GetDefaultPolicyAsync()
{
return Task.FromResult(_options.DefaultPolicy);
}
public Task<AuthorizationPolicy?> GetFallbackPolicyAsync()
{
return Task.FromResult(_options.FallbackPolicy);
}
public Task<AuthorizationPolicy?> GetPolicyAsync(string policyName)
{
if(policyName.StartsWith(LevelAuthorizationHandler.POLICY_NAME,StringComparison.OrdinalIgnoreCase))
{
// 比如,策略名 Level4,得到等级4
// 提取名称最后的数字
int prefixLen = LevelAuthorizationHandler.POLICY_NAME.Length;
if(int.TryParse(policyName.Substring(prefixLen), out int level))
{
// 动态生成策略
AuthorizationPolicyBuilder plcyBd = new AuthorizationPolicyBuilder();
plcyBd.AddAuthenticationSchemes(CookieAuthenticationDefaults.AuthenticationScheme);
plcyBd.AddRequirements(new LevelAuthorizationRequirement(level));
// Build 方法生成策略
return Task.FromResult(plcyBd.Build())!;
}
}
// 未处理,交由选项类去返回默认的策略
return Task.FromResult(_options.GetPolicy(policyName));
}
}
这样可以根据给定的策略名称,生成与用户等级相关的配置。例如,名称“Level3”,就是等级3;“Level5”就是等级5。于是,在配置服务容器时,我们不再需要 AddAuthorizationBuilder 一大段代码了,直接把 LevelAuthorizationPolicyProvider 注册一下就行了。builder.Services.AddSingleton<IAuthorizationHandler, LevelAuthorizationHandler>(); builder.Services.AddTransient<IAuthorizationPolicyProvider, LevelAuthorizationPolicyProvider>(); builder.Services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme).AddCookie(opt => ……然后,在MVC控制器上咱们就可以666地玩了。
public class HomeController : Controller
{
[HttpGet("/")]
[Authorize(Policy = $"{LevelAuthorizationHandler.POLICY_NAME}3")]
public IActionResult Index()
{
return View();
}
[HttpGet("/music")]
[Authorize(Policy = $"{LevelAuthorizationHandler.POLICY_NAME}2")]
public IActionResult Foo()
=> Content("2星级用户扰民音乐俱乐部");
[HttpGet("/movie")]
[Authorize(Policy = $"{LevelAuthorizationHandler.POLICY_NAME}5")]
public IActionResult Movies()
=> Content("5星级鬼畜影院");
}
这样一来,配置不同等级的授权就方便多了。