NET Core裏Jwt的生成卻是不麻煩,就是要踩完坑才知道正確的生成姿式……javascript
Jwt的結構java
jwt的結構是{Header}.{Playload}.{Signature}三截。其中Header和Playload是base64編碼字符串,Signature是簽名字符串。算法
Header是比較固定的app
typ是固定的「JWT」。ide
alg是你使用的簽名算法,一般有HS256和RS256兩種。測試
例子:ui
{ "alg": "RS256", "typ": "JWT" }
Playload你要寫入自定義內容的區域。編碼
例子:加密
{ "role": "myRole", "org": "myOrg", "jti": "a8b8ea421e834fd1b90ac09dbf40e158", "nbf": 1498397026, "exp": 1498483426, "iat": 1498397026, "iss": "Zonciu" }
Signature是使用Header中alg的算法來對{Header}.{Playload}這個結構進行簽名生成一個字符串,而後接到Jwt串的最後,造成帶簽名的Jwt。在簽發Token以後,其餘應用就可使用相同的算法和Key來從新運算並對比簽名,由此判斷Token中的信息是否被修改過。spa
注意:Jwt默認是明文的,不要把敏感數據放到playload裏去,固然你能夠先把要放進去的數據先加密,把密文放到playload裏(可是最好不要這樣作)。
Jwt建立過程
在NET Core 1.1裏是經過JwtSecurityTokenHandler(程序集System.IdentityModel.Tokens.Jwt)這個類來建立Jwt。
可是建立Jwt以前還要先生成一個SecurityTokenDescriptor(程序集Microsoft.IdentityModel.Tokens),因此就比較繞。
Jwt工廠代碼:
我這裏用的是RS256簽名(RsaSecurityKey),是用私鑰簽名,公鑰驗證。若是是要用HS256的話,就把簽名和驗證的SecurityKey都換成SymmetricSecurityKey(若是我沒記錯的話)
這裏的公鑰密鑰是方便測試因此硬編碼的,生產環境不要這樣搞。
public class JwtFactory { public const string publicKey = @"-----BEGIN PUBLIC KEY----- MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEArM1i3Q9ukD7DWhuYWFFH fAR0Ao8W5OnrlaZH2aB2G+dgvrlW6VzFVtjZQLWkQ488j65MTS7Nr0GITsoGB5r4 LRdnua5PwkLCML9ZaqOejMYix4mc7ZfencsQvy5bHotfvEAad42IhvHROseqC77W 5Zbt+YDtA7aU2aBKzHufZ1vPgWKPOgGVJup6sjqviXz3qP2HD5K9ae0iyYDptKKN e5kb36DNTD7P62yWrVpZpy0MpMkCBZJdDeUgtA3lsxY5FcEaB5Bk+O695djogq84 vsyTKP1Jp6GrgszIuJCb52dI5c1lY5tN6bxsMTYB/Hxhgo7dGG/LBU9lMoT83E15 KQIDAQAB -----END PUBLIC KEY----- "; public const string privateKey = @"-----BEGIN RSA PRIVATE KEY----- MIIEowIBAAKCAQEArM1i3Q9ukD7DWhuYWFFHfAR0Ao8W5OnrlaZH2aB2G+dgvrlW 6VzFVtjZQLWkQ488j65MTS7Nr0GITsoGB5r4LRdnua5PwkLCML9ZaqOejMYix4mc 7ZfencsQvy5bHotfvEAad42IhvHROseqC77W5Zbt+YDtA7aU2aBKzHufZ1vPgWKP OgGVJup6sjqviXz3qP2HD5K9ae0iyYDptKKNe5kb36DNTD7P62yWrVpZpy0MpMkC BZJdDeUgtA3lsxY5FcEaB5Bk+O695djogq84vsyTKP1Jp6GrgszIuJCb52dI5c1l Y5tN6bxsMTYB/Hxhgo7dGG/LBU9lMoT83E15KQIDAQABAoIBACvHrXCMZFqvTBcc PrDBhvboueucDRTaHxG/Gx0MBmBzcpNfqaFeG7ExJ3m5i3CCbbmJU1OKtBne5IXx sS1kGdRyxZjJjPOOrlxjXmgiJB1OZalgOB4KCCC6Pffx6qwGa67qHsqDVT+7LGNU CsUHCLMKViiMfYAfVf79GXZNK8mnki8pPCXc50qCGre3LRq6Egmb8NIsSIj05aHM UeQbOuOM+Bbf/dICYLV8qFmR2xpM3G5CmVX07LzGCX5k320z0kHrxH/r6QXl/bEP X5kMRdoYfUoX6jDnd71aoLVDaPqZvDLDOMDG59riqcMsaWVqv7iZn2keWT6WTPfE ZwGl4gECgYEA5GJlVXFcg91lSHWVprXeJHwIT4um8reGB6xt1CMxmhGx/e5vUiSo KirrYEf4sDlE81MY0oLo0oZzDzadvTlPDFazacZZlNOVattOdC/L2TKzkfmsR18o j1LsQApnDVVXGYLzGoQmASPk9GtfOE1phKZSyXZ3pV3D/JFQ1vHWmVkCgYEAwbJ0 ohW369yGSZUdV/vnpcpAmqav3duT1vx7UIUW7OUv5TTYmeXnpHV9m3Egdtsgy4Cy eULrnKJqQ0Cnon4Lg/wzZPVKKdnBH94+duhSNu4+Q5DNFp9IEi76KFm7UI8vOX4e 4QtAIQUUBQjnjcW0fLlOw1r1Nkqcrwbw8dVMFFECgYAVTmCpwfOhkav7QIz/ioP4 32FfGmYuypREbv+oBMiB2RjD2dSk0yqlFG/1AYHf3tfh42SzbucNjOF7D9tTZd9M BWKjgY+l5L9Rwrfk+viHgMVj3ukFl4kPJetIZjAK/GUtyhun46AwBws7CjFN7Vrk tyeOB/FNihvYmi3yf4lHsQKBgQC1pntVClMq4ewaE7qqKbart4pwvoPN5z+1baDj +Xxve9w38yBy67YaeIjsfuI4NPaDgtVdfVHi2joXignsDJMWGy3Dr3n2150TGuSv tN5tX26LBMAhSA1Z6C54KvbM7QsXutyQpnFkxhNpSVmGjnPeSBbChInUeZKJXlQW J7eqkQKBgDX88tAAM/FIZANoKPfmuoiFJ33USdC5mwNsHNBZvMAR2UsSeBSJZzy3 iar0ldCuTBolGpwRkLs1+pgoc4XDGDdV9367gjppQa0EqvrMwqNe8hcR7K/Dm+MU B1lk88g8TJK7fUd6ibkJtmWTZXMGdCSC2+NG1mjeRbf1d2TB+zHM -----END RSA PRIVATE KEY----- "; public const string JwtAlgorithm = "RS256"; public const string Issuer = "Zonciu"; public const int Lifttime = 86400; private TokenValidationParameters _tokenValidationParameters { get; set; } = new TokenValidationParameters() { ValidateActor = false, ValidateAudience = false, ValidateIssuer = true, ValidIssuer = "Zonciu", ValidateIssuerSigningKey = true, IssuerSigningKey = new RsaSecurityKey(Rsa.CreateFromPublicKey(publicKey)), ValidateLifetime = true, RequireExpirationTime = true, }; private SigningCredentials _signingCredentials { get; set; } = new SigningCredentials(new RsaSecurityKey(Rsa.CreateFromPrivateKey(privateKey)), JwtAlgorithm); private JwtSecurityTokenHandler _jwtSecurityTokenHandler { get; set; } = new JwtSecurityTokenHandler(); /// <summary> /// 建立編碼後的Jwt /// </summary> /// <param name="claimsIdentity">身份聲明</param> /// <param name="jwtId">令牌Id</param> /// <returns></returns> public string CreateEncodedJwt(ClaimsIdentity claimsIdentity, string jwtId) { var jwtDesc = CreateSecurityTokenDescriptor(claimsIdentity, jwtId); return _jwtSecurityTokenHandler.CreateEncodedJwt(jwtDesc); } /// <summary> /// 建立Jwt /// </summary> /// <param name="descriptor"></param> /// <returns></returns> public JwtSecurityToken CreateJwt(SecurityTokenDescriptor descriptor) { return _jwtSecurityTokenHandler.CreateJwtSecurityToken(descriptor); } /// <summary> /// 建立Jwt描述 /// </summary> /// <param name="claimsIdentity">身份聲明</param> /// <param name="jwtId">令牌Id</param> /// <returns></returns> public SecurityTokenDescriptor CreateSecurityTokenDescriptor(ClaimsIdentity claimsIdentity, string jwtId) { claimsIdentity.AddClaim(new Claim("jti", jwtId)); var issueTime = DateTime.Now; var jwtDesc = new SecurityTokenDescriptor { Issuer = Issuer, IssuedAt = issueTime, Expires = issueTime + TimeSpan.FromSeconds(Lifttime), SigningCredentials = _signingCredentials, Subject = claimsIdentity }; return jwtDesc; } /// <summary> /// 校驗Jwt /// </summary> /// <param name="jwtToken"></param> /// <param name="securityToken"></param> /// <returns></returns> public ClaimsPrincipal ValidateJwtToken(string jwtToken, out SecurityToken securityToken) { return _jwtSecurityTokenHandler.ValidateToken(jwtToken, _tokenValidationParameters, out securityToken); } }
ValidateJwtToken中校驗失敗直接拋出異常,校驗成功則返回ClaimsPrincipal(Controller裏HttpContext.User這貨),並out出從string解析到的SecurityToken(Jwt反序列化的意思)。在NET Core 1.1裏不用主動調用驗證方法,這裏只是用來作測試或者其餘用途。
測試:
public static void JwtTest() { var jwtFactory = new JwtFactory(); var claims = new List<Claim>() { new Claim("role", "myRole"), new Claim("org", "myOrg") }; var jti = Guid.NewGuid().ToString("N"); var jwtString = jwtFactory.CreateEncodedJwt(new ClaimsIdentity(claims), jti); var jwt = jwtFactory.CreateJwt(jwtFactory.CreateSecurityTokenDescriptor(new ClaimsIdentity(claims), jti)); var claimsPrincipal = jwtFactory.ValidateJwtToken(jwtString, out var token); var jwtClaims = claimsPrincipal.Claims.Select( claim => new { claim.Type, claim.Value }).ToList(); Console.WriteLine( $@" playloadClaims: {jwtClaims.ToJsonString(camelCase: false, indented: true)} jwtString: {jwtString} token.ToJsonString: {token.ToJsonString(camelCase: false, indented: true)} jwt: {jwt}.{jwt.RawSignature} "); }
輸出結果:
playloadClaims: [ { "Type": "http://schemas.microsoft.com/ws/2008/06/identity/claims/role", "Value": "myRole" }, { "Type": "org", "Value": "myOrg" }, { "Type": "jti", "Value": "a8b8ea421e834fd1b90ac09dbf40e158" }, { "Type": "nbf", "Value": "1498397026" }, { "Type": "exp", "Value": "1498483426" }, { "Type": "iat", "Value": "1498397026" }, { "Type": "iss", "Value": "Zonciu" } ] jwtString: eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoibXlSb2xlIiwib3JnIjoibXlPcmciLCJqdGkiOiJhOGI4ZWE0MjFlODM0ZmQxYjkwYWMwOWRiZjQwZTE1OCIsIm5iZiI6MTQ5ODM5NzAyNiwiZXhwIjoxNDk4NDgzNDI2LCJpYXQiOjE0OTgzOTcwMjYsImlzcyI6IlpvbmNpdSJ9.F5g9XanhffrPGwlvve5YA7fxU9-ENnunkqxTuRKE5rTWo2fthxs_MHXew44LJ21MJOxeWgS6j5Asws9VAjHGjOuxclFxhuf4jTE9otk4w8JEKf8IHhERJy4qKO1ooNUM3wp-WGzreJNCWRNxax2eT4EiCJZsawUBZtl-r4yztNMUGea37wgnta4CyzZTzNErzDeaAZLMb96YYdOjKSKP5Od5Hm-W0tqWaRhyzOTQ9nqHW7AV_g7qffUBQGUwqdL8H4dsDxzelS2xY5Ypjr-a2Z6hByYPPwyiBXkwXJYO9wKCSVuG72Y54UG_6R-dbowPmTwvirsnyeqWjQ2dFY0l9Q token.ToJsonString: { "Actor": null, "Audiences": [], "Claims": [ { "Issuer": "Zonciu", "OriginalIssuer": "Zonciu", "Properties": {}, "Subject": null, "Type": "role", "Value": "myRole", "ValueType": "http://www.w3.org/2001/XMLSchema#string" }, { "Issuer": "Zonciu", "OriginalIssuer": "Zonciu", "Properties": {}, "Subject": null, "Type": "org", "Value": "myOrg", "ValueType": "http://www.w3.org/2001/XMLSchema#string" }, { "Issuer": "Zonciu", "OriginalIssuer": "Zonciu", "Properties": {}, "Subject": null, "Type": "jti", "Value": "a8b8ea421e834fd1b90ac09dbf40e158", "ValueType": "http://www.w3.org/2001/XMLSchema#string" }, { "Issuer": "Zonciu", "OriginalIssuer": "Zonciu", "Properties": {}, "Subject": null, "Type": "nbf", "Value": "1498397026", "ValueType": "http://www.w3.org/2001/XMLSchema#integer" }, { "Issuer": "Zonciu", "OriginalIssuer": "Zonciu", "Properties": {}, "Subject": null, "Type": "exp", "Value": "1498483426", "ValueType": "http://www.w3.org/2001/XMLSchema#integer" }, { "Issuer": "Zonciu", "OriginalIssuer": "Zonciu", "Properties": {}, "Subject": null, "Type": "iat", "Value": "1498397026", "ValueType": "http://www.w3.org/2001/XMLSchema#integer" }, { "Issuer": "Zonciu", "OriginalIssuer": "Zonciu", "Properties": {}, "Subject": null, "Type": "iss", "Value": "Zonciu", "ValueType": "http://www.w3.org/2001/XMLSchema#string" } ], "EncodedHeader": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9", "EncodedPayload": "eyJyb2xlIjoibXlSb2xlIiwib3JnIjoibXlPcmciLCJqdGkiOiJhOGI4ZWE0MjFlODM0ZmQxYjkwYWMwOWRiZjQwZTE1OCIsIm5iZiI6MTQ5ODM5NzAyNiwiZXhwIjoxNDk4NDgzNDI2LCJpYXQiOjE0OTgzOTcwMjYsImlzcyI6IlpvbmNpdSJ9", "Header": { "alg": "RS256", "typ": "JWT" }, "Id": "a8b8ea421e834fd1b90ac09dbf40e158", "Issuer": "Zonciu", "Payload": { "role": "myRole", "org": "myOrg", "jti": "a8b8ea421e834fd1b90ac09dbf40e158", "nbf": 1498397026, "exp": 1498483426, "iat": 1498397026, "iss": "Zonciu" }, "InnerToken": null, "RawAuthenticationTag": null, "RawCiphertext": null, "RawData": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoibXlSb2xlIiwib3JnIjoibXlPcmciLCJqdGkiOiJhOGI4ZWE0MjFlODM0ZmQxYjkwYWMwOWRiZjQwZTE1OCIsIm5iZiI6MTQ5ODM5NzAyNiwiZXhwIjoxNDk4NDgzNDI2LCJpYXQiOjE0OTgzOTcwMjYsImlzcyI6IlpvbmNpdSJ9.F5g9XanhffrPGwlvve5YA7fxU9-ENnunkqxTuRKE5rTWo2fthxs_MHXew44LJ21MJOxeWgS6j5Asws9VAjHGjOuxclFxhuf4jTE9otk4w8JEKf8IHhERJy4qKO1ooNUM3wp-WGzreJNCWRNxax2eT4EiCJZsawUBZtl-r4yztNMUGea37wgnta4CyzZTzNErzDeaAZLMb96YYdOjKSKP5Od5Hm-W0tqWaRhyzOTQ9nqHW7AV_g7qffUBQGUwqdL8H4dsDxzelS2xY5Ypjr-a2Z6hByYPPwyiBXkwXJYO9wKCSVuG72Y54UG_6R-dbowPmTwvirsnyeqWjQ2dFY0l9Q", "RawEncryptedKey": null, "RawInitializationVector": null, "RawHeader": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9", "RawPayload": "eyJyb2xlIjoibXlSb2xlIiwib3JnIjoibXlPcmciLCJqdGkiOiJhOGI4ZWE0MjFlODM0ZmQxYjkwYWMwOWRiZjQwZTE1OCIsIm5iZiI6MTQ5ODM5NzAyNiwiZXhwIjoxNDk4NDgzNDI2LCJpYXQiOjE0OTgzOTcwMjYsImlzcyI6IlpvbmNpdSJ9", "RawSignature": "F5g9XanhffrPGwlvve5YA7fxU9-ENnunkqxTuRKE5rTWo2fthxs_MHXew44LJ21MJOxeWgS6j5Asws9VAjHGjOuxclFxhuf4jTE9otk4w8JEKf8IHhERJy4qKO1ooNUM3wp-WGzreJNCWRNxax2eT4EiCJZsawUBZtl-r4yztNMUGea37wgnta4CyzZTzNErzDeaAZLMb96YYdOjKSKP5Od5Hm-W0tqWaRhyzOTQ9nqHW7AV_g7qffUBQGUwqdL8H4dsDxzelS2xY5Ypjr-a2Z6hByYPPwyiBXkwXJYO9wKCSVuG72Y54UG_6R-dbowPmTwvirsnyeqWjQ2dFY0l9Q", "SecurityKey": null, "SignatureAlgorithm": "RS256", "SigningCredentials": null, "EncryptingCredentials": null, "SigningKey": { "HasPrivateKey": false, "KeySize": 2048, "Parameters": { "D": null, "DP": null, "DQ": null, "Exponent": null, "InverseQ": null, "Modulus": null, "P": null, "Q": null }, "Rsa": { "LegalKeySizes": [ { "MinSize": 512, "MaxSize": 16384, "SkipSize": 64 } ], "KeySize": 2048 }, "KeyId": null, "CryptoProviderFactory": { "CustomCryptoProvider": null } }, "Subject": null, "ValidFrom": "2017-06-25T13:23:46Z", "ValidTo": "2017-06-26T13:23:46Z" } jwt: {"alg":"RS256","typ":"JWT"}.{"role":"myRole","org":"myOrg","jti":"a8b8ea421e834fd1b90ac09dbf40e158","nbf":1498397026,"exp":1498483426,"iat":1498397026,"iss":"Zonciu"}.F5g9XanhffrPGwlvve5YA7fxU9-ENnunkqxTuRKE5rTWo2fthxs_MHXew44LJ21MJOxeWgS6j5Asws9VAjHGjOuxclFxhuf4jTE9otk4w8JEKf8IHhERJy4qKO1ooNUM3wp-WGzreJNCWRNxax2eT4EiCJZsawUBZtl-r4yztNMUGea37wgnta4CyzZTzNErzDeaAZLMb96YYdOjKSKP5Od5Hm-W0tqWaRhyzOTQ9nqHW7AV_g7qffUBQGUwqdL8H4dsDxzelS2xY5Ypjr-a2Z6hByYPPwyiBXkwXJYO9wKCSVuG72Y54UG_6R-dbowPmTwvirsnyeqWjQ2dFY0l9Q
公鑰和密鑰是openssl生成的2048位key
openssl genrsa -out rsa_private_key.pem 2048
openssl rsa -in rsa_private_key.pem -pubout -out rsa_public_key.pem
把jwtString和公鑰、密鑰拿去https://jwt.io/驗證成功
Jwt在NET Core 1.1的引入方式:
在Startup的Configure方法中加入這個(JwtOptions和JwtEventsDefaults是我本身寫的類)
由於Jwt校驗失敗的緣由有不少種,校驗失敗時觸發OnAuthenticationFailed事件,這個部分能夠本身實現,也能夠無論,默認會在Response的Header裏添加錯誤信息。
app.UseJwtBearerAuthentication( new JwtBearerOptions { RequireHttpsMetadata = jwtFactory.JwtOptions.EnableHttps, AutomaticAuthenticate = true, AutomaticChallenge = true, Events = new JwtBearerEvents() { OnAuthenticationFailed = JwtEventsDefaults.AuthenticationFailed }, ClaimsIssuer = jwtFactory.JwtOptions.Issuer, TokenValidationParameters = new TokenValidationParameters { ValidateAudience = false, ValidateActor = false, ValidateIssuer = true, ValidIssuer = jwtFactory.JwtOptions.Issuer, ValidateLifetime = true, ValidateIssuerSigningKey = true, IssuerSigningKey = new RsaSecurityKey(jwtFactory.JwtOptions.PublicRsa), RequireExpirationTime = true, } });
有一個比較頭疼的地方就是role這個claim,在jwt裏是「role」沒有變,解析以後會變成「http://schemas.microsoft.com/ws/2008/06/identity/claims/role」,這裏要當心。(話說誰有辦法解決這個問題嗎?雖然說改個名字就能避免被轉換,可是好彆扭也好憋屈……)
Jwt的刪除辦法
在個人實現中是添加了「jti」這個鍵,即Jwt Id,當服務端須要使Jwt提早失效,只能經過stateful的方式處理(由於你沒辦法保證客戶端那邊真的刪掉了這個Jwt,好比證件你只能登報聲明做廢,可是若是其餘單位沒有檢查做廢信息,別人拿着你的證件去搞事情同樣會經過),即在服務端把這個jti加入黑名單,黑名單刪除時間是Jwt有效期以後的時間。這樣的話就可使得Jwt失效前經過黑名單來完成拒絕,黑名單清除以後經過Jwt的exp來完成拒絕。