首页
学习
活动
专区
圈层
工具
发布
社区首页 >问答首页 >使用JWT令牌从Blazor WebAssembly调用WebAssembly

使用JWT令牌从Blazor WebAssembly调用WebAssembly
EN

Stack Overflow用户
提问于 2022-09-22 15:29:16
回答 1查看 137关注 0票数 1

在Blazor WebAssembly中使用JWT时,我收到了来自Api的意外的未经授权的响应。注意,我并不试图保护WebAssembly客户机上的任何东西;只是保护API端点。我故意忽略了有效期的验证。

服务器

appsettings.json

代码语言:javascript
复制
{
  "JwtSecurity": {
    "Key": "RANDOM_KEY_MUST_NOT_BE_SHARED",
    "Issuer": "https://localhost",
    "Audience": "https://localhost",
    "ExpiryDays": 1
  }
}

Program.cs

代码语言:javascript
复制
// Service registration
builder.Services
    .AddAuthentication(auth =>
    {
        auth.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
        auth.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
    })
    .AddJwtBearer(options =>
    {
        options.SaveToken = true;
        options.TokenValidationParameters = new TokenValidationParameters
        {
            ValidateIssuer = true,
            ValidIssuer = builder.Configuration["JwtSecurity:Issuer"],
            ValidateAudience = true,
            ValidAudience = builder.Configuration["JwtSecurity:Audience"],
            ValidateIssuerSigningKey = true,
            IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(builder.Configuration["JwtSecurity:Key"])),
            RequireExpirationTime = false,
            ValidateLifetime = false
        };
    });

// Configure the HTTP request pipeline.

// SignalR Compression
app.UseResponseCompression();

if (app.Environment.IsDevelopment())
{
    app.UseWebAssemblyDebugging();
}
else
{
    app.UseExceptionHandler("/Error");
    // The default HSTS value is 30 days. You may want to change this for production scenarios, see https://aka.ms/aspnetcore-hsts.
    app.UseHsts();
}

app.UseHttpsRedirection();

// Logs the received token
app.UseJwtTokenHandler();

//explicitly only use blazor when the path doesn't start with api
app.MapWhen(ctx => !ctx.Request.Path.StartsWithSegments("/api"), blazor =>
{

    blazor.UseBlazorFrameworkFiles();
    blazor.UseStaticFiles();

    blazor.UseRouting();

    blazor.UseEndpoints(endpoints =>
    {
        endpoints.MapHub<Cosmos.App.Server.Hubs.TillSiteHub>("/tradingsessionhub");
        endpoints.MapFallbackToFile("index.html");
    });
});

//explicitly map api endpoints only when path starts with api
app.MapWhen(ctx => ctx.Request.Path.StartsWithSegments("/api"), api =>
{
    api.UseStaticFiles();
    api.UseRequestLogging();

    api.UseRouting();
    api.UseAuthentication();
    api.UseAuthorization();
    // HAVE ALSO TRIED
    // api.UseAuthentication();
    // api.UseRouting();
    // api.UseAuthorization();

    api.UseErrorHandling();

    api.UseEndpoints(endpoints =>
    {
        endpoints.MapControllers();
    });
});

app.Run();
  • 这个链接建议UseRouting应该先于UseAuthentication和UseAuthorisation。
  • 这个链接建议UseRouting应该介入他们之间。

两次尝试都没有结果。

基于登录的令牌生成

帮助者阶级

代码语言:javascript
复制
public class JwtHelper
{
    public static JwtSecurityToken GetJwtToken(
        string username,
        string signingKey,
        string issuer,
        string audience,
        TimeSpan expiration,
        Claim[] additionalClaims = null)
    {
        var claims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub,username),
            // this guarantees the token is unique
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString())
        };

        if (additionalClaims is object)
        {
            var claimList = new List<Claim>(claims);
            claimList.AddRange(additionalClaims);
            claims = claimList.ToArray();
        }

        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(signingKey));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);

        return new JwtSecurityToken(
            issuer: issuer,
            audience: audience,
            expires: DateTime.UtcNow.Add(expiration),
            claims: claims,
            signingCredentials: creds
        );
    }
}

登录控制器方法

代码语言:javascript
复制
    Guid userGid = await loginManager.LoginAsync(request.Email!, request.Password!);

    if (userGid == default)
    {
        return base.NotFound();
    }

    List<Claim> claims = new List<Claim>();
    claims.Add(new Claim(ClaimTypes.NameIdentifier, userGid.ToString()));
    claims.Add(new Claim(ClaimTypes.Name, userGid.ToString()));

    string key = configuration["JwtSecurity:Key"];
    string issuer = configuration["JwtSecurity:Issuer"];
    string audience = configuration["JwtSecurity:Audience"];
    string expiryDays = configuration["JwtSecurity:ExpiryDays"];
    TimeSpan expiry = TimeSpan.FromDays(Convert.ToInt32(expiryDays));

    var token = JwtHelper.GetJwtToken(
        userGid.ToString(),
        key,
        issuer,
        audience,
        expiry,
        claims.ToArray());

    LoginResponse response = new(new JwtSecurityTokenHandler().WriteToken(token));

    return base.Ok(response);

令牌字符串响应存储在本地存储中。

客户端

向Http客户端添加标头

代码语言:javascript
复制
// GetTokenAsync retrieve the string from Local Storage
_httpClient.DefaultRequestHeaders.Authorization =
    new System.Net.Http.Headers.AuthenticationHeaderValue(
        "bearer",
        (await _accessControlStateManager.GetTokenAsync())!.Write());

中间件对接收令牌的服务器日志记录

代码语言:javascript
复制
public class JwtTokenHandlerMiddleware
{
    readonly RequestDelegate _next;
    readonly ILogger _logger;

    public JwtTokenHandlerMiddleware(
        RequestDelegate next,
        ILoggerFactory loggerFactory)
    {
        _next = next;
        _logger = loggerFactory.CreateLogger(typeof(JwtTokenHandlerMiddleware).FullName!);
    }

    public async Task Invoke(HttpContext context)
    {
        JwtSecurityToken? jwt = context.GetJwtTokenFromAuthorizationHeader();

        if (jwt != null)
        {
            _logger.LogInformation("Request received with token: {uri}", context.Request.GetDisplayUrl());
            _logger.LogInformation("Token: {token}", jwt.Write());

        }
        else
        {
            _logger.LogInformation("Request received from ANONYMOUS: {uri}", context.Request.GetDisplayUrl());
        }

        await _next(context);
    }
}

        public static JwtSecurityToken? GetJwtTokenFromAuthorizationHeader(this HttpContext httpContext)
        {
            string text = httpContext.Request.Headers["Authorization"].FirstOrDefault()?.Split(" ").Last();
            if (string.IsNullOrWhiteSpace(text))
            {
                return null;
            }

            return new JwtSecurityTokenHandler().ReadJwtToken(text);
        }
  • 我已从日志中确认正在收到JWT。
  • 使用jwt.io,我可以确认登录在请求中间件中的令牌是否可以读取。

jwt.io输出

鉴于以上所述,似乎:

  • 我正在正确地生成JWT。
  • 正在从本地存储中正确地存储和检索JWT
  • 在请求中的授权头中接收JWT
  • 控制器级别使用授权属性(没有提到的角色)来保护控制器。

但我还是被授权了。

有什么建议吗?

试图隔离

禁用验证参数

试过:

代码语言:javascript
复制
options.TokenValidationParameters = new TokenValidationParameters
{
    ValidateIssuer = false,
    ValidateAudience = false,
    ValidateIssuerSigningKey = false,
    RequireExpirationTime = false,
    ValidateLifetime = false
};

并添加了简单的测试控制器:

代码语言:javascript
复制
using Microsoft.AspNetCore.Authorization;

namespace Cosmos.App.Server.Controllers;
[Route("api/[controller]")]
[ApiController]
[Authorize]
public class TestController : ControllerBase
{
    [HttpGet]
    [Route("test")]
    public async Task<IActionResult> TestAsync()
    {
        await Task.Delay(1);

        return Ok("Hello");
    }
}

但同样的问题。

EN

回答 1

Stack Overflow用户

回答已采纳

发布于 2022-09-22 20:02:43

找到了!

问题是,我将登录返回的字符串缓存为令牌,以便能够快速访问客户端中的声明(同时将从登录LocalStorage中收到的字符串保存起来)。

然后,当将令牌放在HttpClient上时,如果我有一个缓存的令牌,我就会将它写成字符串来填充Http请求上的授权。

问题在于从初始登录中接收到的字符串,例如,

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGU2YWZhNy00NGYwLTRlNTUtODgxMy0xMTRmNGY1OWE2NzIiLCJqdGkiOiI2MWYwYTRiMi0wNjQwLTRiMjgtYmM2Mi0zMDZlYTVmYmJiM2UiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6Ijk0ZTZhZmE3LTQ0ZjAtNGU1NS04ODEzLTExNGY0ZjU5YTY3MiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiI5NGU2YWZhNy00NGYwLTRlNTUtODgxMy0xMTRmNGY1OWE2NzIiLCJleHAiOjE2NjM5NjEwMjMsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0IiwiYXVkIjoiaHR0cHM6Ly9sb2NhbGhvc3QifQ.5l9LRYIx3wXruW7BMa1DDbEoltVgP6Fbfkc2O03XAAY

在读取回缓存令牌时被截断。它似乎截断了我认为是签名密钥的内容。缺少了以下内容:

5l9LRYIx3wXruW7BMa1DDbEoltVgP6Fbfkc2O03XAAY

这意味着我将完整的字符串存储在本地存储中,但是没有签名密钥的缓存令牌。

然后,当使用缓存的令牌为我得到的授权头写入字符串时:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiI5NGU2YWZhNy00NGYwLTRlNTUtODgxMy0xMTRmNGY1OWE2NzIiLCJqdGkiOiI2MWYwYTRiMi0wNjQwLTRiMjgtYmM2Mi0zMDZlYTVmYmJiM2UiLCJodHRwOi8vc2NoZW1hcy54bWxzb2FwLm9yZy93cy8yMDA1LzA1L2lkZW50aXR5L2NsYWltcy9uYW1laWRlbnRpZmllciI6Ijk0ZTZhZmE3LTQ0ZjAtNGU1NS04ODEzLTExNGY0ZjU5YTY3MiIsImh0dHA6Ly9zY2hlbWFzLnhtbHNvYXAub3JnL3dzLzIwMDUvMDUvaWRlbnRpdHkvY2xhaW1zL25hbWUiOiI5NGU2YWZhNy00NGYwLTRlNTUtODgxMy0xMTRmNGY1OWE2NzIiLCJleHAiOjE2NjM5NjEwMjMsImlzcyI6Imh0dHBzOi8vbG9jYWxob3N0IiwiYXVkIjoiaHR0cHM6Ly9sb2NhbGhvc3QifQ.

没有签名键的后缀。

这意味着对该字符串的验证在服务器上失败,即使jwt.io会很高兴地读取它。

使用本地存储中存储的完整字符串,而不是缓存令牌中的“书面”字符串解决了问题。

票数 0
EN
页面原文内容由Stack Overflow提供。腾讯云小微IT领域专用引擎提供翻译支持
原文链接:

https://stackoverflow.com/questions/73817224

复制
相关文章

相似问题

领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档