【笔记】ASP.NET Core Web API之Token验证

打印 上一主题 下一主题

主题 579|帖子 579|积分 1737

在实际开发中常常需要对外提供接口以便客户获取数据,由于数据属于私密信息,并不能随意供其他人访问,所以就需要验证客户身份。那么如何才能验证客户的身份呢?一个简单的小例子,简述ASP.NET Core Web API开发过程中,常用的一种JWT身份验证方式。

1.什么是JWT?

JSON WEB TokenJWT,读作 [/dʒɒt/]),是一种基于JSON的、用于在网络上声明某种主张的令牌(token)。主要用于认证和保护API之间信息互换。
JWT通常由三部分构成: 头信息(header), 消息体(payload)和签名(signature)。
2.JWT 构成

 JWT 由三部分构成:Header,Payload,Signature 三个部分构成,并且末了由.拼接而成 xxxxx.yyyyy.zzzzz。

2.1 头部(Header)

Header 一样平常由alg 和 typ 两个部分构成:
  1. {
  2.   "alg": "HS256",  (使用的hash算法,如:HMAC SHA256或RSA)
  3.   "typ": "JWT"      (Token的类型,在这里就是:JWT)
  4. }
复制代码
然后利用Base64Url编码成第一部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9
2.2. 载荷(Payload)

这部分是JWT主要的信息存储部分,其中包含了许多种的声明(claims)。
Claims的实体一样平常包含用户和一些元数据,这些claims分成三种类型:


  • reserved claims:预界说的 一些声明,并不是逼迫的但是推荐,它们包括 iss (issuer), exp (expiration time), sub (subject),aud(audience) 等(这里都利用三个字母的原因是保证 JWT 的紧凑)。
  • public claims: 公有声明,这个部分可以随便界说,但是要注意和 IANA JSON Web Token 辩论。
  • private claims: 私有声明,这个部分是共享被认定信息中自界说部分。
Pyload可以是如许子的:
  1. {
  2.     -- 官方的字段
  3.     "iss" (issuer):签发人,
  4.     "exp" (expiration time):过期时间,
  5.     "sub" (subject):主题,
  6.     "aud" (audience):订阅者,
  7.     "nbf" (Not Before):生效时间,
  8.     "iat" (Issued At):签发时间,
  9.     "jti" (JWT ID):编号,
  10.     -- 自定义的字段
  11.     "user_id": 123456,
  12.     "name": "John Doe",
  13.     "admin": true
  14. }
复制代码
这部分同样利用Base64Url编码成第二部分:
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.
2.3 签名(Signature)

Signature是对前两部分的签名,是用来验证发送者的JWT的同时也能确保在期间不被篡改。
起首需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。
这个密钥(secret)是大于16位的随机字符串(数字),
天生jwt:
  1. // 由 HMACSHA256 算法进行签名,secret 不能外泄
  2. const sign = HMACSHA256(base64.encode(header) + '.' + base64.encode(payload), secret)
  3. 算出签名以后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔.
  4. // jwt 由三部分拼接而成
  5. const jwt = base64.encode(header) + '.' + base64.encode(payload) + '.' + sign
复制代码
为啥要用base64编码,由于 ASCII 码称为了国际标准,所以我们要把其它字符转成 ASCII 就要用到 base64。
  1. utf-8 -> base64(编码) -> ASCII
  2. ASCII -> base64(解码) -> utf-8
复制代码
如许就可以让只支持 ASCII 的计算机支持 utf-8 了。

2.4 JWT的结构

eyJhbGci0iJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LZA1L21kZW50aXR5L2NsYW1tcy9zaWQi0iIxIiwiaHRBcDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MVMjAWNS8wNS9pZGVudG1Bes9jbGFpbXMvbmFtZSI6IuWFrOWtkOWwjWFrSIsImhBdHA6Ly9zY2hlbWFzLm1pY3Jvc29mdC5jb20vd3MvMjAw0C8wNi9pZGVudG1Bes9jbGFpbXMvcm9sZSI6IkFkbWluIiwiZXhwIjoxNjg3NZEyMDE1LCJpc3Mi0iLlhazlrzDlsI_lhagiLcJhdWQi0iLlhazlrZDlsI_1hagifQ.QeZ1Cy5JPV0s8fPfFR59g-rVI3SNKPNP2ZcODzr308Y
打开:JSON Web Tokens - jwt.ioJSON Web Token (JWT) is a compact URL-safe means of representing claims to be transferred between two parties. The claims in a JWT are encoded as a JSON object that is digitally signed using JSON Web Signature (JWS).
https://jwt.io/

3.应用JWT步调

 安装JWT授权库

采用JWT举行身份验证,需要安装【Microsoft.AspNetCore.Authentication.JwtBearer】,可通过Nuget包管理器举行安装,如下所示:

添加JWT身份验证服务

在启动类Program.cs中,添加JWT身份验证服务,如下所示:
  1. builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme)
  2.         .AddJwtBearer(options =>
  3.         {
  4.             options.TokenValidationParameters = new TokenValidationParameters
  5.             {
  6.                 ValidateIssuer = true,
  7.                 ValidateAudience = true,
  8.                 ValidateLifetime = true,
  9.                 ValidateIssuerSigningKey = true,
  10.                 ValidIssuer = TokenParameter.Issuer,
  11.                 ValidAudience = TokenParameter.Audience,
  12.                 IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(TokenParameter.Secret))
  13.             };
  14.         });
复制代码
应用鉴权授权中间件

在启动类Program.cs中,添加鉴权授权中间件,如下所示:
  1. app.UseAuthentication();
  2. app.UseAuthorization();
复制代码
设置Swagger身份验证输入(可选)

在启动类Program.cs中,添加Swagger服务时,设置Swagger可以输入身份验证方式,如下所示:
  1. builder.Services.AddSwaggerGen(options =>
  2. {
  3.     options.AddSecurityDefinition("Bearer", new OpenApiSecurityScheme
  4.     {
  5.         Description = "请输入token,格式为 Bearer xxxxxxxx(注意中间必须有空格)",
  6.         Name = "Authorization",
  7.         In = ParameterLocation.Header,
  8.         Type = SecuritySchemeType.ApiKey,
  9.         BearerFormat = "JWT",
  10.         Scheme = "Bearer"
  11.     });
  12.     //添加安全要求
  13.     options.AddSecurityRequirement(new OpenApiSecurityRequirement {
  14.         {
  15.             new OpenApiSecurityScheme{
  16.                 Reference =new OpenApiReference{
  17.                     Type = ReferenceType.SecurityScheme,
  18.                     Id ="Bearer"
  19.                 }
  20.             },new string[]{ }
  21.         }
  22.     });
  23. });
复制代码
注意:此处设置主要是方便测试,如果采用Postman或者其他测试工具,此步调可以省略。
创建JWT帮助类

创建JWT帮助类,主要用于天生Token,如下所示:
  1. using DemoJWT.Models;
  2. using Microsoft.AspNetCore.Authentication.Cookies;
  3. using Microsoft.IdentityModel.Tokens;
  4. using System.IdentityModel.Tokens.Jwt;
  5. using System.Security.Claims;
  6. using System.Text;
  7. namespace DemoJWT.Authorization
  8. {
  9.     public class JwtHelper
  10.     {
  11.         public static string GenerateJsonWebToken(User userInfo)
  12.         {
  13.             var securityKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(TokenParameter.Secret));
  14.             var credentials = new SigningCredentials(securityKey, SecurityAlgorithms.HmacSha256);
  15.             var claimsIdentity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
  16.             claimsIdentity.AddClaim(new Claim(ClaimTypes.Sid, userInfo.Id));
  17.             claimsIdentity.AddClaim(new Claim(ClaimTypes.Name, userInfo.Name));
  18.             claimsIdentity.AddClaim(new Claim(ClaimTypes.Role, userInfo.Role));
  19.             var token = new JwtSecurityToken(TokenParameter.Issuer,
  20.               TokenParameter.Audience,
  21.               claimsIdentity.Claims,
  22.               expires: DateTime.Now.AddMinutes(120),
  23.               signingCredentials: credentials);
  24.             return new JwtSecurityTokenHandler().WriteToken(token);
  25.         }
  26.     }
  27. }
复制代码
其中用到的TokenParameter主要用于设置Token验证的颁发者,吸收者,签名秘钥等信息,如下所示:
  1. namespace DemoJWT.Authorization
  2. {
  3.     public class TokenParameter
  4.     {
  5.         public const string Issuer = "公子小六";//颁发者        
  6.         public const string Audience = "公子小六";//接收者        
  7.         public const string Secret = "1234567812345678";//签名秘钥        
  8.         public const int AccessExpiration = 30;//AccessToken过期时间(分钟)
  9.     }
  10. }
复制代码
创建Token获取接口

创建对应的AuthController/GetToken方法,用于获取Token信息,如下所示:
  1. using DemoJWT.Authorization;
  2. using DemoJWT.Models;
  3. using Microsoft.AspNetCore.Http;
  4. using Microsoft.AspNetCore.Mvc;
  5. using System.IdentityModel.Tokens.Jwt;
  6. namespace DemoJWT.Controllers
  7. {
  8.     [Route("api/[controller]/[Action]")]
  9.     [ApiController]
  10.     public class AuthController : ControllerBase
  11.     {
  12.         [HttpPost]
  13.         public ActionResult GetToken(User user)
  14.         {
  15.             string token = JwtHelper.GenerateJsonWebToken(user);
  16.             return Ok(token);
  17.         }
  18.     }
  19. }
复制代码
创建测试接口

创建测试接口,用于测试Token身份验证。如下所示:
  1. using DemoJWT.Models;
  2. using Microsoft.AspNetCore.Authorization;
  3. using Microsoft.AspNetCore.Http;
  4. using Microsoft.AspNetCore.Mvc;
  5. using System.Security.Claims;
  6. namespace DemoJWT.Controllers
  7. {
  8.     [Authorize]
  9.     [Route("api/[controller]/[Action]")]
  10.     [ApiController]
  11.     public class TestController : ControllerBase
  12.     {
  13.         [HttpPost]
  14.         public ActionResult GetTestInfo()
  15.         {
  16.             var claimsPrincipal = this.HttpContext.User;
  17.             var name = claimsPrincipal.Claims.FirstOrDefault(r => r.Type == ClaimTypes.Name)?.Value;
  18.             var role = claimsPrincipal.Claims.FirstOrDefault(r => r.Type == ClaimTypes.Role)?.Value;
  19.             var test = new Test()
  20.             {
  21.                 Id = 1,
  22.                 Name = name,
  23.                 Role = role,
  24.                 Author = "公子小六",
  25.                 Description = "this is a test.",
  26.             };
  27.             return Ok(test);
  28.         }
  29.     }
  30. }
复制代码
接口测试

运行步伐,看到公开了两个接口,如下所示:

 校验逻辑



  • 1)客户端向授权服务系统发起哀求,申请获取“令牌”。
  • 2)授权服务根据用户身份,天生一张专属“令牌”,并将该“令牌”以JWT规范返回给客户端
  • 3)客户端将获取到的“令牌”放到http哀求的headers中后,向主服务系统发起哀求。主服务系统收到哀求后会从headers中获取“令牌”,并从“令牌”中解析出该用户的身份权限,然后做出相应的处理(同意或拒绝返回资源)


1. 获取Token
运行api/Auth/GetToken接口,输入用户信息,点击Execute,在返回的ResponseBody中,就可以获取接口返回的Token

2. 设置Token
在Swagger上方,点击Authorize,弹出身份验证设置窗口,如下所示:

3. 接口测试
设置好身份认证信息后,调用/api/Test/GetTestInfo接口,获取信息如下:

如果扫除掉Token设置,再举行访问/api/Test/GetTestInfo接口,则会返回401未授权信息,如下所示:


代码例子
JWT 与 Session

有无状态对比



  • Session 是一种记录服务器和客户端会话状态的机制,需要在数据库或者 Redis 中保存用户信息和token信息,所以它是有状态的。
  • JWT 看完了前面的 JWT 结构和 JWT 校验原理,在后端并不需要存储数据,直接通过私有密钥验证就可以了。
当有如许的一个需求,一家公司下同时关联了多个业务,A业务网站,B业务网站,但是现在要求用户在A网站登陆过,再访问B网站的时候可以或许主动登陆,JWT 就可以很快的实现这个需求,把 JWT 直接存储在前端,后端只要校验 JWT 就可以了。
   注:这个需求用 session 也是可以实现的,只是会存储状态,查询存储,没有 JWT 方便而已。
  
适用场景对比

邮箱验证

                很多网站在注册乐成后添加了邮箱验证功能,功能实现:用户注册乐成后,完善邮箱,服务端会给用户邮箱发一个链接,用户点开链接校验乐成,这个功能利用 JWT 是个不错的选择。

  1. // 把邮箱以及用户id绑定在一起,设置生效时间
  2. const code = jwt.sign({ email, userId }, secret, { expiresIn: 60 * 30 })
  3. // 在此链接校验验证码
  4. const link = `https://www.inode.club/code=${code}`
复制代码
做那些短期的验证需求

        比如在 BFF 层,用 JWT 去验证通报一些数据还是不错的选择,可以把有用时间设置的短一些,过期了就需要重新去哀求,我这么直接表述你可能还不太懂,举个现实生存中的例子。
   我们上学的时候,有班主任学科老师这两个概念,有一天你想请假,你需要先去找班主任开一个请假条,然后请教条你的班主任签完字之后,你会将请假条交给你的学科课西席,学科西席确认签字无误后,把请假条收了,并在请假记录表中作出了相应记录。
          上诉的例子中,“请假申请单”就是JWT中的payload,领导签字就是base64后的数字签名,领导是issuer,“学科西席的老王”即为JWT的audience,audience需要验证班主任签名是否合法,验证合法后根据payload中哀求的资源给予相应的权限,同时将JWT收回。
放到一些系统集成的应用场景中,例如我前面说的 BFF 中其实 JWT 更适合一次性操纵的认证:
   服务 B 你好, 服务 A 告诉我,我可以操纵 <JWT内容>, 这是我的凭据(即 JWT )
          在这里,服务 A 负责认证用户身份(类似于上例班主任答应请假),并颁布一个很短过期时间的JWT给欣赏器(相当于上例的请假单),欣赏器(相当于上例请假的我们)在向服务 B 的哀求中带上该 JWT,则服务 B(相当于上例的任课西席)可以通过验证该 JWT 来判断用户是否有权限执行该操纵。通过如许,服务 B 就成为一个安全的无状态的服务。
个人还是认为 JWT 更适合做一些一次性的安全认证,好多其他场景思量多了之后又做回了 session,传统的 cookie-session 机制工作得更好,但是对于一次性的安全认证,颁发一个有用期极短的JWT,即使暴露了伤害也很小。上面的邮箱验证其实也是一次性的安全认证。
跨域认证

因为 JWT 并不利用 Cookie ,所以你可以利用任何域名提供你的 API 服务而不需要担心跨域资源共享题目(CORS)。JWT 确实是跨域认证的一个解决方案,但是对于跨域场景时要注意一点。 客户端收到服务器返回的 JWT,可以储存在 Cookie 里面,也可以储存在 localStorage。
今后,客户端每次与服务器通信,都要带上这个 JWT。你可以把它放在 Cookie 里面主动发送,但是如许不能跨域,所以更好的做法是放在 HTTP 哀求的头信息Authorization字段里面。
Authorization: Bearer另一种做法是,跨域的时候,JWT 就放在 POST 哀求的数据体里面。
跨域知识扩展

跨域这两个字就像一块狗皮膏药一样黏在每一个开发者身上,无论你在工作上或者口试中无可避免会遇到这个题目。为了应付口试,我们每次都随便背几个方案。但是如果突然问你为什么会有跨域这个题目出现? ...停顿几秒,这里只是普及一下,知道的可以忽略掉。

登陆验证

登陆验证:不需要控制登录设备数量以及注销登陆环境,无状态的 jwt 是一个不错的选择。详细实现流程,可以看上文中的校验原理,校验原理利用的登陆验证例子。
当需求中出现控制登陆设备数量,或者可以注销掉用户时,可以思量利用原有的 session 模式,因为针对这种登陆需求,需要举行的状态存储对 jwt 添加额外的状态支持,增加了认证的复杂度,此时选用 session 是一个不错的选择。 针对上面的特殊需求,可能也有小同伴仍喜欢利用 jwt ,增补一下特殊案例
注销登陆

用户注销时候要思量 token 的过期时间。


  • session: 只需要把 user_id 对应的 token 清掉即可 ;
  • jwt: 利用 redis,需要维护一张黑名单,用户注销时把该 token 参加黑名单,过期时间与 jwt 的过期时间保持同等。
用户登陆设备控制



  • session: 利用 sql 类数据库,维护一个用户验证token表,每次登陆重置表中 token 字段,每次哀求需要权限接口时,根据 token 查找 user_id(也可以利用 redis 维护 token 数据的存储)
  • jwt: 倘使利用 sql 类数据库,维护一个用户验证token表,表中添加 token 字段,每次登陆重置 token 字段,每次哀求需要权限接口时,根据 jwt 获取 user_id,根据 user_id 查用户表获取 token 判断 token 是否同等。(也可以利用 redis 维护 token 数据的存储)
适合做那些事来讲的,其实也就是针对JWT的优势来说的,还有一些辩证性的理解。接下来说说 JWT 的缺点。
JWT 缺点



  • 更多的空间占用。如果将原存在服务端session中的信息都放在JWT中保存,会造成JWT占用的空间变大,需要思量客户端cookie的空间限定等因素,如果放在Local Storage,则可能会受到 XSS 攻击。
  • 无法作废已颁布的令牌。JWT 利用时由于服务器不需要存储 Session 状态,因此利用过程中无法废弃某个 Token 或者更改 Token 的权限。也就是说一旦 JWT 签发了,到期之前就会始终有用,除非服务器摆设额外的逻辑。
  • 用户信息安全。通过J WT 的构成结构可以看出,Payload 存储的一些用户信息,它是通过Base64加密的,可以直接解密,不能将机密数据写入 JWT,如果利用需要对 JWT 举行二次加密。





免责声明:如果侵犯了您的权益,请联系站长,我们会及时删除侵权内容,谢谢合作!更多信息从访问主页:qidao123.com:ToB企服之家,中国第一个企服评测及商务社交产业平台。

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
回复

使用道具 举报

0 个回复

正序浏览

快速回复

您需要登录后才可以回帖 登录 or 立即注册

本版积分规则

立山

金牌会员
这个人很懒什么都没写!

标签云

快速回复 返回顶部 返回列表