简体   繁体   English

ASP.NET核心中基于令牌的身份验证(刷新)

[英]Token Based Authentication in ASP.NET Core (refreshed)

I'm working with ASP.NET Core application. 我正在使用ASP.NET Core应用程序。 I'm trying to implement Token Based Authentication but can not figure out how to use new Security System . 我正在尝试实现基于令牌的身份验证,但无法弄清楚如何使用新的安全系统

My scenario: A client requests a token. 我的场景:客户端请求令牌。 My server should authorize the user and return access_token which will be used by the client in following requests. 我的服务器应该授权用户并返回access_token,客户端将在以下请求中使用该access_token。

Here are two great articles about implementing exactly what I need: 这里有两篇关于正确实现我需要的文章:

The problem is - it is not obvious for me how to do the same thing in ASP.NET Core. 问题是 - 对于我来说,如何在ASP.NET Core中执行相同的操作并不明显。

My question is: how to configure ASP.NET Core Web Api application to work with token based authentication? 我的问题是:如何配置ASP.NET Core Web Api应用程序以使用基于令牌的身份验证? What direction should I pursue? 我应该追求什么方向? Have you written any articles about the newest version, or know where I could find ones? 你有没有写过关于最新版本的文章,或者知道我在哪里可以找到它?

Thank you! 谢谢!

Working from Matt Dekrey's fabulous answer , I've created a fully working example of token-based authentication, working against ASP.NET Core (1.0.1). 根据Matt Dekrey的精彩回答 ,我创建了一个基于令牌的身份验证的完整工作示例,针对ASP.NET Core(1.0.1)。 You can find the full code in this repository on GitHub (alternative branches for 1.0.0-rc1 , beta8 , beta7 ), but in brief, the important steps are: 你可以找到完整的代码在这个仓库在GitHub上 (替代分支1.0.0-RC1beta8β7的 ),但在短暂的,重要的步骤是:

Generate a key for your application 为您的应用程序生成密钥

In my example, I generate a random key each time the app starts, you'll need to generate one and store it somewhere and provide it to your application. 在我的示例中,每次应用程序启动时,我都会生成一个随机密钥,您需要生成一个并将其存储在某个位置并将其提供给您的应用程序。 See this file for how I'm generating a random key and how you might import it from a .json file . 请参阅此文件,了解我如何生成随机密钥以及如何从.json文件导入它 As suggested in the comments by @kspearrin, the Data Protection API seems like an ideal candidate for managing the keys "correctly", but I've not worked out if that's possible yet. 正如@kspearrin的评论中所建议的那样, Data Protection API似乎是“正确”管理密钥的理想候选者,但如果可能的话,我还没有解决。 Please submit a pull request if you work it out! 如果您解决了,请提交拉取请求!

Startup.cs - ConfigureServices Startup.cs - ConfigureServices

Here, we need to load a private key for our tokens to be signed with, which we will also use to verify tokens as they are presented. 在这里,我们需要为我们的令牌加载私钥,我们还将使用它来验证令牌。 We're storing the key in a class-level variable key which we'll re-use in the Configure method below. 我们将密钥存储在类级变量key ,我们将在下面的Configure方法中重复使用它。 TokenAuthOptions is a simple class which holds the signing identity, audience and issuer that we'll need in the TokenController to create our keys. TokenAuthOptions是一个简单的类,它包含我们在TokenController中创建密钥所需的签名标识,受众和发布者。

// Replace this with some sort of loading from config / file.
RSAParameters keyParams = RSAKeyUtils.GetRandomKey();

// Create the key, and a set of token options to record signing credentials 
// using that key, along with the other parameters we will need in the 
// token controlller.
key = new RsaSecurityKey(keyParams);
tokenOptions = new TokenAuthOptions()
{
    Audience = TokenAudience,
    Issuer = TokenIssuer,
    SigningCredentials = new SigningCredentials(key, SecurityAlgorithms.Sha256Digest)
};

// Save the token options into an instance so they're accessible to the 
// controller.
services.AddSingleton<TokenAuthOptions>(tokenOptions);

// Enable the use of an [Authorize("Bearer")] attribute on methods and
// classes to protect.
services.AddAuthorization(auth =>
{
    auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder()
        .AddAuthenticationSchemes(JwtBearerDefaults.AuthenticationScheme‌​)
        .RequireAuthenticatedUser().Build());
});

We've also set up an authorization policy to allow us to use [Authorize("Bearer")] on the endpoints and classes we wish to protect. 我们还设置了一个授权策略,允许我们在我们希望保护的端点和类上使用[Authorize("Bearer")]

Startup.cs - Configure Startup.cs - 配置

Here, we need to configure the JwtBearerAuthentication: 在这里,我们需要配置JwtBearerAuthentication:

app.UseJwtBearerAuthentication(new JwtBearerOptions {
    TokenValidationParameters = new TokenValidationParameters {
        IssuerSigningKey = key,
        ValidAudience = tokenOptions.Audience,
        ValidIssuer = tokenOptions.Issuer,

        // When receiving a token, check that it is still valid.
        ValidateLifetime = true,

        // This defines the maximum allowable clock skew - i.e.
        // provides a tolerance on the token expiry time 
        // when validating the lifetime. As we're creating the tokens 
        // locally and validating them on the same machines which 
        // should have synchronised time, this can be set to zero. 
        // Where external tokens are used, some leeway here could be 
        // useful.
        ClockSkew = TimeSpan.FromMinutes(0)
    }
});

TokenController TokenController

In the token controller, you need to have a method to generate signed keys using the key that was loaded in Startup.cs. 在令牌控制器中,您需要有一个方法来使用Startup.cs中加载的密钥生成签名密钥。 We've registered a TokenAuthOptions instance in Startup, so we need to inject that in the constructor for TokenController: 我们在Startup中注册了一个TokenAuthOptions实例,所以我们需要在T​​okenController的构造函数中注入它:

[Route("api/[controller]")]
public class TokenController : Controller
{
    private readonly TokenAuthOptions tokenOptions;

    public TokenController(TokenAuthOptions tokenOptions)
    {
        this.tokenOptions = tokenOptions;
    }
...

Then you'll need to generate the token in your handler for the login endpoint, in my example I'm taking a username and password and validating those using an if statement, but the key thing you need to do is create or load a claims-based identity and generate the token for that: 然后,您需要在处理程序中为登录端点生成令牌,在我的示例中,我使用用户名和密码并使用if语句验证这些令牌,但您需要做的关键是创建或加载声明基于身份并生成令牌:

public class AuthRequest
{
    public string username { get; set; }
    public string password { get; set; }
}

/// <summary>
/// Request a new token for a given username/password pair.
/// </summary>
/// <param name="req"></param>
/// <returns></returns>
[HttpPost]
public dynamic Post([FromBody] AuthRequest req)
{
    // Obviously, at this point you need to validate the username and password against whatever system you wish.
    if ((req.username == "TEST" && req.password == "TEST") || (req.username == "TEST2" && req.password == "TEST"))
    {
        DateTime? expires = DateTime.UtcNow.AddMinutes(2);
        var token = GetToken(req.username, expires);
        return new { authenticated = true, entityId = 1, token = token, tokenExpires = expires };
    }
    return new { authenticated = false };
}

private string GetToken(string user, DateTime? expires)
{
    var handler = new JwtSecurityTokenHandler();

    // Here, you should create or look up an identity for the user which is being authenticated.
    // For now, just creating a simple generic identity.
    ClaimsIdentity identity = new ClaimsIdentity(new GenericIdentity(user, "TokenAuth"), new[] { new Claim("EntityID", "1", ClaimValueTypes.Integer) });

    var securityToken = handler.CreateToken(new Microsoft.IdentityModel.Tokens.SecurityTokenDescriptor() {
        Issuer = tokenOptions.Issuer,
        Audience = tokenOptions.Audience,
        SigningCredentials = tokenOptions.SigningCredentials,
        Subject = identity,
        Expires = expires
    });
    return handler.WriteToken(securityToken);
}

And that should be it. 这应该是它。 Just add [Authorize("Bearer")] to any method or class you want to protect, and you should get an error if you attempt to access it without a token present. 只需将[Authorize("Bearer")]到您要保护的任何方法或类中,如果您在没有令牌存在的情况下尝试访问它,则应该收到错误。 If you want to return a 401 instead of a 500 error, you'll need to register a custom exception handler as I have in my example here . 如果要返回401而不是500错误,则需要在我的示例中注册自定义异常处理程序。

This is really a duplicate of another answer of mine , which I tend to keep more up-to-date as it gets more attention. 这真的是我的另一个答案的重复,我倾向于保持更新,因为它得到更多的关注。 Comments there may also be useful to you! 那里的评论也可能对你有用!

Updated for .Net Core 2: 针对.Net Core 2进行了更新:

Previous versions of this answer used RSA; 此答案的先前版本使用RSA; it's really not necessary if your same code that is generating the tokens is also verifying the tokens. 如果生成令牌的相同代码也在验证令牌,则实际上没有必要。 However, if you're distributing the responsibility, you probably still want to do this using an instance of Microsoft.IdentityModel.Tokens.RsaSecurityKey . 但是,如果您正在分配责任,您可能仍希望使用Microsoft.IdentityModel.Tokens.RsaSecurityKey实例执行此操作。

  1. Create a few constants that we'll be using later; 创建一些我们稍后将使用的常量; here's what I did: 这就是我做的:

     const string TokenAudience = "Myself"; const string TokenIssuer = "MyProject"; 
  2. Add this to your Startup.cs's ConfigureServices . 将其添加到Startup.cs的ConfigureServices We'll use dependency injection later to access these settings. 我们稍后将使用依赖注入来访问这些设置。 I'm assuming that your authenticationConfiguration is a ConfigurationSection or Configuration object such that you can have a different config for debug and production. 我假设您的authenticationConfigurationConfigurationSectionConfiguration对象,因此您可以使用不同的配置进行调试和生产。 Make sure you store your key securely! 确保您安全地存放钥匙! It can be any string. 它可以是任何字符串。

     var keySecret = authenticationConfiguration["JwtSigningKey"]; var symmetricKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(keySecret)); services.AddTransient(_ => new JwtSignInHandler(symmetricKey)); services.AddAuthentication(options => { // This causes the default authentication scheme to be JWT. // Without this, the Authorization header is not checked and // you'll get no results. However, this also means that if // you're already using cookies in your app, they won't be // checked by default. options.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme; }) .AddJwtBearer(options => { options.TokenValidationParameters.ValidateIssuerSigningKey = true; options.TokenValidationParameters.IssuerSigningKey = symmetricKey; options.TokenValidationParameters.ValidAudience = JwtSignInHandler.TokenAudience; options.TokenValidationParameters.ValidIssuer = JwtSignInHandler.TokenIssuer; }); 

    I've seen other answers change other settings, such as ClockSkew ; 我见过其他答案会改变其他设置,比如ClockSkew ; the defaults are set such that it should work for distributed environments whose clocks aren't exactly in sync. 设置默认值,使其适用于时钟不完全同步的分布式环境。 These are the only settings you need to change. 这些是您需要更改的唯一设置。

  3. Set up Authentication. 设置身份验证。 You should have this line before any middleware that requires your User info, such as app.UseMvc() . 您应该在需要User信息的任何中间件之前使用此行,例如app.UseMvc()

     app.UseAuthentication(); 

    Note that this will not cause your token to be emitted with the SignInManager or anything else. 请注意,这不会导致您的令牌与SignInManager或其他任何内容一起发出。 You will need to provide your own mechanism for outputting your JWT - see below. 您需要提供自己的输出JWT的机制 - 见下文。

  4. You may want to specify an AuthorizationPolicy . 您可能想要指定AuthorizationPolicy This will allow you to specify controllers and actions that only allow Bearer tokens as authentication using [Authorize("Bearer")] . 这将允许您使用[Authorize("Bearer")]指定仅允许承载令牌作为身份验证的控制器和操作。

     services.AddAuthorization(auth => { auth.AddPolicy("Bearer", new AuthorizationPolicyBuilder() .AddAuthenticationTypes(JwtBearerDefaults.AuthenticationType) .RequireAuthenticatedUser().Build()); }); 
  5. Here comes the tricky part: building the token. 这里有一个棘手的部分:构建令牌。

     class JwtSignInHandler { public const string TokenAudience = "Myself"; public const string TokenIssuer = "MyProject"; private readonly SymmetricSecurityKey key; public JwtSignInHandler(SymmetricSecurityKey symmetricKey) { this.key = symmetricKey; } public string BuildJwt(ClaimsPrincipal principal) { var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256); var token = new JwtSecurityToken( issuer: TokenIssuer, audience: TokenAudience, claims: principal.Claims, expires: DateTime.Now.AddMinutes(20), signingCredentials: creds ); return new JwtSecurityTokenHandler().WriteToken(token); } } 

    Then, in your controller where you want your token, something like the following: 然后,在您需要令牌的控制器中,如下所示:

     [HttpPost] public string AnonymousSignIn([FromServices] JwtSignInHandler tokenFactory) { var principal = new System.Security.Claims.ClaimsPrincipal(new[] { new System.Security.Claims.ClaimsIdentity(new[] { new System.Security.Claims.Claim(System.Security.Claims.ClaimTypes.Name, "Demo User") }) }); return tokenFactory.BuildJwt(principal); } 

    Here, I'm assuming you already have a principal. 在这里,我假设你已经有了校长。 If you are using Identity, you can use IUserClaimsPrincipalFactory<> to transform your User into a ClaimsPrincipal . 如果您使用的是Identity,则可以使用IUserClaimsPrincipalFactory<>将您的User转换为ClaimsPrincipal

  6. To test it : Get a token, put it into the form at jwt.io . 测试它 :获取一个令牌,将其放入jwt.io的表单中。 The instructions I provided above also allow you to use the secret from your config to validate the signature! 我上面提供的说明还允许您使用配置中的秘密来验证签名!

  7. If you were rendering this in a partial view on your HTML page in combination with the bearer-only authentication in .Net 4.5, you can now use a ViewComponent to do the same. 如果您在HTML页面上的部分视图中结合使用.Net 4.5中的仅承载身份验证,您现在可以使用ViewComponent执行相同操作。 It's mostly the same as the Controller Action code above. 它与上面的Controller Action代码大致相同。

To achieve what you describe, you'll need both an OAuth2/OpenID Connect authorization server and a middleware validating access tokens for your API. 要实现您描述的内容,您需要OAuth2 / OpenID Connect授权服务器和验证API访问令牌的中间件。 Katana used to offer an OAuthAuthorizationServerMiddleware , but it doesn't exist anymore in ASP.NET Core. Katana过去常常提供OAuthAuthorizationServerMiddleware ,但它在ASP.NET Core中不再存在。

I suggest having a look to AspNet.Security.OpenIdConnect.Server , an experimental fork of the OAuth2 authorization server middleware which is used by the tutorial you mentioned: there's an OWIN/Katana 3 version, and an ASP.NET Core version that supports both net451 (.NET Desktop) and netstandard1.4 (compatible with .NET Core). 我建议看一下AspNet.Security.OpenIdConnect.Server ,这是OAuth2授权服务器中间件的实验分支,你提到的教程使用它:有一个OWIN / Katana 3版本,以及一个支持这两个版本的ASP.NET核心版本net451 (.NET桌面)和netstandard1.4 (与.NET Core兼容)。

https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server

Don't miss the MVC Core sample that shows how to configure an OpenID Connect authorization server using AspNet.Security.OpenIdConnect.Server and how to validate the encrypted access tokens issued by the server middleware: https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Server/blob/dev/samples/Mvc/Mvc.Server/Startup.cs 不要错过MVC Core示例,该示例演示了如何使用AspNet.Security.OpenIdConnect.Server配置OpenID Connect授权服务器以及如何验证服务器中间件发出的加密访问令牌: https//github.com/aspnet-的contrib / AspNet.Security.OpenIdConnect.Server /团块的/ dev /样品/ MVC / Mvc.Server / Startup.cs

You can also read this blog post, that explains how to implement the resource owner password grant, which is the OAuth2 equivalent of basic authentication: http://kevinchalet.com/2016/07/13/creating-your-own-openid-connect-server-with-asos-implementing-the-resource-owner-password-credentials-grant/ 您还可以阅读此博客文章,其中解释了如何实施资源所有者密码授予,这是与基本身份验证相当的OAuth2: http//kevinchalet.com/2016/07/13/creating-your-own-openid-连接服务器与- ASOS -实施-在资源所有者密码,证书发放/

Startup.cs Startup.cs

public class Startup
{
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddAuthentication();
    }

    public void Configure(IApplicationBuilder app)
    {
        // Add a new middleware validating the encrypted
        // access tokens issued by the OIDC server.
        app.UseOAuthValidation();

        // Add a new middleware issuing tokens.
        app.UseOpenIdConnectServer(options =>
        {
            options.TokenEndpointPath = "/connect/token";

            // Override OnValidateTokenRequest to skip client authentication.
            options.Provider.OnValidateTokenRequest = context =>
            {
                // Reject the token requests that don't use
                // grant_type=password or grant_type=refresh_token.
                if (!context.Request.IsPasswordGrantType() &&
                    !context.Request.IsRefreshTokenGrantType())
                {
                    context.Reject(
                        error: OpenIdConnectConstants.Errors.UnsupportedGrantType,
                        description: "Only grant_type=password and refresh_token " +
                                     "requests are accepted by this 
                    return Task.FromResult(0);
                }

                // Since there's only one application and since it's a public client
                // (i.e a client that cannot keep its credentials private),
                // call Skip() to inform the server the request should be
                // accepted without enforcing client authentication.
                context.Skip();

                return Task.FromResult(0);
            };

            // Override OnHandleTokenRequest to support
            // grant_type=password token requests.
            options.Provider.OnHandleTokenRequest = context =>
            {
                // Only handle grant_type=password token requests and let the
                // OpenID Connect server middleware handle the other grant types.
                if (context.Request.IsPasswordGrantType())
                {
                    // Do your credentials validation here.
                    // Note: you can call Reject() with a message
                    // to indicate that authentication failed.

                    var identity = new ClaimsIdentity(context.Options.AuthenticationScheme);
                    identity.AddClaim(OpenIdConnectConstants.Claims.Subject, "[unique id]");

                    // By default, claims are not serialized
                    // in the access and identity tokens.
                    // Use the overload taking a "destinations"
                    // parameter to make sure your claims
                    // are correctly inserted in the appropriate tokens.
                    identity.AddClaim("urn:customclaim", "value",
                        OpenIdConnectConstants.Destinations.AccessToken,
                        OpenIdConnectConstants.Destinations.IdentityToken);

                    var ticket = new AuthenticationTicket(
                        new ClaimsPrincipal(identity),
                        new AuthenticationProperties(),
                        context.Options.AuthenticationScheme);

                    // Call SetScopes with the list of scopes you want to grant
                    // (specify offline_access to issue a refresh token).
                    ticket.SetScopes("profile", "offline_access");

                    context.Validate(ticket);
                }

                return Task.FromResult(0);
            };
        });
    }
}

project.json project.json

{
  "dependencies": {
    "AspNet.Security.OAuth.Validation": "1.0.0",
    "AspNet.Security.OpenIdConnect.Server": "1.0.0"
  }
}

Good luck! 祝好运!

You can use OpenIddict to serve the tokens (logging in) and then use UseJwtBearerAuthentication to validate them when an API/Controller is accessed. 您可以使用OpenIddict来提供令牌(登录),然后在访问API / Controller时使用UseJwtBearerAuthentication来验证它们。

This is essentially all the configuration you need in Startup.cs : 这基本上是您在Startup.cs需要的所有配置:

ConfigureServices: ConfigureServices:

services.AddIdentity<ApplicationUser, ApplicationRole>()
    .AddEntityFrameworkStores<ApplicationDbContext>()
    .AddDefaultTokenProviders()
    // this line is added for OpenIddict to plug in
    .AddOpenIddictCore<Application>(config => config.UseEntityFramework());

Configure 配置

app.UseOpenIddictCore(builder =>
{
    // here you tell openiddict you're wanting to use jwt tokens
    builder.Options.UseJwtTokens();
    // NOTE: for dev consumption only! for live, this is not encouraged!
    builder.Options.AllowInsecureHttp = true;
    builder.Options.ApplicationCanDisplayErrors = true;
});

// use jwt bearer authentication to validate the tokens
app.UseJwtBearerAuthentication(options =>
{
    options.AutomaticAuthenticate = true;
    options.AutomaticChallenge = true;
    options.RequireHttpsMetadata = false;
    // must match the resource on your token request
    options.Audience = "http://localhost:58292/";
    options.Authority = "http://localhost:58292/";
});

There are one or two other minor things, such as your DbContext needs to derive from OpenIddictContext<ApplicationUser, Application, ApplicationRole, string> . 还有一两个其他小问题,例如您的DbContext需要从OpenIddictContext<ApplicationUser, Application, ApplicationRole, string>派生。

You can see a full length explanation (including the functioning github repo) on this blog post of mine: http://capesean.co.za/blog/asp-net-5-jwt-tokens/ 我可以在这篇博文中看到一个完整的解释(包括正在运行的github repo): http//capesean.co.za/blog/asp-net-5-jwt-tokens/

You can have a look at the OpenId connect samples which illustrate how to deal with different authentication mechanisms, including JWT Tokens: 您可以查看OpenId连接示例,它们说明了如何处理不同的身份验证机制,包括JWT令牌:

https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Samples https://github.com/aspnet-contrib/AspNet.Security.OpenIdConnect.Samples

If you look at the Cordova Backend project, the configuration for the API is like so: 如果您查看Cordova后端项目,API的配置如下:

app.UseWhen(context => context.Request.Path.StartsWithSegments(new PathString("/api")), 
      branch => {
                branch.UseJwtBearerAuthentication(options => {
                    options.AutomaticAuthenticate = true;
                    options.AutomaticChallenge = true;
                    options.RequireHttpsMetadata = false;
                    options.Audience = "localhost:54540";
                    options.Authority = "localhost:54540";
                });
    });

The logic in /Providers/AuthorizationProvider.cs and the RessourceController of that project are also worth having a look at ;). /Providers/AuthorizationProvider.cs中的逻辑和该项目的RessourceController也值得一看;)。

Moreover, I have implemented a single page application with token based authentication implementation using the Aurelia front end framework and ASP.NET core. 此外,我使用Aurelia前端框架和ASP.NET核心实现了一个基于令牌的身份验证实现的单页面应用程序。 There is also a signal R persistent connection. 还有一个信号R持久连接。 However I have not done any DB implementation. 但是我没有做过任何数据库实现。 Code can be seen here: https://github.com/alexandre-spieser/AureliaAspNetCoreAuth 代码可以在这里看到: https//github.com/alexandre-spieser/AureliaAspNetCoreAuth

Hope this helps, 希望这可以帮助,

Best, 最好,

Alex 亚历克斯

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

相关问题 ASP.NET Core 中基于令牌的身份验证 - Token Based Authentication in ASP.NET Core 检查用户是否使用ASP.NET Core中的基于令牌的身份验证登录 - Check if user is logged in with Token Based Authentication in ASP.NET Core ASP.NET Core 3 with Angular 8, ASP.NET Core Identity, Roles and token based authentication - ASP.NET Core 3 with Angular 8, ASP.NET Core Identity, Roles and token based authentication ASP.NET Core 中的承载令牌身份验证 - Bearer Token Authentication in ASP.NET Core ASP.NET Core 中的 Jwt 令牌身份验证 - Jwt token authentication in ASP.NET Core 为什么要使用Identity Server和asp.net core 2在基于令牌的身份验证上使用cookie - Why having cookies on token based authentication using Identity Server and asp.net core 2 Mongodb数据存储的asp.net核心中基于令牌的简单身份验证/授权 - Simple token based authentication/authorization in asp.net core for Mongodb datastore ASP.NET核心网站使用JWT令牌进行WebApi身份验证 - ASP.NET Core Website to WebApi authentication using JWT token 无法识别ASP.NET Core 2.1 Jwt身份验证令牌 - ASP.NET Core 2.1 Jwt Authentication Token not recognised 一个将身份验证令牌存储在ASP.Net Core中的位置 - Where should one store the authentication token in ASP.Net Core
 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM