精通.NET鉴权与授权
授权在.NET 中是指确定经过身份验证的用户是否有权访问特定资源或执行特定操作的过程。这就好比一个公司,身份验证(鉴权)是检查你是不是公司的员工,而授权则是看你这个员工有没有权限进入某个特定的办公室或者使用某台设备。
两个非常容易混淆的单词
-
鉴权Authentication --验证用户身份的过程,即确认用户是否是他们所声称的那个人
-
授权Authorization --看一下你有没有相应权限
鉴权使用体验
- 创建一个web api程序
- 在program.cs中使用鉴权授权中间件。builder.Services.AddAuthentication是告诉框架,如何进行鉴权,app.UseAuthentication();告诉框架,需要做鉴权。
//告诉框架,如何进行鉴权
builder.Services.AddAuthentication("Cookies")
//设置使用cookie方式
.AddCookie("Cookies", opt =>
{
//没有登录,直接跳转到/NotLoggedIn
opt.LoginPath = "/test/login";
//没有权限,直接跳转到/NotLoggedIn
opt.AccessDeniedPath = "/test/NotLoggedIn";
});
var app = builder.Build();
...
//一定注意中间件的顺序,必须先鉴权,才能授权
//告诉框架,需要做鉴权
app.UseAuthentication();
//告诉框架,需要做授权
app.UseAuthorization();
app.MapControllers();
app.Run();
- 新建一个Controller。在Action方法上面标记
[Authorize]
表示使用该接口需要鉴权,也可以直接将[Authorize]
特性放在Controller上面,对于不需要鉴权的Action上面放置[AllowAnonymous]
[HttpGet]
[Authorize]
public IActionResult UseCookie()
{
return Ok("初步使用-使用cookie");
}
[HttpGet]
public async Task<IActionResult> Login(string name,string password)
{
var claimsIdentity = new ClaimsIdentity("user");
claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, name));
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "user"));
var principal = new ClaimsPrincipal(claimsIdentity);
//ClaimsIdentity 是一个表示用户身份的对象,它是基于声明(Claims)的概念构建的。它是一种描述用户的方式,例如用户的姓名、角色、身份验证方式等信息。可以将 ClaimsIdentity 看作是一个身份容器,里面包含了一系列用于识别用户身份的声明。
//ClaimsPrincipal 是一个更高级别的概念,它代表一个主体(可以是用户或者其他实体),这个主体拥有一个或多个身份(ClaimsIdentity)。可以把 ClaimsPrincipal 想象成一个包含多个身份卡片(ClaimsIdentity)的钱包,其中每个身份卡片都可以用于证明主体的某种身份。
await HttpContext.SignInAsync(principal,new AuthenticationProperties
{
ExpiresUtc = DateTimeOffset.UtcNow.AddMinutes(30)
});
return Ok("登录成功");
}
- 未登录时直接访问
/Test/UseCookie
,报错,原因是未登录直接跳转到/test/login
,而没有name和password参数,导致报400错误。
-
使用
/Test/Login?name=1&password=1
登录后,调用/test/login
,访问成功。
理解鉴权授权
常见的鉴权方式有Cookie、JWT,其实无论是什么鉴权方式,都是一个套路,其本质就是HTTP是无状态的,每次请求都是新的,服务器不知道他是不是之前的那个请求,所以鉴权授权都是分3步:
- 请求服务端,获取凭证
- 再次请求服务端,带上第一步获取到的凭证
- 服务端识别凭证, 判断是否允许访问
自定义鉴权
其实框架给我们封装好了很多鉴权方式,为了搞清原理,我们来个自定义鉴权。整个鉴权流程就是读取凭证—解析凭证—检验凭证----保存并向下一个管道传递。
- 新建一个类,继承自
IAuthenticationHandler
public class CustomAuthenticationHandler : IAuthenticationHandler
{
private AuthenticationScheme _AuthenticationScheme = null;
private HttpContext _HttpContext = null;
//初始化
public Task InitializeAsync(AuthenticationScheme scheme, HttpContext context)
{
Console.WriteLine($"初始化自定义鉴权方法");
_AuthenticationScheme = scheme;
_HttpContext = context;
return Task.CompletedTask;
}
//鉴权的核心动作 就都在这里---找到凭证-解析凭证-检测有效
public Task<AuthenticateResult> AuthenticateAsync()
{
Console.WriteLine($"开始鉴权的核心动作-找到凭证-解析凭证-检测有效");
//完全自定义方式
string userInfo = _HttpContext.Request.Query["UrlToken"].ToString();
if (string.IsNullOrWhiteSpace(userInfo))
{
return Task.FromResult(AuthenticateResult.NoResult());//没有凭证
}
else if ("abc".Equals(userInfo)) //这里简单检验一下凭证是否为abc
{
//识别信息后,要传递到下一个管道,供下一个管道使用
var claimsIdentity = new ClaimsIdentity("custom");
claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, "Test"));
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "Admin"));
ClaimsPrincipal claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
return Task.FromResult<AuthenticateResult>(AuthenticateResult.Success(new AuthenticationTicket(claimsPrincipal, null, _AuthenticationScheme.Name)));
}
else
{
return Task.FromResult<AuthenticateResult>(AuthenticateResult.Fail("登录凭证失败"));
}
}
//未登录时的处理方式
public Task ChallengeAsync(AuthenticationProperties? properties)
{
Console.WriteLine($"没有登录");
string redirectUri = "/test/login";
_HttpContext.Response.Redirect(redirectUri);
return Task.CompletedTask;
}
//没有权限时的处理方式
public Task ForbidAsync(AuthenticationProperties? properties)
{
Console.WriteLine($"没有权限");
_HttpContext.Response.StatusCode = 403;
return Task.CompletedTask;
}
}
- 在program.cs中删除其他鉴权设定,增加以下设定
builder.Services.AddAuthentication(opt =>
{
opt.AddScheme<CustomAuthenticationHandler>("custom", "custom-Demo");
opt.DefaultScheme = "custom";
});
- 在控制器中新增一个Action来验证登录
[Authorize]
public async Task<IActionResult> CustomLogin()
{
var userOrigin = base.HttpContext.User;
var result = await HttpContext.AuthenticateAsync("custom");
if(result.Principal is null)
{
return Forbid("认证失败");
}
else
{
base.HttpContext.User = result.Principal;
foreach (var item in result.Principal.Claims)
{
Console.WriteLine($"{item.Type}:{item.Value}");
}
return Ok("登录成功");
}
}
当访问/CustomLogin?UrlToken=abc
时,正确访问,并打印出
授权的使用
授权检测可以有两层,1:没有任何要求,只要登录有凭证就行。2:要求用户有相应的权限才行
授权的三个属性
- Role角色
- Policy策略
- AuthenticationSchemes方案
角色Role
单角色
直接加上Roles =“Admin”
[Authorize(Roles ="Admin")]
public IActionResult RoleAdmin()
多角色
Admin和User都可以,满足一个就可以
[Authorize(Roles ="Admin,User")]
public IActionResult RoleAdmin()
如果是需要既能满足Roles ="Admin"又能满足Roles =“User”,则】
[Authorize(Roles ="Admin")]
[Authorize(Roles ="User")]
public IActionResult RoleAdmin()
注意:这里的Roles需要在生成凭证的时候使用ClaimTypes.Name
的方式,而不能是new Claim(“Role”, “user”)
var claimsIdentity = new ClaimsIdentity("user");
claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, name));
claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, "user"));
角色是最通用最简单的使用方式,但是不能满足个性化需求
策略Policy
Policy支持更灵活的自定义策略
builder.Services.AddAuthorization(opt =>
{
opt.AddPolicy("AdminPolicy", config =>
{
config.RequireRole("Admin")//等价于 Roles=Admin
.RequireUserName("abc")
.RequireClaim(ClaimTypes.Email)//要求有Country属性
//更灵活的配置要求
.RequireAssertion(context =>
{
return context.User.Claims.FirstOrDefault(c => c.Type.Equals(ClaimTypes.Email))?.Value.EndsWith("@qq.com") ?? false;
});
});
});
直接使用特性标注[Authorize(Policy = "AdminPolicy")]
自定义Requirement
可以单独写一个Requirement
public class CustomRequirement : AuthorizationHandler<CustomRequirement>,IAuthorizationRequirement
{
public CustomRequirement(string requiredName)
{
if (requiredName == null)
{
throw new ArgumentNullException(nameof(requiredName));
}
RequiredName = requiredName ?? "@qq.com";
}
private string RequiredName { get; }
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, CustomRequirement requirement)
{
if (context.User is not null && context.User.HasClaim(c => c.Type == ClaimTypes.Email))
{
var emailClaimList = context.User.FindAll(c => c.Type == ClaimTypes.Email);
if (emailClaimList.Any(c => c.Value.EndsWith(RequiredName)))
{
context.Succeed(requirement);
}
else
{
context.Fail();
}
}
return Task.CompletedTask;
}
}
直接使用AddRequirements
builder.Services.AddAuthorization(opt =>
{
opt.AddPolicy("CustomPolicy", config =>
{
config.AddRequirements(new CustomRequirement("@163.com"));
});
});
使用
[HttpGet]
[Authorize(Policy = "CustomPolicy")]
public IActionResult CustomRequirement()
{
return Ok("初步使用-自定义Requirement,要求使用163邮箱");
}
Policy的多条件组合
第一种方式
-
And
直接在策略里增加即可
policyBuilder.RequireRole("Admin")//都属于框架封装好的
.RequireUserName("Admin")//Role UserName都是最常用的
.RequireClaim(ClaimTypes.Country)//只要求有Country属性
.RequireClaim(ClaimTypes.DateOfBirth)
.RequireAssertion(context =>
{
return context.User.Claims.Any(c => c.Type.Equals(ClaimTypes.Country));//等价于RequireClaim
})
.Require....
- Or
.RequireAssertion(context =>
{
return 条件1 || 条件2 ;
});
第二种方式(推荐)
注意:这种方式的“与,或”是通过写不写context.Fail()
来确定的。
- 结合自定义Requirement的方式,先建立一个父类
public class EmailRequirement : IAuthorizationRequirement{}
- 集成父类,设置条件,此处我们可以验证QQ邮箱和搜狗邮箱
- qq邮箱
public class QQMailHandler : AuthorizationHandler<EmailRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, EmailRequirement requirement)
{
if (context.User is not null && context.User.HasClaim(c => c.Type == ClaimTypes.Email))
{
var emailClaimList = context.User.FindAll(c => c.Type == ClaimTypes.Email);
if (emailClaimList.Any(c => c.Value.EndsWith("@qq.com")))
{
context.Succeed(requirement);
}
else
{
//注意,如果是或的关系,就不要写这句话,表示交给其他去处理
//如果是与的关系,就要加上这句话,表示只要Fail一次,就是失败
context.Fail();
}
}
return Task.CompletedTask;
}
}
- sougou邮箱
public class SouGouMailHandler : AuthorizationHandler<EmailRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, EmailRequirement requirement)
{
if (context.User is not null && context.User.HasClaim(c => c.Type == ClaimTypes.Email))
{
var emailClaimList = context.User.FindAll(c => c.Type == ClaimTypes.Email);
if (emailClaimList.Any(c => c.Value.EndsWith("@sougou.com")))
{
context.Succeed(requirement);
}
else
{
//注意,如果是或的关系,就不要写这句话,表示交给其他去处理
//如果是与的关系,就要加上这句话,表示只要Fail一次,就是失败
context.Fail();
}
}
return Task.CompletedTask;
}
}
- 在Program.cs中使用ioc进行注册
builder.Services.AddSingleton<IAuthorizationHandler,QQMailHandler>();
builder.Services.AddSingleton<IAuthorizationHandler,SouGouMailHandler>();
- 在策略中增加
opt.AddPolicy("OrPolicy", config =>
{
//此处只需要增加父类即可
config.AddRequirements(new EmailRequirement());
});
- 使用
[HttpGet]
[Authorize(Policy = "OrPolicy")]
public IActionResult OrRequirement()
{
return Ok("初步使用-使用Requirement,要求使用QQ或者sougou邮箱");
}
多Scheme
- 在progress.cs中增加两个鉴权设置,一定要注意前后顺序,后面的会覆盖前面的
//告诉框架,如何进行鉴权
builder.Services.AddAuthentication(opt =>
{
opt.AddScheme<CustomAuthenticationHandler>("custom", "custom-Demo");
opt.DefaultScheme = "custom";
});
builder.Services.AddAuthentication("Cookies")
//设置使用cookie方式
.AddCookie("Cookies", opt =>
{
//没有登录,直接跳转到/NotLoggedIn
opt.LoginPath = "/test/login";
//没有权限,直接跳转到/NotLoggedIn
opt.AccessDeniedPath = "/test/NotLoggedIn";
});
- 增加Action
[HttpGet]
[Authorize(Policy = "CountryPolicy", AuthenticationSchemes = "Cookies,UrlTokenScheme")]
public async Task<IActionResult> MultiToken()
{
var r1 = await base.HttpContext.AuthenticateAsync("Cookies");
var r2 = await base.HttpContext.AuthenticateAsync("custom");
Console.WriteLine($"cookies:{r1?.Principal?.Claims.First().Value}");
Console.WriteLine($"custom:{r2?.Principal?.Claims.First().Value}");
return Ok("访问成功");
}
- 先登录获取cookie,
/Test/Login?name=1&password=1
,再访问/Test/MultiToken?UrlToken=abc
这样就能得到两个Scheme的认证信息
JWT的鉴权与授权
- nuget安装
Microsoft.AspNetCore.Authentication.JwtBearer
。 - Program中定义
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme).AddJwtBearer(opt =>
{
//此处配置为删减版,可根据需求自定义设定
opt.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = false,//是否验证Issuer
ValidateAudience = false,//是否验证Audience
ValidateLifetime = false,//是否验证失效时间
ValidateIssuerSigningKey = true,//是否验证SecurityKey
//秘钥,秘钥一般是和配置文件关联起来,此处为了方便直接写字符串
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("MyTestCustomSecurityKeySymmetricSecurityKey"))
};
});
- 在登录Action中生成一个token
public IActionResult GetToken(string name, string password)
{
var claims = new[]
{
new Claim("Name", name),
new Claim("id", "11"),
new Claim(ClaimTypes.Name, "Test"),
new Claim("EMail", "test@qq.com"),
new Claim("Account", "Administrator"),
new Claim(ClaimTypes.Role,"Admin"),
new Claim("Sex", "1")
};
//这个地方的秘钥一般是和配置文件关联起来,此处为了方便直接写字符串
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("MyTestCustomSecurityKeySymmetricSecurityKey"));
//加密算法
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
var token = new JwtSecurityToken(
claims: claims,
expires: DateTime.Now.AddSeconds(60 * 10),//10分钟有效期
signingCredentials: creds
);
string returnToken = new JwtSecurityTokenHandler().WriteToken(token);
//返回加密的token
return Ok(returnToken);
}
- 使用
// 最简单的使用,如果有详细的授权验证,请参考上文
[Authorize]
public IActionResult JWTTest()
请求时,需要在请求头中Authorization:
中带上Bearer 生成的token
,注意Bearer后面有一个空格