Node.Js_Express_Part3.Cookie及Session技術介紹與登入登出狀態顯示切換實作


Cookie:是一種儲存在用戶端(瀏覽器)的一小段文字內容,而且用純文字記錄。
作用:為了實踐用戶端跟伺服器端之間狀態保持
通常不建議使用cookie保存敏感訊息,此外用戶自己瀏覽器端還能設定要不要啟用。


  • 儲存於客戶端:cookie 存在使用者的瀏覽器中,通常以文字檔案形式儲存。
  • 多個網頁狀態共用。
  • 有大小限制,包括數量與容量大小。(一般瀏覽器限制每個 cookie 不超過 4KB。)
  • 可以設定 cookie 的有效期限,讓它過期後自動刪除。若未設定有效期限,則為 session cookie,會在瀏覽器關閉後自動刪除。(當設定到期日後即便瀏覽器關閉後仍有效,只要沒過期。)
  • 安全性考量:cookie 內容容易被第三方攔截和解讀,因此通常需要加密敏感數據或搭配 HttpOnly 和 Secure 屬性來提升安全性。
在 Node.js 中,可以使用 cookie-parser 中介軟體來處理 cookie。




儲存位置
• Chrome:
C:\Users\XXX\AppData\Local\Google\Chrome\User Data\Default\Cookies




Session 是在伺服器端維持的一組狀態資料,通常用來儲存敏感或不希望存放在客戶端的數據。當使用者登入後,伺服器會建立一個 session 並將 session ID 回傳給客戶端(通常透過 cookie 傳遞)。之後的請求中,客戶端帶上這個 session ID,伺服器即可根據 session ID 獲取相應的 session 資料,達到狀態維護的效果。

Session 特點
  • 儲存於伺服器端:session 資料儲存在伺服器端,客戶端僅保存 session ID。
  • 安全性較高:由於 session 資料存在伺服器端,比 cookie 更安全。
  • 依賴 cookie 或 URL 參數:session 通常透過 cookie 或 URL 參數來管理和識別。
  • 連線關閉或瀏覽器關閉Session就消失,會過期。
  • 每個連線一個獨立的Session。
在 Node.js 中,可以使用 express-session 來管理 session。




總結:

對於不太敏感的使用者偏好設定,可能使用 cookie 存放;而對於登入資訊等敏感數據,則適合使用 session 來保護。

Step1.安裝session模組
npm i express-session -S



Step2.導入Session模組

//導入session模組
const session = require('express-session')


Step3.在Express中使用Session的中介軟體,透過app.use




Step4.把私有資料保存在當前請求的session中


app.js程式(增加session保存處理機制)

const express = require('express')
//const path = require('path');
const app = express()
const bodyParser = require('body-parser')
//導入session模組
const session = require('express-session')
//註冊session中間件,之後只要可存取到req物件,就必定可存取到req.session。
app.use(session({
        secret:'這段是加密的密鑰',
        resave:false,
        saveUninitialized:false,
    })
)

const mysql = require('mysql')
const moment = require('moment') //獲取當前時間戳
const conn = mysql.createConnection({
    port:3306,
    host: '127.0.0.1',
    database: 'blog_db',
    user: 'root',
    password: 'root'
})


app.set('view engine','ejs') //設置默認採用模板引擎名稱
app.set('views','./views') //設置模板頁面存放路徑

app.use(bodyParser.urlencoded({extended:false}))

//將node_modules資料夾,託管為靜態資源目錄
app.use('/node_modules',express.static('./node_modules'))
//app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));

app.get('/', (req,res) => {
    //使用render函數之前,必須確保已經安裝和配置好ejs模板引擎
    res.render('index.ejs',{
        user: req.session.user,
        islogin: req.session.islogin
    })
})

app.get('/register' , (req,res) => {
    res.render('./user/register.ejs',{})
})

app.post('/register' , (req,res) => {
    const body = req.body
    console.log(body)
    if(body.username.trim().length <= 0 || 
        body.password.trim().length <=0 || 
        body.nickname.trim().length <=0){
            return res.send({msg: '請填寫完整表單欄位,再註冊帳號!',status:501})
        }
    //確認是否帳號名有重複
    const sql_str = 'select count(*) as count from blog_users where username=?'
    conn.query(sql_str,body.username,(err,result) => {
        console.log(err)
        console.log('result1:')
        console.log(result)
        if(err) 
            return res.send({msg:'帳號名查重異常!',status:502})
        if(result[0].count !==0) 
            return res.send({msg:'請改用其他帳號名重新註冊!',status:503})
        
        body.ctime = moment().format('YYYY-MM-DD HH:mm:ss')
        const sql_str2 = 'insert into blog_users set ?'
        console.log(body)
        conn.query(sql_str2 , body , (err,result) => {
            console.log('result2:')
            console.log(result)
            if(err || result.affectedRows !== 1) {
                console.log(err)
                return res.send({msg:'註冊新帳號失敗!',status:504})
            }
                
            res.send({msg:'註冊新帳號成功' , status:200})
        })
    })
})

app.get('/login' , (req,res) => {
    res.render('./user/login.ejs',{})
})

app.post('/login' , (req,res) => {
    const body = req.body
    const sql_str = 'select * from blog_users where username = ? and password=?'
    conn.query(sql_str , [body.username,body.password] , (err,result) => {
        if(err || result.length !== 1)
            return res.send({msg:'帳號登入失敗' , status:501})
        //打印查看目前session內容
        console.log(req.session)
        //將登入成功之後的帳號資訊、狀態結果掛載到session上
        console.log(result)//登入成功後的帳號資訊
        req.session.user = result[0]//帳號資訊
        req.session.islogin = true//狀態結果
        res.send({msg:'ok',status:200})
    })
})


app.listen(80, () => {
    console.log('server running at http://127.0.0.1')
})

在對默認首頁get請求中調整了傳入參數,採用req.session自定義兩個屬性user跟islogin。
為了之後畫面右上方狀態顯示切換判定依據,可以在index.ejs畫面做參數嵌套判斷。


./views/index.ejs

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap-theme.min.css">
    <script src="/node_modules/jquery/dist/jquery.min.js"></script>
    <!-- 叮嚀:bootstrap的js是有依賴jquery文件的,要在之前先配置jquery。 -->
    <script src="/node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
</head>
<body>

    <!-- 導覽列區塊 -->
    <nav class="navbar navbar-default">
        <div class="container-fluid">
          <!-- Brand and toggle get grouped for better mobile display -->
          <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
              <span class="sr-only">Toggle navigation</span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">論壇</a>
          </div>
      
          <!-- Collect the nav links, forms, and other content for toggling -->
          <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">

            <% if(islogin) {%>
              <div class="nav navbar-nav navbar-right navbar-form">
                <button class="btn btn-warning">歡迎</button>
                <button class="btn btn-danger">註銷</button>
              </div>
              <ul class="nav navbar-nav navbar-right">
                <li class="dropdown">
                  <a href="#" class="dropdown-toggle"
                    data-toggle="dropdown" role="button"
                    aria-haspopup="true" 
                    aria-expanded="false">發表
                    <span class="caret"></span>
                  </a>
                  <ul class="dropdown-menu">
                    <li>
                      <a href="#">文章</a>
                    </li>
                    <li>
                      <a href="#">問題</a>
                    </li>
                  </ul>
                </li>
              </ul>

            <%  }  else {%>
            <div class="nav navbar-nav navbar-right navbar-form">
              <a class="btn btn-success" href="/register">註冊</a>
              <a class="btn btn-primary" href="/login">登入</a>
            </div>
            <% } %>
 
          </div><!-- /.navbar-collapse -->
        </div><!-- /.container-fluid -->
    </nav>

    <h1>貼文列表</h1>

    <!-- 版權聲明區塊 -->
      <div class="text-center text-muted">
        AAA © BBB 2025
      </div>

</body>
</html>


效果呈現
預設尚未登入只顯示註冊登入


當成功登入後就會顯示其他按鈕群組


可以在歡迎按鈕多串出登入帳號暱稱


Step5.註銷(登出)


./views/index.ejs(註冊登出事件)

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Document</title>
    <link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap.min.css">
    <link rel="stylesheet" href="/node_modules/bootstrap/dist/css/bootstrap-theme.min.css">
    <script src="/node_modules/jquery/dist/jquery.min.js"></script>
    <!-- 叮嚀:bootstrap的js是有依賴jquery文件的,要在之前先配置jquery。 -->
    <script src="/node_modules/bootstrap/dist/js/bootstrap.min.js"></script>
</head>
<body>

    <!-- 導覽列區塊 -->
    <nav class="navbar navbar-default">
        <div class="container-fluid">
          <!-- Brand and toggle get grouped for better mobile display -->
          <div class="navbar-header">
            <button type="button" class="navbar-toggle collapsed" data-toggle="collapse" data-target="#bs-example-navbar-collapse-1" aria-expanded="false">
              <span class="sr-only">Toggle navigation</span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
              <span class="icon-bar"></span>
            </button>
            <a class="navbar-brand" href="/">論壇</a>
          </div>
      
          <!-- Collect the nav links, forms, and other content for toggling -->
          <div class="collapse navbar-collapse" id="bs-example-navbar-collapse-1">

            <% if(islogin) {%>
              <div class="nav navbar-nav navbar-right navbar-form">
                <button class="btn btn-warning">歡迎<strong><%=user.nickname%></strong></button>
                <a class="btn btn-danger" href="/logout">登出</a>
              </div>
              <ul class="nav navbar-nav navbar-right">
                <li class="dropdown">
                  <a href="#" class="dropdown-toggle"
                    data-toggle="dropdown" role="button"
                    aria-haspopup="true" 
                    aria-expanded="false">發表
                    <span class="caret"></span>
                  </a>
                  <ul class="dropdown-menu">
                    <li>
                      <a href="#">文章</a>
                    </li>
                    <li>
                      <a href="#">問題</a>
                    </li>
                  </ul>
                </li>
              </ul>

            <%  }  else {%>
            <div class="nav navbar-nav navbar-right navbar-form">
              <a class="btn btn-success" href="/register">註冊</a>
              <a class="btn btn-primary" href="/login">登入</a>
            </div>
            <% } %>
 
          </div><!-- /.navbar-collapse -->
        </div><!-- /.container-fluid -->
    </nav>

    <h1>貼文列表</h1>

    <!-- 版權聲明區塊 -->
      <div class="text-center text-muted">
        AAA © BBB 2025
      </div>

</body>
</html>

app.js(增加登出請求)

const express = require('express')
//const path = require('path');
const app = express()
const bodyParser = require('body-parser')
//導入session模組
const session = require('express-session')
//註冊session中間件,之後只要可存取到req物件,就必定可存取到req.session。
app.use(session({
        secret:'這段是加密的密鑰',
        resave:false,
        saveUninitialized:false,
    })
)

const mysql = require('mysql')
const moment = require('moment') //獲取當前時間戳
const conn = mysql.createConnection({
    port:3306,
    host: '127.0.0.1',
    database: 'blog_db',
    user: 'root',
    password: 'root'
})


app.set('view engine','ejs') //設置默認採用模板引擎名稱
app.set('views','./views') //設置模板頁面存放路徑

app.use(bodyParser.urlencoded({extended:false}))

//將node_modules資料夾,託管為靜態資源目錄
app.use('/node_modules',express.static('./node_modules'))
//app.use('/node_modules', express.static(path.join(__dirname, 'node_modules')));

app.get('/', (req,res) => {
    //使用render函數之前,必須確保已經安裝和配置好ejs模板引擎
    res.render('index.ejs',{
        user: req.session.user,
        islogin: req.session.islogin
    })
})

app.get('/register' , (req,res) => {
    res.render('./user/register.ejs',{})
})

app.get('/logout', (req,res) =>{
    req.session.destroy(function(){
        res.redirect('/')//重新跳轉到首頁
    })
})

app.post('/register' , (req,res) => {
    const body = req.body
    console.log(body)
    if(body.username.trim().length <= 0 || 
        body.password.trim().length <=0 || 
        body.nickname.trim().length <=0){
            return res.send({msg: '請填寫完整表單欄位,再註冊帳號!',status:501})
        }
    //確認是否帳號名有重複
    const sql_str = 'select count(*) as count from blog_users where username=?'
    conn.query(sql_str,body.username,(err,result) => {
        console.log(err)
        console.log('result1:')
        console.log(result)
        if(err) 
            return res.send({msg:'帳號名查重異常!',status:502})
        if(result[0].count !==0) 
            return res.send({msg:'請改用其他帳號名重新註冊!',status:503})
        
        body.ctime = moment().format('YYYY-MM-DD HH:mm:ss')
        const sql_str2 = 'insert into blog_users set ?'
        console.log(body)
        conn.query(sql_str2 , body , (err,result) => {
            console.log('result2:')
            console.log(result)
            if(err || result.affectedRows !== 1) {
                console.log(err)
                return res.send({msg:'註冊新帳號失敗!',status:504})
            }
                
            res.send({msg:'註冊新帳號成功' , status:200})
        })
    })
})

app.get('/login' , (req,res) => {
    res.render('./user/login.ejs',{})
})

app.post('/login' , (req,res) => {
    const body = req.body
    const sql_str = 'select * from blog_users where username = ? and password=?'
    conn.query(sql_str , [body.username,body.password] , (err,result) => {
        if(err || result.length !== 1)
            return res.send({msg:'帳號登入失敗' , status:501})
        //打印查看目前session內容
        console.log(req.session)
        //將登入成功之後的帳號資訊、狀態結果掛載到session上
        console.log(result)//登入成功後的帳號資訊
        req.session.user = result[0]//帳號資訊
        req.session.islogin = true//狀態結果
        res.send({msg:'ok',status:200})
    })
})


app.listen(80, () => {
    console.log('server running at http://127.0.0.1')
})







留言

這個網誌中的熱門文章

何謂淨重(Net Weight)、皮重(Tare Weight)與毛重(Gross Weight)

Architecture(架構) 和 Framework(框架) 有何不同?_軟體設計前的事前規劃的藍圖概念

經得起原始碼資安弱點掃描的程式設計習慣培養(五)_Missing HSTS Header