問題描述
當我從我的 Angular 應用程序向我的 .Net Core 2 API 發出請求時,JWT 與請求標頭中發送的不同.
When I make a request to my .Net Core 2 API from my Angular app the JWT is not the same as the one sent in the request header.
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();
}
}
這是頒發令牌的地方(在應用登錄時)
This is where the token gets issued (on app login)
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.
I have confirmed through the Chrome debugger Network tab that the JWT in my request is the JWT I want the API to get.
因此,我將在這篇文章中保留 Angular 請求代碼.
這是一個通過 UserId 返回項目的控制器
Here is a Controller that returns items by UserId
[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
中,我嘗試了幾種訪問當前用戶的不同方法.我檢查了從控制器傳遞的 this.User
.
In the UserService.cs
I have attempted a few different means of accessing the current user. I check the this.User
that was passed from the Controller.
我還在 _context
中檢查當前上下文 - 您可以在 Startup.cs
中看到一個單例.
I also check the current context in _context
- a Singleton you can see in the Startup.cs
.
還有 _caller
來自 Startup.cs
services.AddTransient<ClaimsPrincipal>(s => s.GetService<IHttpContextAccessor>().HttpContext.User);
當我檢查任何這些變量時,Claims
對象不包含與請求期間發送的 JWT 相同的聲明.
When I inspect any of those variables, the Claims
object does not contain the same claims as the JWT that was sent during the request.
我已通過檢查 jwt.io 上的聲明驗證聲明不匹配.
I have verified the claims do not match by checking the claims at jwt.io.
具體來說,我給出一個場景:
To be specific, I'll give a scenario:
我使用電子郵件 user@example.com
登錄我的應用程序.然后,該電子郵件在 AuthController.cs
的 CreateUserClaims()
函數中設置為聲明 (Sub) 為 user.UserName
::p>
I sign into my app with email user@example.com
. That email is then set as a claim (Sub) as user.UserName
inside the CreateUserClaims()
function in the AuthController.cs
:
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
中.
Then some other properties are set and eventually the token is returned to the client. The client stores it in localStorage
.
然后客戶端發出請求,包括標頭中的 JWT,并將其添加到請求選項中,如下所示(Angular 服務):
The client then makes a request, including the JWT in the header and adds it to the request options like this (Angular service):
private headers = new Headers(
{
'Content-Type': 'application/json',
'Authorization': 'Bearer ' + this.authService.token
});
private options = new RequestOptions({ headers: this.headers });
我在網絡"選項卡中檢查標題,它包含 JWT - 我在 jwt.io 上檢查它看起來不錯 - 有正確的電子郵件和其他聲明.
I check the Header in the Network tab and it contains the JWT - I check it on jwt.io and it looks good - has the proper email and other claims.
現在我可以退出應用程序,以其他用戶身份登錄,獲取新的 JWT,然后向上面顯示的同一控制器發出請求,JWT 將收到之前的電子郵件,而不是我剛剛登錄的新用戶.
Now I can logout of the app, sign in as a different user, get a new JWT, and make the request to that same controller shown above and the JWT will have the previous email, not the new one that I just signed in as.
我確實進行了相同的檢查,檢查了網絡選項卡上標題中的 JWT,以確保聲明包含作為 sub
的新電子郵件以及其他聲明.
And I did go through the same checks, checking the JWT in the Header on the Network tab to ensure the claims contain the new email as the sub
as well as the other claims.
這意味著我在新登錄時獲得了正確的 JWT,但不知何故 API 仍在查看舊的 JWT.
So that means I was issued the proper JWT on the new sign in, but somehow the API is still looking at the old JWT.
這有多瘋狂?
我注意到的另一件事是,即使在第一次登錄時(假設我剛剛使用 dotnet run
啟動 API,然后我向上面顯示的同一個控制器發出第一個請求,它也會丟失nameid
聲明.我可以去檢查在 Header 請求中發送的 JWT,它確實有 nameid
聲明. 所以,再次,api 會發出正確的 JWT,但是當我在請求中通過 HTTP 將其發回時,API 的 JWT 與我在請求中發送的不同.
Something else I have noticed is that even on that first login (pretend I just started the API with dotnet run
and then I make my first request to the same controller shown above it will be missing the nameid
claim. I can go check the JWT that was sent in the Header request and it does have the nameid
claim. So, again, 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.
還有一件事為簡單起見,我將 JWT 記錄在控制臺中.我回去發現我今天早上 9 點開始使用的第一個.它的 jti
與當前在 .net 核心 API 中的相同.現在是下午 4 點 45 分.在這兩次(上午 9 點和下午 4 點 45 分)之間,我的控制臺中有 9 個不同的 JTW,它們都是從 API 發出的.但該 API 似乎保留了它今天早上創建的第一個 - 即使在我多次停止并啟動該項目之后也是如此.
ONE MORE THING
I log the JWT in the console for simplicity. I went back and found the first one I started using today, at 9am. Its jti
is the same as the one that is currently in the .net core API. It's now 4:45pm. I have 9 different JTWs in my console between those two times (9am and 4:45pm), all issued from the API. But the API seems to have kept the first one it created this morning - even after I have stopped and started the project numerous times.
請幫助我理解我做錯了什么.我一定沒有完全理解 JWT 的處理方式.
Please help me understand what I am doing wrong. I must not be fully understanding how JWTs are handled.
推薦答案
我已經解決了部分問題.
I have figured out part of my problem.
我說來自 UI 的令牌不同于 .net API 接收的令牌是錯誤的.我說我正在檢查網絡選項卡中的標題,我是,但不是正確的請求.我的 UI 發送了幾個請求——來自不同的 Angular 模塊.我在每個模塊中注入了一個新的身份驗證服務(我的令牌存儲在其中).在注銷時,模塊不會被刷新,因此那些沒有保留其舊令牌副本的模塊.因此,在登錄時,只有受影響的模塊(在我的例子中是我的主 app.module.ts
)被刷新.那些沒有被觸及的保留他們相同的身份驗證服務副本.
I was wrong in saying the token coming from the UI was different than what the .net API was receiving. I said I was inspecting the Header in the Network tab, and I was, but just not the correct request. My UI was sending several requests - from different Angular modules. I was injecting a new authentication service (where my token is stored) in each module. On logout, not ever module was getting refreshed, so those that were not kept their old copy of the token. Therefore, upon login, only the affected modules (in my case, my main app.module.ts
) were getting refreshed. The ones that had not been touched kept their same copy of the authentication service.
我從每個模塊中刪除了注入,并讓它們從主 app.module.ts
繼承這解決了 UI 和 API 似乎具有不同令牌的問題.
I removed the injection from each module and let them inherit from the main app.module.ts
That fixed the issue of the UI and API appearing to have different tokens.
我提到的另一個無法看到 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/聲明/名稱標識符
.每個都有正確的 Value
.我不確定這是在哪里或如何發生的.我創建了以下自定義中間件代碼,以查看進入時令牌是什么,它具有 Claim
作為 sub
和 nameid
.
The other issue I mentioned of not being able to see the nameid
claim is partially resolved. I have a total of 10 Claims
inside User
. When I decode the JWT it says I have sub
and nameid
. However, when I inspect Claims
in my UserService.cs
they are not listed as nameid
and sub
, but rather http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
and http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
. Each have the correct Value
. I am not sure where or how this happens. I created the following custom middleware code to see what the token was when coming in and it has the Claim
as sub
and nameid
.
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
正在從 nameid
和 sub
更改為 http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
So, the variable nameid
is right and contains the expected value. Somewhere along the line the Type
is changing from nameid
and sub
to http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier
這篇關于Request Header 中的 JWT 在接收 .Net Core API 時不一樣的文章就介紹到這了,希望我們推薦的答案對大家有所幫助,也希望大家多多支持html5模板網!