В настоящее время я создаю простое веб-приложение, которое создаст Jwt с токеном обновления (хранящимся в БД). Никаких проблем здесь нет, но когда генерируется токен обновления, он продолжает добавляться к аудитории внутри Jwt, и я не знаю, почему. Создание и обновление обрабатываются ядром dot net core 8 с использованием Angular 17 в качестве внешнего интерфейса.
Вот что я получаю после нескольких обновлений токена:
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/name": "[Removed]",
"http://schemas.microsoft.com/ws/2008/06/identity/claims/role": "[Removed]",
"http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier": "[Removed]",
"exp": 1718882692,
"iss": "https://localhost:4200",
"aud": [
"https://localhost:4200",
"https://localhost:4200",
"https://localhost:4200",
"https://localhost:4200",
"https://localhost:4200",
"https://localhost:4200",
"https://localhost:4200",
"https://localhost:4200",
"https://localhost:4200"
]
}
Я использовал это руководство по адаптации моего кода для создания токена обновления. В коде есть незначительные изменения, поскольку пользовательские данные, хранящиеся в Jwt, представляют собой значение идентификатора пользователя Entra, а не значение идентификатора пользователя БД.
Я хочу убедиться, что эмитент и аудитория проверены, поэтому я обязательно включил их, и они определены в appsettings.json (я только что использовал URL-адрес адреса, на котором они работают, либо локальный хост, либо окончательный веб-домен).
Вот применимый код, показывающий добавление аудитории в Jwt, а также код обновления и связанные модели.
Программа.cs
builder.Services.AddTransient<ITokenService, TokenService>();
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var jwtSettings = builder.Configuration.GetSection("JwtSettingsCommon");
var environment = builder.Configuration.GetSection("Production");
if (env == "Development")
{
environment = builder.Configuration.GetSection("Development");
}
var JwtSecret = jwtSettings["SigningKey"];
if (JwtSecret != null)
{
builder.Services.AddAuthentication(opt =>
{
opt.DefaultAuthenticateScheme = JwtBearerDefaults.AuthenticationScheme;
opt.DefaultChallengeScheme = JwtBearerDefaults.AuthenticationScheme;
})
.AddJwtBearer(options =>
{
options.TokenValidationParameters = new TokenValidationParameters
{
ValidateIssuer = true,
ValidateAudience = true,
ValidateLifetime = true,
ValidateIssuerSigningKey = true,
ValidIssuer = environment["URI"],
ValidAudience = environment["URI"],
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSecret))
};
});
}
Пользовательдатаконтроллер.cs
[HttpPost("refresh")]
public async Task<IActionResult> Refresh(JwtToken tokenApiModel)
{
if (tokenApiModel is null)
return BadRequest(new { status = "error", message = "Invalid client request (JwtToken)" });
string accessToken = tokenApiModel.Token;
string refreshToken = tokenApiModel.RefreshToken;
var principal = _tokenService.GetPrincipalFromExpiredToken(accessToken);
var token = new JwtSecurityToken(accessToken);
var userId = token.Claims.First(x => x.Type == "http://schemas.xmlsoap.org/ws/2005/05/identity/claims/nameidentifier").Value;
var userQuery = from users in _dbContext.Set<UserData>()
join refreshTokens in _dbContext.Set<JwtRefreshTokens>()
on users.EntraUserID equals userId
select refreshTokens;
var user = userQuery.FirstOrDefault();
if (user is null || user.RefreshToken != refreshToken)
return BadRequest(new { status = "error", message = "Invalid client request (checking Jwt refresh token in DB" });
else if (user.RefreshTokenExpiryTime <= DateTime.Now)
return Ok(new { status = "error", message = "Refresh token expired" });
var newAccessToken = _tokenService.GenerateAccessToken(principal.Claims);
var newRefreshToken = _tokenService.GenerateRefreshToken();
user.RefreshToken = newRefreshToken;
user.RefreshTokenExpiryTime = DateTime.Now.AddDays(1);
_dbContext.Update(user);
await _dbContext.SaveChangesAsync();
return Ok(new JwtToken()
{
Token = newAccessToken,
RefreshToken = newRefreshToken
});
}
auth.service.ts
async tryRefreshingTokens(token: string): Promise<boolean> {
const refreshToken: string = localStorage.getItem("refreshToken");
if (!token || !refreshToken) {
console.info('No token or refreshToken')
return false;
}
const credentials = JSON.stringify({ token: token, refreshToken: refreshToken });
let isRefreshSuccess: boolean;
const refreshRes = await new Promise<JwtToken>((resolve, reject) => {
this.http.post<JwtToken>(`${environment.apiUrl}/userdata/refresh`, credentials, {
headers: new HttpHeaders({
"Content-Type": "application/json"
})
}).subscribe({
next: (res: JwtToken) => resolve(res),
error: (_) => { reject; isRefreshSuccess = false; }
});
});
var validToken = this.jwtHelper.isTokenExpired();
if (validToken) {
localStorage.setItem("jwt", refreshRes.token);
localStorage.setItem("refreshToken", refreshRes.refreshToken);
isRefreshSuccess = true;
} else {
isRefreshSuccess = false;
}
return isRefreshSuccess;
}
auth.guard.ts
export const canActivate: CanActivateFn = (
route: ActivatedRouteSnapshot,
state: RouterStateSnapshot
) => {
const jwtHelper = inject(JwtHelperService);
const authService = inject(AuthService);
const router = inject(Router)
const token = localStorage.getItem("jwt");
if (token && !jwtHelper.isTokenExpired(token)) {
return true;
}
const isRefreshSuccess = authService.tryRefreshingTokens(token);
if (!isRefreshSuccess) {
router.navigate(['/unauthorised']);
}
return isRefreshSuccess;
}
ТокенService.cs
public class TokenService(IConfiguration configuration) : ITokenService
{
private readonly IConfiguration _configuration = configuration;
public string GenerateAccessToken(IEnumerable<Claim> claims)
{
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var jwtSettings = _configuration.GetSection("JwtSettingsCommon");
var environment = _configuration.GetSection("Production");
if (env == "Development")
{
environment = _configuration.GetSection("Development");
}
var JwtSecret = jwtSettings["SigningKey"];
var Issuer = environment["URI"];
var Audience = environment["URI"];
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSecret));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
int expiry;
try
{
expiry = Int32.Parse(jwtSettings["Expires"]);
}
catch (FormatException)
{
expiry = 10;
}
var tokenOptions = new JwtSecurityToken(
issuer: Issuer,
audience: Audience,
claims: claims,
expires: DateTime.Now.AddMinutes(expiry),
signingCredentials: signinCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions);
return tokenString;
}
public string GenerateRefreshToken()
{
var randomNumber = new byte[32];
using (var rng = RandomNumberGenerator.Create())
{
rng.GetBytes(randomNumber);
return Convert.ToBase64String(randomNumber);
}
}
public ClaimsPrincipal GetPrincipalFromExpiredToken(string token)
{
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var jwtSettings = _configuration.GetSection("JwtSettingsCommon");
var environment = _configuration.GetSection("Production");
if (env == "Development")
{
environment = _configuration.GetSection("Development");
}
var JwtSecret = jwtSettings["SigningKey"];
var Issuer = environment["URI"];
var Audience = environment["URI"];
var tokenValidationParameters = new TokenValidationParameters
{
ValidateAudience = true,
ValidateIssuer = true,
ValidateIssuerSigningKey = true,
IssuerSigningKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSecret)),
ValidateLifetime = false,
ValidIssuer = Issuer,
ValidAudience = Audience
};
var tokenHandler = new JwtSecurityTokenHandler();
SecurityToken securityToken;
var principal = tokenHandler.ValidateToken(token, tokenValidationParameters, out securityToken);
var jwtSecurityToken = securityToken as JwtSecurityToken;
if (jwtSecurityToken == null || !jwtSecurityToken.Header.Alg.Equals(SecurityAlgorithms.HmacSha256, StringComparison.InvariantCultureIgnoreCase))
throw new SecurityTokenException("Invalid token");
return principal;
}
}
ITokenService.cs
public interface ITokenService
{
string GenerateAccessToken(IEnumerable<Claim> claims);
string GenerateRefreshToken();
ClaimsPrincipal GetPrincipalFromExpiredToken(string token);
}
JwtToken.cs
public class JwtToken
{
public string Token { get; set; } = string.Empty;
public string RefreshToken { get; set; } = string.Empty;
}
Вам следует изменить этот метод, чтобы аудитория не добавлялась повторно. Ставьте лайк var newClaims = claims.Where(claim => claim.Type != JwtRegisteredClaimNames.Aud).ToList(); newClaims.Add(new Claim(JwtRegisteredClaimNames.Aud, Audience)); и используйте его в JwtSecurityToken(claims: newClaims,)
Абсолютно идеально, спасибо, при проверке теперь к претензиям больше аудиторий не добавляется. Я просто не мог увидеть или понять, почему добавлялись только «Аудитории», а не другие поля. Мне пришлось удалить аудиторию из new JwtSecurityToken(), тогда все заработало без дублирования.
Привет, Джейсон, не против, если я обобщу свои комментарии в виде ответа ниже, это поможет другим, кто столкнется с той же проблемой в будущем, спасибо.
Пожалуйста, не стесняйтесь — я добавлю свои комментарии и покажу обновленный код. Спасибо за вашу неоценимую помощь!
Привет Джейсон, спасибо за понимание.





Измените метод GenerateAccessToken, как показано ниже, чтобы аудитория не добавлялась повторно.
public string GenerateAccessToken(IEnumerable<Claim> claims)
{
var env = Environment.GetEnvironmentVariable("ASPNETCORE_ENVIRONMENT");
var jwtSettings = _configuration.GetSection("JwtSettingsCommon");
var environment = _configuration.GetSection("Production");
if (env == "Development")
{
environment = _configuration.GetSection("Development");
}
var JwtSecret = jwtSettings["SigningKey"];
var Issuer = environment["URI"];
var Audience = environment["URI"];
var secretKey = new SymmetricSecurityKey(Encoding.UTF8.GetBytes(JwtSecret));
var signinCredentials = new SigningCredentials(secretKey, SecurityAlgorithms.HmacSha256);
int expiry;
try
{
expiry = Int32.Parse(jwtSettings["Expires"]);
}
catch (FormatException)
{
expiry = 10;
}
// Suggestion: Remove duplicate audiences if any
var distinctAudiences = claims.Where(c => c.Type == JwtRegisteredClaimNames.Aud).Select(c => c.Value).Distinct().ToList();
var filteredClaims = claims.Where(c => c.Type != JwtRegisteredClaimNames.Aud).ToList();
filteredClaims.Add(new Claim(JwtRegisteredClaimNames.Aud, Audience));
var tokenOptions = new JwtSecurityToken(
issuer: Issuer,
audience: filteredClaims,
claims: claims,
expires: DateTime.Now.AddMinutes(expiry),
signingCredentials: signinCredentials
);
var tokenString = new JwtSecurityTokenHandler().WriteToken(tokenOptions);
return tokenString;
}
Метод GenerateAccessToken в вашем TokenService.cs может создавать новые JWT на основе существующих утверждений, что приводит к накоплению.