認證功能-登入

甚麼是 Session?

追蹤同一使用者透過瀏覽器發出的連續請求時,可以製作一組憑證,紀錄設備間的交流,這種機制叫 session。

session 是擁有所有使用者登入紀錄的龐大物件,物件裡都是 key-value pair, session id(sid)、 value(session_data)。 如同飲料店(伺服器)的會員機制。

存放 session 物件的地方稱為 session store。

cookie,以飲料店的會員卡為例, cookie 為會員卡,通常會員卡放在皮夾中,皮夾即為瀏覽器。

瀏覽器讓伺服器可以把 session id 存在瀏覽器的 cookie 上,使瀏覽器每次對伺服器發送請求時,都帶有cookie資訊,於是伺服器可以透過 cookie 取得 session id。

使用 dev tool 打開 network

打開看伺服器傳給瀏覽器的 Response Headers,可以發現 set-cookie,這串特殊編,解譯後會包含session id。

Response Headers 中的 cookie長串訊息,包含著上次伺服器告訴瀏覽器的通關密碼。 所以這次 request-response 流程, 瀏覽器把這段密碼放到 cookie 傳送給伺服器,於是伺服器就可以辨識出是哪個瀏覽器了。

session 安全性

如果 session id 被盜用,被放到另外一個 cookie 中,將 session id 修改成跟你一樣,就可以在不需要登入的狀況下操作他人帳號。

最普遍防止方法是透過 簽章(signature),在伺服器端,開發者設定 密鑰(secret), 與要傳送出去的 session id 透過認證演算法整合,產生一組邊碼將之存在 cookie。

於是伺服器端可以使用 secret,還原 session id,如果被惡意篡改,還原會發生錯誤而導致比對無效。

登入功能

實務上稱為 使用者認證(user authentication)。 建立登入功能,其中簡單的方法是如同註冊功能,檢查資料庫使用者註冊過的資料,使用 User.findOne查詢資料庫,再 比對密碼是否正確

另一種做法使用 Express-session 與 passport,皆為 middleware。

Express-session

建立 session 的 middleware,使用 session(option)。

app.use(session({
secret: 'xxxxxx',
resave: false,
saveUninitialized: true,
cookie: { secure: true }
}))

以上四個參數都是 session middleware 提供的 參數(option) 之一,未來將用以載入,放在修改 app.js 部分。

secret 是可以用來自訂的通關密語,會與資訊做認證演算法整合。

Passport

使用來完成認證功能的 middleware。

Passport Strategies

Passport 須配合認證策略(strategies),passport 目前提供超過500種認證策略,讓開法者可以容易將自己產品與各種大型平台服務整合。

設定策略語法(configure strategy)

passport-local 本地策略的策略語法

Serialize 與 deserialize

Passport 的序列化會把 使用者實例(user instance) 轉化為字串存在伺服器端的 session 資料。

反序列化是透過 session 資料,取回擁有 login session 的使用者實例。

// 序列化
passport.serializerUser(function (user, done){
done(null, user.id)
})
// 反序列化
passport.derserializerUser(function (id, done){
user.findById(id, function (err, user){
done(err,user)
})
})

登入認證實作步驟

  1. 安裝 passport、passport-local、express-session
  2. 新增config資料夾,命名 passport.js
  3. 修改 app.js 使用 passport、 express-session
  4. 修改使用者路由 routes/user.js

安裝 middleware

npm install passport passport-local express-session

建立 passport middleware in config/passport.js

關注點分離,新增config資料夾,命名 passport.js,載入 middleware、model,宣告 LocalStrategy 物件, 引入官方語法,定義usernameField 為 email(以 email 代表用戶名稱)。

const LocalStrategy =require('passport-local').Strategy
const mongoose = require('mongoose')

// include User model
const User = require('../models/user')
module.exports = passport => {
passport.use(
new LocalStrategy({ usernameField: 'email' }, (email, password, done) => { // use LocalStrategy
User.findOne({
email: email
}).then(user => {
if (!user) {
return done(null, false, {message: 'That email is not registered'})
}
if (user.password != password) {
return done(null, false, {message: 'Email or Password incorrect'})
}
return done(null, user)
})
})
)

// 序列化
passport.serializerUser(function (user, done){
done(null, user.id)
})
// 反序列化
passport.derserializerUser(function (id, done){
user.findById(id, function (err, user){
done(err,user)
})
})
}

修改 app.js

  1. 載入 express-session、passport middleware
  2. 使用 app.use 初始化使用 session、passport
  3. 載入 config/passport.js,設定一個 local variable 儲存 user

2019-6-27 : 新增變數,這個變數的值會從瀏覽器傳到伺服器,讓伺服器知道使用者是否已經登入。

變數: req.local.isAuthenticatedisAuthenticated 是現成方法, 在設定認證也有相關內容。

const session = require('express-session')
const passport = require('passport')

app.use(session({ //使用者認證
secret: 'your secret word' // 這裡只用 secret
}))

// 初始化 passport 使用 passport session(放在 session 之後)
app.use(passport.initialize())
app.use(possport.session())

// passport config 引入前面產生的session id所產生的值,從 req.user 放在 res.local 裡面
require('./config/passport')(passport) // passport instance 套件引入瀏覽器中的 session id(passport)

app.use((req, res, next) => {
res.locals.user = req.user
res.locals.isAuthenticated = req.isAuthenticated // 加入到這,讓 view可以使用
next()
})

修改路由 routes/user.js and controller

const express = require('express')
const router = express.Router()
const passport = require('passport') // 載入 passport

// 登入
router.get('/login', (req, res) => {
res.render('login')
})

// 送出檢查
router.post('/login', (req, res, next) => { // 增加下一棒 next
passport.authenticate('local', {
successRedirect: '/', //成功 轉到跟目錄
failureRedirect: '/users/login' // 失敗 到登入頁面
})(req, res, next)

修改登入頁面

由於使用 handlebars 所以在 template 的地方加入相關語法可以使登出登入按鈕有所變化,而這變化是吃最後在 app.js 中 app.use 有設定到抓取 req.isAuthenticated 的程式碼。

{{#if isAuthenticated}}
<a href="/users/logout">Logout</a>
{{else}}
<a href="/users/login">Login</a>
{{/if}}