[鐵人賽 Day11] ASP.NET Core 2 系列 - Cookies & Session

-- Pageviews

基本上 HTTP 是沒有紀錄狀態的協定,但可以透過 Cookies 將 Request 來源區分出來,並將部分資料暫存於 Cookies 及 Session,是寫網站常用的用戶資料暫存方式。
本篇將介紹如何在 ASP.NET Core 使用 Cookie 及 Session。

iT 邦幫忙 2018 鐵人賽 - Modern Web 組參賽文章:
[Day11] ASP.NET Core 2 系列 - Cookies & Session

Cookies

Cookies 是將用戶資料存在 Client 的瀏覽器,每次 Request 都會把 Cookies 送到 Server。
在 ASP.NET Core 中要使用 Cookie,可以透過 HttpContext.RequestHttpContext.Response 存取:

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace MyWebsite
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
}

public void Configure(IApplicationBuilder app)
{
app.Run(async (context) =>
{
string message;

if (!context.Request.Cookies.TryGetValue("Sample", out message))
{
message = "Save data to cookies.";
}
context.Response.Cookies.Append("Sample", "This is Cookies.");
// 刪除 Cookies 資料
//context.Response.Cookies.Delete("Sample");

await context.Response.WriteAsync($"{message}");
});
}
}
}

從 HTTP 可以看到傳送跟收到的 Cookies 資訊:

[鐵人賽 Day11] ASP.NET Core 2 系列 - Cookies & Session - Cookies

當存在 Cookies 的資料越多,封包就會越大,因為每個 Request 都會帶著 Cookies 資訊。

Session

Session 是透過 Cookies 內的唯一識別資訊,把用戶資料存在 Server 端記憶體、NoSQL 或資料庫等。
要在 ASP.NET Core 使用 Session 需要先加入兩個服務:

  • Session 容器
    Session 可以存在不同的地方,透過 DI IDistributedCache 物件,讓 Session 服務知道要將 Session 存在哪邊。
    (之後的文章會介紹到 IDistributedCache 分散式快取)
  • Session 服務
    在 DI 容器加入 Session 服務。並將 Session 的 Middleware 加入 Pipeline。

Startup.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.DependencyInjection;

namespace MyWebsite
{
public class Startup
{
public void ConfigureServices(IServiceCollection services)
{
// 將 Session 存在 ASP.NET Core 記憶體中
services.AddDistributedMemoryCache();
services.AddSession();
}

public void Configure(IApplicationBuilder app)
{
// SessionMiddleware 加入 Pipeline
app.UseSession();

app.Run(async (context) =>
{
context.Session.SetString("Sample", "This is Session.");
string message = context.Session.GetString("Sample");
await context.Response.WriteAsync($"{message}");
});
}
}
}

HTTP Cookies 資訊如下:

[鐵人賽 Day11] ASP.NET Core 2 系列 - Cookies & Session - Session

可以看到多出了 .AspNetCore.Session.AspNetCore.Session 就是 Session 的唯一識別資訊。
每次 Request 時都會帶上這個值,當 Session 服務取得這個值後,就會去 Session 容器找出專屬這個值的 Session 資料。

物件型別

以前 ASP.NET 可以將物件型別直接存放到 Session,現在 ASP.NET Core Session 不再自動序列化物件到 Sesson。
如果要存放物件型態到 Session 就要自己序列化了,這邊以 JSON 格式作為範例:

Extensions\SessionExtensions.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
using Microsoft.AspNetCore.Http;
using Newtonsoft.Json;

namespace MyWebsite.Extensions
{
public static class SessionExtensions
{
public static void SetObject<T>(this ISession session, string key, T value)
{
session.SetString(key, JsonConvert.SerializeObject(value));
}

public static T GetObject<T>(this ISession session, string key)
{
var value = session.GetString(key);
return value == null ? default(T) : JsonConvert.DeserializeObject<T>(value);
}
}
}

透過上例擴充方法,就可以將物件存取至 Session,如下:

1
2
3
4
using MyWebsite.Extensions;
// ...
var user = context.Session.GetObject<UserModel>("user");
context.Session.SetObject("user", user);

安全性

雖然 Session 資料都存在 Server 端看似安全,但如果封包被攔截,只要拿到 .AspNetCore.Session 就可以取到該用戶資訊,也是有風險。
有些安全調整建議實作:

  • SecurePolicy
    限制只有在 HTTPS 連線的情況下,才允許使用 Session。如此一來變成加密連線,就不容易被攔截。
  • IdleTimeout
    修改合理的 Session 到期時間。預設是 20 分鐘沒有跟 Server 互動的 Request,就會將 Session 變成過期狀態。
    (20分鐘有點長,不過還是要看產品需求。)
  • Name
    沒必要將 Server 或網站技術的資訊爆露在外面,所以預設 Session 名稱 .AspNetCore.Session 可以改掉。
1
2
3
4
5
6
7
8
9
10
11
// ...
public void ConfigureServices(IServiceCollection services)
{
services.AddDistributedMemoryCache();
services.AddSession(options =>
{
options.Cookie.SecurePolicy = CookieSecurePolicy.Always;
options.Cookie.Name = "mywebsite";
options.IdleTimeout = TimeSpan.FromMinutes(5);
});
}

強型別

由於 Cookies 及 Session 預設都是使用字串的方式存取資料,弱型別無法在開發階段判斷有沒有打錯字,還是建議包裝成強行別比較好。
而且直接存取 Cookies/Session 的話邏輯相依性太強,對單元測試很不友善,所以還是建議包裝一下。

Wappers\SessionWapper.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
using Microsoft.AspNetCore.Http;
using MyWebsite.Extensions;
// ...

public interface ISessionWapper
{
UserModel User { get; set; }
}

public class SessionWapper : ISessionWapper
{
private static readonly string _userKey = "session.user";
private readonly IHttpContextAccessor _httpContextAccessor;

public SessionWapper(IHttpContextAccessor httpContextAccessor)
{
_httpContextAccessor = httpContextAccessor;
}

private ISession Session
{
get
{
return _httpContextAccessor.HttpContext.Session;
}
}

public UserModel User
{
get
{
return Session.GetObject<UserModel>(_userKey);
}
set
{
Session.SetObject(_userKey, value);
}
}
}

在 DI 容器中加入 IHttpContextAccessorISessionWapper,如下:

Startup.cs

1
2
3
4
5
6
// ...
public void ConfigureServices(IServiceCollection services)
{
services.AddSingleton<IHttpContextAccessor, HttpContextAccessor>();
services.AddSingleton<ISessionWapper, SessionWapper>();
}

  • IHttpContextAccessor
    ASP.NET Core 實作了 IHttpContextAccessor,讓 HttpContext 可以輕鬆的注入給需要用到的物件使用。
    由於 IHttpContextAccessor 只是取用 HttpContext 實例的接口,用 Singleton 的方式就可以供其它物件使用。

在 Controller 就可以直接注入 ISessionWapper,以強行別的方式存取 Session,如下:

Controllers/HomeController.cs

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
using Microsoft.AspNetCore.Mvc;
using MyWebsite.Wappers;

namespace MyWebsite.Controllers
{
public class HomeController : Controller
{
private readonly ISessionWapper _sessionWapper;

public HomeController(ISessionWapper sessionWapper)
{
_sessionWapper = sessionWapper;
}

public IActionResult Index()
{
var user = _sessionWapper.User;
_sessionWapper.User = user;
return Ok(user);
}
}
}

參考

Introduction to session and application state in ASP.NET Core