JWT简单的权限验证
名词解释
- Claim, ClaimsIdentity, ClaimsPrincipal这三个概念:
- Claim:可以理解为身份证的中的名字,性别等等的每一条信息,然后Claim组成一个ClaimIdentity 就是组成一个身份证,可以自定义key、value,也可以用固定字段;
- ClaimsIdentity:一组claims构成了一个identity,具有这些claims的identity就是 ClaimsIdentity ,驾照就是一种ClaimsIdentity,可以把ClaimsIdentity理解为“证件”,驾照是一种证件,护照也是一种证件。
- ClaimsPrincipal:ClaimsIdentity的持有者就是 ClaimsPrincipal ,一个ClaimsPrincipal可以持有多个ClaimsIdentity,就比如一个人既持有驾照,又持有护照,存在HttpContext的User属性里。
- Scheme:以什么授权方式进行授权的,比如以 cookie 的方式授权或是以 OpenId 的方式授权,或是像这里我们使用 Jwt Bearer 的方式进行授权。
- Token:服务端生成的一串字符串,以作客户端进行请求的一个令牌,当第一次登录后,服务器生成一个Token便将此Token返回给客户端,以后客户端只需带上这个Token前来请求数据即可,无需再次带上用户名和密码;
- JWT:Json web token (JWT), Token的编码方式,是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密
。JWT通常由三部分组成: 头信息(header), 消息体(payload)和签名(signature),用逗号分隔,JWT格式:xxx.yyy.zzz,JWT 的Payload(载荷)规定了7个官方字段,供选用。- iss (issuer):签发人
- exp (expiration time):过期时间
- sub (subject):主题
- aud (audience):受众
- nbf (Not Before):生效时间
- iat (Issued At):签发时间
- jti (JWT ID):编号
//理解了Claim, ClaimsIdentity, ClaimsPrincipal这三个概念,就能理解生成登录Cookie为什么要用下面的代码
var claimsIdentity = new ClaimsIdentity(new Claim[] { new Claim(ClaimTypes.Name, loginName) }, "Basic");
var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
await context.Authentication.SignInAsync(_cookieAuthOptions.AuthenticationScheme, claimsPrincipal);
//要用Cookie代表一个通过验证的主体,必须包含Claim, ClaimsIdentity, ClaimsPrincipal这三个信息;
//以一个持有合法驾照的人做比方:
//ClaimsPrincipal就是持有证件的人
//ClaimsIdentity就是证件
//Basic就是证件类型(这里假设是驾照)
//Claim就是驾照中的信息。
- challenge:质询,服务器对客户端的请求发送质询,客户端根据质询提供身份验证凭证,Default Challenge:默认质询;
- Authorize:授权,可以理解为设置访问规则,比如需要合法Token或者Token里要指定的角色等待,项目里不同接口可以设置不同的访问规则,比如有的只需登录即可(有合法的Token),有的接口需要管理员角色等等,按策略授权常见有:直接按角色授权(RequireRole)、按申明授权(RequireClaim)、按请求授权(Requirements);
- Authentication:认证,处理请求是否满足授权规则,比如解析Token内容再匹配规则,按接口的授权规则走不同的认证处理流程;
大致步骤
- 在正式环境里,用户登录成功后,服务端要给客户端返回一个JWT字符串;
- 所以服务端需要新增一个生成JWT字符串的方法,比如根据登录的用户,在JWT字符串里加入用户的ID、角色(方便后面验证接口访问权限)等非敏感信息;
- 给控制器或方法增加访问策略(这个就是授权,需要什么样的权限才能访问),即添加
Authorize
特性,默认都要判断发行人、订阅人、密钥3项,程序员还可以加入用户名、用户角色或其他规则的判断; - 服务端再注册一个认证服务(
services.AddAuthentication(...)
去解析请求头里的JWT字符串是否满足要求),就可以访问需要权限的接口,注意如果没有添加认证服务,只有授权,会提示:没有默认的认证方案和质询方案,即注册、初始化服务、添加认证中间件等; - 客户端获取到JWT后,在请求头的“Authorization”加入JWT字符串;
- 生成JWT字符串的方法里的信息要与接口验证所需的信息匹配才行,比如接口要判断角色,那JWT里要有角色的信息才能判断,注:JWT携带的与接口所需的信息如何比对的,是框架内部实现的,我们自己也可以增加一层判断:新增类,继承接口
IAuthorizationHandler
并重写方法HandleAsync()
,在这个HandleAsync()
方法,调用Succeed()
代表认证通过,但是官方认证处理同时存在,如果官方的认证失败,最终也会认知失败; - 以上总结为:给接口设置授权规则(加
Authorize
特性)、登录成功后发令牌(生成JWT字符串)、请求携带令牌访问接口、认证服务解析JWT字符串、通过验证访问资源;
新增TokenModel类
- 新增一个Token的实体类,方便读取appsettings.json的Token配置后,组装成一个实体,也可以用
Configuration["TokenConfig:Issuer"]
这样的形式获取配置文件里的值
public class TokenModel
{
public string Issuer { get; set; }
public string Audience { get; set; }
public string Secret { get; set; }
public DateTime Expire { get; set; }
}
在appsettings.json里配置Token信息
//发放的token里有这些,客户端访问时,服务端就是验证这些
"TokenConfig": {
"Issuer": "MyDemo.com",
"Audience": "MyDemo.com",
"Secret": "45678tyuivghjkt-ryuwghd" //16位及以上长度
}
修改Startup,验证哪些内容
//修改方法ConfigureServices
TokenModel tokenConfig = Configuration.GetSection("TokenConfig").Get<TokenModel>();
services.AddAuthentication("Bearer")
.AddJwtBearer(option => option.TokenValidationParameters = new TokenValidationParameters()
{
ValidateAudience = true,//是否验证Audience
ValidAudience = tokenConfig.Audience,//配置文件里的Audience值(MyDemo.com)才是有效的,其他无效
ValidateIssuer = true,//是否验证Issuer
ValidIssuer = tokenConfig.Issuer,//配置文件里的Issuer值(MyDemo.com)才是有效的,其他无效
ValidateIssuerSigningKey = true,//是否验证密钥IssuerSigningKey
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenConfig.Secret)),
RequireExpirationTime = true, //是否验证过期时间
ValidateLifetime = true,
}
);
services.Configure<TokenModel>(Configuration.GetSection("TokenConfig"));
开启中间件,启用认证功能
//修改Configure方法,添加中间件,中间件先后顺序如下,不能修改,先UseRouting后认证UseAuthentication,再授权UseAuthorization
app.UseRouting();
//先启用认证UseAuthentication,后启用授权UseAuthorization
app.UseAuthentication(); //1
app.UseAuthorization(); //2
创建Token令牌方法
- 声明
claim
数组,可以自定义键值对,new Claim("UserNo", "A001"),
,类似Session
,也可以用core定义好的new Claim(ClaimTypes.Role,"User")
,还可以用jwt封装的new Claim(JwtRegisteredClaimNames.Acr,"abc")
; - 实例
JwtSecurityToken
对象; - 将
JwtSecurityToken
对象转换为字符串;
public string GetToken()
{
//1. 声明`claim`数组;
Claim[] myClaims = new Claim[] {
new Claim("UserNo", userNo),
new Claim(ClaimTypes.Role,"User"),
new Claim(ClaimTypes.Role, "Admin"), //服务端将要验证的信息
new Claim(JwtRegisteredClaimNames.Acr,"abc"),
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(tokenModel.Secret));
//2. 实例`JwtSecurityToken`对象;
var jwtSecurityToken = new JwtSecurityToken(
issuer: tokenModel.Issuer,
audience: tokenModel.Audience,
claims: myClaims,
notBefore: null,
expires: DateTime.Now.AddMinutes(3),
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256));
//3. 将`JwtSecurityToken`对象转换为字符串;
return new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
}
给控制器或方法增加特性(设置访问权限)
[HttpGet]
[Authorize(Roles = "Admin")] //按角色判断,客户端Token里的Role要是Admin才行
//[Authorize(policy:"AdminAndUser")] //单一角色不能满足权限控制,可以按策略判断,内含多个角色的与、或关系,这种要修改Sartup,代码在下面
//[AllowAnonymous]//不受授权控制,任何人都可访问
//[Authorize]如果我们仅仅想给接口增加一个验证,而不要求角色信息,就可以这么操作。
public IActionResult Get()
{
return Content("ok");
}
//按策略判断,内含多个角色的与、或关系,这种要修改Sartup
services.AddAuthorization(op =>
{
op.AddPolicy("Client", policy => policy.RequireRole("Client"));
op.AddPolicy("Admin", policy => policy.RequireRole("Admin"));
op.AddPolicy("SystemOrAdmin", policy => policy.RequireRole("Admin", "System")); //或的关系
op.AddPolicy("SystemAndAdmin", policy => policy.RequireRole("Admin").RequireRole("System"));//且的关系
op.AddPolicy("zhangsan", policy => policy.RequireClaim(ClaimTypes.Name,"zhangsan"));
});
验证
- 直接访问接口,会返回401错误,无权限;
- 登录,成功后会得到Token;
- 把token值放请求头里,格式:Bearer {token},Bearer与token之间有空格,再访问接口,就可以了;
参考资料:ASP.NET Core系列:JWT身份认证
asp.net core 集成JWT(一)
ASP.NET Core 认证与授权[4]:JwtBearer认证
从壹开始前后端分离【 .NET Core2.2/3.0 +Vue2.0 】框架之五 || Swagger的使用 3.3 JWT权限验证【必看】