Rob*_*Rob 5 oauth access-token openid-connect refresh-token blazor-server-side
Microsoft 建议不要HttpContext在 Blazor Server 中使用(此处)。为了解决如何将用户令牌传递到 Blazor Server 应用程序的问题,Microsoft 建议将令牌存储在服务中Scoped(此处)。Jon McGuire\xe2\x80\x99s 博客建议采用类似的方法将令牌存储在Cache(此处)中。
只要用户保持在同一个 Blazor 服务器连接中,上述 Microsoft\xe2\x80\x99s 方法就可以正常工作。但是,如果access_token刷新并且用户通过按 F5 或将 URL 粘贴到地址栏中来重新加载页面,则会尝试从 cookie 中检索令牌。此时, cookie中的和access_token就refresh_token不再有效了。Jon McGuire 在他的博客文章末尾提到了这个问题,并将其称为 Stale Cookies(此处)。他给出了有关可能的解决方案的提示,但对实施说明却很少提及。该帖子的底部有许多来自无法实施解决方案的人的评论,但没有提出明显的可行解决方案。我花了很多时间寻找解决方案,但我发现人们都在寻求解决方案,但没有收到任何有效的答案。
找到了一个似乎运行良好并且似乎相当有原则的解决方案后,我认为可能值得在这里分享我的解决方案。我欢迎任何建设性的批评或对任何重大改进的建议。
\n编辑 20220715:在 Dominic Baier 对我们的方法提供一些反馈后,我们删除了我们的Scoped UserSubProvider服务,转而使用AuthenticationStateProvider。这简化了我们的方法。我编辑了以下答案以反映这一变化。
此方法结合了 Microsoft 关于如何将令牌传递到 Blazor Server 应用程序(此处)的建议,以及在服务中为所有用户提供令牌的服务器端存储Singleton(受到 GitHub 上的 Dominick Baier\xe2\x80\x99s Blazor Server 示例项目的启发,此处))。
我们没有捕获_Host.cshtml文件中的令牌并将其存储在Scoped服务中(就像 Microsoft 在其示例中所做的那样),而是OnTokenValidated以与 Dominick Baier\xe2\x80\x99s 示例类似的方式使用该事件,将令牌存储在Singleton为所有人持有代币的服务Users,我们称之为服务ServerSideTokenStore。
当我们使用 ourHttpClient来调用 API 并且它需要一个access_token( 或refresh_token) 时,它会User从注入的 中检索 \xe2\x80\x99s sub AuthenticationStateProvider,使用它来调用ServerSideTokenStore.GetTokensAsync(),这会返回一个UserTokenProvider(类似于 Microsoft\xe2\x80\x99s TokenProvider)包含令牌。如果HttpClient需要刷新令牌,则它会填充 aUserTokenProvider并通过调用 来保存它ServerSideTokenStore.SetTokensAsync()。
我们遇到的另一个问题是,如果在应用程序重新启动时打开 Web 浏览器的单独实例(因此丢失 中保存的数据ServerSideTokenStore),用户仍将使用 cookie 进行身份验证,但我们\xe2\x80\x99已经丢失了access_token和refresh_token。如果重新启动应用程序,这种情况可能会在生产中发生,但在开发环境中发生的频率要高得多。如果我们无法OnValidatePrincipal获得RejectPrincipal()合适的access_token. 这会强制往返 IdentityServer,后者提供了新access_token的refresh_token. 这种方法来自这个堆栈溢出线程。
(为了清楚/重点,下面的一些代码排除了一些标准错误处理、日志记录等)
\n从 AuthenticationStateProvider 获取 User 子声明
\n我们从注入HttpClient的. 它在调用and时使用字符串。subAuthenticationStateProvideruserSubServerSideTokenStore.GetTokensAsync()ServerSideTokenStore.SetTokensAsync()
var state = await AuthenticationStateProvider.GetAuthenticationStateAsync();\n string userSub = state.User.FindFirst("sub")?.Value;\nRun Code Online (Sandbox Code Playgroud)\n用户令牌提供者
\n public class UserTokenProvider\n {\n public string AccessToken { get; set; }\n public string RefreshToken { get; set; }\n public DateTimeOffset Expiration { get; set; }\n }\nRun Code Online (Sandbox Code Playgroud)\n服务器端令牌存储
\n public class ServerSideTokenStore\n {\n private readonly ConcurrentDictionary<string, UserTokenProvider> UserTokenProviders = new();\n \n public Task ClearTokensAsync(string userSub)\n {\n UserTokenProviders.TryRemove(userSub, out _);\n return Task.CompletedTask;\n }\n \n public Task<UserTokenProvider> GetTokensAsync(string userSub)\n {\n UserTokenProviders.TryGetValue(userSub, out var value);\n return Task.FromResult(value);\n }\n \n public Task StoreTokensAsync(string userSub, UserTokenProvider userTokenProvider)\n {\n UserTokenProviders[userSub] = userTokenProvider;\n Return Task.CompletedTask;\n }\n }\nRun Code Online (Sandbox Code Playgroud)\nStartup.csConfigureServices(或等效位置,如果使用.NET 6/其他)
\n public void ConfigureServices(IServiceCollection services)\n {\n // \xe2\x80\xa6\n services.AddAuthentication(\xe2\x80\xa6)\n .AddCookie(\xe2\x80\x9cCookies\xe2\x80\x9d, options =>\n {\n // \xe2\x80\xa6\n options.Events.OnValidatePrincipal = async context =>\n {\n if (context.Principal.Identity.IsAuthenticated)\n {\n // get user sub \n var userSub = context.Principal.FindFirst(\xe2\x80\x9csub\xe2\x80\x9d).Value;\n // get user\'s tokens from server side token store\n var tokenStore =\n context.HttpContext.RequestServices.GetRequiredService<IServerSideTokenStore>();\n var tokens = await tokenStore.GetTokenAsync(userSub);\n if (tokens?.AccessToken == null \n || tokens?.Expiration == null \n || tokens?.RefreshToken == null)\n {\n // if we lack either an access or refresh token,\n // then reject the Principal (forcing a round trip to the id server)\n context.RejectPrincipal();\n return;\n }\n // if the access token has expired, attempt to refresh it\n if (tokens.Expiration < DateTimeOffset.UtcNow) \n {\n // we have a custom API client that takes care of refreshing our tokens \n // and storing them in ServerSideTokenStore, we call that here\n // \xe2\x80\xa6\n // check the tokens have been updated\n var newTokens = await tokenStore.GetTokenAsync(userSubProvider.UserSub);\n if (newTokens?.AccessToken == null \n || newTokens?.Expiration == null \n || newTokens.Expiration < DateTimeOffset.UtcNow)\n {\n // if we lack an access token or it was not successfully renewed, \n // then reject the Principal (forcing a round trip to the id server)\n context.RejectPrincipal();\n return;\n }\n }\n }\n }\n }\n .AddOpenIdConnect(\xe2\x80\x9coidc\xe2\x80\x9d, options =>\n {\n // \xe2\x80\xa6\n options.Events.OnTokenValidated = async n =>\n {\n var svc = n.HttpContext.RequestServices.GetRequiredService<IServerSideTokenStore>();\n var culture = new CultureInfo(\xe2\x80\x9cEN\xe2\x80\x9d) ;\n var exp = DateTimeOffset\n .UtcNow\n .AddSeconds(double.Parse(n.TokenEndpointResponse !.ExpiresIn, culture));\n var userTokenProvider = new UserTokenProvider() \n {\n AcessToken = n.TokenEndpointResponse.AccessToken,\n Expiration = exp,\n RefreshToken = n.TokenEndpointResponse.RefreshToken\n }\n await svc.StoreTokensAsync(n.Principal.FindFirst(\xe2\x80\x9csub\xe2\x80\x9d).Value, userTokenProvider);\n };\n // \xe2\x80\xa6\n });\n // \xe2\x80\xa6\n }\nRun Code Online (Sandbox Code Playgroud)\n
| 归档时间: |
|
| 查看次数: |
2135 次 |
| 最近记录: |