Session 复活


本文仅适用于 Asp.Net Core MVC

  在使用 Cookies + Session 做身份验证时,Session 过期导致频繁登录是不可避免的问题,设置 Session 过期时间不是最佳的解决办法。Fireasy 提供了一种机制,即在 Session 过期后的第一次请求,通过拿取 Cookies 里的用户ID再次构建 Session 信息,以实现会话的长效性。

  在 Asp.Net MVC 里,可以在 Global.cs 的 Session_Start 方法里处理这件事,但是在 Asp.Net Core MVC 里,必须采用中间件技术。

  你需要在 Startup.ConfigureServices 方法里使用 AddSessionRevive 方法来指定一个实现了 ISessionReviveNotification 接口的类型,同时在 Configure 方法里使用 UseSessionRevive 引入中间件。如下所示:

namespace demo
{
    public class Startup
    {
        public Startup(IConfiguration configuration)
        {
            Configuration = configuration;
        }

        public IConfiguration Configuration { get; }

        // This method gets called by the runtime. Use this method to add services to the container.
        public void ConfigureServices(IServiceCollection services)
        {
            services.AddSessionRevive<SessionReviveNotification>();

            services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme)
                .AddCookie(options =>
                    {
                        options.LoginPath = new PathString("/login");
                    });
        }

        // This method gets called by the runtime. Use this method to configure the HTTP request pipeline.
#if NETCOREAPP2_2
        public void Configure(IApplicationBuilder app, IHostingEnvironment env)
        {
            if (env.IsDevelopment())
            {
#elif NETCOREAPP3_1
        public void Configure(IApplicationBuilder app, IWebHostEnvironment env, IServiceProvider svp)
        {
            if (env.IsDevelopment())
            {
#endif
                app.UseDeveloperExceptionPage();
            }
            else
            {
                app.UseExceptionHandler("/Home/Error");
            }

            //添加静态文件映射
            app.UseStaticFiles();
            app.UseSession();

#if NETCOREAPP2_2
            app.UseAuthentication();
            app.UseSessionRevive();
            app.UseMvc(routes =>
                {
                    routes.MapRoute(
                        name: "areas",
                        template: "{area:exists}/{controller=Home}/{action=Index}/{id?}");
                    routes.MapRoute(
                        name: "default",
                        template: "{controller=Home}/{action=Index}/{id?}");

                });
#elif NETCOREAPP3_1
            app.UseRouting();
            app.UseAuthentication();
            app.UseSessionRevive();
            app.UseAuthorization();
            app.UseEndpoints(c =>
                {
                    c.MapControllerRoute("areas", "{area:exists}/{controller=Home}/{action=Index}/{id?}");
                    c.MapControllerRoute("default", "{controller=Home}/{action=Index}/{id?}");
                });
#endif
        }
    }
}

  SessionReviveNotification 类实现 ISessionReviveNotification 接口的 InvokeAsync 方法。注意 IAdminService 不能从构造函数里注入。如下所示:

/// <summary>
/// Session 复活时自动设置 <see cref="SessionContext"/> 对象。
/// </summary>
public class SessionReviveNotification : ISessionReviveNotification
{
    public async Task InvokeAsync(HttpContext context)
    {
        var adminService = context.RequestServices.GetRequiredService<IAdminService>();
        var userId = GetIdentity(context);
        if (userId != 0)
        {
            //根据UserId获取用户信息
            var user = await adminService.GetUserAsync(userId);
            if (user != null)
            {
                var session = new SessionContext 
                { 
                    UserID = userId,
                    UserName = user.Name, 
                    OrgID = user.OrgID 
                };
                SetSession(context, session);
            }
        }
    }

    // 从 Cookies 里获取用户ID
    private static int GetIdentity(HttpContext context)
    {
        var claim = context.User.Claims.FirstOrDefault(s => s.Type == ClaimTypes.Sid);
        return claim == null ? 0 : claim.Value.To<int>();
    }

    // 将信息写到 Session
    private static void SetSession(HttpContext context, SessionContext session)
    {
        var json = new JsonSerializer().Serialize(session);
        context.Session.Set("__user__info", Encoding.UTF8.GetBytes(json));
    }
}