前言
最近沉寂了一段, 主要是上半年相当于休息和调整了一段时间, 接下来我将开始陆续学习一些新的技术, 比如 Docker,Jenkins 等, 都会以生活实例从零开始讲解起, 到时一并和大家分享和交流. 接下来几节课的内容将会讲解 JWT, 关于 JWT 的原理解析等等园子里大有文章, 就不再叙述, 这里我们讲解使用和一些注意的地方.
为什么要使用 JWT
在. NET Core 之前对于 web 应用程序跟踪用户登录状态最普通的方式则是使用 Cookie, 当用户点击登录后将对其信息进行加密并响应写入到用户浏览器的 Cookie 里, 当用户进行请求时, 服务端将对 Cookie 进行解密, 然后创建用户身份, 整个过程都是那么顺其自然, 但是这是客户端是基于浏览器的情况, 如果是客户端是移动 App 或者桌面应用程序呢? 关于 JWT 原理可以参考系列文章 https://www.cnblogs.com/RainingNight/p/jwtbearer-authentication-in-asp-net-core.html, 当然这只是其中一种限制还有其他. 如果我们使用 JSON Web Token 简称为 JWT 而不是使用 Cookie, 此时 Token 将代表用户, 同时我们不再依赖浏览器的内置机制来处理 Cookie, 我们仅仅只需要请求一个 Token 就好. 这个时候就涉及到 Token 认证, 那么什么是 Token 认证呢? 一言以蔽之: 将令牌 (我们有时称为 AccessToken 或者是 Bearer Token) 附加到 HTTP 请求中并对其进行身份认证的过程. Token 认证被广泛应用于移动端或 SPA.
JSON Web Token 基础
JWT 由三部分构成, Base64 编码的 Header,Base64 编码的 Payload, 签名, 三部分通过点隔开. 第一部分以 Base64 编码的 Header 主要包括 Token 的类型和所使用的算法, 例如:
- {
- "alg": "HS265",
- "typ": "JWT"
- }
第二部分以 Base64 编码的 Payload 主要包含的是声明(Claims), 例如, 如下:
- {
- "sub": "765032130654732",
- "name": "jeffcky"
- }
第三部分则是将 Key 通过对应的加密算法生成签名, 最终三部分以点隔开, 比如如下形式:
- eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.
- eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiSmVmZmNreSIsImVtYWlsIjoiMjc1MjE1NDg0NEBxcS5jb20iLCJleHAiOjE1NjU2MTUzOTgsIm5iZiI6MTU2MzE5NjE5OCwiaXNzIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAwIiwiYXVkIjoiaHR0cDovL2xvY2FsaG9zdDo1MDAxIn0.
- OJjlGJOnCCbpok05gOIgu5bwY8QYKfE2pOArtaZJbyI
到这里此时我们应该知道: JWT 包含的信息并没有加密, 比如为了获取 Payload, 我们大可通过比如谷歌控制台中的 API(atob)对其进行解码, 如下:
那如我所说既然 JWT 包含的信息并没有加密, 只是进行了 Base64 编码, 岂不是非常不安全呢? 当然不是这样, 还没说完, 第三部分就是签名, 虽然我们对 Payload(姑且翻译为有效负载), 未进行加密, 但是若有蓄意更换 Payload, 此时签名将能充分保证 Token 无效, 除非将签名的 Key 不小心暴露在光天化日之下, 否则必须是安全的. 好了, 到了这里, 我们稍稍讲解了下 JWT 构成, 接下来我们进入如何在. NET Core 中使用 JWT.
.NET Core 中使用 JWT
在. NET Core 中如何使用 JWT, 那么我们必须得知晓如何创建 JWT, 接下来我们首先创建一个端口号为 5000 的 API, 创建 JWT, 然后我们需要安装 System.IdentityModel.Tokens.Jwt 包, 如下:
我们直接给出代码来创建 Token, 然后一一对其进行详细解释, 代码如下:
- var claims = new Claim[]
- {
- new Claim(ClaimTypes.Name, "Jeffcky"),
- new Claim(JwtRegisteredClaimNames.Email, "2752154844@qq.com"),
- new Claim(JwtRegisteredClaimNames.Sub, "D21D099B-B49B-4604-A247-71B0518A0B1C"),
- };
- var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456"));
- var token = new JwtSecurityToken(
- issuer: "http://localhost:5000",
- audience: "http://localhost:5001",
- claims: claims,
- notBefore: DateTime.Now,
- expires: DateTime.Now.AddHours(1),
- signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
- );
- var jwtToken = new JwtSecurityTokenHandler().WriteToken(token);
如上我们在声明集合中初始化声明时, 我们使用了两种方式, 一个是使用 ClaimTypes , 一个是 JwtRegisteredClaimNames , 那么这二者有什么区别? 以及我们到底应该使用哪种方式更好? 或者说两种方式都使用是否有问题呢? 针对 ClaimTypes 则来自命名空间 System.Security.Claims , 而 JwtRegisteredClaimNames 则来自命名空间 System.IdentityModel.Tokens.Jwt , 二者在获取声明方式上是不同的, ClaimTypes 是沿袭微软提供获取声明的方式, 比如我们要在控制器 Action 方法上获取上述 ClaimTypes.Name 的值, 此时我们需要 F12 查看 Name 的常量定义值是多少, 如下:
接下来则是获取声明 Name 的值, 如下:
var sub = User.FindFirst(d => d.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name")?.Value;
那么如果我们想要获取声明 JwtRegisterClaimNames.Sub 的值, 我们是不是应该如上同样去获取呢? 我们来试试.
var sub = User.FindFirst(d => d.Type == JwtRegisteredClaimNames.Sub)?.Value;
此时我们发现为空没有获取到, 这是为何呢? 这是因为获取声明的方式默认是走微软定义的一套映射方式, 如果我们想要走 JWT 映射声明, 那么我们需要将默认映射方式给移除掉, 在对应客户端 Startup 构造函数中, 添加如下代码:
JwtSecurityTokenHandler.DefaultInboundClaimTypeMap.Clear();
如果用过并熟悉 IdentityServer4 的童鞋关于这点早已明了, 因为在 IdentityServer4 中映射声明比如用户 Id 即 (sub) 是使用的 JWT, 也就是说使用的 JwtRegisteredClaimNames, 此时我们再来获取 Sub 看看.
所以以上对于初始化声明两种方式的探讨并没有用哪个更好, 因为对于使用 ClaimTypes 是沿袭以往声明映射的方式, 如果要出于兼容性考虑, 可以结合两种声明映射方式来使用. 接下来我们来看生成签名代码, 生成签名是如下代码:
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456"));
如上我们给出签名的 Key 是 1234567890123456, 是不是给定 Key 的任意长度皆可呢, 显然不是, 关于 Key 的长度至少是 16, 否则会抛出如下错误
接下来我们再来看实例化 Token 的参数, 即如下代码:
- var token = new JwtSecurityToken(
- issuer: "http://localhost:5000",
- audience: "http://localhost:5001",
- claims: claims,
- notBefore: DateTime.Now,
- expires: DateTime.Now.AddHours(1),
- signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
- );
issuer 代表颁发 Token 的 Web 应用程序, audience 是 Token 的受理者, 如果是依赖第三方来创建 Token, 这两个参数肯定必须要指定, 因为第三方本就不受信任, 如此设置这两个参数后, 我们可验证这两个参数. 要是我们完全不关心这两个参数, 可直接使用 JwtSecurityToken 的构造函数来创建 Token, 如下:
- var claims = new Claim[]
- {
- new Claim(ClaimTypes.Name, "Jeffcky"),
- new Claim(JwtRegisteredClaimNames.Email, "2752154844@qq.com"),
- new Claim(JwtRegisteredClaimNames.Sub, "D21D099B-B49B-4604-A247-71B0518A0B1C"),
- new Claim(JwtRegisteredClaimNames.Exp, $"{new DateTimeOffset(DateTime.Now.AddMilliseconds(1)).ToUnixTimeSeconds()}"),
- new Claim(JwtRegisteredClaimNames.Nbf, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}")
- };
- var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456"));
- var jwtToken = new JwtSecurityToken(new JwtHeader(new SigningCredentials(key, SecurityAlgorithms.HmacSha256)), new JwtPayload(claims));
这里需要注意的是 Exp 和 Nbf 是基于 Unix 时间的字符串, 所以上述通过实例化 DateTimeOffset 来创建基于 Unix 的时间. 到了这里, 我们已经清楚的知道如何创建 Token, 接下来我们来使用 Token 获取数据. 我们新建一个端口号为 5001 的 Web 应用程序, 同时安装包[ Microsoft.AspNetCore.Authentication.JwtBearer ] 接下来在 Startup 中 ConfigureServices 添加如下代码:
- services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
- .AddJwtBearer(options =>
- {
- options.TokenValidationParameters = new TokenValidationParameters
- {
- ValidateIssuerSigningKey = true,
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes("1234567890123456")),
- ValidateIssuer = true,
- ValidIssuer = "http://localhost:5000",
- ValidateAudience = true,
- ValidAudience = "http://localhost:5001",
- ValidateLifetime = true,
- ClockSkew = TimeSpan.FromMinutes(5)
- };
- });
如上述若 Token 依赖于第三方而创建, 此时必然会配置 issuer 和 audience, 同时在我方也如上必须验证 issuer 和 audience, 上述我们也验证了签名, 我们通过设置 ValidateLifetime 为 true, 说明验证过期时间而并非 Token 中的值, 最后设置 ClockSkew 有效期为 5 分钟. 对于设置 ClockSkew 除了如上方式外, 还可如下设置默认也是 5 分钟.
ClockSkew = TimeSpan.Zero
如上对于认证方案我们使用的是 JwtBearerDefaults.AuthenticationScheme 即 Bearer, 除此之外我们也可以自定义认证方案名称, 如下:
最后别忘记添加认证中间件在 Configure 方法中, 认证中间件必须放在使用 MVC 中间件之前, 如下:
- App.UseAuthentication();
- App.UseMvc(routes =>
- {
- routes.MapRoute(
- name: "default",
- template: "{controller=Home}/{action=Index}/{id?}");
- });
到了这里, 我们通过端口为 5000 的 Web API 创建了 Token, 并配置了端口号为 5001 的 Web 应用程序使用 JWT 认证, 接下来最后一步则是调用端口号为 5000 的 API 获取 Token, 并将 Token 设置到请求头中 Authorization 键的值, 格式如下(注意 Bearer 后面有一个空格):
('Authorization', 'Bearer' + token);
我们在页面上放置一个按钮点击获取端口号为 5000 的 Token 后, 接下来请求端口号为 5001 的应用程序, 如下:
- $(function () {
- $('#btn').click(function () {
- $.get("http://localhost:5000/api/token").done(function (token) {
- $.Ajax({
- type: 'get',
- contentType: 'application/json',
- url: 'http://localhost:5001/api/home',
- beforeSend: function (xhr) {
- if (token !== null) {
- xhr.setRequestHeader('Authorization', 'Bearer' + token);
- }
- },
- success: function (data) {
- alert(data);
- },
- error: function (xhr) {
- alert(xhr.status);
- }
- });
- });
- });
- });
总结
来源: https://www.cnblogs.com/CreateMyself/p/11123023.html