有关JWT的基础
看这文章之前需要先对JWT有了解,不过这部分已经有很多很好的文章,这边我就不再叙述,你可以看如 ASP.NET Core 认证与授权4:JwtBearer认证 前面的介绍。
ASP.NET Core使用JWT
这里我按照正常项目那样,分成两个项目,JWT.Server项目负责生成JWT,JWT.DemoApi则负责提供Api接口服务对于受限接口会验证JWT。
JWT.Server生成JWT
新建ASP.NET Core WebApi项目,我这里使用了3.1版本
nuget安装
1
2// 需要根据自己的.net core版本选择相应版本
Install-Package Microsoft.AspNetCore.Authentication.JwtBearer在
appsettings.json
中添加上JWT相关配置1
2
3
4
5
6
7
8
9
10"Jwt": {
//密钥,需要大于等于16个字符,生产中密钥当然不能如下面这么简单
"Secret": "123456789@qwerasdf",
//签发者
"Iss": "https://hushitong.github.io",
//使用者
"Aud": "api",
//设置过期时间
"ExpireSeconds": 300
}新建
JWTController
,添加生成JWT相关代码1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49namespace JWT.Server.Controllers
{
[ ]
[ ]
public class JWTController : ControllerBase
{
readonly IConfiguration configuration;
public JWTController(IConfiguration configuration)
{
this.configuration = configuration;
}
[ ]
public IActionResult Authenticate(string userName, string pwd)
{
//实际项目这里应该做登陆验证,这里就写死了
if (userName == "admin" && pwd == "123456")
{
var jwtConfig = configuration.GetSection("Jwt");
//定义签名使用的密钥,以及使用Hmacsha256签名算法
var securityKey = new SigningCredentials(new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig.GetValue<string>("Secret"))), SecurityAlgorithms.HmacSha256);
//有效载荷
var claims = new Claim[] {
new Claim(JwtRegisteredClaimNames.Iss,jwtConfig.GetValue<string>("Iss")),
new Claim(JwtRegisteredClaimNames.Aud,jwtConfig.GetValue<string>("Aud")),
new Claim(ClaimTypes.Name,"admin"),
new Claim(ClaimTypes.NameIdentifier,"1"),
new Claim(ClaimTypes.Role,"system"),
new Claim(ClaimTypes.Role,"admin")
};
SecurityToken securityToken = new JwtSecurityToken(
signingCredentials: securityKey,
expires: DateTime.Now.AddSeconds(jwtConfig.GetValue<int>("ExpireSeconds")), //过期时间
claims: claims
);
//生成jwt令牌
return Content(new JwtSecurityTokenHandler().WriteToken(securityToken));
}
else
{
return BadRequest("登陆失败");
}
}
}
}测试访问接口,可获得JWT
1
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJpc3MiOiJodHRwczovL2h1c2hpdG9uZy5naXRodWIuaW8iLCJhdWQiOiJhcGkiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYWRtaW4iLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6IjEiLCJodHRwOi8vc2NoZW1hcy5taWNyb3NvZnQuY29tL3dzLzIwMDgvMDYvaWRlbnRpdHkvY2xhaW1zL3JvbGUiOlsic3lzdGVtIiwiYWRtaW4iXSwiZXhwIjoxNjQ0ODA4NjU0fQ.aqdRxdA9CakK_jrz-J3jTn2Rgu_2WkriHtLCJC61IcM
JWT.DemoApi使用JWT验证
新建ASP.NET Core WebApi项目,我这里使用了3.1版本
nuget安装
1
2// 需要根据自己的.net core版本选择相应版本
Install-Package Microsoft.AspNetCore.Authentication.JwtBearer在
appsettings.json
中添加上JWT相关配置,注意配置内容与JWT.Server里的基本一致,只是少了ExpireSeconds
1
2
3
4
5
6
7
8"Jwt": {
//密钥,需要大于等于16个字符,生产中密钥当然不能如下面这么简单
"Secret": "123456789@qwerasdf",
//签发者
"Iss": "https://hushitong.github.io",
//使用者
"Aud": "api"
}Startup.ConfigureServices
里注册JWT1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19//获得使用的密钥
var jwtConfig = Configuration.GetSection("Jwt");
var signingKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(jwtConfig.GetValue<string>("Secret")));
//认证参数
services.AddAuthentication("Bearer")
.AddJwtBearer(o =>
{
o.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,//是否验证签名,不验证的画可以篡改数据,不安全
IssuerSigningKey = signingKey,//使用的密钥
ValidateIssuer = true,//是否验证签发者,就是验证载荷中的Iss是否对应ValidIssuer参数
ValidIssuer = jwtConfig.GetValue<string>("Iss"),//签发者
ValidateAudience = true,//是否验证使用者,就是验证载荷中的Aud是否对应ValidAudience参数
ValidAudience = jwtConfig.GetValue<string>("Aud"),//使用者
ValidateLifetime = true,//是否验证过期时间,过期了就拒绝访问
ClockSkew = TimeSpan.Zero,//这个是缓冲过期时间,也就是说,即使我们配置了过期时间,这里也要考虑进去,过期时间+缓冲,默认好像是7分钟,你可以直接设置为0
};
});确保
Startup.Configure
方法中添加 了app.UseAuthorization()
及app.UseAuthentication()
,没有就加上注意:
app.UseAuthentication()
你不加应用也不会报错,但是后续请求需要JWT验证的接口时会一直报401错误。新建
JWTTestController
,添加相关测试代码,需要JWT验证的需要在Action上加上特性[Authorize]
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29[ ]
[ ]
public class JWTTestController : ControllerBase
{
//不需要验证JWT
[ ]
public ActionResult GetWithoutAuth()
{
return Ok("访问成功");
}
//只验证JWT是否通过
[ ]
[ ]
public ActionResult GetWithAuth()
{
//使用HttpContext.User.Claims可以获得当前用户Payload里的信息
HttpContext.User.Claims.ToList().ForEach(x => Console.WriteLine(x));
return Ok("访问成功");
}
//验证JWT是否通过同时还得验证其payload中是否由符合SuperAdmin的Role
[ ]
[ ]
public ActionResult GetWithRoleAuth()
{
return Ok("访问成功");
}
}
JWT测试
准备工作:运行JWT.Server,获得JWT
访问JWT.DemoApi请求头不带JWT
1 | -- 请求: |
可以看到添加了[Authorize]
特性的接口都返回401 (401 unauthorized,表示发送的请求需要有通过 HTTP 认证的认证信息)
访问JWT.DemoApi请求头带JWT
使用Postman或其他你顺手的工具,在请求头加上Authorization: Bearer JWTString
,即可使用JWT测试,我这里使用的时Swagger带JWT的办法。
结果如下:
1 | -- 响应: |
请求头添加了JWT参数后,GetWithAuth
接口成功,但是GetWithRoleAuth
却是返回403 (403 forbidden,表示对请求资源的访问被服务器拒绝) ,这是因为该接口有特性[Authorize(Roles ="SuperAdmin")]
限定了需要有SuperAdmin角色才能访问,而前面JWT.Server所发放的JWT里并不包含该角色,想要测试通过,只需要在JWT.Server发放JWT时payload里添加上SuperAdmin角色,重新生成JWT再测试
1 | //有效载荷 |
访问JWT.DemoApi请求头带过期的JWT
我们获得JWT后,等待300秒(前面设定的)让其过期,然后测试,结果和 访问JWT.DemoApi请求头不带JWT 一致
1 | -- 响应: |
TokenValidationParameters常用内容说明
TokenValidationParameters
是和token验证有关的参数配置,进行token验证时需要用到,下面是我对着该类写常用内容说明
1 | public class TokenValidationParameters |
JwtBearerEvents
在验证JWT时,Microsoft.AspNetCore.Authentication.JwtBearer
同时提供了一些额外事件来提供更有力的支持。
OnMessageReceived事件
假设我们的接口会接受网页与App端请求,网页使用Cookies保存JWT信息,而App使用请求头。
那我们需要在请求头中获得不了JWT信息时,再尝试去Cookie中获取,我们可以使用OnMessageReceived
事件解决该需求。
1 | //注册时添加OnMessageReceived |
由JWT.Server获得JWT后,先访问SetCookiesToken
接口设置token,然后你就不需要再在请求头中添加JWT参数,同样可以访问需要验证接口,结果同 访问JWT.DemoApi请求头带JWT
其他事件
通过查看Microsoft.AspNetCore.Authentication.JwtBearer.JwtBearerEvents
,发现除了OnMessageReceived
事件外,还提供了如下几个事件:
1 | namespace Microsoft.AspNetCore.Authentication.JwtBearer |
强制JWT失效
使用JWT本身是有一个问题的,那就是JWT本身在过期时间前都是有效的,这就会导致一些问题,如:账号被封后仍然能用,账号下线后实际其JWT仍可能被其他人利用,账号权限更改后不能实时生效等。
有一些其他办法解决该问题,我这里提供其中一种办法,就是每个用户新对应一个JWTGenerations字段(int类型,从1开始,写入数据库)
签发端:
在JWT的Payload部分添加一个新字段
Generations
,其值为最新的JWTGenerations值。服务端:
验证JWT时,同时拿其
Generations
与最新的JWTGenerations值比较,只要小于最新的JWTGenerations值,那判定该JWT失效,让其重新登陆。
每当进行使该用户JWT失效的操作(如:用户登陆、用户登出、用户被封等)时,让该用户JWTGenerations值+1。
JWTGenerations值在可以使用Redis等缓存保存以提升速度,然后每过10秒左右(时间根据实际自己定)主动去数据库拉取最新数据,也可以在进行使该用户JWT失效的操作后主动更新该信息都可以。
注意:该方案会造成只允许一个账号只能在一端登陆,其他端会被下线,需要其他办法再支持。
更多阅读
ASP.NET Core 认证与授权4:JwtBearer认证