學習後端鑑權系列: 基於JWT的會話管理

內容回顧

上一節講了基於Cookie+Session的認證方案javascript

因爲基於Session方案的一些缺點,基於token的無狀態的會話管理方案誕生了,所謂無狀態就是指服務端再也不存儲信息。html

基於JWT的簡單鑑權流程

對jwt不熟悉的推薦閱讀:java

node實現jwt認證

技術實現方案: node + koa2 + mongodbnode

目錄結構

開發前準備

  • node
  • mongodb

記得啓動node服務以前,先本地啓動mongodbgit

user.jsgithub

const mongoose = require("mongoose");
const { Schema } = mongoose;

const userSchema = new Schema({
  name: String,
  password: String,
  salt: String,
  isAdmin: Boolean,
  age: Number
});

module.exports = mongoose.model("User", userSchema);
複製代碼

config.jsweb

module.exports = {
	'secret': 'ilovescotchyscotch', // 密鑰
	'db': 'mongodb://localhost:27017/test'
}
複製代碼

package.jsonmongodb

{
  "name": "token",
  "version": "1.0.0",
  "description": "",
  "main": "index.js",
  "dependencies": {
    "crypto-js": "^3.1.9-1",
    "jsonwebtoken": "^8.5.1",
    "koa": "^2.8.2",
    "koa-bodyparser": "^4.2.1",
    "koa-router": "^7.4.0",
    "mongoose": "^5.7.3"
  },
  "devDependencies": {
    "nodemon": "^1.19.3"
  },
  "scripts": {
    "start": "nodemon ./app.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC"
}

複製代碼

app.js數據庫

const Koa = require("koa");
const Router = require("koa-router");
const bodyParser = require("koa-bodyparser");
const md5 = require("crypto-js/md5");
const jwt = require("jsonwebtoken");
const mongoose = require("mongoose");
const User = require("./models/user.js");
const config = require("./config.js");

const app = new Koa();
const router = new Router();

mongoose.connect(config.db, { useUnifiedTopology: true });

app.use(bodyParser());

/** * @description 建立用戶 */
router.post("/user", async (ctx, next) => {
  const { username = "", password = "", age, isAdmin } = ctx.request.body || {};
  if (username === "" || password === "") {
    ctx.status = 401;
    return (ctx.body = {
      success: false,
      code: 10000,
      msg: "用戶名或者密碼不能爲空"
    });
  }
  // 先對密碼md5
  const md5PassWord = md5(String(password)).toString();
  // 生成隨機salt
  const salt = String(Math.random()).substring(2, 10);
  // 加鹽再md5
  const saltMD5PassWord = md5(`${md5PassWord}:${salt}`).toString();
  try {
    // 相似用戶查找,保存的操做通常咱們都會封裝到一個實體裏面,本demo只是演示爲主, 生產環境不要這麼寫
    const searchUser = await User.findOne({ name: username });
    if (!searchUser) {
      const user = new User({
        name: username,
        password: saltMD5PassWord,
        salt,
        isAdmin,
        age
      });
      const result = await user.save();
      ctx.body = {
        success: true,
        msg: "建立成功"
      };
    } else {
      ctx.body = {
        success: false,
        msg: "已存在同名用戶"
      };
    }
  } catch (error) {
    // 通常這樣的咱們在生成環境處理異常都是直接拋出 異常類, 再有全局錯誤處理去處理
    ctx.body = {
      success: false,
      msg: "serve is mistakes"
    };
  }
});

/** * @description 用戶登錄 */
router.post("/login", async (ctx, next) => {
  const { username = "", password = "" } = ctx.request.body || {};
  if (username === "" || password === "") {
    ctx.status = 401;
    return (ctx.body = {
      success: false,
      code: 10000,
      msg: "用戶名或者密碼不能爲空"
    });
  }
  // 通常客戶端對密碼須要md5加密傳輸過來, 這裏我就本身加密處理,假設客戶端不加密。
  // 相似用戶查找,保存的操做通常咱們都會封裝到一個實體裏面,本demo只是演示爲主, 生產環境不要這麼寫
  try {
    // username在註冊時候就不會容許重複
    const searchUser = await User.findOne({ name: username });
    if (!searchUser) {
      ctx.body = {
        success: false,
        msg: "用戶不存在"
      };
    } else {
      // 須要去數據庫驗證用戶密碼
      const md5PassWord = md5(String(password)).toString();
      const saltMD5PassWord = md5(
        `${md5PassWord}:${searchUser.salt}`
      ).toString();
      if (saltMD5PassWord === searchUser.password) {
        // Payload: 負載, 不建議存儲一些敏感信息
        const payload = {
          id: searchUser._id
        };
        const token = jwt.sign(payload, config.secret, {
          expiresIn: "2h"
        });
        ctx.body = {
          success: true,
          data: {
            token
          }
        };
      } else {
        ctx.body = {
          success: false,
          msg: "密碼錯誤"
        };
      }
    }
  } catch (error) {
    ctx.body = {
      success: false,
      msg: "serve is mistakes"
    };
  }
});

/** * @description 獲取用戶信息 */
router.get(
  "/user",
  async (ctx, next) => {
    // 這裏應該抽成一個auth中間件
    const token = ctx.request.query.token || ctx.request.headers["token"];
    if (token) {
      jwt.verify(token, config.secret, async function(err, decoded) {
        if (err) {
          return (ctx.body = {
            success: false,
            msg: "Failed to authenticate token."
          });
        } else {
          ctx.decoded = decoded;
          await next();
        }
      });
    } else {
      ctx.status = 401;
      ctx.body = {
        success: false,
        msg: "need token"
      };
    }
  },
  async (ctx, next) => {
    try {
      const { id } = ctx.decoded;
      const { name, age, isAdmin } = await User.findOne({ _id: id });
      ctx.body = {
        success: true,
        data: { name, age, isAdmin }
      };
    } catch (error) {
      ctx.body = {
        success: false,
        msg: "server is mistakes"
      };
    }

  }
);

app.use(router.routes()).use(router.allowedMethods());
app.on("error", (err, ctx) => {
  console.error("server error", err, ctx);
});
app.listen(3000, () => {
  console.log("Server listening on port 3000");
});

複製代碼

使用postman測試全部接口。

  • 驗證建立用戶接口

  • 去數據庫驗證

  • 驗證業務api

基於JWT鑑權方案解決了哪些問題

  • 服務端再也不須要存儲與用戶鑑權相關的信息,鑑權信息會被加密到token中,服務器只須要讀取token中包含的用戶信息便可。
  • 避免了共享Session不易擴展的問題
  • 不依賴於Cookie, 有效避免Cookie帶來的CORS攻擊問題
  • 經過CORS有效解決跨域問題

關於JWT與Token的認識

經過這篇關於jwt與token討論我糾正了本身的一些錯誤的觀點,下一篇像記錄關於token的學習。json

備註

有錯誤的地方歡迎你們斧正, 源碼地址

最後有興趣的關注一波公衆號。

相關文章
相關標籤/搜索