dotnet core JWT Demo
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)