首页 文章

请求标头中的JWT与接收.Net Core API不同

提问于
浏览
3

当我从Angular应用程序向我的.Net Core 2 API发出请求时,JWT与请求标头中发送的请求不同 .

Startup.cs

public class Startup
{
    public Startup(IHostingEnvironment env)
    {
        var builder = new ConfigurationBuilder()
            .SetBasePath(env.ContentRootPath)
            .AddJsonFile("appsettings.json", optional: false, reloadOnChange: true)
            .AddJsonFile($"appsettings.{env.EnvironmentName}.json", optional: true)
            .AddEnvironmentVariables();
        _config = builder.Build();
    }

    IConfigurationRoot _config;

    // This method gets called by the runtime. Use this method to add services to the container.
    public void ConfigureServices(IServiceCollection services)
    {
        services.AddSingleton(_config);
        services.AddDbContext<ApplicationDbContext>(ServiceLifetime.Transient);

        services.AddTransient<IEmailSender, AuthMessageSender>();
        services.AddTransient<ISmsSender, AuthMessageSender>();

        services.AddSingleton<IUserTwoFactorTokenProvider<ApplicationUser>, DataProtectorTokenProvider<ApplicationUser>>();

        // Add application services.

        // Add application repositories.

        // Add options.
        services.AddOptions();
        services.Configure<StorageAccountOptions>(_config.GetSection("StorageAccount"));

        // Add other.
        services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
        services.AddTransient<ApiExceptionFilter>();

        // this makes "this.User" reflect the properties of the jwt sent in the request
        services.AddTransient<ClaimsPrincipal>(s => s.GetService<IHttpContextAccessor>().HttpContext.User);

        services.AddIdentity<ApplicationUser, IdentityRole>(options =>
        {
            // set password complexity requirements
            options.Password.RequireDigit = true;
            options.Password.RequireLowercase = true;
            options.Password.RequireUppercase = false;
            options.Password.RequireNonAlphanumeric = false;
            options.Password.RequiredLength = 6;

            options.Tokens.ProviderMap.Add("Default",
            new TokenProviderDescriptor(typeof(IUserTwoFactorTokenProvider<ApplicationUser>)));
        }).AddEntityFrameworkStores<ApplicationDbContext>();

        services.AddAuthentication(options =>
        {
            options.DefaultScheme = JwtBearerDefaults.AuthenticationScheme;
            options.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
        }).AddJwtBearer(config =>
            {
                config.RequireHttpsMetadata = false;
                config.SaveToken = true;
                config.TokenValidationParameters = new TokenValidationParameters()
                {
                    ValidIssuer = _config["Tokens:Issuer"],
                    ValidAudience = _config["Tokens:Audience"],
                    ValidateIssuerSigningKey = true,
                    IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Tokens:Key"])),
                    ValidateLifetime = true
                };
            });
        services.AddAuthorization(config =>
        {
            config.AddPolicy("Subscribers", p => p.RequireClaim("Subscriber", "True"));
            config.AddPolicy("Artists", p => p.RequireClaim("Artist", "True"));
            config.AddPolicy("Admins", p => p.RequireClaim("Admin", "True"));
        });

        services.Configure<DataProtectionTokenProviderOptions>(o =>
        {
            o.Name = "Default";
            o.TokenLifespan = TimeSpan.FromHours(1);
        });
        services.Configure<AuthMessageSenderOptions>(_config);

        // Add framework services.
        services.AddMvc(opt =>
        {
            //opt.Filters.Add(new RequireHttpsAttribute());
        }
        ).AddJsonOptions(opt =>
        {
            opt.SerializerSettings.ReferenceLoopHandling = ReferenceLoopHandling.Ignore;
        });
    }

    // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
    public void Configure(IApplicationBuilder app, IHostingEnvironment env, ILoggerFactory loggerFactory)
    {
        loggerFactory.AddConsole(_config.GetSection("Logging"));
        loggerFactory.AddDebug();

        app.Use(async (context, next) =>
        {
            // just to check the context.User.Claims on request
            var temp = context;
            await next();
        });
        app.UseAuthentication();
        app.UseMvc();
    }
}

这是令牌发布的地方(在应用登录时)

AuthController.cs

private async Task<IList<Claim>> CreateUserClaims(ApplicationUser user)
    {
        var userClaims = await _userManager.GetClaimsAsync(user);
        var newClaims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim(JwtRegisteredClaimNames.NameId, user.Id)
        }.Union(userClaims).ToList();
        return newClaims;
    }
    private Object CreateToken(IList<Claim> claims)
    {
        var key = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(_config["Tokens:Key"]));
        var creds = new SigningCredentials(key, SecurityAlgorithms.HmacSha256);
        var token = new JwtSecurityToken(
            issuer: _config["Tokens:Issuer"],
            audience: _config["Tokens:Audience"],
            claims: claims,
            expires: DateTime.UtcNow.AddDays(29),
            signingCredentials: creds
        );
        return new
        {
            token = new JwtSecurityTokenHandler().WriteToken(token),
            expiration = token.ValidTo
        };
    }
    private async Task<Object> CreateToken(ApplicationUser user)
    {
        var claims = await CreateUserClaims(user);
        var token = CreateToken(claims);
        return token;
    }
[HttpPost("token")]
    [AllowAnonymous]
    public async Task<IActionResult> CreateToken([FromBody] CredentialModel model)
    {
        var user = await _userManager.FindByNameAsync(model.UserName);
        if (user != null)
        {
            if (_hasher.VerifyHashedPassword(user, user.PasswordHash, model.Password)
                == PasswordVerificationResult.Success)
            {
                var token = await CreateToken(user);
                return Ok(token);
            }
        }
        throw new ApiException("Bad email or password.");
    }

我已通过Chrome调试器网络选项卡确认我的请求中的JWT是我希望API获取的JWT .

Because of that I will leave the Angular request code out of this post.

这是一个通过UserId返回项目的Controller

[HttpGet]
    public async Task<IActionResult> Get()
    {
        var artists = await _manageArtistService.GetAllByUser(this.User);
        if (artists == null) return NotFound($"Artists could not be found");
        return Ok(artists);
    }

这是控制器调用的服务

public async Task<IEnumerable<ManageArtistView>> GetAllByUser(ClaimsPrincipal user)
    {
        // gets all artists of a given user, sorted by artist
        var userId = _userService.GetUserId(user);
        var artists = await _manageArtistRepository.GetAllByUser(userId);
        return artists;
    }

UserService.cs 中,我尝试了一些不同的方法来访问当前用户 . 我检查从Controller传递的 this.User .

我还检查 _context 中的当前上下文 - 您可以在 Startup.cs 中看到的单例 .

还有 _caller 来自 Startup.cs 这一行

services.AddTransient<ClaimsPrincipal>(s => s.GetService<IHttpContextAccessor>().HttpContext.User);

当我检查这些变量中的任何一个时, Claims 对象 does not 包含与请求期间发送的JWT相同的声明 .

I have verified the claims do not match by checking the claims at jwt.io.

具体来说,我将给出一个场景:

我通过电子邮件 user@example.com 登录我的应用程序 . 然后将该电子邮件设置为 AuthController.csCreateUserClaims() 函数内的 user.UserName 声明(Sub):

var newClaims = new[]
        {
            new Claim(JwtRegisteredClaimNames.Sub, user.UserName),
            new Claim(JwtRegisteredClaimNames.Jti, Guid.NewGuid().ToString()),
            new Claim(JwtRegisteredClaimNames.NameId, user.Id)
        }.Union(userClaims).ToList();

然后设置一些其他属性,最终将令牌返回给客户端 . 客户端将其存储在 localStorage 中 .

然后客户端发出请求,包括标头中的JWT,并将其添加到这样的请求选项(Angular服务):

private headers = new Headers(
    {
        'Content-Type': 'application/json',
        'Authorization': 'Bearer ' + this.authService.token
    });
private options = new RequestOptions({ headers: this.headers });

我检查网络选项卡中的 Headers ,它包含JWT - 我在jwt.io检查它看起来不错 - 有正确的电子邮件和其他声明 .

现在我可以退出应用程序,以其他用户身份登录,获取新的JWT,并向上面显示的 same controller 发出请求,JWT将收到上一封电子邮件,而不是我刚登录的新电子邮件 .

我确实完成了相同的检查,检查网络选项卡上的 Headers 中的JWT,以确保声明包含新电子邮件作为 sub 以及其他声明 .

这意味着我在新登录时获得了正确的JWT,但不知何故,API仍在关注旧的JWT .

这有多疯狂?

我注意到的其他事情是即使在第一次登录时(假装我刚用 dotnet run 启动了API,然后我向上面显示的同一个控制器发出第一个请求,它将缺少 nameid 声明 . 我可以去查看JWT在Header请求中发送它 does have the nameid claim. 所以,再次, the api will issue the proper JWT but when I send it back over HTTP in a request the API does not have the same JWT that I sent in the request.

ONE MORE THING 为了简单起见,我在控制台中记录了JWT . 我回去找到了我今天上午9点开始使用的第一个 . 它的 jti 与当前在.net核心API中的相同 . 现在是下午4:45 . 在这两次(上午9点和下午4点45分)我的控制台中有9个不同的JTW,都是从API发出的 . 但是API似乎保留了今天早上创建的第一个 - 即使在我停止并多次启动项目之后 .

请帮我理解我做错了什么 . 我不能完全理解如何处理JWT .

1 回答

  • 3

    我已经找到了部分问题 .

    来自UI的令牌与.net API接收的令牌不同,我说错了 . 我说我正在检查网络选项卡中的 Headers ,而我是,但是不是正确的请求 . 我的UI发送了几个请求 - 来自不同的Angular模块 . 我在每个模块中注入了一个新的身份验证服务(我的令牌存储在其中) . 注销时,并不是模块正在刷新,所以那些没有保留旧令牌的副本 . 因此,登录时,只有受影响的模块(在我的情况下,我的主 app.module.ts )才会刷新 . 未被触及的那些保留了身份验证服务的相同副本 .

    我从每个模块中删除了注入,并让它们从主 app.module.ts 继承 . 这解决了UI和API的问题似乎有不同的令牌 .

    我提到的另一个无法看到 nameid 索赔的问题已部分解决 . User 里面共有10个 Claims . 当我解码JWT时,它说我有 subnameid . 但是,当我在 UserService.cs 中检查 Claims 时,它们未被列为 nameidsub ,而是 http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifierhttp://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier . 每个都有正确的 Value . 我不确定这种情况发生在何处或如何发生 . 我创建了以下自定义中间件代码,以查看进入时令牌是什么,它的 Claimsubnameid .

    app.Use(async (context, next) =>
            {
                var authHeader = context.Request.Headers["Authorization"].ToString();
                if (authHeader != null && authHeader.StartsWith("bearer", StringComparison.OrdinalIgnoreCase))
                {
                    var tokenStr = authHeader.Substring("Bearer ".Length).Trim();
                    System.Console.WriteLine(tokenStr);
                    var handler = new JwtSecurityTokenHandler();
                    var token = handler.ReadToken(tokenStr) as JwtSecurityToken;
                    var nameid = token.Claims.First(claim => claim.Type == "nameid").Value;
    
                    var identity = new ClaimsIdentity(token.Claims);
                    context.User = new ClaimsPrincipal(identity);
                }
                await next();
            });
    

    因此,变量 nameid 是正确的并包含期望值 . 沿着这条线的某个地方, Type 正在从 nameidsub 变为 http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier

相关问题