今日我們將延續昨天的內容,從網站開發中常見的登入註冊功能切入,引導大家逐步認識 Cookie、Session 等重要概念。
網站需要使用者註冊和登入主要有以下幾個原因:
你還有想到哪一些呢?可以在底下留言討論。
一個基本的登入註冊功能通常包含以下必要元素:
你有想過為什麼在瀏覽器登入之後,再關閉瀏覽器,重新開啟卻仍然是已經登入的畫面呢?
其實在登入成功的當下,伺服器有在回應封包中送一個標頭,讓瀏覽器紀錄。叫做 Cookie 機制。
延伸思考:為什麼需要 Cookie 機制?
首先,複習一下,我們透過瀏覽器跟伺服器之間互動,並利用 HTTP 這個協定,也就是溝通的方法。
但是 HTTP 是一個無狀態(stateless)的協定。代表每次瀏覽器向伺服器發送請求時,伺服器都會將其視為一個全新的請求,不會保留之前請求的相關資訊。
假設每一次關掉瀏覽器,然後需要重新輸入帳號密碼,會造成很多人使用者體驗不好,因此需要一種方法來「記住」這個人曾經輸入過帳號密碼。
這就是為什麼我們需要 Cookie 和 Session。

Cookie 和 Session 是什麼?先透過工作流程來了解其運作:
Cookie 是儲存在使用者瀏覽器中的文字。當使用者瀏覽網站時,伺服器可以發送一些資料並儲存在 Cookie 中。之後,瀏覽器會在每次向該網站發送請求時自動加上這些 Cookie。
Session 是在伺服器端保存的一個資料結構,用來跟蹤使用者的狀態。每個 Session 都有一個唯一的標識符,通常稱為 Session ID。
雖然 Session 資料儲存在伺服器端,但攻擊者仍然可能透過一些手段,例如跨站請求偽造(CSRF)攻擊,來盜取或偽造使用者的 Session ID,進而操控使用者的 Session 資訊
| 特性 | Cookie | Session | 
|---|---|---|
| 儲存位置 | 使用者端 (瀏覽器) | 伺服器端 | 
| 儲存資料類型 | 小型文字資料,例如使用者名稱、購物車資訊等 | 使用者狀態資訊,例如登入狀態、購物車內容、使用者偏好設定等。可以儲存物件或複雜資料結構。 | 
| 安全性 | 安全性較低,因為 Cookie 儲存在使用者端,可能被竊取或偽造。 | 安全性較高,因為 Session 資料儲存在伺服器端,使用者無法直接訪問或修改。 | 
| 存取方式 | 由瀏覽器自動處理,網站可以透過 JavaScript 或伺服器端程式碼設定和讀取 Cookie。 | 由伺服器端程式碼管理,通常透過 Session ID 來識別和操作每個使用者的 Session 資料。 | 
| 生命週期 | 可以設定過期時間,過期後瀏覽器會自動刪除;也可以設定為 Session Cookie,在瀏覽器關閉時刪除。 | 通常在使用者關閉瀏覽器或一段時間不活動後過期。 | 
| 大小限制 | 約 4KB | 無嚴格限制,但過大的 Session 資料會影響伺服器效能。 | 
| 對伺服器效能的影響 | 影響較小,因為 Cookie 資料儲存在使用者端,不會占用伺服器資源。 | 可能影響較大,因為 Session 資料儲存在伺服器端,如果同時有大量使用者線上,會占用伺服器記憶體和其他資源。 | 
| 典型應用場景 | * 儲存使用者偏好設定,例如語言、主題等。 | * 追蹤使用者登入狀態。 | 
| * 記錄使用者行為,例如瀏覽歷史、購物車內容等。 | * 儲存需要在不同頁面之間共用的使用者資訊。 | |
| * 防止跨站請求偽造 (CSRF) 攻擊,例如在表單中嵌入隱藏的 Session ID 或權杖,驗證請求來源是否合法。 | 
注意事項:
總結:
Cookie 和 Session 都是網站開發中常用的技術,它們各有優缺點,網站可以根據具體需求選擇合適的技術來儲存和管理使用者資訊和狀態。
實作練習:
version: '3'
services:
  # ... 其他服務 ...
  postgres:
    image: postgres:13
    environment:
      - POSTGRES_USER=postgres
      - POSTGRES_PASSWORD=postgresPassword
    volumes:
      - ./init.sql:/docker-entrypoint-initdb.d/init.sql
      - postgres_data:/var/lib/postgresql/data 
volumes:
  postgres_data:
-- 檢查 myapp 資料庫是否存在,如果不存在則建立資料庫
SELECT 'CREATE DATABASE myapp'
WHERE NOT EXISTS (SELECT FROM pg_database WHERE datname = 'myapp')\gexec
-- 連接到 myapp 資料庫
\c myapp
-- 建立表(如果不存在)
CREATE TABLE IF NOT EXISTS users (
    id SERIAL PRIMARY KEY,
    username VARCHAR(50) UNIQUE NOT NULL,
    email VARCHAR(100) UNIQUE NOT NULL,
    password VARCHAR(255) NOT NULL,
    created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 插入初始資料(如果表是空的)
INSERT INTO users (username, email, password)
SELECT 'testuser', 'testuser@example.com', 'password123' 
WHERE NOT EXISTS (SELECT 1 FROM users);
docker-compose.yml 文件中,我們新增了一個 volume 映射,將 init.sql 文件掛載到 PostgreSQL 容器的 /docker-entrypoint-initdb.d/ 目錄。這樣,當容器首次啟動時,會自動執行這個 SQL 腳本。init.sql 文件包含了建立 myapp 資料庫、users 表,以及插入一個測試使用者的 SQL 指令。init.sql 文件,內容如上所示。docker-compose up -d --build 重新建立與啟動服務。init.sql 腳本。// 引入 session 套件
const session = require('express-session');
// 使用 session Middleware 來啟用 session 功能
app.use(session({
  secret: 'your_session_secret', // Hard-coded 密鑰,應該要更換
  // secret: process.env.SESSION_SECRET, // 從環境變數取得密鑰,比較安全
  resave: false, // 是否重新保存 session
  saveUninitialized: true, // 是否保存未初始化的 session
  cookie: { secure: false } // 在非 HTTPS 環境下也能使用 session
  // cookie: { secure: true } // 在 HTTPS 環境下使用 session
}));
set-cookie

實現使用者登入和登出功能。
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
router.post('/login', authController.login);
router.post('/logout', authController.logout);
module.exports = router;
const db = require('../db/postgres');
const authHandler = {
    async login(req, res) {
        const { username, password } = req.body;
        try {
            const result = await db.query('SELECT * FROM users WHERE username = $1', [username]);
            
            if (result.rows.length === 0) {
                return res.status(401).json({ message: '使用者不存在或密碼錯誤' });
            }
            const user = result.rows[0];
            const isPasswordValid = password === user.password;
            
            if (!isPasswordValid) {
                return res.status(401).json({ message: '使用者不存在或密碼錯誤' });
            }
        
            req.session.userId = user.id;
            req.session.username = user.username;
            res.json({ message: '登入成功', user: { id: user.id, username: user.username, email: user.email } });
        } catch (error) {
            res.status(500).json({ error: error.message });
        }
    },
    async logout(req, res) {
        req.session.destroy(err => {
            if (err) {
                return res.status(500).json({ message: '登出失敗' });
            }
            res.json({ message: '登出成功' });
        });
    }
};
module.exports = authHandler;
const express = require('express');
const session = require('express-session');
const authRoutes = require('./routes/authRoutes');
const app = express();
app.use(express.json());
app.use(express.urlencoded({ extended: true }));
app.use(session({
  secret: 'your_session_secret',
  resave: false,
  saveUninitialized: true,
  cookie: { secure: false }
}));
app.use('/api/auth', authRoutes);
// ... 其他路由和Middleware ...
<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用者登入</title>
    <style>
        /* ... 樣式省略 ... */
    </style>
</head>
<body>
    <div class="container">
        <h1>使用者登入</h1>
        <form id="loginForm">
            <label for="username">帳號</label>
            <input type="text" id="username" name="username" required>
            
            <label for="password">密碼</label>
            <input type="password" id="password" name="password" required>
            
            <button type="submit">登入</button>
        </form>
        <div id="errorMessage" class="error-message"></div>
    </div>
    <script>
        document.getElementById('loginForm').addEventListener('submit', async (e) => {
            e.preventDefault();
            
            const username = document.getElementById('username').value;
            const password = document.getElementById('password').value;
            const errorMessage = document.getElementById('errorMessage');
            
            try {
                const response = await fetch('/api/auth/login', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ username, password }),
                });
                
                if (response.ok) {
                    const result = await response.json();
                    alert('登入成功!');
                    window.location.href = '/dashboard';
                } else {
                    const error = await response.json();
                    errorMessage.textContent = error.message || '登入失敗,請檢查您的使用者名和密碼';
                }
            } catch (error) {
                errorMessage.textContent = '發生錯誤,請稍後再試';
                console.error('Error:', error);
            }
        });
    </script>
</body>
</html>
authRoutes.js 來處理登入和登出路由。authController.js 包含了登入和登出的邏輯。server.js 中,我們新增了 session Middleware和認證路由。login.html 提供了一個登入表單,並使用 JavaScript 來處理表單提交。現在,我們來實現使用者註冊功能。
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');
router.post('/login', authController.login);
router.post('/logout', authController.logout);
router.post('/register', authController.register);  // 新增這行
module.exports = router;
const db = require('../db/postgres');
const authHandler = {
    // ... 保留原有的 login 和 logout 方法 ...
    async register(req, res) {
        const { username, email, password } = req.body;
        try {
            const result = await db.query(
                'INSERT INTO users (username, email, password) VALUES ($1, $2, $3) RETURNING id, username, email',
                [username, email, password]
            );
            
            const newUser = result.rows[0];
            res.status(201).json({ message: '註冊成功', user: newUser });
        } catch (error) {
            if (error.code === '23505') { // unique_violation
                res.status(400).json({ message: '使用者名稱或電子郵件已存在' });
            } else {
                res.status(500).json({ error: error.message });
            }
        }
    }
};
module.exports = authHandler;
<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用者註冊</title>
    <style>
        /* ... 樣式省略 ... */
    </style>
</head>
<body>
    <div class="container">
        <h1>使用者註冊</h1>
        <form id="registrationForm">
            <label for="username">帳號</label>
            <input type="text" id="username" name="username" required>
            
            <label for="email">電子郵件</label>
            <input type="email" id="email" name="email" required>
            
            <label for="password">密碼</label>
            <input type="password" id="password" name="password" required>
            
            <button type="submit">註冊</button>
        </form>
    </div>
    <script>
        document.getElementById('registrationForm').addEventListener('submit', async (e) => {
            e.preventDefault();
            
            const username = document.getElementById('username').value;
            const email = document.getElementById('email').value;
            const password = document.getElementById('password').value;
            
            try {
                const response = await fetch('/api/auth/register', {
                    method: 'POST',
                    headers: {
                        'Content-Type': 'application/json',
                    },
                    body: JSON.stringify({ username, email, password }),
                });
                
                if (response.ok) {
                    const result = await response.json();
                    alert('註冊成功!');
                    window.location.href = '/login';
                } else {
                    const error = await response.json();
                    alert(`註冊失敗: ${error.message}`);
                }
            } catch (error) {
                alert('發生錯誤,請稍後再試');
                console.error('Error:', error);
            }
        });
    </script>
</body>
</html>
app.get('/register', (req, res) => {
  res.sendFile(path.join(__dirname, 'public', 'register.html'));
});
authRoutes.js 中新增了註冊路由。authController.js 中,我們實現了註冊邏輯,將新使用者資訊插入資料庫。register.html,提供註冊表單和處理表單提交的 JavaScript 程式碼。server.js 中新增了註冊頁面的路由。
const express = require('express');
const path = require('path');
// ... 其他導入和Middleware ...
app.set('view engine', 'ejs');
app.set('views', path.join(__dirname, 'views'));
app.get('/dashboard', (req, res) => {
  if (req.session.userId) {
    res.render('dashboard', {
      username: req.session.username,
    });
  } else {
    res.redirect('/login');
  }
});
<!DOCTYPE html>
<html lang="zh-TW">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>使用者儀表板</title>
    <style>
        /* ... 樣式省略 ... */
    </style>
</head>
<body>
    <header>
        <h1>使用者儀表板</h1>
        <div class="user-info">
            歡迎, <span id="username"><%= username %></span>!
            <button id="logoutBtn" class="logout-btn">登出</button>
        </div>
    

<h1>test<h1>

思考: 為什麼 html 沒有被觸發
本文深入探討了網站登入註冊功能的實現,涵蓋了 Cookie、Session 的概念及其在使用者身份驗證中的應用。主要內容包括:
問題: 為什麼在儀表板頁面中,使用 <h1>test<h1> 作為使用者名稱註冊並登入後,HTML 標籤沒有被觸發?
答案: 這是因為 EJS 模板引擎在渲染變數時會自動進行 HTML 轉義,將特殊字符(如 < 和 >)轉換為對應的 HTML 實體(< 和 >),從而防止 XSS 攻擊。這是一種安全機制,確保使用者輸入的內容不會被誤解為 HTML 執行。
Cookie 儲存在哪裡?
Session 資料通常儲存在哪裡?
哪種方法不適合用來儲存敏感資訊?
EJS 是什麼?
以下哪項不是實現使用者認證的常見方法?