學習後端鑑權系列: 基於Cookie, Session認證

提及鑑權你們應該都很熟悉, 不過做爲前端開發來說, 鑑權的流程大頭都在後端小哥那邊, 可是做爲一個有志氣的開發者確定要好好學習整個鑑權流程以及方案, 否則怎麼跟後端合做😄。javascript

常見的鑑權方案

基於Cookie, Session認證

關於Cookie使用推薦閱讀,HTTP cookies前端

先上你們常見的一張Cookie, Session流程圖。java

demo展現

下面經過node + koa2 + redis + mongodb來展現上述的流程。node

實現思路:git

  • 建立用戶
1. 密碼首先md5, 生成隨機鹽, 再次加鹽md5保存數據庫
2. 記得salt鹽也要保存

複製代碼
  • 登錄時候
1. 驗證密碼是否正確(取出salt,對用戶傳過來的密碼+salt再次簽名去批評數據庫保存的密碼是否一致)
2. 正確後建立session對象(userID)存在redis,並設置過時時間

複製代碼
  • 業務api
1. 獲取客戶端傳過來的cookie
2. 用cookie+簽名去redis讀取是否有session對象,存在的話取出該用戶id去數據庫查詢用戶信息
複製代碼

開發前準備

  • 安裝node
  • 安裝redis而且本地啓動
  • 安裝mongodb而且本地啓動

note: 下面代碼只是供demo展現, 具體代碼結構設計在生產環境可不能這麼寫, 後面我會總結一篇關於koa最佳實踐文章github

啓動mongodb

這裏就不截圖了,關於GUI推薦使用Robo 3T。redis

啓動redis

而後經過終端查看你的redis有麼有存儲數據。mongodb

app.js數據庫

// app.js

const Koa = require("koa");
const Router = require("koa-router");
const bodyParser = require("koa-bodyparser");
const session = require("koa-session2");
const md5 = require("crypto-js/md5");
const mongoose = require("mongoose");

const config = require("./config.js");
const Store = require("./Store.js");
const User = require("./models/user.js");

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

app.keys = ["this is my secret key"];
mongoose.connect(config.db, { useUnifiedTopology: true });
app.use(bodyParser());

app.use(
  session({
    key: "jssessionId"
  })
);

/** * @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"
    };
  }
});

// 模擬登錄
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) {
        const store = new Store();
        const sid = await store.set(
          {
            id: searchUser._id
          },
          {
            maxAge: 1000 * 60 * 2 // 設定只有120s的有效時間
          }
        );
        ctx.cookies.set("jssessionId", sid);
        ctx.body = {
          success: true,
          msg: "登錄成功"
        };
      } else {
        ctx.body = {
          success: false,
          msg: "密碼錯誤"
        };
      }
    }
  } catch (error) {
    ctx.body = {
      success: false,
      msg: "serve is mistakes"
    };
  }
});

// 獲取用戶信息
router.get(
  "/user",
  async (ctx, next) => {
    const store = new Store();
    const jssessionId = ctx.cookies.get("jssessionId");
    const userSession = await store.get(jssessionId);
    console.log("獲取到請求的cookie", jssessionId, "session", userSession);
    if (!userSession) {
      ctx.status = 401;
      ctx.body = {
        success: false,
        msg: "oAuth Faill"
      };
    } else {
      ctx.userSession = userSession;
      await next();
    }
  },
  async (ctx, next) => {
    try {
      const { id } = ctx.userSession;
      const { name, age, isAdmin } = await User.findOne({ _id: id });
      ctx.body = {
        success: true,
        data: { name, age, isAdmin }
      };
    } catch (error) {
      ctx.body = {
        success: false,
        msg: "serve 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");
});

複製代碼

config.js後端

module.exports = {
	'db': 'mongodb://localhost:27017/test'
}

複製代碼

user.js

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);

複製代碼

Store.js

const Redis = require("ioredis");
const { Store } = require("koa-session2");

class RedisStore extends Store {
  constructor() {
    super();
    this.redis = new Redis(); // Connect to 127.0.0.1:6379
  }
  async get(sid, ctx) {
    try {
      const data = await this.redis.get(`jssessionId:${sid}`);
      return JSON.parse(data);
    } catch (err) {
      throw new Error(err);
    }
  }

  async set(session, { sid = this.getID(24), maxAge = 1000000 } = {}, ctx) {
    try {
      // EX: redis支持過了有效期自動刪除
      await this.redis.set(
        `jssessionId:${sid}`,
        JSON.stringify(session),
        "EX",
        maxAge / 1000
      );
    } catch (err) {
      throw new Error(err);
    }
    return sid;
  }
}

module.exports = RedisStore;

複製代碼

postman測試接口

注意看返回的Set-Cookie, 接着咱們看下redis

已經存在一條數據, 另外它的有效時間是120S,過了120S該數據會自動清除。

經過另外接口去訪問用戶信息。

是能夠獲取到用戶信息的,說明一切正常。

120S以後再次調用該接口測試是否已經失效。

redis裏面也確實自動清除了該條數據。

基於Session的方案存在的問題

  • 服務端須要存儲Session
  • 因爲Session須要常常進行快速查找,所以咱們通常存儲在內存中或者內- 存服務器中,當用戶數量大的時候,須要佔用大量的服務器資源
  • 當你須要擴展時候,建立Session服務器不必定是驗證Session的服務器,所以你須要把全部Session單獨存儲並共享
  • 因爲客戶端使用 Cookie 存儲 SessionID,在跨域場景下須要進行兼容性處理,同時這種方式也難以防範 CSRF 攻擊。

備註

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

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

相關文章
相關標籤/搜索