[什么是 JWT]
JSON web Token(JWT)是目前最流行的跨域身份验证解决方案.
JWT 的官网地址: https://jwt.io/
通俗地来讲, JWT 是能代表用户身份的令牌, 可以使用 JWT 令牌在 API 接口中校验用户的身份以确认用户是否有访问 API 的权限.
JWT 中包含了身份认证必须的参数以及用户自定义的参数, JWT 可以使用秘密 (使用 HMAC 算法) 或使用 RSA 或 ECDSA 的公钥 / 私钥对进行签名.
[什么时候应该使用 JSON Web 令牌?]
授权: 这是使用 JWT 的最常见方案. 一旦用户登录, 每个后续请求将包括 JWT, 允许用户访问该令牌允许的路由, 服务和资源. Single Sign On 是一种现在广泛使用 JWT 的功能, 因为它的开销很小, 并且能够在不同的域中轻松使用.
信息交换: JSON Web 令牌是在各方之间安全传输信息的好方法. 因为 JWT 可以签名 - 例如, 使用公钥 / 私钥对 - 您可以确定发件人是他们所说的人. 此外, 由于使用标头和有效负载计算签名, 您还可以验证内容是否未被篡改.
[JWT 有什么优势?]
我们先看我们传统的身份校验方式
用户向服务器发送用户名和密码.
服务器验证通过后, 在当前对话 (session) 里面保存相关数据, 比如用户角色, 登录时间等等.
服务器向用户返回一个 session_id, 写入用户的 Cookie.
用户随后的每一次请求, 都会通过 Cookie, 将 session_id 传回服务器.
服务器收到 session_id, 找到前期保存的数据, 由此得知用户的身份.
这种模式的问题在于, 扩展性 (scaling) 不好. 单机当然没有问题, 如果是服务器集群, 或者是跨域的服务导向架构, 就要求 session 数据共享, 每台服务器都能够读取 session. 如果 session 存储的节点挂了, 那么整个服务都会瘫痪, 体验相当不好, 风险也很高.
相比之下, JWT 的实现方式是将用户信息存储在客户端, 服务端不进行保存. 每次请求都把令牌带上以校验用户登录状态, 这样服务就变成了无状态的, 服务器集群也很好扩展.
[JWT 令牌结构]
在紧凑的形式中, JSON Web Tokens 由 dot(.)分隔的三个部分组成, 它们是:
Header 头
Payload 有效载荷
Signature 签名
因此, JWT 通常如下所示:
xxxxx.yyyyy.zzzzz
1.Header 头
标头通常由两部分组成: 令牌的类型, 即 JWT, 以及正在使用的签名算法, 例如 HMAC SHA256 或 RSA.
例如:
- {
- "alg": "HS256",
- "typ": "JWT"
- }
然后, 这个 JSON 被编码为 Base64Url, 形成 JWT 的第一部分.
2.Payload 有效载荷
Payload 部分也是一个 JSON 对象, 用来存放实际需要传递的数据. JWT 规定了 7 个官方字段, 供选用.
iss (issuer): 签发人
exp (expiration time): 过期时间
sub (subject): 主题
aud (audience): 受众
nbf (Not Before): 生效时间
iat (Issued At): 签发时间
jti (JWT ID): 编号
除了官方字段, 你还可以在这个部分定义私有字段, 下面就是一个例子. 例如:
- {
- "sub": "1234567890",
- "name": "John Doe",
- "admin": true
- }
注意, JWT 默认是不加密的, 任何人都可以读到, 所以不要把秘密信息放在这个部分. 这个 JSON 对象也要使用 Base64URL 算法转成字符串.
3.Signature 签名
Signature 部分是对前两部分的签名, 防止数据篡改.
首先, 需要指定一个密钥(secret). 这个密钥只有服务器才知道, 不能泄露给用户. 然后, 使用 Header 里面指定的签名算法(默认是 HMAC SHA256), 按照下面的公式产生签名.
- HMACSHA256(
- base64UrlEncode(header) + "." +
- base64UrlEncode(payload),
- secret)
签名用于验证消息在此过程中未被更改, 并且, 在使用私钥签名的令牌的情况下, 它还可以验证 JWT 的发件人是否是它所声称的人.
把他们三个全部放在一起
输出是三个由点分隔的 Base64-URL 字符串, 可以在 html 和 HTTP 环境中轻松传递, 而与基于 xml 的标准 (如 SAML) 相比更加紧凑.
下面显示了一个 JWT, 它具有先前的头和有效负载编码, 并使用机密签名.
如果您想使用 JWT 并将这些概念付诸实践, 您可以使用 jwt.io Debugger http://jwt.io/ 来解码, 验证和生成 JWT.
[JSON Web 令牌如何工作?]
在身份验证中, 当用户使用其凭据成功登录时, 将返回 JSON Web 令牌. 由于令牌是凭证, 因此必须非常小心以防止出现安全问题. 一般情况下, 您不应该将令牌保留的时间超过要求.
每当用户想要访问受保护的路由或资源时, 用户代理应该使用承载模式发送 JWT, 通常在 Authorization 标头中. 标题的内容应如下所示:
Authorization: Bearer <token>
在某些情况下, 这可以是无状态授权机制. 服务器的受保护路由将检查 Authorization 标头中的有效 JWT, 如果存在, 则允许用户访问受保护资源. 如果 JWT 包含必要的数据, 则可以减少查询数据库以进行某些操作的需要, 尽管可能并非总是如此.
如果在标 Authorization 头中发送令牌, 则跨域资源共享 (CORS) 将不会成为问题, 因为它不使用 cookie.
下图显示了如何获取 JWT 并用于访问 API 或资源:
应用程序向授权服务器请求授权
校验用户身份, 校验成功, 返回 token
应用程序使用访问令牌访问受保护的资源
[ASP.NET Core 集成 JWT]
前面我们介绍了 JWT 的原理, 下面我们在 ASP.NET core 实际项目中集成 JWT.
首先我们新建一个 Demo ASP.NET core 空 Web 项目
添加数据访问模拟 API,ValuesController
其中 API/value1 是可以直接访问的, API/value2 添加了权限校验特性标签 [Authorize]
- using System;
- using System.Collections.Generic;
- using System.Linq;
- using System.Threading.Tasks;
- using Microsoft.AspNetCore.Authorization;
- using Microsoft.AspNetCore.Mvc;
- namespace Demo.Jwt.Controllers
- {
- [ApiController]
- public class ValuesController : ControllerBase
- {
- [HttpGet]
- [Route("api/value1")]
- public ActionResult<IEnumerable<string>> Get()
- {
- return new string[] { "value1", "value1" };
- }
- [HttpGet]
- [Route("api/value2")]
- [Authorize]
- public ActionResult<IEnumerable<string>> Get2()
- {
- return new string[] { "value2", "value2" };
- }
- }
- }
添加模拟登陆, 生成 Token 的 API,AuthController
这里模拟一下登陆校验, 只验证了用户密码不为空即通过校验, 真实环境完善校验用户和密码的逻辑.
- using System;
- using System.Collections.Generic;
- using System.IdentityModel.Tokens.Jwt;
- using System.Linq;
- using System.Security.Claims;
- using System.Text;
- using System.Threading.Tasks;
- using Microsoft.AspNetCore.Authorization;
- using Microsoft.AspNetCore.Http;
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.IdentityModel.Tokens;
- namespace Demo.Jwt.Controllers
- {
- [Route("api/[controller]")]
- [ApiController]
- public class AuthController : ControllerBase
- {
- [AllowAnonymous]
- [HttpGet]
- public IActionResult Get(string userName, string pwd)
- {
- if (!string.IsNullOrEmpty(userName) && !string.IsNullOrEmpty(pwd))
- {
- var claims = new[]
- {
- new Claim(JwtRegisteredClaimNames.Nbf,$"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}") ,
- new Claim (JwtRegisteredClaimNames.Exp,$"{new DateTimeOffset(DateTime.Now.AddMinutes(30)).ToUnixTimeSeconds()}"),
- new Claim(ClaimTypes.Name, userName)
- };
- var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey));
- var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
- var token = new JwtSecurityToken(
- issuer: Const.Domain,
- audience: Const.Domain,
- claims: claims,
- expires: DateTime.Now.AddMinutes(30),
- signingCredentials: creds);
- return Ok(new
- {
- token = new JwtSecurityTokenHandler().WriteToken(token)
- });
- }
- else
- {
- return BadRequest(new { message = "username or password is incorrect." });
- }
- }
- }
- }
Startup 添加 JWT 验证的相关配置
- using Microsoft.AspNetCore.Authentication.JwtBearer;
- using Microsoft.AspNetCore.Builder;
- using Microsoft.AspNetCore.Hosting;
- using Microsoft.AspNetCore.Mvc;
- using Microsoft.Extensions.Configuration;
- using Microsoft.Extensions.DependencyInjection;
- using Microsoft.IdentityModel.Tokens;
- using System;
- using System.Text;
- namespace Demo.Jwt
- {
- public class Startup
- {
- public Startup(IConfiguration configuration)
- {
- Configuration = configuration;
- }
- public IConfiguration Configuration { get; }
- // This method gets called by the runtime. Use this method to add services to the container.
- public void ConfigureServices(IServiceCollection services)
- {
- // 添加 jwt 验证:
- services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
- .AddJwtBearer(options => {
- options.TokenValidationParameters = new TokenValidationParameters
- {
- ValidateIssuer = true,// 是否验证 Issuer
- ValidateAudience = true,// 是否验证 Audience
- ValidateLifetime = true,// 是否验证失效时间
- ClockSkew = TimeSpan.FromSeconds(30),
- ValidateIssuerSigningKey = true,// 是否验证 SecurityKey
- ValidAudience = Const.Domain,//Audience
- ValidIssuer = Const.Domain,//Issuer, 这两项和前面签发 jwt 的设置一致
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))// 拿到 SecurityKey
- };
- });
- services.AddMvc().SetCompatibilityVersion(CompatibilityVersion.Version_2_2);
- }
- // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
- public void Configure(IApplicationBuilder App, IHostingEnvironment env)
- {
- /// 添加 jwt 验证
- App.UseAuthentication();
- if (env.IsDevelopment())
- {
- App.UseDeveloperExceptionPage();
- }
- App.UseMvc(routes =>
- {
- routes.MapRoute(
- name: "default",
- template: "{controller=Home}/{action=Index}/{id?}");
- });
- }
- }
- }
最后把代码里面用到的一些相关常量也粘贴过来, Const.cs
- namespace Demo.Jwt
- {
- public class Const
- {
- /// <summary>
- /// 这里为了演示, 写死一个密钥. 实际生产环境可以从配置文件读取, 这个是用网上工具随便生成的一个密钥
- /// </summary>
- public const string SecurityKey = "MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQDI2a2EJ7m872v0afyoSDJT2o1+SitIeJSWtLJU8/Wz2m7gStexajkeD+Lka6DSTy8gt9UwfgVQo6uKjVLG5Ex7PiGOODVqAEghBuS7JzIYU5RvI543nNDAPfnJsas96mSA7L/mD7RTE2drj6hf3oZjJpMPZUQI/B1Qjb5H3K3PNwIDAQAB";
- public const string Domain = "http://localhost:5000";
- }
- }
到这里, 已经是我们项目的所有代码了.
如果需要完整的项目代码, GitHub 地址: https://github.com/sevenTiny/Demo.Jwt
[JWT 测试]
我们找一个趁手的工具, 比如 fiddler, 然后把我们的 Web 站点运行起来
首先调用无权限的接口: http://localhost:5000/API/value1
正确地返回了数据, 那么接下来我们测试 JWT 的流程
1. 无权限
首先我们什么都不加调用接口: http://localhost:5000/API/value2
返回了状态码 401, 也就是未经授权: 访问由于凭据无效被拒绝. 说明 JWT 校验生效了, 我们的接口收到了保护.
2. 获取 Token
调用模拟登陆授权接口: http://localhost:5000/API/Auth?userName=zhangsan&pwd=123
这里的用户密码是随便写的, 因为我们模拟登陆只是校验了下非空, 因此写什么都能通过
成功得到了响应
然后我们得到了一个 xxx.yyy.zzz 格式的 token 值. 我们把 token 复制出来
3. 在刚才 401 的接口请求 HEADER 中添加 JWT 的参数, 把我们的 token 加上去
再次调用我们的模拟数据接口, 但是这次我们加了一个 HEADER:http://localhost:5000/API/value2
把内容粘出来
- User-Agent: Fiddler
- Host: localhost:5000
- Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNTYwMzQ1MDIxIiwiZXhwIjoxNTYwMzQ2ODIxLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiemhhbmdzYW4iLCJpc3MiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAiLCJhdWQiOiJodHRwOi8vbG9jYWxob3N0OjUwMDAifQ.x7Slk4ho1hZc8sR8_McVTB6VEYLz_v-5eaHvXtIDS-o
这里需要注意 Bearer 后面是有一个空格的, 然后就是我们上一步获取到的 token
嗯, 没有 401 了, 成功返回了数据
4.JWT 的 Token 过期
我们且倒一杯开水, 坐等 30 分钟(我们代码中设置的过期时间), 然后再次调用数据接口: http://localhost:5000/API/value2
又变成了 401, 我们看下详细的返回数据
这里有标注, 错误描述 token 过期, 说明我们设置的 token 过期时间生效了
[结束]
到这里, 我们 JWT 的简介以及 ASP.NET core 集成 JWT 已经完美完成, 当然了这只是一个 demo, 在实际的应用中需要补充和完善的地方还有很多.
如果想要完整项目源码的, 可以参考地址: https://github.com/sevenTiny/Demo.Jwt
如果有幸能帮助到你, 高抬贵手点个 star 吧~
来源: https://www.cnblogs.com/7tiny/p/11012035.html