Danh sách bài viết

Bài 79: Session Expiration — Absolute, Sliding, Hybrid

Session expiration không chỉ là đặt một số giây vào lệnh SET. Có ba chiến lược với đặc tính hoàn toàn khác nhau: Absolute TTL cố định từ lúc tạo, Sliding Window reset mỗi khi có request, và Hybrid kết hợp cả hai. Bài này phân tích cơ chế, trade-off UX/security của từng chiến lược, code Python triển khai hoàn chỉnh, cách throttle lệnh EXPIRE để tránh write storm, remember-me token tách biệt, Lua script cho atomic TTL refresh, và TTL phân theo role.

01/06/2026
16 phút đọc
0 lượt xem
1

Ba Chiến Lược Expiration

Redis cung cấp TTL native cho mọi key qua lệnh EXPIRE / SET ... EX. Việc dùng TTL này như thế nào — đặt một lần cố định hay reset liên tục — tạo ra ba chiến lược với đặc tính khác nhau:

  • Absolute (Fixed TTL): đặt TTL một lần tại thời điểm tạo session, không bao giờ thay đổi. Session hết hạn sau đúng X giây kể từ lúc login, bất kể user có đang hoạt động hay không.
  • Sliding Window: mỗi khi có request hợp lệ, server gọi lại EXPIRE để reset TTL về giá trị ban đầu. User tiếp tục hoạt động thì session tiếp tục tồn tại.
  • Hybrid: áp dụng sliding TTL để UX tốt, nhưng kèm thêm một giá trị expires_at tuyệt đối được lưu trong session data — một trần tối đa mà dù user có active liên tục cũng không vượt qua được.

Chọn sai chiến lược ảnh hưởng trực tiếp đến cả UX lẫn bề mặt tấn công. Phần dưới đây phân tích từng chiến lược.

2

Absolute (Fixed) TTL

Triển khai đơn giản nhất: gọi SET ... EX một lần khi login và không bao giờ gọi EXPIRE thêm:

import redis
import secrets
import json

r = redis.Redis(host="localhost", port=6379, decode_responses=True)
SESSION_TTL = 3600  # 60 phút

def gen_session_id() -> str:
    return secrets.token_urlsafe(32)

def login(user_id: int) -> str:
    sid = gen_session_id()
    data = json.dumps({"user_id": user_id})
    r.set(f"session:{sid}", data, ex=SESSION_TTL)
    return sid

def get_session(sid: str):
    raw = r.get(f"session:{sid}")
    if raw is None:
        return None
    return json.loads(raw)  # KHÔNG gọi EXPIRE — TTL không bao giờ reset

Hành vi: user login lúc 9:00, TTL = 3600 giây. Lúc 10:00 session tự expire dù user đang gõ vào form. Không có gì thay đổi TTL này.

Pros

  • Dự đoán được: biết chắc session sẽ hết sau đúng X phút kể từ lúc tạo.
  • Không có write thêm trên Redis khi đọc session.
  • Giới hạn rõ cửa sổ tấn công: nếu session bị đánh cắp, kẻ tấn công chỉ có tối đa X phút.

Cons

  • UX kém với session ngắn: user đang điền form checkout 45 phút thì bị logout giữa chừng khi TTL = 3600 giây.
  • Không phân biệt được user idle và user đang hoạt động — cả hai đều hết session vào cùng thời điểm.

Use case phù hợp

Banking, admin panel, hệ thống y tế — bất kỳ nơi nào security là ưu tiên hàng đầu và TTL 5–15 phút được chấp nhận.

3

Sliding Window

Mỗi lần đọc session thành công, server gọi EXPIRE để reset TTL:

SESSION_TTL = 3600  # idle timeout: 60 phút không hoạt động → expire

def get_session(sid: str):
    data = r.get(f"session:{sid}")
    if data is None:
        return None
    # Reset TTL mỗi request: đồng hồ đếm ngược bắt đầu lại từ đầu
    r.expire(f"session:{sid}", SESSION_TTL)
    return json.loads(data)

Hành vi: user login lúc 9:00, SESSION_TTL = 3600. Nếu user có request lúc 9:59, TTL được reset về 3600. Session thực tế sống tới 10:59. Mỗi request "đẩy" thời điểm expire ra xa thêm.

Pros

  • UX tốt: user active không bị logout.
  • Phản ánh đúng ý nghĩa "idle timeout" — chỉ expire khi user thực sự không làm gì.

Cons & Security risk

  • Session có thể tồn tại vô hạn: user (hoặc script keep-alive) gửi request định kỳ → TTL liên tục reset → session không bao giờ expire.
  • Session leak nguy hiểm hơn: nếu kẻ tấn công có session token và gửi request định kỳ (ping script), session sẽ không bao giờ expire — cửa sổ tấn công là vô hạn.
  • Không có giới hạn tối đa → không thể buộc re-authentication sau thời gian dài.
4

Hybrid — Sliding + Absolute Cap

Kết hợp sliding TTL (idle timeout) với một trần tuyệt đối (absolute cap) lưu trong session data. Redis TTL xử lý idle timeout; application logic kiểm tra expires_at để enforce cap:

import time

SESSION_TTL = 3600        # idle timeout: 60 phút
MAX_LIFETIME = 86400 * 7  # absolute cap: tối đa 7 ngày

def login(user_id: int) -> str:
    sid = gen_session_id()
    now = int(time.time())
    data = {
        "user_id": str(user_id),
        "expires_at": str(now + MAX_LIFETIME),  # trần tuyệt đối
    }
    r.hset(f"session:{sid}", mapping=data)
    r.expire(f"session:{sid}", SESSION_TTL)  # idle TTL
    return sid

def get_session(sid: str):
    data = r.hgetall(f"session:{sid}")
    if not data:
        return None

    # Kiểm tra absolute cap
    if int(data["expires_at"]) < time.time():
        r.delete(f"session:{sid}")
        return None

    # Sliding: reset idle TTL
    r.expire(f"session:{sid}", SESSION_TTL)
    return data

Hành vi: user active 24/7 vẫn bị logout sau đúng 7 ngày kể từ login. Nhưng nếu idle 60 phút bất kỳ, cũng bị logout trước khi đến 7 ngày.

Lý do dùng Hash thay vì String

Lưu session data bằng HSET (Hash) cho phép đọc từng field riêng lẻ và thêm field sau (ví dụ last_ip, device) mà không cần deserialize toàn bộ JSON. TTL vẫn apply ở key-level — EXPIRE áp dụng cho cả Hash key.

Tại sao đây là chiến lược được khuyến nghị

  • Active user không bị interrupt vô lý (UX tốt như Sliding).
  • Có trần tuyệt đối → buộc re-authentication sau tối đa MAX_LIFETIME (security tốt hơn thuần Sliding).
  • Dễ audit: expires_at là giá trị rõ ràng trong session data, không phải suy luận từ TTL hiện tại.
5

TTL Theo Use Case

Không có một con số TTL đúng cho tất cả ứng dụng. Các giá trị dưới đây là điểm khởi đầu phổ biến — cần điều chỉnh theo yêu cầu bảo mật và hành vi người dùng thực tế:

App type Sliding TTL (idle) Absolute cap
Banking / tài chính5–15 phút1 giờ
E-commerce30 phút7 ngày
Social media1 giờ30 ngày
Email web client30 phút14 ngày
Internal admin tool15 phút8 giờ (1 ngày làm việc)
"Remember me" web1 giờ30–90 ngày

Khi tăng MAX_LIFETIME, bề mặt tấn công từ session leak cũng tăng tương ứng. E-commerce chấp nhận 7 ngày vì mất session chỉ cần login lại; banking không thể chấp nhận điều đó với 7 ngày.

6

Remember Me — Token Tách Biệt

Khi user tick "Ghi nhớ đăng nhập", giải pháp đúng không phải tăng SESSION_TTL của session thường lên 30 ngày. Thay vào đó dùng hai loại token tách biệt:

  • session_id cookie: short-lived, sliding TTL (1 giờ idle). Đây là session thực dùng để authorize request.
  • remember_token cookie: long-lived (30 ngày), opaque token lưu trong Redis. Khi session_id expire, server kiểm tra remember_token và nếu hợp lệ thì tự động tạo session mới (auto re-login).
import secrets

REMEMBER_TTL = 30 * 86400  # 30 ngày

def issue_remember_token(user_id: int) -> str:
    """Tạo remember token sau khi user tick 'ghi nhớ đăng nhập'."""
    token = secrets.token_urlsafe(32)
    r.set(f"remember:{token}", str(user_id), ex=REMEMBER_TTL)
    return token

def auto_login_from_remember(token: str):
    """
    Gọi khi session_id không còn nhưng remember_token vẫn hợp lệ.
    Trả về session_id mới nếu token hợp lệ, None nếu không.
    """
    user_id = r.get(f"remember:{token}")
    if user_id is None:
        return None
    # Xoá token cũ, tạo token mới (token rotation để giảm replay risk)
    r.delete(f"remember:{token}")
    new_token = issue_remember_token(int(user_id))
    new_sid = login(int(user_id))
    return {"session_id": new_sid, "remember_token": new_token}

def logout_remember(token: str):
    """Logout hoàn toàn: xoá cả remember token."""
    r.delete(f"remember:{token}")

Token rotation

Mỗi lần dùng remember_token để auto-login, xoá token cũ và phát token mới (pattern trên). Điều này giảm cửa sổ tấn công trong trường hợp token bị sniff — sau lần dùng đầu tiên, token cũ vô hiệu.

Lưu ý

Remember token PHẢI có TTL. Không bao giờ lưu r.set(f"remember:{token}", user_id) mà không có ex=. Token không TTL tồn tại vĩnh viễn trên Redis và nếu bị leak sẽ cho phép đăng nhập mãi mãi.

7

Atomic TTL Refresh Bằng Lua

Trong sliding window, có race condition nhỏ giữa lệnh đọc và lệnh EXPIRE:

t0  Thread A: HGETALL session:abc  → trả data (key còn tồn tại)
t1  Thread B: DEL session:abc      (logout hoặc TTL hết đúng khoảnh khắc này)
t2  Thread A: EXPIRE session:abc 3600  → tạo lại key zombie không có data

Redis EXPIRE trên key không tồn tại không gây lỗi nhưng cũng không tạo key. Tuy nhiên ở kịch bản trên, nếu key vừa expire ở t1 thì lệnh EXPIRE ở t2 sẽ trả về 0 (không thành công) — không tạo zombie. Race condition thực sự nguy hiểm hơn là khi HGETALL trả data nhưng trước khi client xử lý xong, key expire và một request khác tạo session mới với cùng sid (xác suất cực thấp với random token 32 bytes).

Để đọc và reset TTL là một thao tác atomic, dùng Lua script:

-- get_and_refresh.lua
-- KEYS[1] = session key, ARGV[1] = new TTL (giây)
local data = redis.call("HGETALL", KEYS[1])
if #data == 0 then
  return nil
end
redis.call("EXPIRE", KEYS[1], ARGV[1])
return data
GET_AND_REFRESH = r.register_script("""
local data = redis.call('HGETALL', KEYS[1])
if #data == 0 then return nil end
redis.call('EXPIRE', KEYS[1], ARGV[1])
return data
""")

def get_session_atomic(sid: str):
    result = GET_AND_REFRESH(keys=[f"session:{sid}"], args=[SESSION_TTL])
    if result is None:
        return None
    # Redis trả list [field1, val1, field2, val2, ...]
    it = iter(result)
    data = dict(zip(it, it))
    if int(data.get("expires_at", 0)) < time.time():
        r.delete(f"session:{sid}")
        return None
    return data

Lua script chạy single-threaded trong Redis nên HGETALL và EXPIRE xảy ra trong cùng một "step" từ góc nhìn concurrency của Redis.

8

Throttle EXPIRE Call

Với sliding window, mỗi request đọc session đều gọi EXPIRE. Với 10.000 request/giây, đó là 10.000 lệnh EXPIRE/giây — phần lớn là không cần thiết vì TTL vừa được reset xong.

Pattern tối ưu: chỉ gọi EXPIRE khi TTL còn lại dưới một ngưỡng (thường là SESSION_TTL / 2):

def get_session_throttled(sid: str):
    key = f"session:{sid}"

    # Pipeline để giảm round-trip: HGETALL + TTL trong một lần
    pipe = r.pipeline(transaction=False)
    pipe.hgetall(key)
    pipe.ttl(key)
    data, ttl = pipe.execute()

    if not data:
        return None

    # Kiểm tra absolute cap
    if int(data.get("expires_at", 0)) < time.time():
        r.delete(key)
        return None

    # Chỉ gọi EXPIRE nếu TTL còn lại < 50% SESSION_TTL
    if 0 < ttl < SESSION_TTL // 2:
        r.expire(key, SESSION_TTL)

    return data

Với SESSION_TTL = 3600: EXPIRE chỉ được gọi khi TTL còn dưới 1800 giây, tức là nếu request đến đều đặn hơn mỗi 30 phút thì không có lệnh EXPIRE nào được gửi. Trong thực tế giảm được ~50% write trên Redis so với naive sliding.

Pipeline HGETALL + TTL trong cùng một lần gửi còn giảm thêm latency so với gửi hai lệnh riêng.

9

Idle Timeout vs Total Timeout

Hai khái niệm này thường bị nhầm lẫn:

  • Idle timeout = sliding TTL. Đồng hồ bắt đầu từ lần activity cuối. Nếu user không có request trong X phút → expire. Đây là điều Redis TTL native thực hiện khi kết hợp với EXPIRE sau mỗi request.
  • Total timeout (absolute cap) = thời gian tối đa kể từ lúc tạo session, bất kể user có active hay không. Đây là giá trị expires_at lưu trong session data.

Chúng giải quyết hai vấn đề khác nhau và thường cần cả hai:

  • Idle timeout bảo vệ khi user quên logout và bỏ máy tính.
  • Total timeout buộc re-authentication định kỳ, ngăn session bị giữ sống vô hạn.

Trong code Hybrid ở phần 4, Redis TTL đóng vai trò idle timeout; expires_at đóng vai trò total timeout. Cả hai độc lập nhau.

10

Session Warning & Extend Explicit

TTL endpoint cho frontend

Frontend có thể hỏi thời gian còn lại của session để hiển thị cảnh báo:

# GET /api/session/ttl
def get_session_ttl(sid: str):
    ttl = r.ttl(f"session:{sid}")
    # ttl = -2 nếu key không tồn tại, -1 nếu không có TTL, >= 0 nếu có TTL
    if ttl < 0:
        return {"remaining": 0, "valid": False}
    return {"remaining": ttl, "valid": True}

Frontend poll endpoint này định kỳ (ví dụ mỗi 30 giây khi tab active) và hiển thị countdown "Phiên làm việc sẽ hết hạn sau 2 phút" khi remaining dưới ngưỡng (ví dụ 120 giây).

Explicit extend

Thay vì auto-reset TTL mỗi request (thuần sliding), một số hệ thống yêu cầu user xác nhận chủ động:

# POST /api/session/extend
def extend_session(sid: str):
    key = f"session:{sid}"
    data = r.hgetall(key)
    if not data:
        return {"ok": False, "reason": "session_not_found"}
    if int(data.get("expires_at", 0)) < time.time():
        r.delete(key)
        return {"ok": False, "reason": "absolute_cap_exceeded"}
    r.expire(key, SESSION_TTL)
    return {"ok": True, "new_ttl": SESSION_TTL}

Pattern này phổ biến trong banking với inactivity warning: user thấy popup "Bạn có muốn tiếp tục không?", nếu click "Tiếp tục" thì frontend gọi POST /session/extend, server reset TTL. Nếu không click trong X giây thì session expire tự nhiên.

11

TTL Theo Role

Không phải mọi user đều cần cùng TTL. Account có quyền cao hơn nên có session ngắn hơn để giảm exposure:

TTL_BY_ROLE = {
    "admin":      900,   # 15 phút
    "moderator": 1800,   # 30 phút
    "user":      3600,   # 60 phút
    "guest":     7200,   # 2 giờ
}

MAX_LIFETIME_BY_ROLE = {
    "admin":      28800,    # 8 giờ
    "moderator":  86400,    # 1 ngày
    "user":       86400 * 7,  # 7 ngày
    "guest":      86400 * 1,  # 1 ngày
}

def get_ttl(role: str) -> int:
    return TTL_BY_ROLE.get(role, 3600)

def get_max_lifetime(role: str) -> int:
    return MAX_LIFETIME_BY_ROLE.get(role, 86400 * 7)

def login_with_role(user_id: int, role: str) -> str:
    sid = gen_session_id()
    now = int(time.time())
    ttl = get_ttl(role)
    max_life = get_max_lifetime(role)
    data = {
        "user_id": str(user_id),
        "role": role,
        "expires_at": str(now + max_life),
    }
    r.hset(f"session:{sid}", mapping=data)
    r.expire(f"session:{sid}", ttl)
    return sid

Lưu role trong session data thay vì chỉ trong DB cho phép server đọc role mà không cần DB lookup thêm — nhưng cần lưu ý: nếu role của user thay đổi trong DB (bị revoke admin), session Redis cũ vẫn có role: admin cho tới khi expire. Xử lý vấn đề này (force invalidate) sẽ ở bài 80.

12

Edge Cases & Các Vấn Đề Liên Quan

Clock skew giữa Redis và app server

Redis TTL dựa vào đồng hồ của Redis server. Trường expires_at được so sánh với time.time() của app server. Nếu hai server lệch nhau vài giây (clock skew), có thể xảy ra:

  • App nghĩ session còn hợp lệ nhưng Redis đã expire key (hoặc ngược lại).
  • Nếu app server chạy trước Redis server vài giây, absolute cap check sẽ expire sớm hơn ý định.

Fix: dùng TIME command của Redis để lấy timestamp từ Redis server thay vì app server khi cần so sánh chính xác; hoặc đảm bảo NTP sync đủ tốt (lệch < 1 giây thường chấp nhận được với TTL tính bằng giờ).

Persistence — TTL với AOF và RDB

  • AOF: lưu lệnh EXPIRE, nên khi restore TTL được replay đúng. TTL giảm chính xác từ thời điểm lệnh được ghi.
  • RDB: snapshot lưu trạng thái key và TTL tuyệt đối (thời điểm expire) tại thời điểm chụp. Khi restore từ RDB, Redis tính lại TTL còn lại từ thời điểm hiện tại — nếu restart chậm vài phút, một số session đã expire nhưng chưa bị xoá khỏi snapshot sẽ được xoá ngay khi restore.
  • Crash → restart nhanh: hầu hết session TTL không bị ảnh hưởng đáng kể. Crash → restart chậm (ví dụ mất điện 10 phút): session có TTL ngắn (< 10 phút) sẽ expire đúng lịch; session TTL dài vẫn còn.

Cleanup orphan reverse index

Nếu lưu reverse index user:sessions:{uid} (Set chứa tất cả sid của một user) để hỗ trợ list/force-logout, khi session expire tự nhiên thì sid đó vẫn còn trong Set (orphan reference). Có hai cách xử lý:

  • Lazy cleanup: khi đọc list sessions, loại bỏ các sid mà EXISTS session:{sid} trả 0.
  • Set TTL cho Set: đặt EXPIRE trên key user:sessions:{uid} bằng MAX_LIFETIME — Set tự expire cùng lúc với session dài nhất của user.

Không cần và không nên chạy SCAN để tìm key expired thủ công — Redis tự dọn dẹp TTL native; SCAN chỉ làm tăng tải.

Logout vs Expire

Cần phân biệt hai sự kiện này khi ghi audit log:

  • Logout: user chủ động — gọi DEL session:{sid}. Ghi log action=logout, reason=user_initiated.
  • Expire: passive, do TTL hết — Redis tự xoá. Server không nhận callback trực tiếp từ Redis khi key expire (không nên dựa vào keyspace notification cho critical flow). Thay vào đó, phát hiện khi get_session trả None và log action=session_expired lúc đó.
13

Anti-patterns

  • Sliding không có absolute cap: session active 24/7 không bao giờ expire. Script keep-alive từ attacker khai thác trực tiếp điều này.
  • Gọi EXPIRE mỗi request không throttle: với traffic lớn tạo write storm không cần thiết trên Redis. Áp dụng pattern throttle (phần 8).
  • Lưu expires_at trong app nhưng cũng có Redis TTL khác: nếu Redis TTL ngắn hơn expires_at, key bị xoá trước khi đến expires_at — absolute cap check trong app code không bao giờ được gọi. Đảm bảo Redis TTL (idle timeout) <= MAX_LIFETIME.
  • Remember me không TTL: token tồn tại vĩnh viễn trên Redis, nếu bị leak cho phép đăng nhập mãi mãi.
  • SCAN để tìm expired session: Redis đã tự xử lý TTL, không cần cron job quét thủ công.
  • Absolute TTL quá ngắn cho user thường: 5 phút absolute TTL cho e-commerce là không hợp lý — user đang xem sản phẩm 6 phút thì bị logout.
  • Manual expires_at check không nhất quán với Redis TTL: một số path trong code gọi r.get rồi trả data mà không check expires_at — absolute cap bị bỏ qua cho path đó.
14

Tổng Kết & Quiz

Tổng kết

  • Absolute TTL: đơn giản, predictable, UX kém khi TTL ngắn. Phù hợp banking, admin.
  • Sliding TTL: UX tốt, session sống theo activity. Risk: session có thể không bao giờ expire nếu không có absolute cap.
  • Hybrid (khuyến nghị): sliding TTL làm idle timeout + expires_at trong data làm absolute cap. Kết hợp UX tốt và giới hạn bảo mật rõ ràng.
  • Throttle EXPIRE: chỉ gọi khi TTL còn lại < SESSION_TTL / 2 để giảm write load ~50%.
  • Remember me: dùng token tách biệt với TTL dài, có token rotation, không tăng TTL của session chính.
  • Lua script: đảm bảo HGETALL + EXPIRE là atomic, tránh edge case key expire giữa hai lệnh.
  • TTL theo role: admin ngắn hơn user thường để giảm exposure.
  • Idle timeout (sliding TTL) và total timeout (absolute cap) là hai khái niệm khác nhau, giải quyết hai vấn đề khác nhau.

Quiz 5 câu

  1. Sự khác nhau chính giữa Absolute TTL và Sliding TTL là gì? Cho ví dụ kịch bản user active 2 giờ với SESSION_TTL = 3600.
  2. Tại sao Sliding TTL không có absolute cap là security risk? Mô tả cụ thể attack scenario.
  3. Trong code Hybrid (phần 4), trường expires_at được lưu trong Hash. Tại sao không dựa vào Redis TTL để tính absolute cap thay vì lưu riêng?
  4. Throttle EXPIRE với ngưỡng SESSION_TTL / 2: nếu SESSION_TTL = 3600 và user gửi request mỗi 10 phút, có bao nhiêu lần EXPIRE được gọi trong 3 giờ? Giải thích.
  5. Remember token rotation nghĩa là gì và nó giảm thiểu được rủi ro nào?

Đáp án gợi ý

  1. Absolute: TTL đặt một lần khi tạo, không bao giờ thay đổi → user active 2 giờ vẫn bị logout đúng lúc TTL hết (lúc 60 phút). Sliding: mỗi request reset TTL → sau 2 giờ với request đều đặn, TTL vẫn còn nguyên SESSION_TTL giây.
  2. Kẻ tấn công lấy được session token và viết script gửi request đơn giản (ping) mỗi 10 phút. TTL liên tục được reset → session không bao giờ expire → attacker duy trì quyền truy cập vô hạn mà không cần credential.
  3. Redis TTL chỉ cho biết "còn bao nhiêu giây tới expire" tính từ lần EXPIRE gần nhất (idle timeout). Không thể biết "khi nào session được tạo" hay "khi nào phải force-expire dù active". Lưu expires_at là timestamp tuyệt đối, tính từ thời điểm login, không bị ảnh hưởng bởi sliding reset.
  4. Mỗi 10 phút = 600 giây, ngưỡng = 1800 giây. Sau reset, TTL = 3600. Request sau 10 phút: TTL = 3000 > 1800 → không EXPIRE. Tiếp tục: 2400, 1800, 1200 → 1200 < 1800 → EXPIRE. Tức là mỗi ~30 phút mới có 1 lệnh EXPIRE. Trong 3 giờ = 180 phút → khoảng 6 lần. Naive sliding sẽ có 18 lần (mỗi 10 phút một lần).
  5. Token rotation: mỗi lần dùng remember_token để auto-login, xoá token cũ và phát token mới. Nếu token bị sniff/replay, kẻ tấn công dùng token trước khi user dùng → token mới được phát cho attacker, nhưng nếu user dùng trước → attacker chỉ có token đã vô hiệu. Giảm thiểu window sử dụng của token bị leak.

Bài tiếp theo

Bài 80 xử lý vấn đề multi-device: một user đăng nhập từ nhiều thiết bị cùng lúc, cách lưu và quản lý nhiều session song song, và force logout — xoá toàn bộ session của một user hoặc từng thiết bị cụ thể.

Tham khảo