Optimizely CMS 11升级至12后页面空白,未命中StartPage控制器
Optimizely CMS 11升级到12后访问显示空白页问题
我正在将站点从Optimizely CMS 11升级到12,访问时显示空白页且无任何报错信息。预期应跳转至Start Page(首页),但调试时断点从未命中StartPage Controller,请求流程在某处中断,直接返回空白页。
代码片段
Startup.cs
public class Startup { private readonly IWebHostEnvironment _webHostingEnvironment; private readonly IConfiguration _configuration; public Startup(IWebHostEnvironment webHostingEnvironment, IConfiguration configuration) { _webHostingEnvironment = webHostingEnvironment; _configuration = configuration; } public void ConfigureServices(IServiceCollection services) { if (_webHostingEnvironment.IsDevelopment()) { AppDomain.CurrentDomain.SetData("DataDirectory", Path.Combine(_webHostingEnvironment.ContentRootPath, "App_Data")); services.Configure<SchedulerOptions>(options => options.Enabled = false); } services.AddApplicationInsightsTelemetry(); services.AddHttpContextAccessor(); services.AddMemoryCache(); services.AddResponseCaching(); services //.AddCmsAspNetIdentity<ApplicationUser>() .AddCms() .AddAlloy() //Service collections Extensions .AddFind() //.AddAdminUserRegistration() .AddEmbeddedLocalization<Startup>(); services.AddCors(opt => { opt.AddPolicy(name: "CorsPolicy", builder => { builder.AllowAnyOrigin() .AllowAnyHeader() .AllowAnyMethod(); }); }); services.AddContentDeliveryApi(); services.AddAlloyHideTabs(); // Required by Wangkanai.Detection to handle multiple devices services.AddDetection(); services.AddSession(options => { options.IdleTimeout = TimeSpan.FromSeconds(10); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; }); services.Configure<CookiePolicyOptions>(options => { options.MinimumSameSitePolicy = SameSiteMode.Strict; options.Secure = CookieSecurePolicy.Always; options.HttpOnly = HttpOnlyPolicy.Always; }); services.AddAuthentication(options => { options.DefaultScheme = "okta.auth"; options.DefaultChallengeScheme = OpenIdConnectDefaults.AuthenticationScheme; }) .AddCookie("okta.auth", options => { options.Events.OnSignedIn = async ctx => { if (ctx.Principal?.Identity is ClaimsIdentity claimsIdentity) { // Syncs user and roles so they are available to the CMS var synchronizingUserService = ctx .HttpContext .RequestServices .GetRequiredService<ISynchronizingUserService>(); await synchronizingUserService.SynchronizeAsync(claimsIdentity); } }; }) .AddOpenIdConnect(options => { options.SignInScheme = "okta.auth"; options.SignOutScheme = "okta.auth"; options.ClientId = _configuration["Okta:ClientId"]; options.ClientSecret = _configuration["Okta:ClientSecret"]; options.Authority = _configuration["Okta:Authority"]; options.SignedOutCallbackPath = _configuration["Okta:PostLogoutRedirectUrl"]; options.CallbackPath = "/signin-oidc"; options.Scope.Add(_configuration["Okta:Scopes"]); options.ResponseType = "code"; options.SaveTokens = true; options.TokenValidationParameters = new TokenValidationParameters { ValidateIssuer = false, NameClaimType = "name", RoleClaimType = "groups" }; options.Events = new OpenIdConnectEvents { OnAuthenticationFailed = async context => { var contentLoader = ServiceLocator.Current.GetInstance<IContentLoader>(); var urlResolver = ServiceLocator.Current.GetInstance<UrlResolver>(); var startPage = contentLoader.Get<StartPage>(ContentReference.StartPage); if (context.Request.Path.Value != null && !context.Request.Path.Value.ToLower().Contains("episerver")) { context.Response.Redirect($"{startPage.LoginPage.GetPublicUrl().TrimEnd('/')}?returnUrl={Uri.EscapeDataString(context.Request.GetEncodedUrl())}"); context.HandleResponse(); } if (ContentReference.StartPage.ID != 0 && context.Exception.Message.Contains("User is not assigned to the client application")) { context.Response.Redirect($"{urlResolver.GetUrl(startPage.LoginPage)}?loginErrorMessage={Uri.EscapeDataString("User is not assigned to the client application. Make sure the user is assigned to the application or to a group assigned to the application in Okta.")}"); context.HandleResponse(); } else { await context.Response.WriteAsync(context.Exception.Message); context.HandleResponse(); } await Task.CompletedTask; }, OnRedirectToIdentityProvider = async context => { if (context.Response.StatusCode == 401) { context.HandleResponse(); } // If signing out, add the id_token_hint if (context.ProtocolMessage.RequestType == OpenIdConnectRequestType.Logout) { var idTokenHint = context.HttpContext.User.FindFirst(OpenIdConnectParameterNames.IdToken); if (idTokenHint != null) { context.ProtocolMessage.IdTokenHint = idTokenHint.Value; } context.HandleResponse(); await Task.CompletedTask; return; } var contentLoader = context.HttpContext.RequestServices.GetRequiredService<IContentLoader>(); if (ContentReference.StartPage.ID != 0) { var startPage = contentLoader.Get<StartPage>(ContentReference.StartPage); var loginPageUrl = startPage.LoginPage.GetPublicUrl(); if (!context.HttpContext.Request.Path.Value.ToLower().Contains("episerver") && startPage.LoginPage == null) { context.HttpContext.Response.StatusCode = 404; context.HandleResponse(); return; } if (!context.HttpContext.Request.Path.Value.ToLower().Contains("episerver") && context.HttpContext.Request.Cookies["Epi-Auth-Flow"] == null) { context.HttpContext.Response.Redirect($"{loginPageUrl.TrimEnd('/')}?returnUrl={Uri.EscapeDataString(context.HttpContext.Request.GetEncodedUrl())}"); context.HandleResponse(); return; } } HandleMultiSiteReturnUrl(context); if (context.HttpContext.Response.StatusCode == 401 && context.HttpContext.User.Identity.IsAuthenticated) { context.HttpContext.Response.StatusCode = 403; context.HandleResponse(); return; } if (context.HttpContext.Response.StatusCode == 401 && IsXhrRequest(context.HttpContext.Request)) { context.HandleResponse(); return; } //context.HandleResponse(); await Task.CompletedTask; }, OnAuthorizationCodeReceived = context => { // Handle authorization code received return Task.CompletedTask; }, OnTokenValidated = (ctx) => { var redirectUri = new Uri(ctx.Properties.RedirectUri, UriKind.RelativeOrAbsolute); if (redirectUri.IsAbsoluteUri) { ctx.Properties.RedirectUri = redirectUri.PathAndQuery; } //// Sync user and the roles to EPiServer in the background //var synchronizingUserService = ctx.HttpContext.RequestServices.GetRequiredService<ISynchronizingUserService>(); return Task.CompletedTask; } }; }); services.AddHttpContextAccessor(); services.AddMemoryCache(); services.AddTransient<IOktaServices>(serviceProvider => new OktaServices(serviceProvider.GetService<IConfiguration>()!)); services.AddControllersWithViews(); services.AddMvc(options => { options.CacheProfiles.Add("PageControllerBaseOutputCacheProfile", new CacheProfile { Duration = 0, // Cache duration in seconds. Set to 0 for no caching. Location = ResponseCacheLocation.None, NoStore = true }); }); } public void Configure(IApplicationBuilder app, IWebHostEnvironment env) { if (env.IsDevelopment()) { app.UseDeveloperExceptionPage(); } // Required by Wangkanai.Detection app.UseDetection(); app.UseSession(); app.UseStaticFiles(); app.UseRouting(); app.UseCors(); app.UseAuthentication(); app.UseAuthorization(); app.UseEndpoints(endpoints => { endpoints.MapControllers(); endpoints.MapRazorPages(); endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); endpoints.MapControllers(); endpoints.MapRazorPages(); endpoints.MapContent(); }); // Map the "/util/logout.aspx" endpoint app.Map("/util/logout", map => { map.Run(async context => { // Sign out the user await context.SignOutAsync(); if (ContentReference.StartPage.ID == 0) { return; } var contentLoader = context.RequestServices.GetRequiredService<IContentLoader>(); var urlResolver = context.RequestServices.GetRequiredService<UrlResolver>(); var startPage = contentLoader.Get<StartPage>(ContentReference.StartPage); // Redirect to the start page context.Response.Redirect( $"{context.Request.Scheme}://{context.Request.Host}{urlResolver.GetUrl(startPage)}"); }); }); } public void ConfigureContainer(ServiceCollection context) { //Register custom implementations that should be used in favor of the default implementations context.AddTransient<IContentRenderer, ErrorHandlingContentRenderer>() .AddTransient<ContentAreaRenderer, AlloyContentAreaRenderer>() .AddTransient<IMessageSender, SmtpSender>(); } private static bool IsXhrRequest(HttpRequest request) { const string xRequestedWith = "X-Requested-With"; var query = request.Query; if (query.ContainsKey(xRequestedWith) && query[xRequestedWith] == "XMLHttpRequest") { return true; } var headers = request.Headers; return headers.ContainsKey(xRequestedWith) && headers[xRequestedWith] == "XMLHttpRequest"; } private void HandleMultiSiteReturnUrl(RedirectContext context) { if (context.ProtocolMessage.RedirectUri == null) { var currentUrl = context.HttpContext.Request.GetEncodedUrl(); context.ProtocolMessage.RedirectUri = currentUrl; } } }
ServiceCollectionExtensions.cs
public static class ServiceCollectionExtensions { public static IServiceCollection AddAlloy(this IServiceCollection services) { services.Configure<RazorViewEngineOptions>(options => options.ViewLocationExpanders.Add(new SiteViewEngineLocationExpander())); services.Configure<TinyMceConfiguration>(config => { config.Default().AddSetting("extended_valid_elements", "iframe[src|alt|title|width|height|align|name],picture,source[srcset|media],span") .AddPlugin( "epi-link epi-image-editor epi-dnd-processor epi-personalized-content print preview searchreplace autolink directionality visualblocks visualchars fullscreen image link media template codesample table charmap hr pagebreak nonbreaking anchor toc insertdatetime advlist lists textcolor " + "wordcount imagetools contextmenu colorpicker textpattern help code") .Toolbar( "bold italic strikethrough forecolor | epi-link image epi-image-editor epi-personalized-content | imagevault-insert-media imagevault-edit-media | bullist numlist | searchreplace fullscreen ", "styleselect formatselect | alignleft aligncenter alignright alignjustify | removeformat | table toc | code" , "") .Menubar("edit insert view format table tools help") .BodyClass("main-content-wrapper") .AddSetting("image_title", false) .AddSetting("image_dimensions", true); }); services.AddTinyMceSpellChecker(); services.Configure<DisplayOptions>(displayOption => { displayOption.Add("full", "/displayoptions/full", Global.ContentAreaTags.FullWidth, string.Empty, "epi-icon__layout--full"); displayOption.Add("wide", "/displayoptions/wide", Global.ContentAreaTags.TwoThirdsWidth, string.Empty, "epi-icon__layout--two-thirds"); displayOption.Add("half", "/displayoptions/half", Global.ContentAreaTags.HalfWidth, string.Empty, "epi-icon__layout--half"); displayOption.Add("narrow", "/displayoptions/narrow", Global.ContentAreaTags.OneThirdWidth, string.Empty, "epi-icon__layout--one-third"); }); services.Configure<MvcOptions>(options => options.Filters.Add<PageContextActionFilter>()); services.AddDisplayResolutions(); services.AddDetection(); return services; } private static void AddDisplayResolutions(this IServiceCollection services) { services.AddSingleton<StandardResolution>(); services.AddSingleton<IpadHorizontalResolution>(); services.AddSingleton<IphoneVerticalResolution>(); services.AddSingleton<AndroidVerticalResolution>(); } }
问题排查与解决方案
1. 认证中间件提前终止请求
OnRedirectToIdentityProvider事件开头的if (context.Response.StatusCode == 401)直接调用context.HandleResponse(),会终止后续请求处理流程,导致请求无法到达Optimizely内容路由。
- 修复:注释或移除该段代码,确保未认证请求能正常触发OpenID Connect挑战流程:
// if (context.Response.StatusCode == 401) // { // context.HandleResponse(); // }
2. 路由注册顺序错误
UseEndpoints中重复注册了MapControllers和MapRazorPages,且MapContent()(Optimizely内容路由)的位置靠后,导致MVC默认路由优先匹配,无法触发StartPage Controller。
- 修复:调整路由注册顺序,优先注册Optimizely内容路由,移除重复注册:
app.UseEndpoints(endpoints => { endpoints.MapContent(); // 优先注册Optimizely内容路由 endpoints.MapControllers(); endpoints.MapRazorPages(); endpoints.MapControllerRoute( name: "default", pattern: "{controller=Home}/{action=Index}/{id?}"); });
3. 首页与登录页配置异常
OnRedirectToIdentityProvider中直接访问ContentReference.StartPage和startPage.LoginPage,如果CMS中未正确设置首页或登录页,会导致空引用或无效重定向,最终返回空白页。
- 修复:增加空值检查,处理配置异常场景:
if (ContentReference.StartPage.ID != 0) { var startPage = contentLoader.Get<StartPage>(ContentReference.StartPage); if(startPage?.LoginPage != null) { var loginPageUrl = startPage.LoginPage.GetPublicUrl(); // 原有重定向逻辑 } else { context.HttpContext.Response.StatusCode = 404; await context.HttpContext.Response.WriteAsync("登录页未配置"); context.HandleResponse(); return; } }
4. 开发环境错误页未生效
确保开发环境下启用详细错误页,方便排查隐藏异常:
- 确认
env.IsDevelopment()分支下的app.UseDeveloperExceptionPage()正常执行,可临时强制启用错误页测试:
// 临时添加,测试后移除 app.UseDeveloperExceptionPage();
内容的提问来源于stack exchange,提问作者surya
相关产品推荐
相关产品推荐

