Fork me on GitHub

关于 IdentityServer4 中的 Jwt Token 与 Reference Token

image

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)

image

载荷(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, JWKJWA 相关的规范)

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 sigrepresents signature.
  • 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.JwtBearerIdentityModel.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.html

https://auth0.com/blog/navigating-rs256-and-jwks/

http://www.ruanyifeng.com/blog/2018/07/json_web_token-tutorial.html

 

posted @ 2018-07-23 23:17 花儿笑弯了腰 阅读(...) 评论(...) 编辑 收藏