文章目录
Jwt简单介绍
什么是Jwt
Json web token (JWT), 是为了在网络应用环境间传递声明而执行的一种基于JSON的开放标准((RFC 7519).该token被设计为紧凑且安全的,特别适用于分布式站点的单点登录(SSO)场景。JWT的声明一般被用来在身份提供者和服务提供者间传递被认证的用户身份信息,以便于从资源服务器获取资源,也可以增加一些额外的其它业务逻辑所必须的声明信息,该token也可直接被用于认证,也可被加密。
以上内容来自下面这位大佬的简书,强烈推荐各位小伙伴去拜读一下。
作者:Dearmadman
链接:https://www.jianshu.com/p/576dbf44b2ae
Jwt由3个信息段构成,分别是:头部(Header),载荷(Payload),签证信息(signature)。以上三个部分通过头部中声明的加密算法加密并以’.'连接就成了Jwt字符串。
头部保存的是声明信息,声明这是Jwt和声明加密算法。
载荷保存的是我们服务器给这个用户的信息,一般是分配一个Guid特定识别这个用户以及这个Token的有效期信息。
签证信息保存的是与加密算法相关的信息,在 ASP.Net Core中这些信息可以调用现成的方法直接生成。
Jwt安全吗
首先,从服务器的角度看,Jwt是服务器自己做出来的,用于特定识别某个用户的登录令牌,也就是说不管是谁,只要能截取到这个登录令牌就能伪造请求。
从客户端的角度来说,这个登录令牌只是一段字符串,在请求非公共接口时在Http请求的Header中带上就完事了,也不用去解析获取令牌里面的载荷信息。除非客户端也有Jwt中的签证信息,也能自己生成一个合法的令牌,不过在客户端中保存签证信息太危险了。
那么,我们能截取到这个登录令牌吗?如果是http请求的话,是能轻易抓取到的,但如果是https请求的话就不简单了,因为https的加密范围是包含请求的Header和Body,只要你不是通过url的形式传递令牌就能被加密到。所以Jwt还是应该配合https一起使用,不过这些都是防君子不防小人,任何东西在网络上传输都不能做到绝对安全。
在 ASP.NET Core中使用Jwt
授权和认证
在上代码之前,有一些概念必须要理解清楚。授权(Authorization)和认证(Authentication)的概念一定要明白。这两个东西都是Auth开头,tion结尾,不仔细看还真挺难区分。
首先,在访问非公开接口前先要经过认证,然后要验证授权。举个例子,认证就好比是登录游戏,但是在游戏里能不能使用VIP特权就要经过授权验证。认证(Authentication)只是说明你是合法用户,但不是每个用户都有权限执行这个操作,这就需要授权(Authorization)。
对于非公共接口,一定要有认证,但不一定要有授权(没有VIP的玩家总不能连刷图都不让吧?)。
三步走代码例子
在 ASP.Net Core 2.2中使用Jwt认证分三步走。
如果不需要自定义授权认证可以忽略第二步。
生成Jwt
首先,我们需要编辑一个登录接口,用户登录成功后就给他一个Jwt令牌,以后访问所有非公共接口都通过这个令牌识别该用户。令牌中包含一个Guid,登录成功后将该Guid和用户信息关联记录起来,后续操作根据Guid获取该用户信息。
[HttpPost("login")]
public IActionResult Login([FromBody]Client user)
{
IActionResult ret = null; // 用户登录验证
if(CheckUser(user))
{
// 验证通过,生成唯一识别码放到Token的Payload(载荷)里面
string guid = Guid.NewGuid().ToString();
List<Claim> payloadList = new List<Claim>();
payloadList.Add(new Claim("Guid", guid));
// payloadList.Add(new Claim("Other", data)); // 根据需要继续添加
Claim[] payload = payloadList.ToArray();
string securityKey = _configuration["SecurityKey"];
// sign the token using a secret key.This secret will be shared between your API and anything that needs to check that the token is legit.
// 读取配置文件中的秘钥
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey));
// 设置加密算法(签证信息)
var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
// 把设置填到token里(这里我省略了发布者和使用者的验证)
JwtSecurityToken token = new JwtSecurityToken(
claims: payload, // payload
signingCredentials: creds, // 签证信息
expires: DateTime.Now.AddMinutes(60)); // 过期时间
string tokenStr = new JwtSecurityTokenHandler().WriteToken(token);
// 把该用户和该Token中的guid关联起来,其他接口根据Token中的guid获取用户信息
SetUserGuid(user, guid);
// 回送Token和有效时长
ret = Ok(new { Token = tokenStr, Expire = 60 }); //做回送
}
else
{
ret = BadRequest();
}
return ret;
}
编辑授权策略
这里需要新建两个类,一个是授权验证策略类,一个是该授权验证策略需要的条件类。
有不明白的地方这里推荐查看微软的官方文档。
https://docs.microsoft.com/zh-cn/aspnet/core/security/authorization/iauthorizationpolicyprovider?view=aspnetcore-2.2
说明一下,这里的Client类是自己写的用户类,有用户名和密码什么的。
认证失败之后统一重定向到指定方法,该方法直接调用Forbid()返回403错误码。
/// <summary>
/// 授权验证策略条件(可以理解为验证该授权需要的东西)
/// </summary>
public class PermissionRequirement : IAuthorizationRequirement
{
public bool CheckPermission(Client user)
{
bool ret = true;
// 检查用户权限
// Coding...
return ret;
}
}
/// <summary>
/// 授权验证策略
/// </summary>
public class TokenPolicy : AuthorizationHandler<PermissionRequirement>
{
protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PermissionRequirement requirement)
{
var httpContext = (context.Resource as Microsoft.AspNetCore.Mvc.Filters.AuthorizationFilterContext).HttpContext;
string guid = "";
if (httpContext.User.Identity.IsAuthenticated)
{
var auth = httpContext.AuthenticateAsync().Result.Principal.Claims;
var guidClaim = auth.FirstOrDefault(s => s.Type == "Guid");
if (guidClaim != null)
{
guid = guidClaim.Value;
// 根据Guid获取用户信息(该方法是自己编写的)
if (GetUserByGuid(guid, out Client user))
{
// 验证成功且拥有权限
if(requirement.CheckPermission(user))
{
context.Succeed(requirement);
}
else
{
// 验证成功但权限不足
httpContext.Response.Redirect($"api/identify/forbidden");
}
}
else
{
// 验证成功,但Guid非法
httpContext.Response.Redirect($"api/identify/forbidden");
}
}
else
{
// 验证成功,但没有包含Guid
httpContext.Response.Redirect($"api/identify/forbidden");
}
}
else
{
// 验证失败,没有包含验证信息
httpContext.Response.Redirect($"api/identify/forbidden");
}
return Task.CompletedTask;
}
}
在StartUp中配置
在StartUp类的ConfigureServices方法中注册Jwt认证和我们自定义的授权验证方法。
如果不需要自定义授权验证,可以不调用AddAuthorization方法。
public void ConfigureServices(IServiceCollection services)
{
// 注册自定义的授权验证方法
services.AddAuthorization(options =>
{
options.AddPolicy("Permission", policy => policy.Requirements.Add(new PermissionRequirement()));
})
.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) // 注册Jwt认证
.AddJwtBearer(option =>
{
// 读取配置文件中的秘钥
string securityKey = Config["SecurityKey"];
option.TokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
ValidateLifetime = true,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
ValidateAudience = false,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey))
};
});
// 不需要自定义授权验证的可以不添加这个单例
services.AddSingleton<IAuthorizationHandler, TokenPolicy>();
services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
}
最后在StartUp类Configure方法中添加我们的方法。这里一定要注意先 UseAuthentication() 再 UserMvc()
public void Configure(IApplicationBuilder app, IHostingEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
app.UseAuthentication();
app.UseMvc();
}
添加特性
以上步骤完事之后我们就可以在需要验证的接口或Controller上添加[Authorize("{授权策略名}")]特性。具有该特性的Controller或者接口在调用前就会自动执行Jwt认证和自定义授权策略的HandleRequirementAsync方法。
在接口方法里用User.Claims.First()方法获取Jwt中包含的载荷(Payload)
[Authorize("Permission")]
[HttpGet("{id}")]
public ActionResult<string> Get(int id)
{
//if (User.HasClaim(s => s.Type == "Guid"))
string guid = User.Claims.First(s => s.Type == "Guid").Value;
// 根据guid获取用户信息继续操作
return guid;
}
没有自定义授权方法的添加[Authorize]特性即可,不用指定授权验证策略。
客户端使用
测试截图
首先是Login方法,用户通过登录验证后获得Jwt令牌和有效时间。
然后我们尝试不用令牌访问带有验证规则的接口
返回了401提示需要登录。
然后我们用刚刚获取到的Jwt令牌,在请求的Header中添加Authorization再访问一次。
这里要注意,Authorization的值要以Bearer开头,空格,再填入刚刚获取到的Jwt令牌,这个是在Jwt验证规则里面默认的。
请求成功。
微信小程序中使用
如果你在Jwt字符串制作时有添加有效期这个东西的话,在小程序中的util.js里面就可能需要一个定时刷新Jwt的方法。
// 定时刷新句柄全局变量
var refreshHandle = 0;
function TokenRefresh(min){
refreshHandle = setInterval(function(){
var app = getApp();
var user = app.globalData.userInfo; //保存在全局变量里的用户信息
wx.request({
url: domain + 'values/login',
method: 'POST',
data: user,
dataType: 'json',
responseType: 'text',
success: function (data) {
var result = data.data;
// 登录成功
if (data.statusCode == 200) {
app.globalData.jwtToken = result.token;
}
else{
// 登录失败
}
}
})
},(min-2) * 60 * 1000); // 这里提前两分钟刷新
}
// 用户登出时停止自动刷新
function StopRefresh(){
clearInterval(refreshHandle);
}
用户登录方法
UserLogin:function(usrName,psd){
var user = {
UserName: usrName,
Password: psd
}
wx.request({
url: domain + 'values/login',
method: 'POST',
data: user,
dataType: 'json',
responseType: 'text',
success: function (data) {
var result = data.data;
// 登录成功
if (data.statusCode == 200) {
// 有效时间
let expire = result.expire;
// 定时刷新Token
Utils.TokenRefresh(expire);
// 记在全局变量里面
getApp().globalData.jwtToken = result.token;
}
else {
// 登录失败
}
},
fail: function () {
// 连接错误
},
complete: function () {}
})
},
访问非公共接口
Test:function(){
let token = getApp().globalData.jwtToken;
// 在Header中带上验证信息
var head = {
'content-type': 'application/json;charset=utf-8',
'Authorization': 'Bearer ' + token
}
wx.request({
url: domain + '/values/66',
method: 'GET',
header: head,
dataType: 'json',
responseType: 'text',
success: function (data) {
// Coding
},
fail:function(){ },
complete:function(){ }
})
}
参考资料
Asp.net Core使用jwt
https://www.jianshu.com/p/294ea94f0087
jwt介绍
https://www.jianshu.com/p/576dbf44b2ae