本文介绍了ASP.NET Core 1.0 Web API 中的简单 JWT 身份验证的处理方法,对大家解决问题具有一定的参考价值,需要的朋友们下面随着小编来一起学习吧!

问题描述

我正在寻找在 ASP.NET Core(也称为 ASP.NET 5)中设置使用 JWT 进行身份验证的 Web API 服务器的最简单方法.这个项目(博客文章/github) 完全符合我的要求,但它使用 ASP.NET 4.

我只想能够:

  1. 设置一个登录路由,该路由可以创建 JWT 令牌并在标头中返回它.我正在将此与现有的 RESTful 服务集成,该服务将告诉我用户名和密码是否有效.在 ASP.NET 4 项目中,我看到这可以通过以下路线完成 https://github.com/stewartm83/Jwt-WebApi/blob/master/src/JwtWebApi/Controllers/AccountController.cs#L24-L54p>

  2. 拦截对需要授权的路由的传入请求,解密并验证标头中的 JWT 令牌,并使路由可以访问 JWT 令牌的有效负载中的用户信息.例如像这样:https://github.com/stewartm83/Jwt-WebApi/blob/master/src/JwtWebApi/App_Start/AuthHandler.cs

我在 ASP.NET Core 中看到的所有示例都非常复杂,并且依赖于我想避免的部分或全部 OAuth、IS、OpenIddict 和 EF.

谁能给我指出如何在 ASP.NET Core 中执行此操作的示例或帮助我开始使用此操作?

答案我最终使用了这个答案:https://stackoverflow.com/a/33217340/373655

解决方案

注意/更新:

以下代码适用于 .NET Core 1.1
由于 .NET Core 1 非常 RTM,身份验证随着从 .NET Core 1 到 2.0 的跳跃而改变(也就是[部分?] 修复了重大更改).
这就是为什么下面的代码不再适用于 .NET Core 2.0.
但它仍然是一个有用的阅读.

2018 更新

同时,您可以在我的 github 测试中找到 ASP.NET Core 2.0 JWT-Cookie-Authentication 的工作示例回购.随附带有 BouncyCastle 的 MS-RSA&MS-ECDSA 抽象类的实现,以及 RSA&ECDSA 的密钥生成器.


死灵法术.
我深入研究了 JWT.以下是我的发现:

您需要添加 Microsoft.AspNetCore.Authentication.JwtBearer

然后你可以设置

app.UseJwtBearerAuthentication(bearerOptions);

在 Startup.cs =>配置

其中 bearerOptions 由您定义,例如作为

var bearerOptions = new JwtBearerOptions(){自动身份验证 = 真,自动挑战 = 真,令牌验证参数 = 令牌验证参数,事件 = 新的 CustomBearerEvents()};//选修的//bearerOptions.SecurityTokenValidators.Clear();//bearerOptions.SecurityTokenValidators.Add(new MyTokenHandler());

CustomBearerEvents 是您可以向 httpContext/Route 添加令牌数据的地方

//https://github.com/aspnet/Security/blob/master/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerEvents.cs公共类 CustomBearerEvents:Microsoft.AspNetCore.Authentication.JwtBearer.IJwtBearerEvents{//////在请求处理过程中抛出异常时调用.除非被抑制,否则将在此事件之后重新抛出异常.///</总结>public FuncOnAuthenticationFailed { 获取;放;} = 上下文 =>任务.FromResult(0);//////在第一次收到协议消息时调用.///</总结>public FuncOnMessageReceived { 获取;放;} = 上下文 =>任务.FromResult(0);//////在安全令牌通过验证并生成 ClaimsIdentity 后调用.///</总结>public FuncOnTokenValidated { 获取;放;} = 上下文 =>任务.FromResult(0);//////在将质询发送回调用者之前调用.///</总结>public FuncOnChallenge { 得到;放;} = 上下文 =>任务.FromResult(0);任务 IJwtBearerEvents.AuthenticationFailed(AuthenticationFailedContext 上下文){返回 OnAuthenticationFailed(上下文);}任务 IJwtBearerEvents.Challenge(JwtBearerChallengeContext 上下文){返回 OnChallenge(上下文);}任务 IJwtBearerEvents.MessageReceived(MessageReceivedContext context){返回 OnMessageReceived(context);}任务 IJwtBearerEvents.TokenValidated(TokenValidatedContext 上下文){返回 OnTokenValidated(上下文);}}

并且 tokenValidationParameters 由您定义,例如

var tokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters{//签名密钥必须匹配!ValidateIssuerSigningKey = true,IssuerSigningKey = 签名密钥,//验证 JWT 颁发者 (iss) 声明ValidateIssuer = 真,ValidIssuer = "ExampleIssuer",//验证 JWT Audience (aud) 声明ValidateAudience = 真,ValidAudience = "ExampleAudience",//验证令牌是否过期ValidateLifetime = 真,//如果要允许一定量的时钟漂移,请在此处设置:ClockSkew = 时间跨度.零,};

如果你想自定义令牌验证,例如

//https://gist.github.com/pmhsfelix/4151369公共类 MyTokenHandler :Microsoft.IdentityModel.Tokens.ISecurityTokenValidator{私有 int m_MaximumTokenByteSize;公共 MyTokenHandler(){ }bool ISecurityTokenValidator.CanValidateToken{得到{//抛出新的 NotImplementedException();返回真;}}int ISecurityTokenValidator.MaximumTokenSizeInBytes{得到{返回 this.m_MaximumTokenByteSize;}放{this.m_MaximumTokenByteSize = 值;}}bool ISecurityTokenValidator.CanReadToken(string securityToken){System.Console.WriteLine(securityToken);返回真;}ClaimsPrincipal ISecurityTokenValidator.ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validateToken){JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();//validatedToken = new JwtSecurityToken(securityToken);尝试{tokenHandler.ValidateToken(securityToken,validationParameters,outvalidatedToken);validateToken = new JwtSecurityToken(jwtEncodedString");}捕获(异常前){System.Console.WriteLine(ex.Message);扔;}ClaimsPrincipal 主体 = null;//SecurityToken validToken = null;验证令牌 = 空;System.Collections.Generic.Listls =new System.Collections.Generic.List();ls.添加(新 System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, "IcanHazUsr_éèêëïàáâäåãæóòôöõõúùûüñçø_ÉÈÊËÏÀÁÄÅÃÆÓÒÔÖÕÕÚÙÚÆÓÚÙÙÕÕÚÙÚÑÑÚÙÙиветеÇ, System.Security.Claims.ClaimValueTypes.String));//System.Security.Claims.ClaimsIdentity id = new System.Security.Claims.ClaimsIdentity(authenticationType");id.AddClaims(ls);主体 = 新 System.Security.Claims.ClaimsPrincipal(id);返还本金;抛出新的 NotImplementedException();}}

棘手的部分是如何获取 AsymmetricSecurityKey,因为您不想传递 rsaCryptoServiceProvider,因为您需要加密格式的互操作性.

创作遵循

//System.Security.Cryptography.X509Certificates.X509Certificate2 cert2 = new System.Security.Cryptography.X509Certificates.X509Certificate2(byte[] rawData);System.Security.Cryptography.X509Certificates.X509Certificate2 cert2 =DotNetUtilities.CreateX509Cert2(mycert");Microsoft.IdentityModel.Tokens.SecurityKey secKey = new X509SecurityKey(cert2);

例如使用来自 DER 证书的 BouncyCastle:

//http://stackoverflow.com/questions/36942094/how-can-i-generate-a-self-signed-cert-without-using-obsolete-bouncycastle-1-7-0公共静态 System.Security.Cryptography.X509Certificates.X509Certificate2 CreateX509Cert2(string certName){var keypairgen = new Org.BouncyCastle.Crypto.Generators.RsaKeyPairGenerator();keypairgen.Init(new Org.BouncyCastle.Crypto.KeyGenerationParameters(新的 Org.BouncyCastle.Security.SecureRandom(new Org.BouncyCastle.Crypto.Prng.CryptoApiRandomGenerator()), 1024));Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair keypair = keypairgen.GenerateKeyPair();//--- 直到这里我们生成一个密钥对var random = new Org.BouncyCastle.Security.SecureRandom(new Org.BouncyCastle.Crypto.Prng.CryptoApiRandomGenerator());//SHA1WITHRSA//SHA256WITHRSA//SHA384WITHRSA//SHA512WITHRSA//SHA1WITHECDSA//SHA224WITHECDSA//SHA256WITHECDSA//SHA384WITHECDSA//SHA512WITHECDSAOrg.BouncyCastle.Crypto.ISignatureFactory 签名工厂 =new Org.BouncyCastle.Crypto.Operators.Asn1SignatureFactory(SHA512WITHRSA", keypair.Private, random);var gen = new Org.BouncyCastle.X509.X509V3CertificateGenerator();var CN = new Org.BouncyCastle.Asn1.X509.X509Name("CN=" + certName);var SN = Org.BouncyCastle.Math.BigInteger.ProbablePrime(120, new Random());gen.SetSerialNumber(SN);gen.SetSubjectDN(CN);gen.SetIssuerDN(CN);gen.SetNotAfter(DateTime.Now.AddYears(1));gen.SetNotBefore(DateTime.Now.Subtract(new TimeSpan(7, 0, 0, 0)));gen.SetPublicKey(keypair.Public);//-- 这些是必要的吗?//public static readonly DerObjectIdentifier AuthorityKeyIdentifier = new DerObjectIdentifier("2.5.29.35");//OID 值:2.5.29.35//OID 描述:id-ce-authorityKeyIdentifier//此扩展可用作证书或 CRL 扩展.//它标识用于验证此证书或 CRL 上的签名的公钥.//它使同一 CA 使用的不同密钥能够被区分(例如,当发生密钥更新时).//http://stackoverflow.com/questions/14930381/generating-x509-certificate-using-bouncy-castle-javagen.AddExtension(Org.BouncyCastle.Asn1.X509.X509Extensions.AuthorityKeyIdentifier.Id,错误的,新的 Org.BouncyCastle.Asn1.X509.AuthorityKeyIdentifier(Org.BouncyCastle.X509.SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(keypair.Public),new Org.BouncyCastle.Asn1.X509.GeneralNames(new Org.BouncyCastle.Asn1.X509.GeneralName(CN)),SN));//OID 值:1.3.6.1.5.5.7.3.1//OID描述:表示一个证书可以作为SSL服务器证书.gen.AddExtension(Org.BouncyCastle.Asn1.X509.X509Extensions.ExtendedKeyUsage.Id,错误的,new Org.BouncyCastle.Asn1.X509.ExtendedKeyUsage(new ArrayList(){new Org.BouncyCastle.Asn1.DerObjectIdentifier(1.3.6.1.5.5.7.3.1")}));//-- 这些是必须的吗?Org.BouncyCastle.X509.X509Certificate bouncyCert = gen.Generate(signatureFactory);byte[] ba = bouncyCert.GetEncoded();System.Security.Cryptography.X509Certificates.X509Certificate2 msCert = new System.Security.Cryptography.X509Certificates.X509Certificate2(ba);返回 msCert;}

随后,您可以添加包含 JWT-Bearer 的自定义 cookie 格式:

app.UseCookieAuthentication(new CookieAuthenticationOptions(){AuthenticationScheme = "MyCookieMiddlewareInstance",CookieName = "SecurityByObscurityDoesntWork",ExpireTimeSpan = new System.TimeSpan(15, 0, 0),LoginPath = new Microsoft.AspNetCore.Http.PathString("/Account/Unauthorized/"),AccessDeniedPath = new Microsoft.AspNetCore.Http.PathString("/Account/Forbidden/"),自动身份验证 = 真,自动挑战 = 真,CookieSecure = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest,CookieHttpOnly = 假,TicketDataFormat = new CustomJwtDataFormat("foo", tokenValidationParameters)//DataProtectionProvider = null,//DataProtectionProvider = new DataProtectionProvider(new System.IO.DirectoryInfo(@"c:shared-auth-ticket-keys"),//委托(DataProtectionConfiguration 选项)//{//var op = new Microsoft.AspNet.DataProtection.AuthenticatedEncryption.AuthenticatedEncryptionOptions();//op.EncryptionAlgorithm = Microsoft.AspNet.DataProtection.AuthenticatedEncryption.EncryptionAlgorithm.AES_256_GCM://options.UseCryptographicAlgorithms(op);//}//),});

CustomJwtDataFormat 与

类似

public class CustomJwtDataFormat : ISecureDataFormat{私有只读字符串算法;私有只读 TokenValidationParameters 验证参数;公共 CustomJwtDataFormat(字符串算法,TokenValidationParameters 验证参数){this.algorithm = 算法;this.validationParameters = 验证参数;}//这个 ISecureDataFormat 实现是仅解码的string ISecureDataFormat.Protect(AuthenticationTicket data){返回我的保护(数据,空);}string ISecureDataFormat.Protect(AuthenticationTicket data, string purpose){返回我的保护(数据,目的);}AuthenticationTicket ISecureDataFormat.Unprotect(string protectedText){返回 MyUnprotect(protectedText, null);}AuthenticationTicket ISecureDataFormat.Unprotect(string protectedText, string purpose){返回 MyUnprotect(protectedText, purpose);}私有字符串 MyProtect(AuthenticationTicket 数据,字符串目的){return "wadehadedudad";抛出新的 System.NotImplementedException();}//http://blogs.microsoft.co.il/sasha/2012/01/20/aggressive-inlining-in-the-clr-45-jit/[System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]private AuthenticationTicket MyUnprotect(字符串protectedText,字符串目的){JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();ClaimsPrincipal 主体 = null;SecurityToken validToken = null;System.Collections.Generic.Listls =new System.Collections.Generic.List();ls.添加(新 System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, "IcanHazUsr_éèêëïàáâäåãæóòôöõõúùûüñçø_ÉÈÊËÏÀÁÄÅÃÆÓÒÔÖÕÕÚÙÚÆÓÚÙÙÕÕÚÙÚÑÑÚÙÙиветеÇ, System.Security.Claims.ClaimValueTypes.String));//System.Security.Claims.ClaimsIdentity id = new System.Security.Claims.ClaimsIdentity(authenticationType");id.AddClaims(ls);主体 = 新 System.Security.Claims.ClaimsPrincipal(id);返回新的 AuthenticationTicket(principal, new AuthenticationProperties(), MyCookieMiddlewareInstance");尝试{principal = handler.ValidateToken(protectedText, this.validationParameters, out validToken);JwtSecurityToken validJwt = validToken as JwtSecurityToken;如果(validJwt == null){throw new System.ArgumentException("Invalid JWT");}if (!validJwt.Header.Alg.Equals(algorithm, System.StringComparison.Ordinal)){throw new System.ArgumentException($"Algorithm must be '{algorithm}'");}//此处对 JWT 声明的其他自定义验证(如果有)}捕获(SecurityTokenValidationException){返回空;}捕获(System.ArgumentException){返回空;}//验证通过.返回有效的 AuthenticationTicket:返回新的 AuthenticationTicket(principal, new AuthenticationProperties(), MyCookieMiddlewareInstance");}}

您还可以使用 Microsoft.IdentityModel.Token 创建 JWT 令牌:

//https://github.com/aspnet/Security/blob/master/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/IJwtBearerEvents.cs//http://codereview.stackexchange.com/questions/45974/web-api-2-authentication-with-jwt公共类 TokenMaker{类 SecurityConstants{公共静态字符串 TokenIssuer;公共静态字符串 TokenAudience;公共静态 int TokenLifetimeMinutes;}公共静态字符串 IssueToken(){安全密钥 sSKey = null;var claimList = new List(){new Claim(ClaimTypes.Name, "userName"),new Claim(ClaimTypes.Role, "role")//不知道这是干什么用的};JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();SecurityTokenDescriptor desc = makeSecurityTokenDescriptor(sSKey, claimList);//JwtSecurityToken tok = tokenHandler.CreateJwtSecurityToken(desc);返回 tokenHandler.CreateEncodedJwt(desc);}公共静态 ClaimsPrincipal ValidateJwtToken(string jwtToken){安全密钥 sSKey = null;var tokenHandler = new JwtSecurityTokenHandler();//从 Base64UrlEncoded 连线形式解析 JWT//(..)JwtSecurityToken 将 Jwt = tokenHandler.ReadToken(jwtToken) 解析为 JwtSecurityToken;TokenValidationParameters 验证参数 =新的 TokenValidationParameters(){RequireExpirationTime = 真,ValidAudience = SecurityConstants.TokenAudience,ValidIssuers = new List() { SecurityConstants.TokenIssuer },ValidateIssuerSigningKey = true,ValidateLifetime = 真,IssuerSigningKey = sSKey,};SecurityToken secT;return tokenHandler.ValidateToken(token", validationParams, out secT);}私有静态 SecurityTokenDescriptor makeSecurityTokenDescriptor(SecurityKey sSKey, List claimList){var now = DateTime.UtcNow;Claim[] claim = claimList.ToArray();返回新的 Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor{主题 = 新的 ClaimsIdentity(claims),发行人 = SecurityConstants.TokenIssuer,观众 = SecurityConstants.TokenAudience,IssuedAt = System.DateTime.UtcNow,过期时间 = System.DateTime.UtcNow.AddMinutes(SecurityConstants.TokenLifetimeMinutes),NotBefore = System.DateTime.UtcNow.AddTicks(-1),SigningCredentials = new SigningCredentials(sSKey, Microsoft.IdentityModel.Tokens.SecurityAlgorithms.EcdsaSha512Signature)};}}

请注意,由于您可以在 cookie 与 http-headers (Bearer) 或您指定的任何其他身份验证方法中指定不同的用户,因此您实际上可以拥有多于 1 个用户!


看看这个:
https://stormpath.com/blog/token-authentication-asp-net-核心

它应该正是您要找的.

还有这两个:

https://goblincoding.com/2016/07/03/issuing-and-authenticating-jwt-tokens-in-asp-net-core-webapi-part-i/

https://goblincoding.com/2016/07/07/issuing-and-authenticating-jwt-tokens-in-asp-net-core-webapi-part-ii/

还有这个
http://blog.novanet.no/hooking-up-asp-net-core-1-rc1-web-api-with-auth0-bearer-tokens/

和 JWT-Bearer 源https://github.com/aspnet/Security/tree/master/src/Microsoft.AspNetCore.Authentication.JwtBearer


如果您需要超高的安全性,您应该通过在每次请求时更新票证来防止重放攻击,并在一定超时后和用户注销后(不仅仅是在有效期到期后)使旧票证失效.


对于那些通过谷歌从这里结束的人,当您想使用自己的 JWT 版本时,可以在 cookie 身份验证中实现 TicketDataFormat.

我不得不考虑使用 JWT,因为我们需要保护我们的应用程序.
因为我还得使用 .NET 2.0,所以我不得不编写自己的库.

本周末我已将结果移植到 .NET Core.你可以在这里找到它:https://github.com/ststeiger/Jwt_Net20/tree/master/CoreJWT

它不使用任何数据库,这不是 JWT 库的工作.
获取和设置 DB 数据是您的工作.
该库允许在 .NET Core 中使用 JWT RFC 中指定的所有算法进行 JWT 授权和验证 列在 IANA JOSE 分配中.
至于向管道添加授权和为路由添加值 - 这是两件应该分开完成的事情,我认为你最好自己做.

您可以在 ASP.NET Core 中使用自定义身份验证.
查看安全"docs.asp.net 上的文档类别.

或者您可以查看 Cookie Middleware without ASP.NET Identity 或进入自定义基于策略的授权.

您还可以在 github 上的身份验证研讨会社交登录 部分或此第 9 频道视频教程.
如果一切都失败了,asp.net 安全的源代码在 on github.


.NET 3.5 的原始项目,这是我的库的来源,在这里:
https://github.com/jwt-dotnet/jwt
我删除了对 LINQ + 扩展方法的所有引用,因为 .NET 2.0 不支持它们.如果您在源代码中包含 LINQ 或 ExtensionAttribute,那么您不能在没有收到警告的情况下更改 .NET 运行时;这就是我完全删除它们的原因.
另外,我添加了 RSA + ECDSA JWS 方法,因此 CoreJWT 项目依赖于 BouncyCastle.
如果您将自己限制为 HMAC-SHA256 + HMAC-SHA384 + HMAC-SHA512,则可以删除 BouncyCastle.

(尚)不支持 JWE.

用法就像 jwt-dotnet/jwt,除了我将命名空间 JWT 更改为 CoreJWT.
我还添加了 PetaJSON 的内部副本作为序列化程序,因此不会干扰其他人项目的依赖项.

创建 JWT 令牌:

var payload = new Dictionary(){{索赔1",0},{ "claim2", "claim2-value";}};var secretKey = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";string token = JWT.JsonWebToken.Encode(payload, secretKey, JWT.JwtHashAlgorithm.HS256);Console.WriteLine(token);

验证 JWT 令牌:

var token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjbGFpbTEiOjAsImNsYWltMI6ImNsYWltMi12YWx1ZSJ9.8pwBI_HtXQFlx8Qsrgsrgsrgsrgsrgsrgsr8srgsrgsrgsrgsrgsrg5rgsrgvar secretKey = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";尝试{字符串 jsonPayload = JWT.JsonWebToken.Decode(token, secretKey);Console.WriteLine(jsonPayload);}捕获(JWT.SignatureVerificationException){Console.WriteLine(无效令牌!");}

对于 RSA &ECSA,您必须传递 (BouncyCastle) RSA/ECDSA 私钥而不是 secretKey.

命名空间 BouncyJWT{公共类 JwtKey{公共字节[] MacKeyBytes;public Org.BouncyCastle.Crypto.AsymmetricKeyParameter RsaPrivateKey;公共 Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters EcPrivateKey;公共字符串 MacKey{得到 { 返回 System.Text.Encoding.UTF8.GetString(this.MacKeyBytes);}set { this.MacKeyBytes = System.Text.Encoding.UTF8.GetBytes(value);}}公共 JwtKey(){ }公共 JwtKey(字符串 macKey){this.MacKey = macKey;}公共 JwtKey(byte[] macKey){this.MacKeyBytes = macKey;}公共 JwtKey(Org.BouncyCastle.Crypto.AsymmetricKeyParameter rsaPrivateKey){this.RsaPrivateKey = rsaPrivateKey;}公共 JwtKey(Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters ecPrivateKey){this.EcPrivateKey = ecPrivateKey;}}}

有关如何使用 BouncyCastle 生成/导出/导入 RSA/ECDSA 密钥,请参阅名为BouncyCastleTests"的项目;在同一个存储库中.我把它留给你来安全地存储和检索你自己的 RSA/ECDSA 私钥.

我已经使用 JWT.io 验证了我的库的 HMAC-ShaXXX 和 RSA-XXX 结果 - 看起来它们没问题.
ECSD 应该也可以,但我没有针对任何东西对其进行测试.
无论如何,我没有进行广泛的测试,仅供参考.

I'm looking for the simplest way to setup a Web API server that uses JWTs for authentication in ASP.NET Core (aka ASP.NET 5). This project (blog post / github) does exactly what I'm looking for but it uses ASP.NET 4.

I just want to be able to:

  1. setup a login route that can create a JWT token and return it in the header. I'm integrating this with an existing RESTful service that will tell me if the username and password are valid. In the ASP.NET 4 project I'm looking at this could be done with the following route https://github.com/stewartm83/Jwt-WebApi/blob/master/src/JwtWebApi/Controllers/AccountController.cs#L24-L54

  2. Intercept incoming requests to routes that require authorization, decrypt and validate the JWT token coming in the header and make the user information in the JWT token's payload accessible to the route. e.g. something like this: https://github.com/stewartm83/Jwt-WebApi/blob/master/src/JwtWebApi/App_Start/AuthHandler.cs

All of the examples I've seen in ASP.NET Core are very complex and rely on some or all of OAuth, IS, OpenIddict, and EF which I would like to avoid.

Can anyone point me towards an example of how to do this in ASP.NET Core or help me get started with this?

EDIT: AnswerI ended up using this answer: https://stackoverflow.com/a/33217340/373655

解决方案

Note/Update:

The below code was for .NET Core 1.1
Since .NET Core 1 was so very RTM, authentication changed with the jump from .NET Core 1 to 2.0 (aka was [partially?] fixed with breaking changes).
That's why the bellow code does NOT work with .NET Core 2.0 anymore.
But it will still be a useful read.

2018 Update

Meanwhile, you can find a working example of ASP.NET Core 2.0 JWT-Cookie-Authentication on my github test repo.Comes complete with an implementation of the MS-RSA&MS-ECDSA abstract class with BouncyCastle, and a key-generator for RSA&ECDSA.


Necromancing.
I digged deeper into JWT. Here are my findings:

You need to add Microsoft.AspNetCore.Authentication.JwtBearer

then you can set

app.UseJwtBearerAuthentication(bearerOptions);

in Startup.cs => Configure

where bearerOptions is defined by you, e.g. as

var bearerOptions = new JwtBearerOptions()
{
    AutomaticAuthenticate = true,
    AutomaticChallenge = true,
    TokenValidationParameters = tokenValidationParameters,
    Events = new CustomBearerEvents()
};

// Optional
// bearerOptions.SecurityTokenValidators.Clear();
// bearerOptions.SecurityTokenValidators.Add(new MyTokenHandler());

where CustomBearerEvents is the place where you could add token data to the httpContext/Route

// https://github.com/aspnet/Security/blob/master/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/JwtBearerEvents.cs
public class CustomBearerEvents : Microsoft.AspNetCore.Authentication.JwtBearer.IJwtBearerEvents
{

    /// <summary>
    /// Invoked if exceptions are thrown during request processing. The exceptions will be re-thrown after this event unless suppressed.
    /// </summary>
    public Func<AuthenticationFailedContext, Task> OnAuthenticationFailed { get; set; } = context => Task.FromResult(0);

    /// <summary>
    /// Invoked when a protocol message is first received.
    /// </summary>
    public Func<MessageReceivedContext, Task> OnMessageReceived { get; set; } = context => Task.FromResult(0);

    /// <summary>
    /// Invoked after the security token has passed validation and a ClaimsIdentity has been generated.
    /// </summary>
    public Func<TokenValidatedContext, Task> OnTokenValidated { get; set; } = context => Task.FromResult(0);


    /// <summary>
    /// Invoked before a challenge is sent back to the caller.
    /// </summary>
    public Func<JwtBearerChallengeContext, Task> OnChallenge { get; set; } = context => Task.FromResult(0);


    Task IJwtBearerEvents.AuthenticationFailed(AuthenticationFailedContext context)
    {
        return OnAuthenticationFailed(context);
    }

    Task IJwtBearerEvents.Challenge(JwtBearerChallengeContext context)
    {
        return OnChallenge(context);
    }

    Task IJwtBearerEvents.MessageReceived(MessageReceivedContext context)
    {
        return OnMessageReceived(context);
    }

    Task IJwtBearerEvents.TokenValidated(TokenValidatedContext context)
    {
        return OnTokenValidated(context);
    }
}

And tokenValidationParameters is defined by you, e.g.

var tokenValidationParameters = new Microsoft.IdentityModel.Tokens.TokenValidationParameters
{
    // The signing key must match!
    ValidateIssuerSigningKey = true,
    IssuerSigningKey = signingKey,

    // Validate the JWT Issuer (iss) claim
    ValidateIssuer = true,
    ValidIssuer = "ExampleIssuer",

    // Validate the JWT Audience (aud) claim
    ValidateAudience = true,
    ValidAudience = "ExampleAudience",

    // Validate the token expiry
    ValidateLifetime = true,

    // If you want to allow a certain amount of clock drift, set that here:
    ClockSkew = TimeSpan.Zero,
};

And MyTokenHandler is optionally defined by you, if you want to customize token validation, e.g.

// https://gist.github.com/pmhsfelix/4151369
public class MyTokenHandler : Microsoft.IdentityModel.Tokens.ISecurityTokenValidator
{
    private int m_MaximumTokenByteSize;

    public MyTokenHandler()
    { }

    bool ISecurityTokenValidator.CanValidateToken
    {
        get
        {
            // throw new NotImplementedException();
            return true;
        }
    }



    int ISecurityTokenValidator.MaximumTokenSizeInBytes
    {
        get
        {
            return this.m_MaximumTokenByteSize;
        }

        set
        {
            this.m_MaximumTokenByteSize = value;
        }
    }

    bool ISecurityTokenValidator.CanReadToken(string securityToken)
    {
        System.Console.WriteLine(securityToken);
        return true;
    }

    ClaimsPrincipal ISecurityTokenValidator.ValidateToken(string securityToken, TokenValidationParameters validationParameters, out SecurityToken validatedToken)
    {
        JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
        // validatedToken = new JwtSecurityToken(securityToken);
        try
        {

            tokenHandler.ValidateToken(securityToken, validationParameters, out validatedToken);
            validatedToken = new JwtSecurityToken("jwtEncodedString");
        }
        catch (Exception ex)
        {
            System.Console.WriteLine(ex.Message);
            throw;
        }



        ClaimsPrincipal principal = null;
        // SecurityToken validToken = null;

        validatedToken = null;


        System.Collections.Generic.List<System.Security.Claims.Claim> ls =
            new System.Collections.Generic.List<System.Security.Claims.Claim>();

        ls.Add(
            new System.Security.Claims.Claim(
                System.Security.Claims.ClaimTypes.Name, "IcanHazUsr_éèêëïàáâäåãæóòôöõõúùûüñçø_ÉÈÊËÏÀÁÂÄÅÃÆÓÒÔÖÕÕÚÙÛÜÑÇØ 你好,世界 Привет	мир"
            , System.Security.Claims.ClaimValueTypes.String
            )
        );

        //

        System.Security.Claims.ClaimsIdentity id = new System.Security.Claims.ClaimsIdentity("authenticationType");
        id.AddClaims(ls);

        principal = new System.Security.Claims.ClaimsPrincipal(id);

        return principal;
        throw new NotImplementedException();
    }


}

The tricky part is how to get the AsymmetricSecurityKey, because you don't want to pass a rsaCryptoServiceProvider, because you need interoperability in crypto format.

Creation goes along the lines of

// System.Security.Cryptography.X509Certificates.X509Certificate2 cert2 = new System.Security.Cryptography.X509Certificates.X509Certificate2(byte[] rawData);
System.Security.Cryptography.X509Certificates.X509Certificate2 cert2 =
    DotNetUtilities.CreateX509Cert2("mycert");
Microsoft.IdentityModel.Tokens.SecurityKey secKey = new X509SecurityKey(cert2);

e.g. with BouncyCastle from a DER Certificate:

    // http://stackoverflow.com/questions/36942094/how-can-i-generate-a-self-signed-cert-without-using-obsolete-bouncycastle-1-7-0
    public static System.Security.Cryptography.X509Certificates.X509Certificate2 CreateX509Cert2(string certName)
    {

        var keypairgen = new Org.BouncyCastle.Crypto.Generators.RsaKeyPairGenerator();
        keypairgen.Init(new Org.BouncyCastle.Crypto.KeyGenerationParameters(
            new Org.BouncyCastle.Security.SecureRandom(
                new Org.BouncyCastle.Crypto.Prng.CryptoApiRandomGenerator()
                )
                , 1024
                )
        );

        Org.BouncyCastle.Crypto.AsymmetricCipherKeyPair keypair = keypairgen.GenerateKeyPair();

        // --- Until here we generate a keypair



        var random = new Org.BouncyCastle.Security.SecureRandom(
                new Org.BouncyCastle.Crypto.Prng.CryptoApiRandomGenerator()
        );


        // SHA1WITHRSA
        // SHA256WITHRSA
        // SHA384WITHRSA
        // SHA512WITHRSA

        // SHA1WITHECDSA
        // SHA224WITHECDSA
        // SHA256WITHECDSA
        // SHA384WITHECDSA
        // SHA512WITHECDSA

        Org.BouncyCastle.Crypto.ISignatureFactory signatureFactory =
            new Org.BouncyCastle.Crypto.Operators.Asn1SignatureFactory("SHA512WITHRSA", keypair.Private, random)
        ;



        var gen = new Org.BouncyCastle.X509.X509V3CertificateGenerator();


        var CN = new Org.BouncyCastle.Asn1.X509.X509Name("CN=" + certName);
        var SN = Org.BouncyCastle.Math.BigInteger.ProbablePrime(120, new Random());

        gen.SetSerialNumber(SN);
        gen.SetSubjectDN(CN);
        gen.SetIssuerDN(CN);
        gen.SetNotAfter(DateTime.Now.AddYears(1));
        gen.SetNotBefore(DateTime.Now.Subtract(new TimeSpan(7, 0, 0, 0)));
        gen.SetPublicKey(keypair.Public);


        // -- Are these necessary ?

        // public static readonly DerObjectIdentifier AuthorityKeyIdentifier = new DerObjectIdentifier("2.5.29.35");
        // OID value: 2.5.29.35
        // OID description: id-ce-authorityKeyIdentifier
        // This extension may be used either as a certificate or CRL extension.
        // It identifies the public key to be used to verify the signature on this certificate or CRL.
        // It enables distinct keys used by the same CA to be distinguished (e.g., as key updating occurs).


        // http://stackoverflow.com/questions/14930381/generating-x509-certificate-using-bouncy-castle-java
        gen.AddExtension(
        Org.BouncyCastle.Asn1.X509.X509Extensions.AuthorityKeyIdentifier.Id,
        false,
        new Org.BouncyCastle.Asn1.X509.AuthorityKeyIdentifier(
            Org.BouncyCastle.X509.SubjectPublicKeyInfoFactory.CreateSubjectPublicKeyInfo(keypair.Public),
            new Org.BouncyCastle.Asn1.X509.GeneralNames(new Org.BouncyCastle.Asn1.X509.GeneralName(CN)),
            SN
        ));

        // OID value: 1.3.6.1.5.5.7.3.1
        // OID description: Indicates that a certificate can be used as an SSL server certificate.
        gen.AddExtension(
            Org.BouncyCastle.Asn1.X509.X509Extensions.ExtendedKeyUsage.Id,
            false,
            new Org.BouncyCastle.Asn1.X509.ExtendedKeyUsage(new ArrayList()
            {
            new Org.BouncyCastle.Asn1.DerObjectIdentifier("1.3.6.1.5.5.7.3.1")
        }));

        // -- End are these necessary ?

        Org.BouncyCastle.X509.X509Certificate bouncyCert = gen.Generate(signatureFactory);

        byte[] ba = bouncyCert.GetEncoded();
        System.Security.Cryptography.X509Certificates.X509Certificate2 msCert = new System.Security.Cryptography.X509Certificates.X509Certificate2(ba);
        return msCert;
    }

Subsequently, you can add a custom cookie-format that contains the JWT-Bearer:

app.UseCookieAuthentication(new CookieAuthenticationOptions()
{
    AuthenticationScheme = "MyCookieMiddlewareInstance",
    CookieName = "SecurityByObscurityDoesntWork",
    ExpireTimeSpan = new System.TimeSpan(15, 0, 0),
    LoginPath = new Microsoft.AspNetCore.Http.PathString("/Account/Unauthorized/"),
    AccessDeniedPath = new Microsoft.AspNetCore.Http.PathString("/Account/Forbidden/"),
    AutomaticAuthenticate = true,
    AutomaticChallenge = true,
    CookieSecure = Microsoft.AspNetCore.Http.CookieSecurePolicy.SameAsRequest,
    CookieHttpOnly = false,
    TicketDataFormat = new CustomJwtDataFormat("foo", tokenValidationParameters)

    // DataProtectionProvider = null,

    // DataProtectionProvider = new DataProtectionProvider(new System.IO.DirectoryInfo(@"c:shared-auth-ticket-keys"),
    //delegate (DataProtectionConfiguration options)
    //{
    //    var op = new Microsoft.AspNet.DataProtection.AuthenticatedEncryption.AuthenticatedEncryptionOptions();
    //    op.EncryptionAlgorithm = Microsoft.AspNet.DataProtection.AuthenticatedEncryption.EncryptionAlgorithm.AES_256_GCM:
    //    options.UseCryptographicAlgorithms(op);
    //}
    //),
});

Where CustomJwtDataFormat is something along the lines of

public class CustomJwtDataFormat : ISecureDataFormat<AuthenticationTicket>
{

    private readonly string algorithm;
    private readonly TokenValidationParameters validationParameters;

    public CustomJwtDataFormat(string algorithm, TokenValidationParameters validationParameters)
    {
        this.algorithm = algorithm;
        this.validationParameters = validationParameters;
    }



    // This ISecureDataFormat implementation is decode-only
    string ISecureDataFormat<AuthenticationTicket>.Protect(AuthenticationTicket data)
    {
        return MyProtect(data, null);
    }

    string ISecureDataFormat<AuthenticationTicket>.Protect(AuthenticationTicket data, string purpose)
    {
        return MyProtect(data, purpose);
    }

    AuthenticationTicket ISecureDataFormat<AuthenticationTicket>.Unprotect(string protectedText)
    {
        return MyUnprotect(protectedText, null);
    }

    AuthenticationTicket ISecureDataFormat<AuthenticationTicket>.Unprotect(string protectedText, string purpose)
    {
        return MyUnprotect(protectedText, purpose);
    }


    private string MyProtect(AuthenticationTicket data, string purpose)
    {
        return "wadehadedudada";
        throw new System.NotImplementedException();
    }


    // http://blogs.microsoft.co.il/sasha/2012/01/20/aggressive-inlining-in-the-clr-45-jit/
    [System.Runtime.CompilerServices.MethodImpl(System.Runtime.CompilerServices.MethodImplOptions.AggressiveInlining)]
    private AuthenticationTicket MyUnprotect(string protectedText, string purpose)
    {
        JwtSecurityTokenHandler handler = new JwtSecurityTokenHandler();
        ClaimsPrincipal principal = null;
        SecurityToken validToken = null;


        System.Collections.Generic.List<System.Security.Claims.Claim> ls =
            new System.Collections.Generic.List<System.Security.Claims.Claim>();

        ls.Add(
            new System.Security.Claims.Claim(
                System.Security.Claims.ClaimTypes.Name, "IcanHazUsr_éèêëïàáâäåãæóòôöõõúùûüñçø_ÉÈÊËÏÀÁÂÄÅÃÆÓÒÔÖÕÕÚÙÛÜÑÇØ 你好,世界 Привет	мир"
            , System.Security.Claims.ClaimValueTypes.String
            )
        );

        //

        System.Security.Claims.ClaimsIdentity id = new System.Security.Claims.ClaimsIdentity("authenticationType");
        id.AddClaims(ls);

        principal = new System.Security.Claims.ClaimsPrincipal(id);
        return new AuthenticationTicket(principal, new AuthenticationProperties(), "MyCookieMiddlewareInstance");


        try
        {
            principal = handler.ValidateToken(protectedText, this.validationParameters, out validToken);

            JwtSecurityToken validJwt = validToken as JwtSecurityToken;

            if (validJwt == null)
            {
                throw new System.ArgumentException("Invalid JWT");
            }

            if (!validJwt.Header.Alg.Equals(algorithm, System.StringComparison.Ordinal))
            {
                throw new System.ArgumentException($"Algorithm must be '{algorithm}'");
            }

            // Additional custom validation of JWT claims here (if any)
        }
        catch (SecurityTokenValidationException)
        {
            return null;
        }
        catch (System.ArgumentException)
        {
            return null;
        }

        // Validation passed. Return a valid AuthenticationTicket:
        return new AuthenticationTicket(principal, new AuthenticationProperties(), "MyCookieMiddlewareInstance");
    }


}

And you can also create the JWT-token with Microsoft.IdentityModel.Token:

// https://github.com/aspnet/Security/blob/master/src/Microsoft.AspNetCore.Authentication.JwtBearer/Events/IJwtBearerEvents.cs
// http://codereview.stackexchange.com/questions/45974/web-api-2-authentication-with-jwt
public class TokenMaker
{

    class SecurityConstants
    {
        public static string TokenIssuer;
        public static string TokenAudience;
        public static int TokenLifetimeMinutes;
    }


    public static string IssueToken()
    {
        SecurityKey sSKey = null;

        var claimList = new List<Claim>()
        {
            new Claim(ClaimTypes.Name, "userName"),
            new Claim(ClaimTypes.Role, "role")     //Not sure what this is for
        };

        JwtSecurityTokenHandler tokenHandler = new JwtSecurityTokenHandler();
        SecurityTokenDescriptor desc = makeSecurityTokenDescriptor(sSKey, claimList);

        // JwtSecurityToken tok = tokenHandler.CreateJwtSecurityToken(desc);
        return tokenHandler.CreateEncodedJwt(desc);
    }


    public static ClaimsPrincipal ValidateJwtToken(string jwtToken)
    {
        SecurityKey sSKey = null;
        var tokenHandler = new JwtSecurityTokenHandler();

        // Parse JWT from the Base64UrlEncoded wire form
        //(<Base64UrlEncoded header>.<Base64UrlEncoded body>.<signature>)
        JwtSecurityToken parsedJwt = tokenHandler.ReadToken(jwtToken) as JwtSecurityToken;

        TokenValidationParameters validationParams =
            new TokenValidationParameters()
            {
                RequireExpirationTime = true,
                ValidAudience = SecurityConstants.TokenAudience,
                ValidIssuers = new List<string>() { SecurityConstants.TokenIssuer },
                ValidateIssuerSigningKey = true,
                ValidateLifetime = true,
                IssuerSigningKey = sSKey,

            };

        SecurityToken secT;
        return tokenHandler.ValidateToken("token", validationParams, out secT);
    }


    private static SecurityTokenDescriptor makeSecurityTokenDescriptor(SecurityKey sSKey, List<Claim> claimList)
    {
        var now = DateTime.UtcNow;
        Claim[] claims = claimList.ToArray();
        return new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor
        {
            Subject = new ClaimsIdentity(claims),
            Issuer = SecurityConstants.TokenIssuer,
            Audience = SecurityConstants.TokenAudience,
            IssuedAt = System.DateTime.UtcNow,
            Expires = System.DateTime.UtcNow.AddMinutes(SecurityConstants.TokenLifetimeMinutes),
            NotBefore = System.DateTime.UtcNow.AddTicks(-1),

            SigningCredentials = new SigningCredentials(sSKey, Microsoft.IdentityModel.Tokens.SecurityAlgorithms.EcdsaSha512Signature)
        };



    }

}

Note that because you can give a different user in the cookie vs. http-headers (Bearer), or any other authentication method that you specify, you can actually have MORE than 1 user !


Take a look at this:
https://stormpath.com/blog/token-authentication-asp-net-core

it should be exactly what you're looking for.

There's also these two:

https://goblincoding.com/2016/07/03/issuing-and-authenticating-jwt-tokens-in-asp-net-core-webapi-part-i/

https://goblincoding.com/2016/07/07/issuing-and-authenticating-jwt-tokens-in-asp-net-core-webapi-part-ii/

and this one
http://blog.novanet.no/hooking-up-asp-net-core-1-rc1-web-api-with-auth0-bearer-tokens/

And the JWT-Bearer sourceshttps://github.com/aspnet/Security/tree/master/src/Microsoft.AspNetCore.Authentication.JwtBearer


If you need ultra-high security, you should protect against replay-attacks by renewing the ticket on each request, and invalidate old tickets after a certain timeout, and after user logout (not just after validity expiration).


For those of you who end up from here via google, you can implement a TicketDataFormat in cookie-authentication when you want to use your own version of JWT.

I had to look into JWT for work, because we needed to secure our application.
Because I still had to use .NET 2.0, I had to write my own library.

I've ported the result of that to .NET Core this weekend.You find it here:https://github.com/ststeiger/Jwt_Net20/tree/master/CoreJWT

It doesn't use any database, that's not the job of a JWT libary.
Getting and setting DB data is your job.
The library allows for JWT authorization and verification in .NET Core with all algorithms specified in the JWT RFC listed on the IANA JOSE assignment.
As for adding authorization to the pipeline and adding values to route - these are two things which should be done separately, and I think you best do that yourselfs.

You can use custom authentication in ASP.NET Core.
Look into the "Security" category of docs on docs.asp.net.

Or you can look into the Cookie Middleware without ASP.NET Identity or into Custom Policy-Based Authorization.

You can also learn more in the auth workshop on github or in the social login section or in this channel 9 video tutorial.
If all else fails, the source code of asp.net security is on github.


The original project for .NET 3.5, which is where my library derives from, is here:
https://github.com/jwt-dotnet/jwt
I removed all references to LINQ + extension methods, because they are not supported in .NET 2.0. If you include either LINQ, or ExtensionAttribute in the sourcecode, then you can't just change the .NET runtime without getting warnings; that's why I have completely removed them.
Also, I've added RSA + ECDSA JWS-methods, for that reason the CoreJWT-project depends on BouncyCastle.
If you limit yourselfs to HMAC-SHA256 + HMAC-SHA384 + HMAC-SHA512, you can remove BouncyCastle.

JWE is not (yet) supported.

Usage is just like jwt-dotnet/jwt, except I changed the namespace JWT to CoreJWT.
I also added an internal copy of PetaJSON as serializer, so there is no interference with other people's project's dependencies.

Create a JWT-token:

var payload = new Dictionary<string, object>()
{
    { "claim1", 0 },
    { "claim2", "claim2-value" }
};
var secretKey = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";
string token = JWT.JsonWebToken.Encode(payload, secretKey, JWT.JwtHashAlgorithm.HS256);
Console.WriteLine(token);

Verify a JWT-token:

var token = "eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJjbGFpbTEiOjAsImNsYWltMiI6ImNsYWltMi12YWx1ZSJ9.8pwBI_HtXqI3UgQHQ_rDRnSQRxFL1SR8fbQoS-5kM5s";
var secretKey = "GQDstcKsx0NHjPOuXOYg5MbeJ1XT0uFiwDVvVBrk";
try
{
    string jsonPayload = JWT.JsonWebToken.Decode(token, secretKey);
    Console.WriteLine(jsonPayload);
}
catch (JWT.SignatureVerificationException)
{
    Console.WriteLine("Invalid token!");
}

For RSA & ECSA, you'll have to pass the (BouncyCastle) RSA/ECDSA private key instead of secretKey.

namespace BouncyJWT
{


    public class JwtKey
    {
        public byte[] MacKeyBytes;
        public Org.BouncyCastle.Crypto.AsymmetricKeyParameter RsaPrivateKey;
        public Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters EcPrivateKey;


        public string MacKey
        {
            get { return System.Text.Encoding.UTF8.GetString(this.MacKeyBytes); }
            set { this.MacKeyBytes = System.Text.Encoding.UTF8.GetBytes(value); }
        }


        public JwtKey()
        { }

        public JwtKey(string macKey)
        {
            this.MacKey = macKey;
        }

        public JwtKey(byte[] macKey)
        {
            this.MacKeyBytes = macKey;
        }

        public JwtKey(Org.BouncyCastle.Crypto.AsymmetricKeyParameter rsaPrivateKey)
        {
            this.RsaPrivateKey = rsaPrivateKey;
        }

        public JwtKey(Org.BouncyCastle.Crypto.Parameters.ECPrivateKeyParameters ecPrivateKey)
        {
            this.EcPrivateKey = ecPrivateKey;
        }
    }


}

For how to generate/export/import RSA/ECDSA-keys with BouncyCastle, see the project called "BouncyCastleTests" in the same repository. I leave it to you to safely store and retrieve your own RSA/ECDSA private keys.

I've verified my library's results for HMAC-ShaXXX and RSA-XXX with JWT.io - it looks like they are OK.
ECSD should be OK, too, but I didn't test it against anything.
I did not run extensive tests anyway, FYI.

这篇关于ASP.NET Core 1.0 Web API 中的简单 JWT 身份验证的文章就介绍到这了,希望我们推荐的答案对大家有所帮助,也希望大家多多支持!

08-15 03:23