Lesson-1 Session
Session是什么
其實就是用戶的認證與授權。認證與授權又是什么?認證,就是讓服務器知道你是誰,授信,就是讓服務器知道你的權限是什么,什么能干,什么不能干
工作原理
以登陸為例,當客戶端通過用戶名與密碼請求服務端,服務端就會生成身份認證相關的Seccion數(shù)據(jù)。生成Session數(shù)據(jù)之后,可能保存在內存里,也可能保存在內存數(shù)據(jù)庫里(比如redis),并將Session ID返回給客戶端(比如請求頭里添加Set-Cookie:session=***)。客戶端將Session ID存放到cookie里。接下來客戶端的所有請求,都將附帶該Session ID,服務端通過該Session ID來查找該用戶相關的數(shù)據(jù)
Session 的優(yōu)勢
- 相比 JWT,最大的優(yōu)勢就在于可以主動清楚session了
- session 保存在服務器端,相對較為安全
- 結合 cookie 使用,較為靈活,兼容性較好(客戶端服務端都可以清除,也可以加密)
Session 的劣勢
- cookie+session 在跨域場景表現(xiàn)并不好(不可跨域,domain變量,需要復雜處理跨域)
- 如果是分布式部署,需要做多機共享 Session 機制(成本增加)
- 基于 cookie 的機制很容易被 CSRF
- 查詢 Session 信息可能會有數(shù)據(jù)庫查詢操作
Session 相關的概念介紹
- session::主要存放在服務器,相對安全
- cookie:主要存放在客戶端,并且不是很安全
- sessionStorage:僅在當前會話下有效,關閉頁面或瀏覽器后被清除
- localstorage:除非被清除,否則永久保存
Lesson-2 JWT 簡介
什么是 JWT?
- JSON Web Token 是一個開放標準(RFC[請求意見稿] 7519)
- 定義了一種緊湊且獨立的方式,可以將各方面之間的信息作為 JSON 對象進行安全傳輸
- 該信息可以驗證和信任,因為是經過數(shù)字簽名的
JWT 的構成
- 頭部(Header)
- 有效載荷(Payload)
- 簽名(Signature)
JWT 的例子
不同顏色,. 號結束,紅色代表Header,紫色代表Payload,藍色代表Signature
Header
Header,本質是JSON,使用 Base64 編碼,因此更加緊湊。Header 包含下面兩個字段:
- typ:token的類型,這里固定為 JWT
- alg:使用 hash 算法,例如 HMAC SHA256 或者 RSA
Header 編碼前后
編碼前:{"alg": "HS256", "typ": "JWT"}
編碼后:'eyJhbGciOiAiSFMyNTYiLCAidHlwIjogIkpXVCJ9'
Payload
- 存儲需要傳遞的信息,如用戶ID、用戶名等
- 還包含元數(shù)據(jù),如過期時間、發(fā)布人等
- 與 Header 不同,Payload 可以加密
Payload 編碼前后
編碼前:{"user_id": "zhangsan"}
編碼后: 'eyJ12VylkIjoiemhhbmdzYW4ifQ=='
由于base64會忽略最后的等號,所以結果為: 'eyJ12VylkIjoiemhhbmdzYW4ifQ'
Signature
- 對 Header 和 Payload 部分進行簽名
- 保證 Token 在傳輸?shù)倪^程中沒有被篡改或者損壞
Signature 算法
Signature = HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret),生成完之后依然需要進行一次 base64 編碼
JWT 原理
以登錄為例,瀏覽器端通過 post 請求將用戶名和密碼發(fā)送給服務端,服務端接受完進行核對,核對成功后將用戶 ID 和其他信息作為有效載荷(Payload),將其與頭部進行 base64 編碼之后,形成一個 JWT。服務端將該 JWT 作為登錄成功的返回結果,返回給瀏覽器端,瀏覽器端將其保存在 localstorage 或 sessionStorage 中。接下來的每次請求,都將帶上該 JWT (請求頭中,Authorization: Bearer*** JWT ***),服務端接收后都將核對(身份,令牌是否過期等),并返回相關的用戶信息
Lesson-3 JWT vs Session
- 可拓展性(水平拓展:加服務器,垂直拓展:增加硬盤容量,內存等,JWT優(yōu)勝)
- 安全性(兩者均有缺陷)
- XSS攻擊(跨站腳本攻擊):JS均能篡改;防范:簽名/加密,敏感信息不要放其中;
- CSRF(跨站請求偽造):兩者都能被篡改
- 重犯攻擊:過期時間盡量短
- 中間人攻擊:HTTPS來解決
- RESTful API,JWT優(yōu)勝,因為RESTful API提倡無狀態(tài),JWT符合要求
- 性能(各有利弊,因為JWT信息較強,所以體積也較大。不過Session每次都需要服務器查找,JWT信息都保存好了,不需要再去查詢數(shù)據(jù)庫)
- 時效性,Session能直接從服務端銷毀,JWT只能等到時效性到了才會銷毀(修改密碼也無法阻止篡奪者的使用)
Lesson-4 在 Nodejs 中使用 JWT
操作步驟
- 安裝 jsonwebtoken
- 簽名
- 驗證
這一節(jié)是直接在 git 中使用命令行來操作代碼片段的
安裝 jsonwebtoken
執(zhí)行 npm i jsonwebtoken
進行安裝插件
簽名
執(zhí)行 node
,進入 node 環(huán)境
執(zhí)行 jwt = require('jsonwebtoken');
引入jwt
執(zhí)行 token = jwt.sign({name: 'yose'}, 'secret');
生成token,secret 則代表密鑰,后面用于驗證使用的
執(zhí)行 jwt.decode(token);
直接解碼,但是并不會驗證,所以并不會用
執(zhí)行 jwt.verify(token, 'secret');
驗證密鑰并解碼,可以看到返回 { name: 'yose', iat: 1565110602 }
iat代表的是簽名時的時間,單位毫秒
驗證
執(zhí)行 jwt.verify(token, 'secret1');
密鑰被篡改,返回 JsonWebTokenError: invalid signature
,密鑰校驗失敗
執(zhí)行 jwt.verify(token.replace('e', 'a'), 'secret');
簽名篡改,返回 JsonWebTokenError: invalid token
,令牌校驗失敗
Lesson-5 實現(xiàn)用戶注冊
操作步驟
- 設計用戶 Schema
- 編寫保證唯一性的邏輯
設計用戶 Schema
userSchema 新增 password 字段,來表示用戶注冊的密碼
// model/users.js
const userSchema = new Schema({
name: { type: String, required: true },
password: { type: String, required: true },
});
調用postman,但是這個時候會把密碼也給返回出來,這是不合理的,所以我們需要把密碼設置為不返回
這里有兩種做法
一種是通過 mongoose 中 Query.prototype.select() 方法,由于是 Query對象,所以不是所有方法都能直接鏈式調用該方法來屏蔽關鍵字段,也就是說前面類似 create 中 save() 方法是不能用的。
這里簡化學習直接用find()方法演示
// controllers/users.js
async find (ctx) {
ctx.body = await User.find().select("-password");
}
第二種方法,通過 Schma 屬性 屬性來過濾(提倡,因為你不用去關注所使用的方法返回的究竟是不是 Query對象)
// models/users.js
const userSchema = new Schema({
__v: { type: Number, select: false },
name: { type: String, required: true },
password: { type: String, required: true, select: false },
});
結果依然如上面 postman 截圖所示,拿到的用戶列表并不會返回 password 字段
編寫保證唯一性的邏輯
真實場景中,我們經常會先對用戶名、手機、郵箱等等進行唯一性的驗證,如果已經被人注冊過了,那么就不能繼續(xù)使用這些數(shù)據(jù)進行注冊,否則用戶可以正常注冊
對到修改用戶,因為真實場景中用戶可能只是更改用戶名,但其他信息不更換,那么根據(jù) RESTful API 規(guī)范,我們需要將 put(全部更改) 方法改成 patch(局部更改)
// routes/users.js
// 修改特定用戶
router.patch('/:id', update); // 將put 改為 patch
create方法新增去重,并對密碼進行校驗(實際是為了報錯信息友好),而更新則需要將校驗全部改為非必傳(因為可以進行局部更改)
async create (ctx) {
ctx.verifyParams({
name: { type: 'string', required: true },
password: { type: 'string', required: true }
});
// 查重
const { name } = ctx.request.body;
const requesteUser = await user.findOne({ name });
if(requesteUser) ctx.throw(409, '用戶已經存在');
// save方法,保存到數(shù)據(jù)庫。并根據(jù) RESTful API最佳實踐,返回增加的內容
const user = await new User(ctx.request.body).save();
ctx.body = user;
}
async update (ctx) {
ctx.verifyParams({
name: { type: 'string', required: false },
password: { type: 'string', required: false }
});
// findByIdAndUpdate,第一個參數(shù)為要修改的數(shù)據(jù)id,第二個參數(shù)為修改的內容
const user = await User.findByIdAndUpdate(ctx.params.id, ctx.request.body);
if(!user) ctx.throw(404, '用戶不存在');
ctx.body = user;
}
Lesson-6 實現(xiàn)登陸并獲取Token
操作步驟
- 登陸接口設計
- 用 jsonwebtoken 生成 token
登陸接口設計
根據(jù) github 接口設計,由于登陸并不屬于增刪改查,所以我們按照github上這種教科書級別的接口設計規(guī)范來設計,采用 post+動詞形式來定義,路由新增login,控制器里添加login方法
// router/users.js
const { find, findById, create, update, delete: del, login } = require('../controllers/users');
// 登陸
router.post('/login', login);
用 jsonwebtoken 生成 token
設計思路:login 方法先對數(shù)據(jù)進行參數(shù)校驗,如果必傳字段都存在,再去數(shù)據(jù)庫中查詢是否有符合請求體中的用戶名及密碼。全部驗證通過了,再生成token。這里的密鑰是直接寫在 config.js 中的,但正常情況下其實密鑰是必須通過環(huán)境變量來獲取的,否則這個密鑰被人家一扒就拿到了
// config.js
module.exports = {
secret: 'zhihu-jwt-secret', // 正常需要通過環(huán)境變量獲取
}
// controllers/users.js
const jsonwebtoken = require('jsonwebtoken');
const { secret } = require('../config');
async login (ctx) {
ctx.verifyParams({
name: { type: 'string', required: true },
password: { type: 'string', required: true }
});
const user = await User.findOne(ctx.request.body);
if(!user) ctx.throw(401, '用戶名或密碼不正確');
const { _id, name } = user;
const token = jsonwebtoken.sign({ _id, name }, secret, { expiresIn: '1d' });
ctx.body = { token };
}
加個小插曲,新建請求,全都編寫好后,發(fā)現(xiàn)一直不通過,總提示兩個字段都為空,檢查后才發(fā)現(xiàn)這里默認新生成的是text類型,注意改回JSON格式,由于習慣復制粘貼,這里一直沒設置,容易忘記設置這個地方
Lesson-7 自己編寫 Koa 中間件實現(xiàn)用戶認證與授權
操作步驟
- 認證:驗證 token,并獲取用戶信息
- 授權:使用中間件保護接口
認證:驗證 token,并獲取用戶信息
前面已經實現(xiàn)了用戶登錄并返回token,接下來便是在 postman 中使用自動化腳本來獲取token,否則每次都去填,這是不可行的
postman中是可以在請求頭中自動加入驗證信息的,這里我們用的是Bearer Token,將登陸后的 token 填入即可。但也如上面所說,每次都去填是不可行的(更換密鑰/過期/用戶更改登陸信息等)。截圖中寫了 {{token}},其實代表的就是自動化腳本中的全局變量
var jsonData = pm.response.json();
pm.globals.set("token", jsonData.token);
以上便完成了自動化腳本獲取 token 的操作,現(xiàn)在我們不用再去復制 token 來手動加到請求頭里了。
授權:使用中間件保護接口
根據(jù)一開始的 洋蔥模型,其實我們所要做的驗證token,實際就是編寫一個中間件,加進需要驗證的控制器中
// router/users.js
const jsonwebtoken = require('jsonwebtoken');
const { secret} = require('../config');
// 認證中間件
const auth = async (ctx, next) => {
const { authorization = '' } = ctx.request.header; // 容錯,沒token得用戶默認為空字符串,否則報語法錯誤
const token = authorization.replace('Bearer ', ''); // 根據(jù)上面截圖,可以看到需要對value進行處理
try {
const user = jsonwebtoken.verify(token, secret); // 不記得的往前看第四節(jié)
ctx.state.user = user; // 約定俗成,一般就是放這里,也是沒有為什么
} catch (err) {
ctx.throw(401, err.message);
}
await next();
}
用戶操作中,關于修改與刪除(實際并不存在刪除,所謂刪除其實只會把數(shù)據(jù)庫中的這一條數(shù)值設置為非激活狀態(tài),現(xiàn)實當中是不會去刪除數(shù)據(jù)庫數(shù)據(jù)的),是必須要驗證用戶權限的,否則當前用戶能修改別人的信息是不合理的,所以需要對用戶的權限進行校驗,思路也很簡單,檢查一下用戶的id跟要修改的用戶數(shù)據(jù)id是否一致即可
// controllers/user.js
async checkOwner (ctx, next) {
if (ctx.params.id !== ctx.state.user._id) ctx.throw('403', '沒有權限');
await next();
}
// routes/users.js
// 修改特定用戶
router.patch('/:id', auth, checkOwner, update);
// 刪除用戶
router.delete('/:id', auth, checkOwner, del);
注意一點,自動化腳本只能寫在登陸上,其他接口不能寫,否則其他接口也會去獲取寫入全局token,這樣會導致token為空~
Lesson-8 用 koa-jwt 中間件實現(xiàn)用戶認證與授權
上一節(jié)自己編寫的只是用來了解原理,實際操作肯定還是用人家造好的輪子的。就像一開始字段的驗證一樣,盡量使用社區(qū)中優(yōu)秀的中間件
操作步驟
- 安裝 koa-jwt
- 使用中間件保護接口
- 使用中間件獲取用戶信息
安裝 koa-jwt
執(zhí)行命令 npm i koa-jwt --save
使用中間件保護接口
// routes/users.js
const jwt = require('koa-jwt');
// 認證中間件
const auth = jwt({ secret }); // 原來的auth整段代碼變成一句,簡潔
使用中間件獲取用戶信息
由于 koa-jwt 自帶 jsonwebtoken ,所以并不需要我們額外再去引入 jsonwebtoken 來解析 token 獲取用戶資料,所以沒代碼。