Unity JWT认证引发内存泄漏:BuildServiceProvider调用问题求助
我正在排查游戏服务器的内存泄漏问题,已定位问题源于Unity JWT令牌的玩家认证模块。通过对需要认证的/global-ranking接口发起数千次请求并对比内存快照,发现关闭认证后泄漏停止,快照差异显示有大量CancellationTokenSource、日志对象等实例增长。
服务器认证配置代码
builder.Services.AddAuthentication(JwtBearerDefaults.AuthenticationScheme) .AddJwtBearer(JwtBearerDefaults.AuthenticationScheme, options => { options.IncludeErrorDetails = true; options.TokenValidationParameters = new TokenValidationParameters { IssuerSigningKeyResolver = GetRsaKey, ValidAudience = "TODO", ValidIssuer = "https://player-auth.services.api.unity.com", RequireSignedTokens = true, RequireExpirationTime = true, ValidateLifetime = true, ValidateAudience = false, ValidateIssuer = true, }; options.Events = new JwtBearerEvents() { OnTokenValidated = tokenValidationContext => { if (!tokenValidationContext.Principal.HasClaim(x => x.Type == "sub")) tokenValidationContext.Fail("Token does not contain data required to identify user"); var userService = builder.Services.BuildServiceProvider().GetRequiredService<IUserService>(); var user = userService.GetUserByUnityId(tokenValidationContext.Principal.UnityUserId()).Result; if (user == null) { tokenValidationContext.Fail("Token does not match any known user"); } else { var claimsIdentity = (ClaimsIdentity)tokenValidationContext.Principal.Identity; claimsIdentity.AddClaim(new Claim("userid", user.Id.ToString())); } return Task.CompletedTask; } }; }) .AddJwtBearer("UserRegisterScheme", options => // 用于注册接口,无需数据库中存在用户即可验证通过 { options.IncludeErrorDetails = true; options.TokenValidationParameters = new TokenValidationParameters { IssuerSigningKeyResolver = GetRsaKey, ValidAudience = "TODO", ValidIssuer = "https://player-auth.services.api.unity.com", RequireSignedTokens = true, RequireExpirationTime = true, ValidateLifetime = true, ValidateAudience = false, ValidateIssuer = true, }; options.Events = new JwtBearerEvents() { OnTokenValidated = tokenValidationContext => { if (!tokenValidationContext.Principal.HasClaim(x => x.Type == "sub")) tokenValidationContext.Fail("Token does not contain data required to identify user"); return Task.CompletedTask; } }; });
内存泄漏根源与警告
两个JWT Bearer认证的TokenValidationParameters中,IssuerSigningKeyResolver需要从Unity HTTP接口获取动态更新的RSA密钥,因此通过服务器元数据服务实现该逻辑。但在Program.cs的GetRsaKey方法中,使用builder.Services.BuildServiceProvider()获取IServerMetadataService,这会创建多个单例实例并引发内存泄漏,同时收到以下警告:
Calling 'BuildServiceProvider' from application code results in an additional copy of singleton services being created. Consider alternatives such as dependency injecting services as parameters to 'Configure'. server
对应的GetRsaKey代码:
IEnumerable<SecurityKey> GetRsaKey( string token, SecurityToken securityToken, string kid, TokenValidationParameters tokenValidationParameters) { var keyParameters = builder.Services.BuildServiceProvider() .GetRequiredService<IServerMetadataService>() .GetUgsTokenKeySignatureParameters().Result; var rsaParameters = new RSAParameters() { Modulus = Base64UrlEncoder.DecodeBytes(keyParameters.modulus), Exponent = Base64UrlEncoder.DecodeBytes(keyParameters.exponent), }; RSA rsa = RSA.Create(rsaParameters); RsaSecurityKey rsaKey = new RsaSecurityKey(rsa); return new List<SecurityKey>() { rsaKey }; }
当前困境
尝试多种解决方案后均未奏效:要么无法解决内存泄漏,要么触发“应用构建后无法修改ServiceCollection”的错误。陷入两难:应用构建前调用服务会导致单例重复创建引发泄漏,构建完成后又无法再修改认证配置。
相关控制器代码
[HttpGet("global-ranking")] public async Task<ActionResult<IEnumerable<UserSeasonViewModel>>> GetGlobalRanking() { var ranking = await _seasonService.GetGlobalRanking(); if (ranking == null) { return BadRequest($"Ranking is empty"); } return Ok(ranking); }
相关服务代码
public async Task<IEnumerable<UserSeasonViewModel>> GetGlobalRanking() { return await _context.UsersSeasonViewModel .FromSqlRaw(/* 查询语句 */) .AsNoTracking() .ToListAsync(); }
内容的提问来源于stack exchange,提问作者Ardoos

