JWT 介绍
JSON web Token(JWT)是目前最流行的跨域身份验证解决方案. JWT 的官网地址: https://jwt.io/ .
通俗地来讲, JWT 是能代表用户身份的令牌, 可以使用 JWT 令牌在 API 接口中校验用户的身份以确认用户是否有访问 API 的权限.
JWT 中包含了身份认证必须的参数以及用户自定义的参数, JWT 可以使用秘密 (使用 HMAC 算法) 或使用 RSA 或 ECDSA 的公钥 / 私钥对进行签名.
组成结构
在紧凑的形式中, JSON Web Tokens 由 dot(.)分隔的三个部分组成, 它们是:
Header 头
Payload 有效载荷
Signature 签名
因此, JWT 通常长这个样子: xxxxx.yyyyy.zzzzz
Header
标头通常由两部分组成: 令牌的类型, 即 JWT, 以及正在使用的签名算法, 例如 HMAC, SHA256 或 RSA.
例如:
- {
- "typ": "JWT",
- "alg": "HS256"
- }
然后, 这个 JSON 被编码为 Base64Url, 形成 JWT 的第一部分.
Payload
Payload 部分也是一个 JSON 对象, 用来存放实际需要传递的数据. JWT 规定了 7 个官方字段:
iss (issuer): 签发人
exp (expiration time): 过期时间
sub (subject): 主题
aud (audience): 受众
nbf (Not Before): 生效时间
iat (Issued At): 签发时间
jti (JWT ID): 编号
除了官方字段, 你还可以在这个部分定义私有字段, 但是它默认是不加密的, 任何人都可以读到, 所以不要把秘密信息放在这个部分. 这个 JSON 对象也要使用 Base64URL 算法转成字符串.
Signature
Signature 部分是对前两部分的签名, 防止数据篡改.
首先需要指定一个密钥(secret), 这个密钥只有服务器才知道, 不能泄露给用户. 然后使用 Header 里面指定的签名算法(默认是 HMAC SHA256), 按照下面的公式产生签名:
- HMACSHA256(
- base64UrlEncode(header) + "." +
- base64UrlEncode(payload),
- secret)
使用方法
项目源码请看我的 Gitee.
项目基础
该项目需要需要使用以下两个 nugget 包:
- System.IdentityModel.Tokens.Jwt
- Microsoft.AspNetCore.Authentication.JwtBearer
从概念上主要分为两个部分:
Authentication: 公司给你发的门禁卡.
Authorization: 财务保险柜的钥匙.
一开始一直对这两个概念模棱两可, 每次都是看了又丢了, 其实不难, 不深究了.
项目结构
下面以项目结构对代码进行一个粗略的解说:
注入服务
- public void ConfigureServices(IServiceCollection services)
- {
- services.AddAuthentication(option =>
- {
- // 添加 JWT Scheme
- option.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
- option.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
- option.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
- }).AddJwtBearer(option =>
- {
- // 添加 JWT 验证
- option.TokenValidationParameters = new TokenValidationParameters()
- {
- ValidateLifetime = true,// 是否验证失效时间
- ClockSkew = TimeSpan.FromSeconds(30),
- ValidateAudience = true,// 是否验证 Audience
- //ValidAudience = Const.GetValidudience(),//Audience
- // 这里采用动态验证的方式, 在重新登陆时, 刷新 token, 旧 token 就强制失效了
- AudienceValidator = (m, n, z) =>
- {
- return m != null && m.FirstOrDefault().Equals(Const.ValidAudience);
- },
- ValidateIssuer = true,// 是否验证 Issuer
- ValidIssuer = Const.Domain,//Issuer, 这两项和前面签发 jwt 的设置一致
- ValidateIssuerSigningKey = true,// 是否验证 SecurityKey
- IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(Const.SecurityKey))// 拿到 SecurityKey
- };
- option.Events = new JwtBearerEvents()
- {
- OnAuthenticationFailed = context =>
- {
- //Token expired
- if (context.Exception.GetType() == typeof(SecurityTokenExpiredException))
- {
- context.Response.Headers.Add("Token-Expired", "true");
- context.Response.Redirect("/");
- }
- return Task.CompletedTask;
- }
- };
- });
- // 添加策略健全模式
- services.AddAuthorization(option =>
- {
- option.AddPolicy("Permission", policy => policy.Requirements.Add(new PolicyRequirement()));
- });
- // 注入授权 Handler
- services.AddSingleton<IAuthorizationHandler, PolicyHandler>();
- // 注入 HttpContext 的祖先
- services.AddHttpContextAccessor();
- services.AddControllersWithViews();
- }
启用服务
App.UseAuthentication();
权限要求
- public class PolicyRequirement : IAuthorizationRequirement
- {
- /// <summary>
- /// 用户权限集合
- /// </summary>
- public List<UserPermission> UserPermissions { get; private set; }
- /// <summary>
- /// 无权限 action
- /// </summary>
- public string DeniedAction { get; set; }
- /// <summary>
- /// 构造
- /// </summary>
- public PolicyRequirement()
- {
- // 没有权限则跳转到这个路由
- DeniedAction = new PathString("/api/auth/nopermission");
- // 用户有权限访问的路由配置, 当然可以从数据库获取
- UserPermissions = new List<UserPermission> {
- new UserPermission { Url="/api/values/authorization", UserName="admin"},
- };
- }
- }
- /// <summary>
- /// 用户权限承载实体
- /// </summary>
- public class UserPermission
- {
- /// <summary>
- /// 用户名
- /// </summary>
- public string UserName { get; set; }
- /// <summary>
- /// 请求 Url
- /// </summary>
- public string Url { get; set; }
- }
权限处理
- public class PolicyHandler : AuthorizationHandler<PolicyRequirement>
- {
- private readonly IHttpContextAccessor _httpContextAccessor;
- public PolicyHandler(IHttpContextAccessor httpContextAccessor)
- {
- _httpContextAccessor = httpContextAccessor;
- }
- protected override Task HandleRequirementAsync(AuthorizationHandlerContext context, PolicyRequirement requirement)
- {
- // 赋值用户权限
- var userPermissions = requirement.UserPermissions;
- // 从 AuthorizationHandlerContext 转成 HttpContext, 以便取出表求信息
- var httpContext = _httpContextAccessor.HttpContext;
- // 请求 Url
- var questUrl = httpContext.Request.Path.Value.ToUpperInvariant();
- // 是否经过验证
- var isAuthenticated = httpContext.User.Identity.IsAuthenticated;
- if (isAuthenticated)
- {
- if (userPermissions.GroupBy(g => g.Url).Any(w => w.Key.ToUpperInvariant() == questUrl))
- {
- // 用户名
- var userName = httpContext.User.Claims.SingleOrDefault(s => s.Type == ClaimTypes.NameIdentifier).Value;
- if (userPermissions.Any(w => w.UserName == userName && w.Url.ToUpperInvariant() == questUrl))
- {
- context.Succeed(requirement);
- }
- else
- {
- // 无权限跳转到拒绝页面
- httpContext.Response.Redirect(requirement.DeniedAction);
- }
- }
- else
- {
- context.Succeed(requirement);
- }
- }
- else
- {
- httpContext.Response.Redirect(requirement.DeniedAction);
- }
- return Task.CompletedTask;
- }
- }
授权
- [HttpGet]
- public IActionResult Get(string userName, string pwd)
- {
- if (CheckAccount(userName, pwd, out string role))
- {
- // 每次登陆动态刷新
- Const.ValidAudience = userName + pwd + DateTime.Now.ToString();
- // push the user's name into a claim, so we can identify the user later on.
- // 这里可以随意加入自定义的参数, key 可以自己随便起
- 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.NameIdentifier, userName),
- new Claim("Role", role)
- };
- //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(Const.SecurityKey));
- var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
- //.NET Core's JwtSecurityToken class takes on the heavy lifting and actually creates the token.
- var token = new JwtSecurityToken(
- // 颁发者
- issuer: Const.Domain,
- // 接收者
- audience: Const.ValidAudience,
- // 过期时间
- expires: DateTime.Now.AddMinutes(30),
- // 签名证书
- signingCredentials: creds,
- // 自定义参数
- claims: claims
- );
- return Ok(new
- {
- token = new JwtSecurityTokenHandler().WriteToken(token)
- });
- }
- else
- {
- return BadRequest(new { message = "username or password is incorrect." });
- }
- }
访问授权
- // Authentication 验证门禁卡
- [Authorize]
- // Authorization 验证保险柜钥匙
- [Authorize("Permission")]
项目源码请看我的 Gitee.
调试方法
Postman
这里用 Postman 调试的时候出现了一点小插曲, 因为. Net Core3.0 会自己生成 https 证书, 不知道是 Postman 不认他还是为什么, 请求一直发不出去, 这里需要设置关闭 ssl 验证.
Chrome
这里可以直接在 Chrome 控制台里面写请求:
- fetch('https://localhost:5001/api/values/authorization',{
- headers: {
- 'Authorization': 'Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJuYmYiOiIxNTcyOTQxNDM1IiwiZXhwIjoxNTcyOTQzMjM1LCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6ImFkbWluIiwiUm9sZSI6ImFkbWluIiwiaXNzIjoiaHR0cHM6Ly9sb2NhbGhvc3Q6NTAwMSIsImF1ZCI6ImFkbWluMTEvMDUvMjAxOSAxNjoxMDozNSJ9.-pQK03wUYH97SDRxaN51CkcpXcs9B6qNwnZ4dfRgv3s'
- }
- })
- .then(res => res.JSON())
- .then(console.log)
来源: http://www.bubuko.com/infodetail-3274036.html