OpenID Connect(Core),OAuth 2.0(RFC 6749),JSON Web Token (JWT)(RFC 7519) 之間有着密不可分聯系,對比了不同語言的實現,還是覺得 IdentityServer4 設計的比較完美,最近把源碼 clone 下來研究了一下,之前介紹過 IdentityServer4 相關的文章(ASP.NET Core 中集成 IdentityServer4 實現 OAuth 2.0 與 OIDC 服務),在配置 Client 客戶端的時候 Token 的類型有兩種,IdentityServer4 默認使用 JWT 類型。
/// <summary> /// Access token types. /// </summary> public enum AccessTokenType { /// <summary> /// Self-contained Json Web Token /// </summary> Jwt = 0, /// <summary> /// Reference token /// </summary> Reference = 1 }
JSON Web Token
JWT 是一個非常輕巧的規范,一般被用來在身份提供者和服務提供者間傳遞安全可靠的信息。常被用於前后端分離,可以和 Restful API 配合使用,常用於構建身份認證機制,一個 JWT 實際上就是一個字符串,它包含了使用.分隔的三部分: Header 頭部 Payload 負載 Signature 簽名(格式:Header.Payload.Signature)
載荷(Payload)
Payload 被定義成一個 JSON 對象,也可以增加一些自定義的信息。
{ "iss": "irving", "iat": 1891593502, "exp": 1891594722, "aud": "www.test.com", "sub": "root@test.com", "ext_age": "18" }
JWT 標准所定義字段
- iss: 該 jwt 的簽發者
- sub: 該 jwt 所面向的用戶
- aud: 接收該 jwt 的一方
- exp(expires): jwt的過期時間,是一個 unix 時間戳
- nbf:定義在什么時間之前該jwt是不可用的
- iat(issued at): jwt的簽發時間
- jti:jwt的唯一標識,主要用作一次性token,避免重放攻擊
將上面的 JSON 對象使用 Base64 編碼得到的字符串就是 JWT 的 Payload(載荷),也可以自定義一些字段另外在載荷里面一般不要加入敏感的數據。
頭部(Header)
頭部用於描述關於該 JWT 的最基本的信息,例如其類型以及簽名所用的算法等
{ "typ": "JWT", "alg": "HS256" }
上述說明這是一個JWT,所用的簽名算法是 HS256(HMAC-SHA256)。對它也要進行 Base64 編碼,之后的字符串就成了 JWT 的 Header(頭部),關於 alg 中定義的簽名算法推薦使用 RSA 或 ECDSA 非對稱加密算法 ,這部分在 JSON Web Algorithms (JWA)[RFC7518] 規范中可以找到。
+--------------+-------------------------------+--------------------+ | "alg" Param | Digital Signature or MAC | Implementation | | Value | Algorithm | Requirements | +--------------+-------------------------------+--------------------+ | HS256 | HMAC using SHA-256 | Required | | HS384 | HMAC using SHA-384 | Optional | | HS512 | HMAC using SHA-512 | Optional | | RS256 | RSASSA-PKCS1-v1_5 using | Recommended | | | SHA-256 | | | RS384 | RSASSA-PKCS1-v1_5 using | Optional | | | SHA-384 | | | RS512 | RSASSA-PKCS1-v1_5 using | Optional | | | SHA-512 | | | ES256 | ECDSA using P-256 and SHA-256 | Recommended+ | | ES384 | ECDSA using P-384 and SHA-384 | Optional | | ES512 | ECDSA using P-521 and SHA-512 | Optional | | PS256 | RSASSA-PSS using SHA-256 and | Optional | | | MGF1 with SHA-256 | | | PS384 | RSASSA-PSS using SHA-384 and | Optional | | | MGF1 with SHA-384 | | | PS512 | RSASSA-PSS using SHA-512 and | Optional | | | MGF1 with SHA-512 | | | none | No digital signature or MAC | Optional | | | performed | | +--------------+-------------------------------+--------------------+
簽名(Signature)
簽名還需要一個 secret ,一般保存在服務端,使用的是 HS256 算法,流程類似於:
header = '{"alg":"HS256","typ":"JWT"}' payload = '{"loggedInAs":"admin","iat":1422779638}' key = 'secretkey' unsignedToken = encodeBase64(header) + '.' + encodeBase64(payload) signature = HMAC-SHA256(key, unsignedToken) jwt_token = encodeBase64(header) + '.' + encodeBase64(payload) + '.' + encodeBase64(signature)
簽名的過程,實際上是對頭部以及載荷內容進行簽名,最后以 Header.Payload.Signature 方式拼接最終得到 JWT。
RSA還是HMAC
HS256 使用密鑰生成固定的簽名,RS256 使用成非對稱進行簽名。簡單地說,HS256 必須與任何想要驗證 JWT的 客戶端或 API 共享秘密。與任何其他對稱算法一樣,相同的秘密用於簽名和驗證 JWT。RS256 生成非對稱簽名,這意味着必須使用私鑰來簽簽名 JWT,並且必須使用對應的公鑰來驗證簽名。與對稱算法不同,使用 RS256 可以保證服務端是 JWT 的簽名者,因為服務端是唯一擁有私鑰的一方。這樣做將不再需要在許多應用程序之間共享私鑰。使用 RS256 和 JWK 規范簽名(JWS(JSON Web Signature),JWS 只是 JWT 的一種實現,除了 JWS 外,有 JWS, JWE, JWK, JWA 相關的規范)。
RS256與JWKS
上述說到因為 header 和 payload 是明文存儲的,為了防止數據被修改,簽名最好使用RS256(RSA 非對稱加密,使用私鑰簽名)。JSON Web Key SET (JWKS) 定義了一組的JWK Set JSON 數據結構,JWKS 包含簽名算法,證書的唯一標識(Kid)等信息,用於驗證授權服務器發出的 JWT,一般從授權服務器中獲得(IdentityServer4 的獲取方式 /.well-known/openid-configuration/jwks)獲得公鑰。IdentityServer4 中使用是微軟 System.IdentityModel.Tokens.Jwt 類庫,采用 RS256 簽名算法,使用 privatekey (保存在服務端)來簽名 publickey 驗簽 。理論上由 IdentityServer4 生成的 JWT Token ,其他不同的語言也能夠去驗簽。
{ "keys": [ { "kty": "RSA", "use": "sig", "kid": "B4F7C5533A06B22E6D349BEFD84B76E730161B55", "x5t": "tPfFUzoGsi5tNJvv2Et25zAWG1U", "e": "AQAB", "n": "zDXSeNo4oO-Tn372eKUywF40D0HG4XXeYtbYtdnpVsIZkDDouZr2jFeq3C-AUb546CJXFqqZj6YZPOMtiHBfzyDGThd45mQvNwQ18B7lae4vab1hvxx9HZGku64Wy5JlqT2jHJ-WR7GS9OZjHSeioMoDE654LhDxJthfj_C2G0jA_RTnPQKnQgciv5JiENTUwrghr9cXzBNgPE0QLAhKrCEoVoSxYOWTL9EBCUc2DB2Vah7RHNfNItrXbrdqvrDQ5rXBH8Rq6irjSF_FjcuIwMkTmLOkswnC_qBN7qjbmgLRIxG3YiSnZR5bgyhjFWNzea0jmuWEiFIIIMwTfPXpPw", "x5c": [ "MIID8TCCAtmgAwIBAgIJAIRTKytMROvuMA0GCSqGSIb3DQEBCwUAMIGOMQswCQYDVQQGEwJ6aDERMA8GA1UECAwIY2hpbmEICAgxETAPBgNVBAcMCHNoYW5naGFpMREwDwYDVQQKDAhob21laW5uczERMA8GA1UECwwIaG9tZWlubnMxDzANBgNVBAMMBmlydmluZzEiMCAGCSqGSIb3DQEJARYTeXR6aG91QGhvbWVpbm5zLmNvbTAeFw0xODA3MjUwNzI3MzZaFw0xOTA3MjUwNzI3MzZaMIGOMQswCQYDVQQGEwJ6aDERMA8GA1UECAwIY2hpbmEICAgxETAPBgNVBAcMCHNoYW5naGFpMREwDwYDVQQKDAhob21laW5uczERMA8GA1UECwwIaG9tZWlubnMxDzANBgNVBAMMBmlydmluZzEiMCAGCSqGSIb3DQEJARYTeXR6aG91QGhvbWVpbm5zLmNvbTCCASIwDQYJKoZIhvcNAQEBBQADggEPADCCAQoCggEBAMw10njaOKDvk59+9nilMsBeNA9BxuF13mLW2LXZ6VbCGZAw6Lma9oxXqtwvgFG+eOgiVxaqmY+mGTzjLYhwX88gxk4XeOZkLzcENfAe5WnuL2m9Yb8cfR2RpLuuFsuSZak9oxyflkexkvTmYx0noqDKAxOueC4Q8SbYX4/wthtIwP0U5z0Cp0IHIr+SYhDU1MK4Ia/XF8wTYDxNECwISqwhKFaEsWDlky/RAQlHNgwdlWoe0RzXzSLa1263ar6w0Oa1wR/Eauoq40hfxY3LiMDJE5izpLMJwv6gTe6o25oC0SMRt2Ikp2UeW4MoYxVjc3mtI5rlhIhSCCDME3z16T8CAwEAAaNQME4wHQYDVR0OBBYEFOK5Y2P7/L8KsOrPB+glPVkKi2VOMB8GA1UdIwQYMBaAFOK5Y2P7/L8KsOrPB+glPVkKi2VOMAwGA1UdEwQFMAMBAf8wDQYJKoZIhvcNAQELBQADggEBAEnXXws/cBx5tA9cBfmkqGWzOU5/YmH9pzWchJ0ssggIqZVx0yd6ok7+C+2vKIRMp5E6GCfXWTB+LI7qjAVEvin1NwGZ06yNEsaYaJYMC/P/0TunoMEZmsLM3rk0aISbzkNciF+LVT16i0C+hT1+Pyr8lP4Ea1Uw0n50Np6SOwQ6e2PMFFOIaqjG94tuCN3RX819IJSQPbq9FtRmNvmbWPM1v2CO6SYT51SvsIHnZyn0rAK+h/hywVQqmI5ngi1nErIQEqybkZj00OhmYpAqsetWYU5Cs1qhJ70kktlrd+jMHdarVB9ko0h+ij6HL22mmBYAb7zVGWyDroNJVhEw6DA=" ], "alg": "RS256" } ] }
- alg: is the algorithm for the key
- kty: is the key type
- use: is how the key was meant to be used. For the example above
sig
representssignature
. - x5c: is the x509 certificate chain
- e: is the exponent for a standard pem
- n: is the moduluos for a standard pem
- kid: is the unique identifier for the key (密鑰ID,用於匹配特定密鑰)
- x5t: is the thumbprint of the x.509 cert
在 IdentityServer4 中的定義
/// <summary> /// Creates the JWK document. /// </summary> public virtual async Task<IEnumerable<Models.JsonWebKey>> CreateJwkDocumentAsync() { var webKeys = new List<Models.JsonWebKey>(); var signingCredentials = await Keys.GetSigningCredentialsAsync(); var algorithm = signingCredentials?.Algorithm ?? Constants.SigningAlgorithms.RSA_SHA_256; foreach (var key in await Keys.GetValidationKeysAsync()) { if (key is X509SecurityKey x509Key) { var cert64 = Convert.ToBase64String(x509Key.Certificate.RawData); var thumbprint = Base64Url.Encode(x509Key.Certificate.GetCertHash()); var pubKey = x509Key.PublicKey as RSA; var parameters = pubKey.ExportParameters(false); var exponent = Base64Url.Encode(parameters.Exponent); var modulus = Base64Url.Encode(parameters.Modulus); var webKey = new Models.JsonWebKey { kty = "RSA", use = "sig", kid = x509Key.KeyId, x5t = thumbprint, e = exponent, n = modulus, x5c = new[] { cert64 }, alg = algorithm }; webKeys.Add(webKey); continue; } if (key is RsaSecurityKey rsaKey) { var parameters = rsaKey.Rsa?.ExportParameters(false) ?? rsaKey.Parameters; var exponent = Base64Url.Encode(parameters.Exponent); var modulus = Base64Url.Encode(parameters.Modulus); var webKey = new Models.JsonWebKey { kty = "RSA", use = "sig", kid = rsaKey.KeyId, e = exponent, n = modulus, alg = algorithm }; webKeys.Add(webKey); } } return webKeys; }
關與 Token 簽名與驗簽 https://jwt.io 中可以找到不同語言的實現。
Self-contained Json Web Token 類型
當使用 AccessTokenType 類型為 Jwt Token 時候,就會使用 Jwt 規范來生成 Token,簽名的算法是采用 RSA (SHA256 簽名) ,在服務端 IdentityServer4 使用私鑰對 Token 進行簽名,當客戶端去資源端獲取資源的時候,API 端(資源服務器)收到第一個請求后去服務端獲得公鑰然后驗簽(調用 /.well-known/openid-configuration/jwks 獲取公鑰這個過程只發生在客戶端第一次請求,所以當服務端更換證書,資源端也需要重啟服務)。一般開發環境使用 AddDeveloperSigningCredential 方法使用臨時證書即可(先判斷 tempkey.rsa 文件是否存在,如果不存在就創建一個新的文件 )。
/// <summary> /// Sets the temporary signing credential. /// </summary> /// <param name="builder">The builder.</param> /// <param name="persistKey">Specifies if the temporary key should be persisted to disk.</param> /// <param name="filename">The filename.</param> /// <returns></returns> public static IIdentityServerBuilder AddDeveloperSigningCredential(this IIdentityServerBuilder builder, bool persistKey = true, string filename = null) { if (filename == null) { filename = Path.Combine(Directory.GetCurrentDirectory(), "tempkey.rsa"); } if (File.Exists(filename)) { var keyFile = File.ReadAllText(filename); var tempKey = JsonConvert.DeserializeObject<TemporaryRsaKey>(keyFile, new JsonSerializerSettings { ContractResolver = new RsaKeyContractResolver() }); return builder.AddSigningCredential(CreateRsaSecurityKey(tempKey.Parameters, tempKey.KeyId)); } else { var key = CreateRsaSecurityKey(); RSAParameters parameters; if (key.Rsa != null) parameters = key.Rsa.ExportParameters(includePrivateParameters: true); else parameters = key.Parameters; var tempKey = new TemporaryRsaKey { Parameters = parameters, KeyId = key.KeyId }; if (persistKey) { File.WriteAllText(filename, JsonConvert.SerializeObject(tempKey, new JsonSerializerSettings { ContractResolver = new RsaKeyContractResolver() })); } return builder.AddSigningCredential(key); } } /// <summary> /// Creates a new RSA security key. /// </summary> /// <returns></returns> public static RsaSecurityKey CreateRsaSecurityKey() { var rsa = RSA.Create(); RsaSecurityKey key; if (rsa is RSACryptoServiceProvider) { rsa.Dispose(); var cng = new RSACng(2048); var parameters = cng.ExportParameters(includePrivateParameters: true); key = new RsaSecurityKey(parameters); } else { rsa.KeySize = 2048; key = new RsaSecurityKey(rsa); } key.KeyId = CryptoRandom.CreateUniqueId(16); return key; }
創建自簽名證書
生成環境(負載集群)一般需要使用固定的證書簽名與驗簽,以確保重啟服務端或負載的時候 Token 都能驗簽通過。
數字證書常見標准
符合PKI ITU-T X509 標准,傳統標准(.DER .PEM .CER .CRT)
符合PKCS#7 加密消息語法標准(.P7B .P7C .SPC .P7R)
符合PKCS#10 證書請求標准(.p10)
符合PKCS#12 個人信息交換標准(.pfx *.p12)X509是數字證書的基本規范,而P7和P12則是兩個實現規范,P7用於數字信封,P12則是帶有私鑰的證書實現規范。
X.509
X.509 是數字證書一個標准,由用戶公共密鑰和用戶標識符組成。此外還包括版本號、證書序列號、CA標識符、簽名算法標識、簽發者名稱、證書有效期等信息。
PKCS#12
一種文件打包格式,為存儲和發布用戶和服務器私鑰、公鑰和證書指定了一個可移植的格式,是一種二進制格式,通常以.pfx或.p12為文件后綴名。使用OpenSSL的pkcs12命令可以創建、解析和讀取這些文件。P12是把證書壓成一個文件 *.pfx 。主要是考慮分發證書,私鑰是要絕對保密的,不能隨便以文本方式散播。所以P7格式不適合分發。.pfx中可以加密碼保護,所以相對安全些。
可以在 Linux 上通過 OpenSSL 相關的命令生成數字證書
sudo apt-get install openssl
#生成私鑰文件 openssl genrsa -out idsrv4.key 2048 #創建證書簽名請求文件 CSR(Certificate Signing Request),用於提交給證書頒發機構(即 Certification Authority (CA))即對證書簽名,申請一個數字證書。 openssl req -new -key idsrv4.key -out idsrv4.csr #生成自簽名證書(證書頒發機構(CA)簽名后的證書,因為自己做測試那么證書的申請機構和頒發機構都是自己,crt 證書包含持有人的信息,持有人的公鑰,以及簽署者的簽名等信息。當用戶安裝了證書之后,便意味着信任了這份證書,同時擁有了其中的公鑰。) openssl x509 -req -days 365 -in idsrv4.csr -signkey idsrv4.key -out idsrv4.crt #自簽名證書與私匙合並成一個文件 openssl pkcs12 -export -in idsrv4.crt -inkey idsrv4.key -out idsrv4.pfx 或 openssl req -newkey rsa:2048 -nodes -keyout idsrv4.key -x509 -days 365 -out idsrv4.cer openssl pkcs12 -export -in idsrv4.cer -inkey idsrv4.key -out idsrv4.pfx
完成后會有三個文件(VS選中配置文件設置文件始終復制),最后把證書路徑和密碼配置到 IdentityServer 中,因為我們自簽名的證書是 PKCS12 (個人數字證書標准,Public Key Cryptography Standards #12) 標准包含私鑰與公鑰)標准,包含了公鑰和私鑰。
root@iZuf60cj5pna5im3va46nlZ:~# tree
.
├── idsrv4.cer
├── idsrv4.key
└── idsrv4.pfx
使用 IdentityModel.Tokens.Jwt 測試簽名與驗簽
public static async Task Run() { try { //https://github.com/AzureAD/azure-activedirectory-identitymodel-extensions-for-dotnet/blob/master/test/System.IdentityModel.Tokens.Jwt.Tests/CreateAndValidateTokens.cs //獲得證書文件 var filePath = Path.Combine(AppContext.BaseDirectory, "Certs\\idsrv4.pfx"); if (!File.Exists(filePath)) { throw new FileNotFoundException("Signing Certificate is missing!"); } var credential = new SigningCredentials(new X509SecurityKey(new X509Certificate2(filePath, "123456")), "RS256"); if (credential == null) { throw new InvalidOperationException("No signing credential is configured. Can't create JWT token"); } var header = new JwtHeader(credential); // emit x5t claim for backwards compatibility with v4 of MS JWT library if (credential.Key is X509SecurityKey x509key) { var cert = x509key.Certificate; var pub_key = cert.GetPublicKeyString(); header["x5t"] = Base64Url.Encode(cert.GetCertHash()); } var payload = new JwtPayload(); payload.AddClaims(ClaimSets.DefaultClaims); var jwtTokenHandler = new JwtSecurityTokenHandler(); var jwtToken = jwtTokenHandler.WriteToken(new JwtSecurityToken(header, payload)); SecurityToken validatedSecurityToken = null; //ValidateToken var vaild = jwtTokenHandler.ValidateToken(jwtToken, new TokenValidationParameters { IssuerSigningKey = credential.Key, RequireExpirationTime = false, RequireSignedTokens = true, ValidateAudience = false, ValidateIssuer = false, ValidateLifetime = false, }, out validatedSecurityToken); //ReadJwtToken var readJwtToken = jwtTokenHandler.ReadJwtToken(jwtToken); } catch (Exception ex) { } }
IdentityServer4 服務端修改代碼
//獲得證書文件 var filePath = Path.Combine(AppContext.BaseDirectory, Configuration["Certs:Path"]); if (!File.Exists(filePath)) { throw new FileNotFoundException("Signing Certificate is missing!"); } var x509Cert = new X509Certificate2(filePath, Configuration["Certs:Pwd"]); var credential = new SigningCredentials(new X509SecurityKey(x509Cert), "RS256"); // configure identity server with in-memory stores, keys, clients and scopes services.AddIdentityServer(options => { options.Events.RaiseErrorEvents = true; options.Events.RaiseFailureEvents = true; options.Events.RaiseInformationEvents = true; options.Events.RaiseSuccessEvents = true; }) //.AddDeveloperSigningCredential() //.AddDeveloperSigningCredential(persistKey: true, filename: "rsakey.rsa")、 .AddSigningCredential(x509Cert) //.AddSigningCredential(credential) .AddInMemoryApiResources(InMemoryConfig.GetApiResources()) .AddInMemoryIdentityResources(InMemoryConfig.GetIdentityResources()) .AddInMemoryClients(InMemoryConfig.GetClients()) .AddTestUsers(InMemoryConfig.GetUsers().ToList()); //.AddResourceOwnerValidator<ResourceOwnerPasswordValidator>() //.AddProfileService<ProfileService>();
運行訪問 /.well-known/openid-configuration/jwks 查詢公鑰的信息(Jwks Endpoint)
GET http://localhost:5000/.well-known/openid-configuration HTTP/1.1 Host: localhost:5000 HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 Server: Kestrel X-SourceFiles: =?UTF-8?B?RDpcZ2l0aHViXFNlY3VyaW5nRm9yV2ViQVBJXElkU3J2NC5Ib3N0U3J2XC53ZWxsLWtub3duXG9wZW5pZC1jb25maWd1cmF0aW9u?= X-Powered-By: ASP.NET Date: Tue, 24 Jul 2018 12:43:48 GMT Content-Length: 1313 {"issuer":"http://localhost:5000","jwks_uri":"http://localhost:5000/.well-known/openid-configuration/jwks","authorization_endpoint":"http://localhost:5000/connect/authorize","token_endpoint":"http://localhost:5000/connect/token","userinfo_endpoint":"http://localhost:5000/connect/userinfo","end_session_endpoint":"http://localhost:5000/connect/endsession","check_session_iframe":"http://localhost:5000/connect/checksession","revocation_endpoint":"http://localhost:5000/connect/revocation","introspection_endpoint":"http://localhost:5000/connect/introspect","frontchannel_logout_supported":true,"frontchannel_logout_session_supported":true,"backchannel_logout_supported":true,"backchannel_logout_session_supported":true,"scopes_supported":["api","user","order","offline_access"],"claims_supported":[],"grant_types_supported":["authorization_code","client_credentials","refresh_token","implicit","password"],"response_types_supported":["code","token","id_token","id_token token","code id_token","code token","code id_token token"],"response_modes_supported":["form_post","query","fragment"],"token_endpoint_auth_methods_supported":["client_secret_basic","client_secret_post"],"subject_types_supported":["public"],"id_token_signing_alg_values_supported":["RS256"],"code_challenge_methods_supported":["plain","S256"]} GET http://localhost:5000/.well-known/openid-configuration/jwks HTTP/1.1 Host: localhost:5000 HTTP/1.1 200 OK Content-Type: application/json; charset=UTF-8 Server: Kestrel X-SourceFiles: =?UTF-8?B?RDpcZ2l0aHViXFNlY3VyaW5nRm9yV2ViQVBJXElkU3J2NC5Ib3N0U3J2XC53ZWxsLWtub3duXG9wZW5pZC1jb25maWd1cmF0aW9uXGp3a3M=?= X-Powered-By: ASP.NET Date: Tue, 24 Jul 2018 12:43:48 GMT Content-Length: 451 {"keys":[{"kty":"RSA","use":"sig","kid":"0fdf841efb8c990ea6f2b09318c0cba2","e":"AQAB","n":"zDMobgJ8pjUAH_e8EqtYZE-t14InmDDcpDqdQp9bT0bGiOpvLpgqgsFJulAwKQfhPwwOwUBKq7Lle461Gb1PRug4L1zN3U-WA9cj0LL4dAHqGCXEazl3FTvWGe8FrQQRTgi8q-I2X_Jhxp8BYQkfatFknVUZSDYudxL-fIDJOSVYus-oEfhupQf_b1Le27UvfMuswVsUhKHbL2wSy_ZtdbY1X8pJ5XoLJwL2AO62Ahfb8ptHBI_Nbc285hAuB4WTPVcIdpp99Oodf6wTiflTVWLGqWP3o48VlxNyixUJCWqWI78BTno06U9cISBTAwbXFLADqjJDYz4OZOAn7Np_DQ","alg":"RS256"}]}
在 IdentityServer4 中當使用 Self-contained Json Web Token (自包含無狀態的 Jwt Token)的時候,生成的Token 即為 Jwt 標准格式(Token 包含了三部分: Header 頭部 Payload 負載 Signature 簽名,使用.分隔的)格式,在資源端(API)就可以完成驗簽的過程,不需要每次再去資源端驗簽以減少網絡請求,缺點就是生成的 Token 會很長,另外 Token 是不可撤銷的,Token 的生命周期(被驗證通過)會一直到票據過期,如果泄露就會比較麻煩。
Reference token 類型
當使用 Reference token 的時候,服務端會對 Token 進行持久化,當客戶端請求資源端(API)的時候,資源端需要每次都去服務端通信去驗證 Token 的合法性[/connect/introspect],IdentityServer4.AccessTokenValidation 中間件中可以配置緩存一定的時候去驗證,並且 Token 是支持撤銷[/connect/revocation]的。
上述涉及到的接口:
- OAuth 2.0 Token Revocation (RFC 7009(This endpoint allows revoking access tokens (reference tokens only) and refresh token. It implements the token revocation specification (RFC 7009).)
- OAuth 2.0 Token Introspection (RFC 7662)(The introspection endpoint is an implementation of RFC 7662.It can be used to validate reference tokens (or JWTs if the consumer does not have support for appropriate JWT or cryptographic libraries). The introspection endpoint requires authentication using a scope secret.
Revocation 與 Introspection 都屬於 OAuth2.0 協議的的標准規范,另外要使用 Introspection 接口的時候, IdentityServer4 中 ApiResource 中需定義 ApiSecrets(資源端去服務端驗證需要相應的參數)。
var api = new ApiResource("api") { ApiSecrets = { new Secret("secret".Sha256()) } }
Token 驗證
API 端(資源服務器)需要每次去訪問 IdentityServer4 服務端來驗證 Token 的合法性(POST /connect/introspect),當然 API 端也可以配置一定的時間來緩存結果,以減少通信的頻率。
POST http://localhost:5000/connect/introspect HTTP/1.1 Accept: application/json Content-Type: application/x-www-form-urlencoded Content-Length: 135 Host: localhost:5000 token=c92ef5a5bbb8333dde392a4aa1e0bba6aa774bc7441d5f71d01ebca1a71f07e5&client_id=api&token_type_hint=access_token&client_secret=api_pwd HTTP/1.1 200 OK Cache-Control: no-store, no-cache, max-age=0 Pragma: no-cache Content-Type: application/json; charset=UTF-8 Server: Kestrel X-SourceFiles: =?UTF-8?B?RDpcZ2l0aHViXFNlY3VyaW5nRm9yV2ViQVBJXElkU3J2NC5Ib3N0U3J2XGNvbm5lY3RcaW50cm9zcGVjdA==?= X-Powered-By: ASP.NET Date: Wed, 25 Jul 2018 10:17:19 GMT Content-Length: 164 {"iss":"http://localhost:5000","nbf":1532513838,"exp":1532517438,"aud":["http://localhost:5000/resources","api"],"client_id":"client_2","active":true,"scope":"api"}
.AddIdentityServerAuthentication(options => { // base-address of your identityserver options.Authority = "https://demo.identityserver.io"; //name of the API resource options.ApiName = "api1"; options.ApiSecret = "secret"; options.EnableCaching = true; options.CacheDuration = TimeSpan.FromMinutes(10); //that's the default })
備注:Access token validation middleware
.Net 中 Jwt token 與 Reference token 相應的中間件也不一樣(Microsoft.AspNetCore.Authentication.JwtBearer,IdentityModel.AspNetCore.OAuth2Introspection ),為了方便官方只是把兩者集成到了一起(IdentityServer4.AccessTokenValidation),只要符合協議規范,其他語言也有相應的集成方式 。
REFER:
https://identityserver4.readthedocs.io/en/release/topics/reference_tokens.html
https://identityserver4.readthedocs.io/en/release/topics/crypto.html#refcrypto
https://blogs.msdn.microsoft.com/webdev/2016/10/27/bearer-token-authentication-in-asp-net-core/
https://www.cnblogs.com/edisonchou/p/identityserver4_foundation_and_quickstart_01.html
用 Identity Server 4 (JWKS 端點和 RS256 算法) 來保護 Python web api
https://www.cnblogs.com/cgzl/p/8270677.html
數字證書原理
http://www.cnblogs.com/JeffreySun/archive/2010/06/24/1627247.html
https://www.cnblogs.com/cuimiemie/p/6442685.html
https://www.cnblogs.com/leslies2/p/7442956.htmlhttps://auth0.com/blog/navigating-rs256-and-jwks/
http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html