Skip to content

Xác thực & Quản lý phiên làm việc

Bài học này giải thích cách hệ thống quản lý xác thực sử dụng Playwright Storage State - một cách tiếp cận đơn giản và hiệu quả.

1. Vấn đề cần giải quyết

Khi ghi/phát lại trên các trang yêu cầu đăng nhập:

Vấn đềHậu quả
Phải đăng nhập mỗi lần ghiMất thời gian, lặp lại
Phiên hết hạnPhát lại thất bại
Lưu tên đăng nhập/mật khẩuRủi ro bảo mật

2. Giải pháp: Trạng thái lưu trữ Playwright

Playwright cung cấp cơ chế --save-storage--load-storage để lưu/load trạng thái browser (cookies, localStorage).

2.1. Quy trình

┌─────────────────────────────────────────────────────────────┐
│                    LƯU PHIÊN (1 lần)                        │
├─────────────────────────────────────────────────────────────┤
│  1. Chạy: playwright codegen --save-storage=auth.json URL   │
│  2. Đăng nhập thủ công trong trình duyệt                    │
│  3. Đóng trình duyệt → Phiên lưu vào auth.json              │
└─────────────────────────────────────────────────────────────┘

┌─────────────────────────────────────────────────────────────┐
│                    TẢI PHIÊN (mỗi lần)                      │
├─────────────────────────────────────────────────────────────┤
│  1. Chạy: playwright codegen --load-storage=auth.json URL   │
│  2. Trình duyệt mở với phiên đã đăng nhập                   │
│  3. Bắt đầu ghi/phát lại ngay                               │
└─────────────────────────────────────────────────────────────┘

2.2. Định dạng trạng thái lưu trữ

json
{
  "cookies": [
    {
      "name": "session_id",
      "value": "abc123...",
      "domain": ".example.com",
      "path": "/",
      "expires": 1702339200,
      "httpOnly": true,
      "secure": true,
      "sameSite": "Lax"
    }
  ],
  "origins": [
    {
      "origin": "https://example.com",
      "localStorage": [
        { "name": "token", "value": "xyz789..." }
      ]
    }
  ]
}

3. Triển khai

3.1. Trình quản lý xác thực (src/auth_manager.py)

Module đơn giản hóa, chỉ quản lý trạng thái lưu trữ:

python
def get_auth_file_path() -> Path:
    """Tìm file auth.json trong nhiều vị trí."""
    search_paths = [
        Path('auth.json'),                              # CWD
        Path(__file__).parent.parent / 'auth.json',     # Project root
    ]
    
    # Nếu chạy từ EXE
    if getattr(sys, 'frozen', False):
        exe_dir = Path(sys.executable).parent
        search_paths.insert(0, exe_dir / 'auth.json')
    
    for path in search_paths:
        if path.exists():
            return path
    
    return Path(__file__).parent.parent / 'auth.json'

3.2. Xác thực trạng thái lưu trữ

python
def is_valid_storage_state(file_path: Path) -> bool:
    """Kiểm tra file có phải Playwright storage state hợp lệ."""
    if not file_path.exists():
        return False
    
    try:
        with open(file_path, 'r', encoding='utf-8') as f:
            data = json.load(f)
        
        # Playwright storage state có 'cookies' và/hoặc 'origins'
        return 'cookies' in data or 'origins' in data
    except (json.JSONDecodeError, IOError):
        return False

3.3. Lấy thông tin xác thực

python
def get_auth_info() -> dict:
    """Lấy thông tin chi tiết về auth state."""
    auth_path = get_auth_file_path()
    
    if not is_valid_storage_state(auth_path):
        return {
            'valid': False,
            'path': str(auth_path),
            'message': 'Chưa có auth state'
        }
    
    with open(auth_path, 'r', encoding='utf-8') as f:
        data = json.load(f)
    
    cookies = data.get('cookies', [])
    
    # Extract domains từ cookies
    domains = set()
    for cookie in cookies:
        domain = cookie.get('domain', '').lstrip('.')
        domains.add(domain)
    
    return {
        'valid': True,
        'path': str(auth_path),
        'cookie_count': len(cookies),
        'domains': list(domains),
        'message': f"✅ {len(cookies)} cookies cho {len(domains)} domain(s)"
    }

4. Tích hợp với bộ ghi

4.1. Tải trạng thái khi ghi hình

python
# src/recorder.py
def start_recording(url: str, output_path: Path) -> bool:
    """Start recording với auth state nếu có."""
    
    from src.auth_manager import get_auth_state_path
    auth_state_path = get_auth_state_path()
    
    # Build command
    cmd = [sys.executable, "-m", "playwright", "codegen",
           "--target", "python", "-o", str(output_path)]
    
    # Thêm auth state nếu có
    if auth_state_path:
        cmd.extend(["--load-storage", str(auth_state_path)])
    
    cmd.append(url)
    
    subprocess.run(cmd, check=True)

4.2. Lưu trạng thái xác thực

python
def save_auth_state(url: str, output_path: Optional[Path] = None) -> bool:
    """Mở browser để user đăng nhập và lưu session."""
    
    if output_path is None:
        output_path = get_auth_file_path()
    
    cmd = [sys.executable, "-m", "playwright", "codegen",
           "--save-storage", str(output_path), url]
    
    print("📝 Hướng dẫn:")
    print("   1. Trình duyệt sẽ mở")
    print("   2. Đăng nhập vào trang web")
    print("   3. Đóng trình duyệt khi đăng nhập xong")
    
    subprocess.run(cmd, check=True)
    
    # Verify
    if is_valid_storage_state(output_path):
        print("✅ Đã lưu auth state thành công!")
        return True
    return False

5. Tích hợp với bộ phát lại

5.1. Tải trạng thái vào ngữ cảnh trình duyệt

python
# src/replayer.py
def _replay_with_browser(browser, actions, images_dir, upload_enabled, upload_fn):
    """Replay với auth state."""
    
    from src.auth_manager import get_auth_state_path, get_auth_info
    
    auth_state_path = get_auth_state_path()
    
    # Context options
    context_options = {'viewport': {'width': 1280, 'height': 800}}
    
    # Load auth state nếu có
    if auth_state_path:
        auth_info = get_auth_info()
        print(f"   🔐 {auth_info.get('message', '')}")
        context_options['storage_state'] = str(auth_state_path)
    
    # Tạo context với auth
    context = browser.new_context(**context_options)
    page = context.new_page()
    
    # Replay actions...

6. Các lệnh CLI

6.1. Lưu xác thực

bash
python guide_ai.py --save-auth --url https://portal.inet.vn

Quy trình:

  1. Mở trình duyệt với Playwright codegen
  2. Người dùng đăng nhập thủ công
  3. Đóng trình duyệt → auth.json được tạo

6.2. Kiểm tra trạng thái

bash
python guide_ai.py --auth-status

Output:

🔐 AUTH STATE STATUS
--------------------------------------------------
   Path: E:\automation-guide\auth.json
   Status: ✅ 5 cookies cho 2 domain(s)
   Domains: portal.inet.vn, sso.inet.vn

6.3. Xóa xác thực

bash
python guide_ai.py --clear-auth

7. So sánh với cách cũ

Tiêu chíCách cũ (Tự điền form)Cách mới (Trạng thái lưu trữ)
Bảo mậtLưu tên đăng nhập/mật khẩuChỉ lưu token phiên
Độ phức tạpPhải phát hiện form, selectorPlaywright xử lý hết
Độ tin cậyPhụ thuộc giao diện trangHoạt động với mọi trang
Bảo trìPhải cập nhật khi giao diện đổiKhông cần bảo trì

8. Thực hành tốt

8.1. Bảo mật

gitignore
# .gitignore
auth.json
*.auth.json

8.2. Hết hạn token

  • Token phiên có thời hạn
  • Khi hết hạn → chạy lại --save-auth
  • Có thể phát hiện qua phản hồi HTTP 401/403

8.3. Nhiều trang web

Hiện tại chỉ hỗ trợ 1 file auth.json. Để hỗ trợ nhiều sites:

bash
# Lưu riêng từng site
playwright codegen --save-storage=auth_portal.json https://portal.inet.vn
playwright codegen --save-storage=auth_admin.json https://admin.inet.vn

# Load theo site (future feature)
python guide_ai.py --auth-file=auth_portal.json --url https://portal.inet.vn

9. Xử lý sự cố

Phiên không được tải

❌ Vấn đề: Trình duyệt mở nhưng chưa đăng nhập
✅ Giải pháp:
   1. Kiểm tra auth.json tồn tại
   2. Chạy --auth-status để xác minh
   3. Nếu không hợp lệ, chạy lại --save-auth

Token hết hạn

❌ Vấn đề: Phát lại chuyển hướng về trang đăng nhập
✅ Giải pháp:
   1. Xóa auth.json cũ: --clear-auth
   2. Tạo mới: --save-auth --url <URL>

Tổng kết

Quản lý xác thực sử dụng Trạng thái lưu trữ Playwright:

  • Đơn giản: Không cần code phức tạp để phát hiện form
  • An toàn: Không lưu thông tin đăng nhập, chỉ lưu phiên
  • Di động: File JSON có thể sao chép giữa các máy
  • Đáng tin cậy: Tính năng gốc của Playwright, hoạt động ổn định

Bài học tiếp theo

Đóng gói & Phân phối

Internal documentation for iNET Portal