NodeJs學習筆記:驗證和授權(Authentication and Authorization)

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中添加角色數組;
在這裏插入圖片描述

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章