【问题标题】:Confusion using JWT when calling API调用 API 时使用 JWT 的困惑
【发布时间】:2021-04-01 06:00:10
【问题描述】:

我编写了一个使用 JWTBearer 身份验证系统的 ASP.NET Core 3.1 API。当我从 Postman 调用 API 时,这个系统运行良好,但我不知道如何通过我自己的应用程序或 ASP.NET Core 3.1 MVC 网站调用它。这是 API 的配置:

API 配置:

在 Startup.cs ConfigrationServices 方法中,我添加了这段经典代码:

services.AddAuthentication(x =>
        {
            x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
            .AddJwtBearer(x =>
            {
                x.RequireHttpsMetadata = true;
                x.SaveToken = true;
                x.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateIssuer = false,
                    ValidateAudience = false,
                    ValidateLifetime = true,
                    ClockSkew = TimeSpan.FromMinutes(5),
                };
            });

然后我在 Configure 方法中添加了中间件app.UseAuthentication();

现在我有一个 UsersController.cs 和一个 SignIn 方法,如果凭据正确,该方法将 JWT 作为字符串返回。

最后,我添加了一个简单的带有[Authorize] 标签的GetUsers() 方法来测试JWT 身份验证,如下所示:

// GET: api/Users
    [Authorize(Roles = "Administrator")]
    [HttpGet]
    public async Task<ActionResult<IEnumerable<User>>> GetUsers()
    {
        return await _context.Users.ToListAsync();
    }

通过邮递员,一切正常。我在 POST 中调用 api/Users/SignIn url,将我的凭据作为 JSON 传递。我用 200 StatusCode 取回我的令牌。

然后我在 GET 中调用 api/Users,将之前获得的 JWT 传递给 Postman 设置 Authorization &gt; Type : Bearer Token。我的 API 返回一个包含我要求的所有数据的成功代码。 API 端一切正常。

MVC 网站配置:

为了简化与 API 的讨论,我写了一个 Class Library 和一个 ClientService.cs 类。

Int ClientService.cs,我有这段简化的代码,它成功地从 API 获取数据:

public async Task<string> GetPage(string model)
{
    var request = 
        new HttpRequestMessage(
            HttpMethod.Get,
            _baseAddress +
            model);

    var client = _clientFactory.CreateClient();
    
    var response = await client.SendAsync(request);

    return await response.Content.ReadAsStringAsync();
}

如果我使用它,假设从api/something 获取信息如下:var smth = await GetPage("something") 或任何其他AllowAnonymous 方法,它将正常工作。

但如果我想让它使用Authorize 方法,我实际上在发送请求之前添加了这段代码:

client.DefaultRequestHeaders.Add("Authorization", "Bearer " + token);

令牌变量包含我在代码中硬写的用于测试目的的 JWT。一切正常。

所以现在我显然在尝试避免硬写 JWT。我决定将它存储在客户端的 HttpOnly Cookie 以及 ASP.NET Core 的 AntiForgeryToken 本机函数中。我写了这段代码来存储cookie:

private void Authenticate(string token)
{
    Response.Cookies.Append(
        "JWT",
        token,
        new CookieOptions()
        {
            HttpOnly = true,
            Secure = true,
        }
    );
}

现在我被困在这里了。因为我将它用作服务,所以我的ClientService 对我的所有用户都是一样的。所以我无法将令牌存储在某处并将其传递给每个请求的新创建的客户端。

在调用ClientService 之前,我尝试将 JWT 添加到标头,如下所示:

public async Task<ActionResult> Index()
{
    Request.Headers.Add("Authorization", "Bearer " + Request.Cookies["JWT"]);
    var users = await ClientService.GetUsersAsync();
    //GetUsersAsync() simply call GetPage("users") method and deserialize the JSON returned as a List<User>
    return View(users);
}

但是因为我的ClientService 创建了自己的客户端,每次我使用此代码从 API 请求一些数据时都会发送自己的请求:

var request = 
    new HttpRequestMessage(
        HttpMethod.Get,,
        _baseAddress +
        model);

var client = _clientFactory.CreateClient();

我之前添加的标头没有传递给 API。

一个简单的解决方案是重写我所有的ClientService 方法以接受Token 参数,但这似乎是多余且痛苦的。

将令牌传递给我的 API 的最佳和最简单的解决方案是什么?

【问题讨论】:

    标签: c# jwt asp.net-core-webapi asp.net-core-3.1


    【解决方案1】:

    你描述的案例其实是Cookie Authentication

    1.登录

     ClaimsIdentity claimsIdentity = new ClaimsIdentity(CookieAuthenticationDefaults.AuthenticationScheme);
    
            //Not mandatory: you can add the token to claim for future usage, like API request from server to server
            claimsIdentity.AddClaim(new Claim("token", tokenId));
    
            var claimsPrincipal = new ClaimsPrincipal(claimsIdentity);
            await httpContext.SignInAsync(CookieAuthenticationDefaults.AuthenticationScheme, claimsPrincipal);
    

    2。添加Cookie认证

       services.AddAuthentication(x =>
        {
            x.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
            x.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        })
            .AddJwtBearer(x =>
            {
                x.RequireHttpsMetadata = true;
                x.SaveToken = true;
                x.TokenValidationParameters = new TokenValidationParameters
                {
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(key),
                    ValidateIssuer = false,
                    ValidateAudience = false,
                    ValidateLifetime = true,
                    ClockSkew = TimeSpan.FromMinutes(5),
                };
            }).AddCookieAuthentication();
    

    AddCookieAuthentication 看起来像:

     public static AuthenticationBuilder AddCookieAuthentication(this IServiceCollection services)
        {
            var authBuilder = services.AddAuthentication(sharedOptions =>
            {
                sharedOptions.RequireAuthenticatedSignIn = false;
                sharedOptions.DefaultScheme = CookieAuthenticationDefaults.AuthenticationScheme;
               
                sharedOptions.DefaultAuthenticateScheme = CookieAuthenticationDefaults.AuthenticationScheme;
            })
            .AddCookie(CookieAuthenticationDefaults.AuthenticationScheme, opts =>
            {
                opts.CookieManager = new ChunkingCookieManager();
                opts.Cookie = new CookieBuilder()
                {
                    Domain = "CookieDomain",
                    Name = "CookieName",
                    Path = "CookiePath",
                    SecurePolicy = CookieSecurePolicy.Always,
                    HttpOnly = true,
                    SameSite = SameSiteMode.Lax,
                };
                opts.ExpireTimeSpan = TimeSpan.FromMinutes(20);
    
                opts.ForwardDefaultSelector = ctx =>
                {
                    var authHeader = ctx.Request.Headers["Authorization"].FirstOrDefault();
                    if (authHeader?.StartsWith(JwtBearerDefaults.AuthenticationScheme) == true)
                    {
                        return JwtBearerDefaults.AuthenticationScheme;
                    }
                    else
                    {
                        return CookieAuthenticationDefaults.AuthenticationScheme;
                    }
                };
            });
    
            return authBuilder;
        }
    

    用户登录的浏览器将接收在 SignIn 方法中创建的 cookie cookie,并且也能够通过客户端填充请求。

    【讨论】:

    • 这不会将标头添加到类库客户端请求中。为此,我们必须将当前上下文共享给 dll,正如我在自己的回答中所说的那样。
    【解决方案2】:

    好的,我自己想通了,这就是我解决问题的方法:

    1) 在 Startup.cs 上,添加一个 HttpContextAccessor :

    我们的 ASP.NET Core 网站中的上下文只能由控制器访问。类库无法访问它。

    要使其可用于类库,我们需要使用 HttPContextAccessor。这是一项我们可以用作类库的依赖注入的服务。我们只需要添加

    services.AddHttpContextAccessor();
    

    在来自Startup.csConfigureServices 方法中。

    2) 适配类库

    ClientService 需要能够接收依赖注入,如下所示:

    public class ClientService : IClientService
    {
        private readonly IHttpClientFactory _clientFactory;
        private readonly IHttpContextAccessor _contextAccessor;
        private readonly string _baseAddress = $@"https://localhost:[PORT]/api/";
    
        public ClientService(IHttpClientFactory clientFactory, IHttpContextAccessor contextAccessor)
        {
            _clientFactory = clientFactory;
            _contextAccessor = contextAccessor;
        }
    }
    

    现在我们可以通过HttpContextAccessor _contextAccessor 变量访问当前上下文。

    接下来我们要修改我们的GetPage(string model)方法:

    public async Task<string> GetPage(string model)
    {
        var request = 
            new HttpRequestMessage(
                HttpMethod.Get,
                _baseAddress +
                model);
    
        var client = _clientFactory.CreateClient();
        
        var authorization = _contextAccessor.HttpContext.Request.Headers.FirstOrDefault(x => x.Key == "Authorization").Value.FirstOrDefault();
    
        if(authorization != null)
        {
            client.DefaultRequestHeaders.Add("Authorization", authorization);
        }
        
        var response = await client.SendAsync(request);
    
        return await response.Content.ReadAsStringAsync();
    }
    

    添加的代码将尝试从请求中获取“授权”标头,如果它不为空,我们将其添加到我们自己的请求中。这允许使用 JWT 向 API 发出请求。

    3) 添加中间件以将 JWT 添加到每个请求中

    现在最后一步是将 JWT 添加到来自 ASP.NET Core 应用程序的每个请求中。如果需要,我们的类库将使用 JWT,而无需更改任何来自库或应用程序的代码。

    为了实现这一点,我们必须在Startup.cs &gt; Configure() 方法中添加一个中间件。

    中间件是这样的:

    // Add header:
    app.Use((context, next) =>
    {
        var jwt = context.Request.Cookies["JWT"];
    
        if(jwt != null)
        {
            context.Request.Headers.Add("Authorization", "Bearer " + jwt);
        }
    
        return next.Invoke();
    });
    

    这只是从请求标头中读取 Cookie,如果 JWT HttpOnly Cookie 不为空,我们将令牌添加到我们的请求中。然后,我们的类库将捕获此请求,以便使用它与 API 对话。

    需要注意的几点:

    1. 将 JWT 添加到每个请求,即使我们不需要它也可能会消耗更多带宽超时。它也可能不太安全(我不是安全专家)
    2. 将 JWT 添加到我们的主请求中,然后在我们的类库中创建一个具有自己请求的本地客户端似乎不是最佳选择。但它按预期工作,我实际上不知道如何优化它。

    请随时指出我的解决方案中的任何问题或对其进行修改以添加一些说明或其他更有效的方法来实现它。

    【讨论】:

      猜你喜欢
      • 2020-04-16
      • 2020-11-21
      • 1970-01-01
      • 1970-01-01
      • 2016-09-24
      • 2014-02-22
      • 1970-01-01
      • 2023-03-29
      • 1970-01-01
      相关资源
      最近更新 更多