Session
- Session是架在Cookie的上層,Session的id是存在Cookie裡面
- 若 Client 的瀏覽器停在某個網頁,使用者可能某些原因久久未再拜訪該網站,或者根
本就已離開該站。此時會依 Session 的存活時間,決定 Session 是否有效。 - Server 是以 Client 最後一次拜訪開始重新計時的,若 Client 在 Session 存活時間內,
持續訪問該站,Session 就會一直有效。 - 利用 Cookie 存放「Session ID」,在 Client 第一次拜訪時將 Session ID 存入 Cookie。
- 有了 Session ID 之後,Server 會在主機(記憶體、檔案或資料庫)為每個 Session ID
建立一個對應的 Session 物件,資料就存在 Session 物件裡。
安裝 express-session:
npm i express-session
範例:顯示頁面刷新次數
const session = require('express-session');
// session()是一個function,呼叫它去建立middleware
app.use(session({
// 新用戶沒有使用到 session 物件時不會建立 session 和發送 cookie
// saveUninitialized與resave是官方建議要設定的,若沒有設定,會有warning
saveUninitialized: false,
resave: false, // 沒變更內容是否強制回存
secret: 'kkk123', // 加密用的字串
// 若把下面的cookie設定拿掉,就等於沒有設定session的存活時間,待瀏覽器關閉才會消失
cookie: {
maxAge: 1200000, // session的存活時間,20分鐘,單位毫秒
}
}));
// top-level middleware設定好session之後,就可以在top-level middleware使用session
app.get('/try-session', (req, res) => {
req.session.my_var = req.session.my_var || 0; // 預設為 0
req.session.my_var++;
res.json({
my_var: req.session.my_var,
session: req.session
});
});
執行結果:
測試:
如果把index.js
隨便新增一行空白,然後儲存,就會觸發nodemon重新啟動node.js server。這時再連到 http://localhost:3000//try-session ,就會發現計數器從0開始計算。
打開chrome的Network,可以觀察client跟server之間的資料傳遞。
Response
裡面的 Set-Cookie 是server傳回來的檔頭,要求使用者的瀏覽器設定Cookie,其中**connect.sid
**是Express-Session的Session ID。另外也有路徑(Path),過期時間(Expires)。
在 Request
的時候,同樣也是把Cookie的資訊從client端送回server端(如下圖),請參考下圖的Request Header的Cookie。
製作登入表單
先做 樣板 ,到 /views
資料夾,新增 /view/login.ejs
,檔案內容如下:
<%- include ('parts/html-head') %>
<%- include ('parts/navbar') %>
<div class="container">
<form name="form1" method="post" onsubmit="return formCheck();"> <!-- onsubmit為事件處理器 -->
<div class="form-group">
<label for="account">Account</label>
<input type="email" class="form-control" id="account" name="account">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
<%- include('parts/scripts') %>
<script>
function formCheck(){
return false; // 目的為不讓表單送出
}
</script>
<%- include('parts/html-foot') %>
在 index.js
新增以下路由:
app.get('/login', (req, res)=>{
res.render('login');
});
app.post('/login', (req, res)=>{
// res.render('login');
const output={
};
});
就可以在 http://localhost:3000/login
看到以下登入畫面:
隨便填資料按下送出,並不會真的送出,因為:
onsubmit="retuen formCheck()"
中,formCheck()
回傳 false
,所以不會將表單送出。
原則上應該要在 formCheck()
設定檢查的功能,如,檢查欄位有沒有值,判斷格式是否正確,如果確認正確再送AJAX的送出資料。
我們接下來要用fetch實作AJAX,關於fetch可以參考:Using Fetch - Web APIs | MDN
login.ejs
的 <script></script>
改為:
<script>
function formCheck() {
const fd = new FormData(document.form1);
fetch('/login', {
method: 'POST',
body: fd
})
.then(r => r.json()) // 後端傳JSON到前端
.then(obj => {
console.log(obj)
});
return false; // 目的為不讓表單送出
};
</script>
實際在 http://localhost:3000/login
填寫表單然後送出資料,會發現body沒有收到任何資訊:
這是因為前端送到後端的資料是 multipart/form-data
,express.js
的中介軟體 expressjs/body-parser
無法解析,所以我們要用一個 middleware
,要做 multer
。
解決方法為將
app.post('/login', (req, res)=>{
}
改為
app.post('/login', upload.none(), (req, res)=>{
}
使用 upload
來解析,但是實際沒有上傳任何檔案,所以後面加上 .none
。
再試一次,測試結果如下,可以真正取到資料了。
記得,使用FormData的時候,要使用expressjs/multer的upload去處理。因為expressjs/body-parser不會處理multipart/form-data資料。
如果一定要用expressjs/body-parser,就不要用FormData,可以改用物件然後做JSON.stringify(),然後記得下檔頭
使用者帳號密碼查驗
因為還沒有設定資料庫連線,所以我們先把特定帳號密碼寫死在程式裡面。
index.js
:
app.post('/login', upload.none(), (req, res) => {
// res.render('login');
const users = {
'[email protected]': { // [email protected]是key值
password: '123',
nickname: 'Benjamin'
},
'[email protected]': {
password: '456',
nickname: 'Benjamin2'
}
};
const output = {
success: false,
body: req.body
};
// 檢查帳密是否正確
if (users[req.body.account] && users[req.body.account].password===req.body.password){
// 若帳密正確,就把使用者資訊寫入session
output.success = true;
req.session.user={
id: req.body.account,
nickname: users[req.body.account].nickname
}
}
output.sess_user = req.session.user
res.json(output);
});
login.ejs
:
<%- include ('parts/html-head') %>
<%- include ('parts/navbar') %>
<div class="container">
<form name="form1" method="post" onsubmit="return formCheck();">
<!-- onsubmit為事件處理器 -->
<div class="form-group">
<label for="account">Account</label>
<input type="email" class="form-control" id="account" name="account">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
<%- include('parts/scripts') %>
<script>
function formCheck() {
const fd = new FormData(document.form1);
fetch('/login', {
method: 'POST',
body: fd
})
.then(r => r.json()) // 後端傳JSON到前端
.then(obj => {
console.log(obj)
});
return false; // 目的為不讓表單送出
};
</script>
<%- include('parts/html-foot') %>
測試登入,登入成功:
顯示登入狀態
目前我們已使用fetch做AJAX實作搭配session做使用者登入的功能。由於使用AJAX,所以登入後,頁面沒有跳轉,目前在網頁上面也看不到登入是否成功。所以接下來我們要在網頁上面加上登入狀態的顯示。
可以到Bootstrap官網挑選合適的元件(component)來使用。
<div class="alert alert-primary" role="alert">
A simple primary alert—check it out!
</div>
login.ejs
改為:
<%- include ('parts/html-head') %>
<%- include ('parts/navbar') %>
<div class="container">
<div id="infobar" class="alert" role="alert" style="display: none;">
</div>
<form name="form1" method="post" onsubmit="return formCheck();">
<!-- onsubmit為事件處理器 -->
<div class="form-group">
<label for="account">Account</label>
<input type="email" class="form-control" id="account" name="account">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
</div>
<%- include('parts/scripts') %>
<script>
const infobar = $('#infobar');
function formCheck() {
infobar.hide();
const fd = new FormData(document.form1);
fetch('/login', {
method: 'POST',
body: fd
})
.then(r => r.json()) // 後端傳JSON到前端
.then(obj => {
console.log(obj)
if (obj.success) {
infobar
.removeClass('alert-danger')
.removeClass('alert-success')
.addClass('alert-success')
.text('登入成功');
setTimeout(() => {
location.reload();
}, 1000);
} else {
infobar
.removeClass('alert-danger')
.removeClass('alert-success')
.addClass('alert-danger')
.text('登入失敗,帳號或密碼錯誤');
}
});
infobar.show();
return false; // 目的為不讓表單送出
};
</script>
<%- include('parts/html-foot') %>
實測結果如下:
新增訊息自動收藏功能
把 login.ejs
的部分程式改為:
if (obj.success) {
infobar
.removeClass('alert-danger')
.removeClass('alert-success')
.addClass('alert-success')
.text('登入成功');
setTimeout(() => {
location.reload();
}, 1000);
} else {
infobar
.removeClass('alert-danger')
.removeClass('alert-success')
.addClass('alert-danger')
.text('登入失敗,帳號或密碼錯誤');
setTimeout(() => {
infobar.slideUp();
}, 1000);
}
實測結果如下:
顯示歡迎訊息
接下來要做,如果登入成功,就把帳密的表單隱藏起來,然後顯示出歡迎訊息。
把 login.ejs
改為:
<%- include ('parts/html-head') %>
<%- include ('parts/navbar') %>
<div class="container">
<% if (sess.user) { %>
<h2><%= sess.user.nickname %>, 你好 </h2>
<p><a href="/logout">登出</a></p>
<% } else { %>
<div id="infobar" class="alert" role="alert" style="display: none;">
</div>
<form name="form1" method="post" onsubmit="return formCheck();">
<!-- onsubmit為事件處理器 -->
<div class="form-group">
<label for="account">Account</label>
<input type="email" class="form-control" id="account" name="account">
</div>
<div class="form-group">
<label for="password">Password</label>
<input type="password" class="form-control" id="password" name="password">
</div>
<button type="submit" class="btn btn-primary">Submit</button>
</form>
<% } %>
</div>
<%- include('parts/scripts') %>
<script>
const infobar = $('#infobar');
function formCheck() {
infobar.hide();
const fd = new FormData(document.form1);
fetch('/login', {
method: 'POST',
body: fd
})
.then(r => r.json()) // 後端傳JSON到前端
.then(obj => {
console.log(obj)
if (obj.success) {
infobar
.removeClass('alert-danger')
.removeClass('alert-success')
.addClass('alert-success')
.text('登入成功');
setTimeout(() => {
location.reload();
}, 1000);
} else {
infobar
.removeClass('alert-danger')
.removeClass('alert-success')
.addClass('alert-danger')
.text('登入失敗,帳號或密碼錯誤');
setTimeout(() => {
infobar.slideUp();
}, 1000);
}
});
infobar.show();
return false; // 目的為不讓表單送出
};
</script>
<%- include('parts/html-foot') %>
把 index.js
改為:
app.use((req, res, next) => {
res.locals.sess = req.session || {};
// 要預處理的事情
// res.locals.userData = {
// name: 'benctw',
// id: 386,
// action: 'edit'
// }
next();
});
實測:
登出
在 index.js
加入以下程式:
app.get('/logout', (req, res) => {
delete req.session.user;
res.redirect('/login');
})
小結 res
的成員:
res.end
res.send
res.render
res.json
res.redirect
將session存入資料庫
瀏覽 express-session專案,在「Compatible Session Stores」一節中,可以看到express-session支援儲存到許多地方,如sqlite, mysql, mongodb, redis等。現在要實作將session存到mysql。
先安裝 express-mysql-session
npm i express-mysql-session
在 index.js
加入:
const session = require('express-session');
const MysqlStore = require('express-mysql-session')(session);
const db = require(__dirname + '/db_connect2');
const sessionStore = new MysqlStore({}, db);
app.use(session({
saveUninitialized: false, // 新用戶沒有使用到 session 物件時不會建立 session 和發送 cookie
resave: false, // 沒變更內容是否強制回存
secret: 'kkk123', // 加密用的字串
store: sessionStore,
cookie: {
maxAge: 1200000, // 20分鐘,單位毫秒
}
}));