1.基本概念
Authentication:認證,就是驗證一個用戶是不是它所聲明的身份的過程。
Authorization:授權,就是判斷是否用戶有做某件事的權利。
2.準備工作
爲以前的vidly項目添加用戶註冊和登錄模塊。
註冊模塊:
models/user.js
const mongoose = require('mongoose')
const Joi = require("joi");
const userSchema = mongoose.Schema({
name:{
type:String,
minlength:3,
maxlength:100,
required:true
},
email:{
type:String,
minlength:5,
maxlength:100,
required:true,
unique:true
},
password:{
type:String,
minlength:6,
maxlength:100,
required:true
}
});
const User = mongoose.model('User',userSchema);
function validateUser(user){
const schema = {
name:Joi.string().required().min(3).max(100),
email:Joi.string().email({ minDomainAtoms: 2 }),
password:Joi.string().required().min(6).max(100),
}
return Joi.validate(user,schema);
}
exports.User = User;
exports.validateUser = validateUser;
routes/user.js
const _ = require('lodash');
const {User,validateUser} = require('../models/user');
const express = require('express');
const router = express.Router();
const mongoose = require('mongoose')
// Register
router.post('/',async (req,res)=>{
const result = validateUser(req.body);
if(result.error){
res.status(400).send(result.error.details[0].message);
}
// 查詢是否已經註冊過
let user = await User.findOne({email:req.body.email});
if(user){
return res.status(400).send("User already exists");
}
user = new User(_.pick(req.body,['name','email','password']));
await user.save();
res.send(_.pick(user,['id','name','email']));
});
module.exports = router;
上面的代碼在新建user對象時使用了lodash庫,它能使對象屬性的篩選和設置更爲漸變。
3.對密碼字段進行哈希加密
我們使用bcrypt庫對用戶的密碼進行加密。
const bcrypt = require('bcrypt');
async function run(){
const salt = await bcrypt.genSalt(10);
console.log(salt);
}
run();
結果爲:
$2b$10$vFVSf8lBRAN.unFl0YMTj.
這就是我們生產的salt。我們每次用這個salt去哈希一個新密碼,都會得到不同的結果。
接下來,我們用這個salt去哈希密碼:
const bcrypt = require('bcrypt');
async function run(){
const salt = await bcrypt.genSalt(10);
const hashPassword = await bcrypt.hash('1234',salt);
console.log(salt);
console.log(hashPassword);
}
run();
結果爲:
$2b$10$jLL5/B2jOYgvvFMQHSbGA.
$2b$10$jLL5/B2jOYgvvFMQHSbGA.1ntHn6pGyh/2bKpIbV/cgzZ.G8In/OW
我們看到,salt包含在哈希之後的密碼中。
接下來,把哈希加密加到註冊中:
// Register
router.post('/',async (req,res)=>{
const result = validateUser(req.body);
if(result.error){
res.status(400).send(result.error.details[0].message);
}
// 查詢是否已經註冊過
let user = await User.findOne({email:req.body.email});
if(user){
return res.status(400).send("User already exists");
}
user = new User(_.pick(req.body,['name','email','password']));
// 哈希密碼
const salt = await bcrypt.genSalt(10);
user.password = await bcrypt.hash(user.password,salt);
await user.save();
res.send(_.pick(user,['id','name','email']));
});
4.驗證用戶
// Register
router.post('/',async (req,res)=>{
const result = validate(req.body);
if(result.error){
res.status(400).send(result.error.details[0].message);
}
// 查詢用戶是否存在
let user = await User.findOne({email:req.body.email});
if(!user){
return res.status(400).send("Error info");
}
// 驗證密碼
const validPassword = await bcrypt.compare(req.body.password,user.password);
if(!validPassword){
return res.status(400).send("Error info");
}
res.send(true);
});
function validate(req){
const schema = {
email:Joi.string().email({ minDomainAtoms: 2 }),
password:Joi.string().required().min(6).max(100),
}
return Joi.validate(req,schema);
}
代碼中的重點在於這一句:const validPassword = await bcrypt.compare(req.body.password,user.password);
它將用戶填寫的明文密碼與數據庫的加密密碼進行比對,返回一個布爾值。
5.JSON網絡令牌
在之前的處理中,用戶的郵箱和密碼符合之後,服務端返回了一個true給客戶端。在實際操作中,我們不能這樣返回,我們需要返回一個JSON WEB TOKEN(JSON網絡令牌,JWT)。
JSON網絡令牌實際上是一個長字符串,用於辨認一個用戶的身份。在客戶端上將會將JWT作爲長字符串保存。這樣,客戶端在未來請求api時需要出示(發送給服務端)這個JWT
// JWT
// 第一個參數:payload,這裏可以根據喜好來裝載jwt的payload
// 第二個參數:私鑰,這裏先用硬編碼,實際應用時千萬不能這麼做,應該在環境變量中設置私鑰。
const token = jwt.sign({_id:user._id,name:user.name},'jwtPrivateKey');
res.send(token);
將該令牌在jwt.io中解析:
如何將私鑰存儲到環境變量中?
1.先安裝config模塊
2.新建config文件夾
3.新建默認配置文件default.json
{
"name":"My express app",
"jwtPrivateKey":""
}
這裏設置爲空字符串,真正的值不寫在這裏,只是爲了應用設置了一個模板。
4.創建custom-environment-variables.json
這個文件將映射環境變量和應用設置的關係。
{
"jwtPrivateKey":"vidly_jwtPrivateKey"
}
5.然後在需要驗證的模塊引入config模塊,將私鑰用config.get()替換即可。
const token = jwt.sign({_id:user._id,name:user.name},config.get("jwtPrivateKey"));
6.最後一步便是在index.js中判斷應用變量是否設置好,否則我們就要中止運行,因爲驗證模塊將無法工作。
// 檢查環境變量是否設置好
if(!config.get('jwtPrivateKey')){
console.error("FATAL ERROR:jwtPrivateKey is not defined");
process.exit(1);
}
多做一點:
我們需要在多個模塊使用token(返回給客戶端),它的有效負載可能會發生更改,這樣我們就需要更改多處代碼。爲了方便起見,我們對token的生成進行封裝。考慮到其不屬於特定的某個模塊,而是屬於user的一個功能屬性,因此我們在user類中添加generateAuthToken方法:
models/user.js
userSchema.methods.generateAuthToken = function(){
const token = jwt.sign({_id:this._id,name:this.name},config.get("jwtPrivateKey"));
return token;
}
在需要生成token時,這樣調用:
const token = user.generateAuthToken();
res.send(token);
7.運行前設置環境變量set vidly_jwtPrivateKey=mySecurekKey
,記得一定是在管理員權限下設置環境變量。
6.設置response的header
如果我們在用戶註冊後就直接自動登錄,則需要在註冊成功時返回給客戶端token。我們將user.js內的res.send()改造成這樣:
// 設置header
const token = jwt.sign({_id:user._id,name:user.name},config.get("jwtPrivateKey"));
res.header('x-auth-token',token).send(_.pick(user,['id','name','email']));
7.保護路由句柄
在訪問某些路由時,需要鑑定用戶是否登錄,如果沒有就需要拒絕請求。
因爲有很多路由需要用到鑑權,因此我們將鑑權函數寫在中間件中。
const jwt = require("jsonwebtoken");
const config = require('config');
function auth(req,res,next){
//讀取header,獲取令牌
const token = req.header('x-auth-token');
//沒有令牌的情況
if(!token){
return res.status(401).send("訪問被拒絕,未提供令牌!");
}
//有令牌但驗證爲非法的情況
try{
//如果這個令牌不合法,它會拋出異常
const decoded = jwt.verify(token,config.get('jwtPrivateKey'));//返回payload
req.user = decoded;
//將控制權轉給下一個中間函數
next();
}
catch(ex){
res.status(400).send('不合法的令牌');
}
}
module.exports = auth;
需要保護的路由句柄:
//增(這裏的第二個參數爲可選的中間函數)
router.post('/',auth,async (req,res)=>{
const result = validate(req.body);
if(result.error){
res.status(400).send(result.error.details[0].message);
}
//console.log(req.body);
let genre = new Genre({name:req.body.name});
genre = await genre.save();
res.send(genre);
});
8.登出
從技術上講,服務端沒有保存任何令牌信息,令牌保存在客戶端。因此,登出操作不需要服務端進行任何操作,只需要客戶端自行刪除token即可。
9.基於角色的授權
admin中間件:
我們需要控制權限:只有管理員才能進行刪除操作。
//刪
router.delete("/:id", [auth,admin],async (req,res)=>{
const result = await Genre.findByIdAndRemove(req.params.id);
if(!result) return res.status(400).send("該分類不存在!");
res.send(result)
});
若需要精細化控制,則需要在model中添加角色數組;