首页 > 解决方案 > 调用 API 时使用 JWT 的困惑

问题描述

我编写了一个使用 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),
                };
            });

然后我将中间件添加app.UseAuthentication();到 Configure 方法中。

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

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

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

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

然后我调用api/Usersin GET 将先前获得的 JWT 传递给 Postman 设置Authorization > 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我决定将它与AntiForgeryTokenASP.NET Core 的本机函数一起存储在客户端。我写了这段代码来存储 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#jwtasp.net-core-webapiasp.net-core-3.1

解决方案


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

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

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

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

services.AddHttpContextAccessor();

ConfigureServices方法中从Startup.cs.

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 > 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 添加到我们的主请求中,然后在我们的类库中创建一个具有自己请求的本地客户端似乎不是最佳选择。但它按预期工作,我实际上不知道如何优化它。

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


推荐阅读