.NET JWT
JWT 》》Json Web Token
header . payload . Signature 三部分组成
JWT 在线生成
》》 https://jwt.io/
》》https://tooltt.com/jwt-encode/
》》解码工具
https://tool.box3.cn/jwt.html
JWT 特点
- 无状态
JWT不需要在服务端存储任何状态,客户端可以携带JWT来访问服务端,从而使服务端变得无状态。
这样,服务端可以更轻松实现扩展(分布式扩展)和负载均衡- 可自定义
JWT的载荷payload 有标准载荷和自定义载荷。可以存储任何JSON格式的数据。- 扩展性强
JWT 有一套标准规范,因此很容易在不同平台和语言之间共享和解析。此外,开发人员可以更加需要自定义声明Claims。
标准载荷和自定义载荷- 调试性好
JWT 的内容是以Base64编码的字符串形式存在的。- 安全性取决于密钥管理
JWT的安全性却决于密钥的管理。密钥只要服务端才知道的,客户端是不能知道的。- 无法撤销
JWT是无状态的,一旦JWT签发,就无法撤销。
如果用户在使用JWT认证期间被注销或禁用,那么服务端就无法阻止改用户继续使用之前的签发的JWT,因此,开发人员需要设计额外的
机制来撤销JWT,例如通过使用黑名单,这就使JWT不是无状态了。跟传统的Session差不多了。- 需要缓存到客户端
JWT包含了用户信息和授权信息,一般需要客户端缓存,这意味着JWT有被窃取的风险。- 载荷大小有限制
JWT是服务端签发,传输到客户端的,因此载荷大小也有限制的。一般不建议载荷超过1KB,会影响性能
JWT优缺点
》》》 优点
- 无状态: JWT本身不需要存储在服务器上,因为可以实现无状态的身份验证和授权
- 可扩展性:JWT载荷可以自定义,因此可以根据需要添加任意信息,推荐不要添加敏感信息
- 可靠性:JWT使用数字签名来保证安全性,因此具有可靠性
- 跨平台性:JWT支持多种编码语言和操作系统,因此具有跨平台性
- 高效性:JWT不需要查询数据库【首次需要的,之后请求就不需要了】,因此具有高效性。
》》》缺点 - 安全性却决于密钥的管理。如果密钥被泄露或者被不当管理,那么JWT将会受到攻击
- 无法撤销,因为jwt是无状态的,一旦签发,就无法撤销
- 需要传输到客户端,由于jwt包含用户信息和授权信息,jwt传输到客户端,存在被窃取的风险
- 载荷大小有限
JWT应用场景
》》1 一次性验证
用户注册成功后发一份激活邮件或者其它业务需要邮箱激活操作,都可以使用JWT
原因:
JWT时效性:让该链接具有时效性(比如 30分钟内激活)
JWT不可纂改性(密钥在服务端):防止纂改以激活其它账户
》》2 RESTful api 的无状态认证
使用JWT做RestFul api 的身份凭证: 当用户身份校验成功,客户端每次接口访问都带上JWT,服务端校验JWT合法性(是否过期、篡改等)
》》3 信息交换
JWT是在各方(项目间、服务间)之间安全传输信息的方式。因为JWT可以签名:例如使用公钥、私钥,所以可以确定发件人是他们自己的人,此外,由于使用标头和有效载荷计算签名,因此可以验证内容是否被篡改
》》 4 JWT令牌登录
JWT令牌登录也是一种应用场景,但也是JWT被诟病最多的地方,因为JWT令牌存在各种不安全。
>1. JWT令牌存储在客户端,容易被泄露并被伪造身份搞破坏
>2. JWT被签发,就无法撤销,当破坏在进行时,后端无法马上禁止
上面问题可通过监控异常JWT访问,设置黑名单+强制下线等方式尽量避免损失
JWT使用注意点
》》1. Redis校验实现令牌泄露保护
JWT无状态的,当JWT令牌被签发,在有效时间内,是无法进行撤销、销毁,所以就存在很多隐患:令牌泄露
》》 解决方案:
颁发JWT令牌时,在Redis中也存缓存一份,当判定某个JWT泄露了,立即移除Redis中的JWT。
当接口发起请求时,强制用户重新进行身份验证,直到验证成功。
》》2 林检JWT限制敏感操作
JWT无状态性,同时泄露可能很大,一些涉及到敏感数据变动,执行临检操作
》》 解决方案:
在涉及到诸如新增、、修改、删除、上传、下载等敏感性操作时,强制检查用户身份,比如手机验证码,人脸识别等
》》3异常JWT监控:超频识别与限制
JWT令牌被盗取,一般会出现高频次的系统访问。针对这种情况,监控用户在单位时间内的请求次数,当单位时间内的请求次数超出预定阀值,则判断该用户JWT令牌异常
》》 解决方案:
当判断JWT令牌异常,直接进行限制(IP限流、JWT黑名单等)
》》4地域检查杜绝JWT泄露可能
一般用户活动范围是固定,意味着JWT客户端访问IP相对固定,JWT泄露之后,可能会异地登录的情况
》》 解决方案:
对JWT进行异地访问检查,有效时间内,IP频繁变动判断JWT泄露
》》5 客户端区分检查防止JWT泄露
对于APP产品来说,一般客户端是固定的,基本为移动设备(app、平板),可以结婚设备机器码进行绑定
》》 解决方案:
将JWT于机器码绑定,存储于服务端,当客户端发起请求时,通过检查客户端的机器码与服务端机器码是否匹配判断JWT是否泄露
》》6 JWT令牌保护 限时、限数、限频
JWT令牌泄露是无法避免,但是我们可以进行泄露识别,做好泄露后补救保证系统安全
》》 解决方案:
对客户端进行合理限制,比如限制每个客户端的JWT令牌数量、访问频率、JWT令牌时效等,以降低JWT令牌泄露的风险
客户端将 JWT保存到本地(通常使用 webstorage【localstorage、Sessionstorage】,也可以使用 cookie【但不能跨域】)
当用户希望访问一个受保护的路由或者资源的时候,
可以把它放在 Cookie 里面自动发送,但是这样不能跨域,
所以更好的做法是放在 HTTP 请求头信息的 Authorization 字段里,使用 Bearer 模式添加 JWT。
1、安装 System.IdentityModel.Tokens.Jwt
jwt 是服务器签发 给客户端的,服务器不保存任何信息,所以服务端的密钥要 不能泄露。
// 设置JWT的密钥
string secretKey = "wedfs5656456456456456hjghjghjgjgzen-token7897";
byte[] keyBytes = Encoding.UTF8.GetBytes(secretKey);
var securityKey = new SymmetricSecurityKey(keyBytes);
// 创建JWT的签名凭证
var signingCredentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
// 设置JWT的Claims
var claims = new[]
{
new Claim(“User_id”, "Ares-Wang"),
new Claim(ClaimTypes.Email, "123@163.com"),
// 添加其他需要的声明
};
// 创建JWT的Token
var token = new JwtSecurityToken(
issuer: "your_issuer",
audience: "your_audience",
claims: claims,
expires: DateTime.Now.AddDays(1),
signingCredentials: signingCredentials
);
// 生成JWT字符串
var jwtToken = new JwtSecurityTokenHandler().WriteToken(token);
Response.Write(jwtToken);
/// <summary>
/// 根据token反向解析
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public string GetInfoFromToken(string token = null)
{
if (token is null || token == "")
return null;
string tokenStr = token.Replace("Bearer ", "");
var handler = new JwtSecurityTokenHandler();
// string tokenStr = token;
var payload = handler.ReadJwtToken(tokenStr).Payload;
var claims = payload.Claims;
var userid = claims.First(claim => claim.Type == "User_id")?.Value;
return userid;
}
/// <summary>
/// 从Token中获取用户身份
/// </summary>
/// <param name="token"></param>
/// <param name="securityKey">securityKey明文,Java加密使用的是Base64</param>
/// <returns></returns>
public ClaimsPrincipal GetPrincipal(string token)
{
try
{
string securityKey = SecretKey;
token = token.Replace("Bearer ", "");
var handler = new JwtSecurityTokenHandler();
TokenValidationParameters tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey)),
ValidateLifetime = false
};
return handler.ValidateToken(token, tokenValidationParameters, out SecurityToken validatedToken);
}
catch (Exception ex)
{
return null;
}
}
/// <summary>
/// 校验Token
/// </summary>
/// <param name="token">token</param>
/// <returns></returns>
public bool CheckToken(string token)
{
var principal = GetPrincipal(token);
if (principal is null)
{
return false;
}
return true;
}
有关于jwt生成和验证token的操作全部记录在jwthelper.cs文件中
/// <summary>
/// 授权JWT类
/// </summary>
public class JwtHelper
{
public readonly string SecretKey = System.Configuration.ConfigurationManager.AppSettings["SecretKey"];
public readonly string AppId = System.Configuration.ConfigurationManager.AppSettings["AppId"];
public readonly string AppKey = System.Configuration.ConfigurationManager.AppSettings["AppKey"];
/// <summary>
/// 创建Token 这里面可以保存自己想要的信息
/// </summary>
/// <param name="user_id"></param>
/// <param name="mobile"></param>
/// <returns></returns>
public string CreateToken(string user_id)
{
// 1. 定义需要使用到的Claims
var claims = new Claim[]
{
new Claim("user_id", user_id),
/* 可以保存自己想要信息,传参进来即可
new Claim("limit", "limit"),
*/
};
// 2. 从 appsettings.json 中读取SecretKey
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecretKey));
// 3. 选择加密算法
var algorithm = SecurityAlgorithms.HmacSha256;
// 4. 生成Credentials
var signingCredentials = new SigningCredentials(secretKey, algorithm);
// 5. 根据以上,生成token
var jwtSecurityToken = new JwtSecurityToken(
issuer: AppId, //Issuer
audience: AppKey, //Audience
claims: claims, //Claims,
notBefore: DateTime.Now, //notBefore
expires: DateTime.Now.AddHours(1), //expires
signingCredentials: signingCredentials //Credentials
);
// 6. 将token变为string
var token = new JwtSecurityTokenHandler().WriteToken(jwtSecurityToken);
return token;
}
/// <summary>
/// 根据token反向解析
/// </summary>
/// <param name="token"></param>
/// <returns></returns>
public string GetInfoFromToken(string token = null)
{
if (token is null || token == "")
return null;
string tokenStr = token.Replace("Bearer ", "");
var handler = new JwtSecurityTokenHandler();
// string tokenStr = token;
var payload = handler.ReadJwtToken(tokenStr).Payload;
var claims = payload.Claims;
var userid = claims.First(claim => claim.Type == "user_id")?.Value;
return userid;
}
/// <summary>
/// 从Token中获取用户身份
/// </summary>
/// <param name="token"></param>
/// <param name="securityKey">securityKey明文,Java加密使用的是Base64</param>
/// <returns></returns>
public ClaimsPrincipal GetPrincipal(string token)
{
try
{
string securityKey = SecretKey;
token = token.Replace("Bearer ", "");
var handler = new JwtSecurityTokenHandler();
TokenValidationParameters tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = false,
ValidateIssuer = false,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(securityKey)),
ValidateLifetime = false
};
return handler.ValidateToken(token, tokenValidationParameters, out SecurityToken validatedToken);
}
catch (Exception ex)
{
return null;
}
}
/// <summary>
/// 校验Token
/// </summary>
/// <param name="token">token</param>
/// <returns></returns>
public bool CheckToken(string token)
{
var principal = GetPrincipal(token);
if (principal is null)
{
return false;
}
return true;
}
参考资料
》》ActionFilterAttribute 在Action执行前 触发OnActionExecuting 执行后 OnActionExecuted
using System;
using System.Collections.Generic;
using System.Linq;
using System.Web;
using System.Web.Http.Controllers;
using System.Web.Http.Filters;
namespace WebApplication11
{
public class CustomActionFilteAttribute:ActionFilterAttribute
{
public readonly string SecretKey = System.Configuration.ConfigurationManager.AppSettings["SecretKey"];
public readonly string AppId = System.Configuration.ConfigurationManager.AppSettings["AppId"];
public readonly string AppKey = System.Configuration.ConfigurationManager.AppSettings["AppKey"];
public override void OnActionExecuting(HttpActionContext actionContext)
{
Jwt.JwtHelper _jwt = new Jwt.JwtHelper();
var token = "";
if (actionContext.HttpContext.Request.Headers.AllKeys.Contains("Authorization"))
{
int index = actionContext.HttpContext.Request.Headers.AllKeys.Select((a, i) => i).Where(i => actionContext.HttpContext.Request.Headers.AllKeys[i] == "Authorization").FirstOrDefault();
token = actionContext.HttpContext.Request.Headers[index];
}
if (token == null || token == "")
{
var Auth = new
{
Status = 401,
Message = "身份验证失败"
};
actionContext.Result = new ContentResult
{
ContentType = "application/json",
Content = JsonConvert.SerializeObject(Auth)
};
return;
}
bool isOK = _jwt.CheckToken(token);
if (!isOK)
{
var Auth = new
{
Status = 401,
Message = "身份验证失败"
};
actionContext.Result = new ContentResult
{
ContentType = "application/json",
Content = JsonConvert.SerializeObject(Auth)
}; return;
}
var userid = _jwt.GetInfoFromToken(token);
base.OnActionExecuting(actionContext);
base.OnActionExecuting(actionContext);
}
public override void OnActionExecuted(HttpActionExecutedContext actionExecutedContext)
{
//在这里可以解决跨越问题
base.OnActionExecuted(actionExecutedContext);
}
}
}
》》JWT 一般是放在 header 的 Authorization 自定义头里
JWT支持跨域访问,cookie不支持, Cookie 跨站是不能共享的,这样的话如果你要实现多应用(多系统)的单点登录(SSO),使用 Cookie 来做需要的话就很困难了。但如果用 token 来实现 SSO 会非常简单,只要在 header 中的 Authorization 字段(或其他自定义)加上 jwt即可完成所有跨域站点的认证。
JWT是无状态的,可以在多个服务器间共享
JWT可以避免CSRF攻击(跨站请求攻击)
JWT易与扩展,在移动端原生请求、小程序等请求 是没有Cookie的,而Session也是依赖于cookie。JWT是默认随着Header中的Authorize 传过来的。所以JWT天生就支持移动平台、可扩展性好