.net core中JWT token身份验证原理与配置,含详细示例代码

-- .net core中JWT token身份验证原理与配置实现
【官网】:#

应用场景

一般遇到身份验证,最普遍的就是session和cookie了,可是它们要么在集群服务器时因为用户不同步骤的请求定位到不同服务器遇到session共享问题,或者就是cookie跨域问题等。再者就是无法实现跨平台,跨客户端的处理。 这个时候推荐用JWT。

基础资源

Nuget(System.IdentityModel.Tokens.Jwt和Microsoft.AspNetCore.Authentication.JwtBearer)

使用须知

由于jwt主要依赖签名机制防止篡改,所以要避免在payload中放置秘钥或密码之类的敏感信息. 对于恶意者来说不需要修改,只需要拿过来用即可。

配置步骤

【为什么要用JWT】

为什么需要用jwt?
A)如果使用session机制,一个用户的多次请求被分发到集群的不同服务器上,就会出现取不到session数据的情况,于是session的共享就成了一个问题。
B)如果使用session(sessionid会存储在cookie)还是cookie加密解密都是依赖浏览器的内置功能,而对于移动app,桌面程序等就无法快速使用了:当然使用webview,webbrowser之类的另说.
C)使用cookie等还会遇到跨域的问题,jwt则是松散耦合的,没有此类问题。

【JWT结构介绍】


JWT由三部分组成,分别是头信息、有效载荷、签名,中间以(.)分隔.

1header(头信息)

由两部分组成,令牌类型(即:JWT)、散列算法(HMACRSASSARSASSA-PSS等)

2Payload(有效载荷)

JWT的第二部分是payload,其中包含claimsclaims是关于实体(常用的是用户信息)和其他数据的声明,claims有三种类型: registered, public, and private claims

Registered claims: 这些是一组预定义的claims,非强制性的,但是推荐使用, iss(发行人), exp(到期时间), sub(主题), aud(观众)等;

Public claims: 自定义claims,注意不要和JWT注册表中属性冲突

Private claims: 这些是自定义的claims,用于在同意使用这些claims的各方之间共享信息,它们既不是Registered claims,也不是Public claims

3Signature

要创建签名部分,必须采用编码的Header,编码的Payload,秘钥,Header中指定的算法,并对其进行签名。



常见问题

快速入门

【步骤1.1:配置jwt的全局参数】

appsetting.json中:

{
"Logging": {
"LogLevel": {
"Default": "Information",
"Microsoft": "Warning",
"Microsoft.Hosting.Lifetime": "Information"
}
},
"JwtTokenSetting": {
"secret": "4CD61F55692A6CE6",
"issuer": "auth.config.net.cn",
"audience": "config.net.cn",
"sedondsExpiration": 20
} ,
"AllowedHosts": "*"
}

【步骤1.2:建立读取上述配置的节点对象】

public class JwtTokenSetting
{
public string secret { get; set; }
public string issuer { get; set; }
public string audience { get; set; }

public int sedondsExpiration { get; set; }
}

【步骤2.1:Startup中初始化配置】

public void ConfigureServices(IServiceCollection services)
{
services.AddControllersWithViews();
//todo: jwt.config
services.Configure(Configuration.GetSection("JwtTokenSetting"));
var tokenSetting = Configuration.GetSection("JwtTokenSetting").Get();
//todo:jwt
services.AddAuthentication(x =>
{
x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
}).AddJwtBearer(x =>
{
x.RequireHttpsMetadata = false;
x.SaveToken = true;
x.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.ASCII.GetBytes(tokenSetting.secret)),
ValidIssuer = tokenSetting.issuer,
ValidAudience = tokenSetting.audience,
ValidateIssuer = false,
ValidateAudience = false,
ValidateLifetime=true, //判断超时失效
ClockSkew = TimeSpan.FromSeconds(5)//注意这是缓冲过期时间,总的有效时间等于这个时间加上jwt的过期时间,如果不配置,默认是5分钟
};
});
}
【步骤2.2:Startup中开启授权】

public void Configure(IApplicationBuilder app, IWebHostEnvironment env)
{
if (env.IsDevelopment())
{
app.UseDeveloperExceptionPage();
}
else
{
app.UseExceptionHandler("/Home/Error");
// The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
app.UseHsts();
}
app.UseHttpsRedirection();
app.UseStaticFiles();
app.UseAuthentication();//jwt enabled
app.UseRouting();

app.UseAuthorization();

app.UseEndpoints(endpoints =>
{
endpoints.MapControllerRoute(
name: "default",
pattern: "{controller=Home}/{action=Index}/{id?}");
});
}


【步骤3.1:建立一个颁发token的工具类】

using Microsoft.IdentityModel.Tokens;
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;

namespace ConfigLab.Dnc.WebView.JwtWrapper
{
///
/// 功能简介:JwtToken的创建工具对象,本类仅用于演示
/// 创建时间:2020-10-8
/// 创建人:config.net.cn
///
public class JwtTokenManager
{
private string SecurityKey = "4CD61F55692A6CE6";//秘钥
private string IssUser = "http://user.config.net.cn";//发布者
private string Audience = "http://config.net.cn";//受理者
private int SecondsOfExpiry = 3600;
private string ClaimTypeName = "";
private string JwtRegisteredClaimNameSub = "";
public JwtTokenManager(string sClaimTypeName,string sClaimNameSub, string sSecurityKey,string sIssuser,string sAudience,int iSecondsOfExpiry)
{
this.ClaimTypeName = sClaimTypeName;
this.JwtRegisteredClaimNameSub = sClaimNameSub;
this.SecurityKey = sSecurityKey;
this.IssUser = sIssuser;
this.Audience = sAudience;
this.SecondsOfExpiry = iSecondsOfExpiry;
}
public string getToken()
{
DateTime dTimeExpire = DateTime.Now.AddSeconds(this.SecondsOfExpiry);
var claims = new Claim[]
{
new Claim(ClaimTypes.Name, this.ClaimTypeName),
//new Claim(JwtRegisteredClaimNames.Email, "taohuadaozhu007@qq.com"),
new Claim(JwtRegisteredClaimNames.Sub, this.JwtRegisteredClaimNameSub),//用户Id
new Claim(ClaimTypes.DateOfBirth, $"{new DateTimeOffset(DateTime.Now).ToUnixTimeSeconds()}"),//token生效时间
new Claim(ClaimTypes.Expiration, $"{new DateTimeOffset(dTimeExpire).ToUnixTimeSeconds()}")//到期时间,按秒数计算
};
var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(SecurityKey));//key至少是16位
var token = new JwtSecurityToken(
issuer: this.IssUser,
audience: this.Audience,
claims: claims,
notBefore: DateTime.Now,
expires: dTimeExpire,
signingCredentials: new SigningCredentials(key, SecurityAlgorithms.HmacSha256)
);
string jwtToken = new JwtSecurityTokenHandler().WriteToken(token);
return jwtToken;
}
}
}


【步骤3.2:建立一个颁发token的接口】


using System;
using System.Collections.Generic;
using System.Linq;
using System.Threading.Tasks;
using ConfigLab.Dnc.WebView.JwtWrapper;
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Http;
using Microsoft.AspNetCore.Mvc;
using Microsoft.Extensions.Options;

namespace ConfigLab.Dnc.WebView.Controllers
{
[Route("[controller]/[action]")]
[ApiController]
public class APIController : ControllerBase
{
private IOptionssettings;
public APIController(IOptionssettings)
{
this.settings = settings;
}
///
/// 这里是颁发token的处理
///
///
///
[AllowAnonymous]
[HttpPost]
public string getToken([FromBody]InputForLogin data)
{
//这里模拟一个通过校验用户身份来获取token(账户密码,短信,社交均可)
if (!(data.username == "user_test" && data.pwd == "123456"))
{
return "get_token_err";
}
//下列配置应该从配置中读取
JwtTokenManager jm = new JwtTokenManager(
settings.Value.issuer,
"user_test",
settings.Value.secret,
settings.Value.issuer,
settings.Value.audience,
settings.Value.sedondsExpiration
);
return jm.getToken();
}

///
/// 这里演示没有token就无法访问的效果
///
///
///
[Authorize]
public string getUserInfo(string username)
{
return $"getUserInfo.{username}.Success";
}
}
}


【步骤4.1:客户端请求-获取token】

请求上述  /api/getToken  

参数示例:{"username":"user_test","pwd":"123456"}

得到token:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYXV0aC5jb25maWcubmV0LmNuIiwic3ViIjoidXNlcl90ZXN0IiwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvZGF0ZW9mYmlydGgiOiIxNjAyMDk4MDg4IiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9leHBpcmF0aW9uIjoiMTYwMjA5ODEwOCIsIm5iZiI6MTYwMjA5ODA4OCwiZXhwIjoxNjAyMDk4MTA4LCJpc3MiOiJhdXRoLmNvbmZpZy5uZXQuY24iLCJhdWQiOiJjb25maWcubmV0LmNuIn0.7PGl0u9M7MQxtA182DVgnMbz42E_3IAbQhBISUtiYCw

【步骤4.2:客户端请求-getUserInfo】

在请求上述  /api/getUserInfo接口时,其它的都正常处理,只需要在header头中携带参数:

Authorization   :  Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1lIjoiYXV0aC5jb25maWcubmV0LmNuIiwic3ViIjoidXNlcl90ZXN0IiwiaHR0cDovL3NjaGVtYXMueG1sc29hcC5vcmcvd3MvMjAwNS8wNS9pZGVudGl0eS9jbGFpbXMvZGF0ZW9mYmlydGgiOiIxNjAyMDk4MDg4IiwiaHR0cDovL3NjaGVtYXMubWljcm9zb2Z0LmNvbS93cy8yMDA4LzA2L2lkZW50aXR5L2NsYWltcy9leHBpcmF0aW9uIjoiMTYwMjA5ODEwOCIsIm5iZiI6MTYwMjA5ODA4OCwiZXhwIjoxNjAyMDk4MTA4LCJpc3MiOiJhdXRoLmNvbmZpZy5uZXQuY24iLCJhdWQiOiJjb25maWcubmV0LmNuIn0.7PGl0u9M7MQxtA182DVgnMbz42E_3IAbQhBISUtiYCw


【5.1】服务端校验方式1-特性标记

[Authorize]
public string getUserInfo(string username)
{
return $"getUserInfo.{username}.Success";
}

【5.2】服务端校验方式2-自定义校验

  //获取秘钥对象


  private ClaimsPrincipal GetPrincipal(string sAuthorization)
        {
            if (string.IsNullOrEmpty(sAuthorization))
                return null;
            string[] AuthItems = sAuthorization.Split(" ",StringSplitOptions.RemoveEmptyEntries);
            if (AuthItems == null || AuthItems.Length<1)
                return null;
            try
            {
                string token = AuthItems[AuthItems.Length - 1];
                var tokenHandler = new JwtSecurityTokenHandler(); // 创建一个JwtSecurityTokenHandler类,用来后续操作
                var jwtToken = tokenHandler.ReadToken(token) as JwtSecurityToken; // 将字符串token解码成token对象
                if (jwtToken == null)
                    return null;
                var symmetricKey = Encoding.UTF8.GetBytes(SecurityKey); //Convert.FromBase64String(SecurityKey); // 生成编码对应的字节数组
                var validationParameters = new TokenValidationParameters() // 生成验证token的参数
                {
                    RequireExpirationTime = true, // token是否包含有效期
                    ValidateIssuer = false, // 验证秘钥发行人,如果要验证在这里指定发行人字符串即可
                    ValidateAudience = false, // 验证秘钥的接受人,如果要验证在这里提供接收人字符串即可
                    IssuerSigningKey = new SymmetricSecurityKey(symmetricKey) // 生成token时的安全秘钥
                };
                SecurityToken securityToken; // 接受解码后的token对象
                var principal = tokenHandler.ValidateToken(token, validationParameters, out securityToken);
                return principal; // 返回秘钥的主体对象,包含秘钥的所有相关信息
            }
            catch(Exception ex)
            {
                return null;
            }
        }
        public string getUserId(string token)
        {
            string sUserId = null;
            var simplePrinciple = this.GetPrincipal(token); // 调用自定义的GetPrincipal获取Token的信息对象
            var identity = simplePrinciple?.Identity as ClaimsIdentity; // 获取主声明标识
            if (identity == null) return string.Empty;
            if (!identity.IsAuthenticated) return string.Empty;
            var userNameClaim = identity.FindFirst(ClaimTypes.NameIdentifier); // 获取声明类型是ClaimTypes.Name的第一个声明
            sUserId = userNameClaim?.Value; // 获取声明的名字,也就是用户名
            if (string.IsNullOrEmpty(sUserId)) return string.Empty;
            return sUserId;
            // 到这里token本身的验证工作已经完成了,因为用户名可以解码出来
            // 后续要验证的就是浏览器的 WWW-Authenticate
            /*
                什么是WWW-Authenticate验证???
                WWW-Authenticate是早期的一种验证方式,很容易被破解,浏览器发送请求给后端,后端服务器会解析传过来的Header验证
                如果没有类似于本文格式的token,那么会发送WWW-Authenticate: Basic realm= "." 到前端浏览器,并返回401
            */
        }




参考资料