当我从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.cs
中 CreateUserClaims()
函数内的 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 回答
我已经找到了部分问题 .
来自UI的令牌与.net API接收的令牌不同,我说错了 . 我说我正在检查网络选项卡中的 Headers ,而我是,但是不是正确的请求 . 我的UI发送了几个请求 - 来自不同的Angular模块 . 我在每个模块中注入了一个新的身份验证服务(我的令牌存储在其中) . 注销时,并不是模块正在刷新,所以那些没有保留旧令牌的副本 . 因此,登录时,只有受影响的模块(在我的情况下,我的主
app.module.ts
)才会刷新 . 未被触及的那些保留了身份验证服务的相同副本 .我从每个模块中删除了注入,并让它们从主
app.module.ts
继承 . 这解决了UI和API的问题似乎有不同的令牌 .我提到的另一个无法看到
nameid
索赔的问题已部分解决 .User
里面共有10个Claims
. 当我解码JWT时,它说我有sub
和nameid
. 但是,当我在UserService.cs
中检查Claims
时,它们未被列为nameid
和sub
,而是http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
和http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
. 每个都有正确的Value
. 我不确定这种情况发生在何处或如何发生 . 我创建了以下自定义中间件代码,以查看进入时令牌是什么,它的Claim
为sub
和nameid
.因此,变量
nameid
是正确的并包含期望值 . 沿着这条线的某个地方,Type
正在从nameid
和sub
变为http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier