zoukankan      html  css  js  c++  java
  • NET Core 1.1中使用Jwt

    NET Core里Jwt的生成倒是不麻烦,就是要踩完坑才知道正确的生成姿势……

    Jwt的结构

    jwt的结构是{Header}.{Playload}.{Signature}三截。其中Header和Playload是base64编码字符串,Signature是签名字符串。

    Header是比较固定的

    typ是固定的“JWT”。

    alg是你使用的签名算法,通常有HS256和RS256两种。

    例子:

    {
      "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中的信息是否被修改过。

    注意: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来完成拒绝。

  • 相关阅读:
    AE旋转
    AE2
    AE1
    面试
    TS 基础数据类型
    vue-cli结构介绍
    js异步加载的5种方式
    slot 插槽的使用
    使用组件的细节点
    Vue 条件渲染
  • 原文地址:https://www.cnblogs.com/zonciu/p/7078153.html
Copyright © 2011-2022 走看看