8.2 JWT(代替Session)
经典的保持登陆状态的办法是Session,也就是用户登陆后,服务器产生唯一标识SessionId,并把SessionId和登陆的用户信息保存在服务器内存中,通时将SessionId发送给浏览器,当浏览器再次访问的时候,http请求中便携带了SessionId,服务器根据该Id在内存中取到用户信息,实现了登陆功能。
Session的缺点:
- 登陆用户多的时候,会占用服务器大量内存
- 每次客户端请求都要向服务器获取一次Session,导致请求速度响应慢
现在多采用JWT代替Session,JWT使用JSON来保存令牌信息,并不把信息保存在服务端,而是保存在客户端。JWT的结构包括头部、负载和签名:
头部中保存的是加密算法说明,负载中保存用户信息,签名是根据头部和负责经过算法算出来的值
JWT的登陆流程:
- 客户端向服务单发送登陆请求
- 服务端检验用户名密码,如果成功将从数据库中提取出这个用户的Id、角色等信息
- 服务端采用自定义的秘钥对用户信息(JSON)进行签名,形成签名数据
- 将用户信息(JSON)和上一步形成的签名拼接形成JWT,发送给客户端
- 客户端每次请求都带上这个JWT
- 每次服务器收到带JWT的请求后,使用自定义的秘钥对JWT的签名进行校验,如果成功则从JWT中取出用户信息
JWT基本使用
- NuGet安装
System.IdentityModel.Tokens.Jwt
- 生成JWT的程序
usingMicrosoft.IdentityModel.Tokens;
usingSystem.IdentityModel.Tokens.Jwt;
usingSystem.Security.Claims;
usingSystem.Text;
//每个claim代表一条信息,一个用户可能有多条信息如Id,name,所以这里用列表
varclaims=newList<Claim>();
//Claim具有两个属性Type和Value,他们都是string类型 Type:用户信息类型 Value:用户信息的值
claims.Add(newClaim(ClaimTypes.NameIdentifier, "6"));
claims.Add(newClaim(ClaimTypes.Name, "yzk"));
claims.Add(newClaim(ClaimTypes.Role, "User"));
claims.Add(newClaim(ClaimTypes.Role, "Admin"));//一个Type下允许有多个value
claims.Add(newClaim("PassPort", "E90000082"));
//签名秘钥,自定义,长一些安全
stringkey="fasdfad&9045dafz222#fadpio@0232";
DateTimeexpires=DateTime.Now.AddDays(1);//设置令牌过期时间
{//根据过期时间、多个claim、秘钥生成JWT
byte[] secBytes=Encoding.UTF8.GetBytes(key);
varsecKey=newSymmetricSecurityKey(secBytes);
varcredentials=newSigningCredentials(secKey,SecurityAlgorithms.HmacSha256Signature);
vartokenDescriptor=newJwtSecurityToken(claims: claims,
expires: expires, signingCredentials: credentials);
stringjwt=newJwtSecurityTokenHandler().WriteToken(tokenDescriptor);
}
Console.WriteLine(jwt);
JWT其实是明文,所以不要将重要的信息放到JWT中,为了防止篡改JWT,服务器需要对JWT签名进行验证
- JWT验证
stringjwt=Console.ReadLine()!;
stringsecKey="fasdfad&9045dafz222#fadpio@0232";//与之前的保持一致
JwtSecurityTokenHandlertokenHandler=new();
TokenValidationParametersvalParam=new();
varsecurityKey=newSymmetricSecurityKey(Encoding.UTF8.GetBytes(secKey));
valParam.IssuerSigningKey=securityKey;
valParam.ValidateIssuer=false;
valParam.ValidateAudience=false;
//解密
ClaimsPrincipalclaimsPrincipal=tokenHandler.ValidateToken(jwt,
valParam, outSecurityTokensecToken);
foreach (varclaiminclaimsPrincipal.Claims)
{
Console.WriteLine($"{claim.Type}={claim.Value}");
}
ASP.NET Core对JWT的封装
- 在配置系统中增加JWT节点,节点下设置秘钥和过期时间,再创建一个对应的配置类
"JWT": {
"SigningKey": "fasdfad&9045dafz222#fadpio@0232",
"ExpireSeconds": "86400"
}
publicclassJWTOptions
{
publicstringSigningKey { get; set; }
publicintExpireSeconds { get; set; }
}
- NuGet安装
Microsoft.AspNetCore.Authentication.JwtBearer
- 对JWT进行配置在builder.Build之前添加
services.Configure<JWTOptions>(builder.Configuration.GetSection("JWT"));//实体配置类
services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)//配置授权的各种属性
.AddJwtBearer(x=>//配置JWT的承载
{
//配置JWT绑定到JWTOptions新的实例,返回一个JWTOptions实例
JWTOptions?jwtOpt=builder.Configuration.GetSection("JWT").Get<JWTOptions>();
byte[] keyBytes=Encoding.UTF8.GetBytes(jwtOpt.SigningKey);
varsecKey=newSymmetricSecurityKey(keyBytes);
x.TokenValidationParameters=new()//设置令牌验证参数
{
ValidateIssuer=false,
ValidateAudience=false,
ValidateLifetime=true,
ValidateIssuerSigningKey=true,
IssuerSigningKey=secKey
};
});
- 在Program.cs中的app.UseAuthorization()前面加上app.UseAuthentication();
- 在控制类中增加登陆并且创建JWT的操作方法
publicrecordLoginRequest (stringUserName,stringPassword)
[HttpPost]
publicasyncTask<IActionResult>Login2(LoginRequestreq,
[FromServices] IOptions<JWTOptions>jwtOptions)
{
stringuserName=req.UserName;
stringpassword=req.Password;
varuser=awaituserManager.FindByNameAsync(userName);
if (user==null)
{
returnNotFound($"用户名不存在{userName}");
}
varsuccess=awaituserManager.CheckPasswordAsync(user, password);
if (!success)
{
returnBadRequest("Failed");
}
//登陆成功
varclaims=newList<Claim>();
claims.Add(newClaim(ClaimTypes.NameIdentifier, user.Id.ToString()));
claims.Add(newClaim(ClaimTypes.Name, user.UserName));
varroles=awaituserManager.GetRolesAsync(user);
foreach (stringroleinroles)
{
claims.Add(newClaim(ClaimTypes.Role, role));
}
stringjwtToken=BuildToken(claims, jwtOptions.Value);//登陆成功后创建JWT
returnOk(jwtToken);//将jwt返回给前端
}
//创建JWT
privatestaticstringBuildToken(IEnumerable<Claim>claims, JWTOptionsoptions)
{
DateTimeexpires=DateTime.Now.AddSeconds(options.ExpireSeconds);
byte[] keyBytes=Encoding.UTF8.GetBytes(options.SigningKey);
varsecKey=newSymmetricSecurityKey(keyBytes);
varcredentials=newSigningCredentials(secKey,
SecurityAlgorithms.HmacSha256Signature);
vartokenDescriptor=newJwtSecurityToken(expires: expires,
signingCredentials: credentials, claims: claims);
return"Bearer "+newJwtSecurityTokenHandler().WriteToken(tokenDescriptor);
}
- 在需要登陆才能访问的控制器类上添加
[Authorize]
[Route("[controller]/[action]")]
[ApiController]
[Authorize]
publicclassTest2Controller : ControllerBase
{
[HttpGet]
publicIActionResultHello()
{
//ControllerBase类中定义的User属性中含有当前登陆用户的所有信息
stringid=this.User.FindFirst(ClaimTypes.NameIdentifier)!.Value;//!不为空
stringuserName=this.User.FindFirst(ClaimTypes.Name)!.Value;
returnOk($"id={id},userName={userName}");
}
}
在请求的时候,ASP.Net Core要求,JWT要放到请求报文头中,其key为Authorization ,值为Bearer JWT值。
对于客户端获得的JWT,在前端项目中可以将其保存在Cookie或者LocalStorage等位置,在退出的时候进行删除。
[Authorize]注意事项
[Authorize]
可以加到控制器上也可以加到操作方法中,在控制器上标注表示该控制器下所有的操作方法都需要身份验证,如果某个操作方法不需要身份验证则在该操作方法上标注[AllowAnonymous]
在Swagger带有Authorize按钮
修改Program.cs
builder.Services.AddSwaggerGen(c=>
{
varscheme=newOpenApiSecurityScheme()
{
Description="Authorization header. \r\nExample: 'Bearer 12345abcdef'", //描述
Reference=newOpenApiReference{Type=ReferenceType.SecurityScheme,
Id="Authorization"},
Scheme="oauth2",Name="Authorization",
In=ParameterLocation.Header,Type=SecuritySchemeType.ApiKey,
};
c.AddSecurityDefinition("Authorization", scheme);
varrequirement=newOpenApiSecurityRequirement();
requirement[scheme] =newList<string>();
c.AddSecurityRequirement(requirement);
});
界面如图:
在对话框中输入Bearer JWT