ASP.NET Core 6+Angular应用IIS部署:会话超时与登录跳转修复
问题
我开发了一个基于ASP.NET Core 6、前端使用Angular的Web应用,并实现了基于Microsoft.AspNetCore.Authentication.AuthenticationHandler的自定义认证处理器,对接第三方自定义API完成认证。当前应用初始化代码如下:
WebApplicationBuilder builder = WebApplication.CreateBuilder(args); builder.Services.AddDbContextFactory<DatabaseContextName>(); builder.Services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>(); builder.Services.AddAuthentication(CustomAuthDefaults.AuthenticationScheme) .AddScheme<CustomAuthSchemeOptions, CustomAuthHandler>( CustomAuthDefaults.AuthenticationScheme, options => { }); builder.Services.AddAuthorization(options => { options.FallbackPolicy = options.DefaultPolicy; }); builder.Services.AddControllers(); builder.Services.AddDistributedMemoryCache(); builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromHours(8); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; }); WebApplication app = builder.Build(); app.UseHttpsRedirection(); app.UseDefaultFiles(); app.UseStaticFiles(); app.UseRouting(); app.UseSession(); app.UseAuthentication(); app.UseAuthorization(); app.MapControllers(); // Backend-Routes for the treatment of 404-Errors app.Map("api/{**slug}", HandleApiFallback); // Fallback-Route to the Frontend app.MapFallbackToFile("{**slug}", "index.html"); app.Run(); static Task HandleApiFallback(HttpContext context) { context.Response.StatusCode = StatusCodes.Status404NotFound; return Task.CompletedTask; }
Angular前端已配置HttpInterceptor,在用户未认证时跳转至登录页,该逻辑在Kestrel主机上正常运行,但部署到IIS后完全失效:
- IIS忽略AddSession配置的8小时会话超时,实际几分钟就过期
- 会话过期后阻断所有HTTP请求,刷新页面时返回HTTP 401错误并显示空白页,而非跳转至登录页
尝试过在AddAuthentication后添加.AddCookie(options => options.LoginPath = "/login"),或配置builder.Services.ConfigureApplicationCookie(options => options.LoginPath = "/login"),均未解决问题。需要配置应用确保IIS下会话超时正确,并修复会话过期后的登录跳转问题。
解决方案
一、修复IIS下的会话超时问题
- 同步IIS应用池超时与Session配置
IIS应用池默认闲置超时为20分钟,超时后会回收进程,直接清空内存中的Session(当前使用DistributedMemoryCache是进程内缓存)。需修改应用池设置:
- 打开IIS管理器,找到对应应用的应用池
- 右键→高级设置→进程模型→闲置超时(分钟),设置为480分钟(8小时)
- 同时调整“回收”选项中的固定时间间隔为超过8小时(或设为0禁用)
- 补全会话Cookie的过期配置
当前仅设置了Session的IdleTimeout,但Cookie默认是会话级(关闭浏览器即失效),需显式匹配Cookie过期时间:
builder.Services.AddSession(options => { options.IdleTimeout = TimeSpan.FromHours(8); options.Cookie.HttpOnly = true; options.Cookie.IsEssential = true; options.Cookie.SecurePolicy = CookieSecurePolicy.SameAsRequest; // 添加Cookie过期时间,与IdleTimeout保持一致 options.Cookie.Expires = TimeSpan.FromHours(8); });
- 改用持久化分布式缓存(可选但推荐)
DistributedMemoryCache依赖进程内存,IIS进程回收会丢失所有缓存数据。若要提升可靠性,可替换为Redis或SQL Server缓存:
// 示例:使用Redis缓存 builder.Services.AddStackExchangeRedisCache(options => { options.Configuration = builder.Configuration.GetConnectionString("Redis"); options.InstanceName = "AppInstance"; });
二、修复会话过期后的登录跳转问题
自定义认证处理器未处理401响应的跳转逻辑,AddCookie配置仅对Cookie认证方案生效,需针对自定义Scheme做适配:
- 重写自定义认证处理器的
HandleChallengeAsync方法
在CustomAuthHandler中添加逻辑,区分API请求与页面请求的响应方式:
protected override async Task HandleChallengeAsync(AuthenticationProperties properties) { // API请求返回401,让前端Interceptor处理跳转 if (Context.Request.Path.StartsWithSegments("/api")) { Response.StatusCode = StatusCodes.Status401Unauthorized; await Response.WriteAsJsonAsync(new { IsAuthenticated = false, RedirectTo = "/login" }); } // 页面请求直接重定向到前端登录页 else { Response.Redirect("/login"); } }
- 调整授权策略,允许匿名访问静态资源与登录页
避免授权拦截前端静态文件和登录路由:
builder.Services.AddAuthorization(options => { options.FallbackPolicy = new AuthorizationPolicyBuilder() .RequireAuthenticatedUser() .AddAuthenticationSchemes(CustomAuthDefaults.AuthenticationScheme) .Build(); }); // 中间件中跳过静态资源和登录页的授权检查 app.Use(async (context, next) => { var path = context.Request.Path.Value; if (path.StartsWith("/css/") || path.StartsWith("/js/") || path.StartsWith("/assets/") || path == "/login" || path == "/") { await next(); return; } await next(); });
- 确认前端Interceptor逻辑正确
确保Angular拦截器捕获401后执行跳转:
@Injectable() export class AuthInterceptor implements HttpInterceptor { constructor(private router: Router) {} intercept(request: HttpRequest<any>, next: HttpHandler): Observable<HttpEvent<any>> { return next.handle(request).pipe( catchError((error: HttpErrorResponse) => { if (error.status === 401) { localStorage.removeItem('authToken'); this.router.navigate(['/login']); } return throwError(() => error); }) ); } }
内容的提问来源于stack exchange,提问作者Chris
相关产品推荐
相关产品推荐

