提及鑑權你們應該都很熟悉, 不過做爲前端開發來說, 鑑權的流程大頭都在後端小哥那邊, 可是做爲一個有志氣的開發者確定要好好學習整個鑑權流程以及方案, 否則怎麼跟後端合做😄。javascript
Token
認證OAuth2
認證SSO
單點登錄LDAP
認證登錄關於Cookie
使用推薦閱讀,HTTP cookies。前端
先上你們常見的一張Cookie
, Session
流程圖。java
下面經過node
+ koa2
+ redis
+ mongodb
來展現上述的流程。node
實現思路:git
1. 密碼首先md5, 生成隨機鹽, 再次加鹽md5保存數據庫
2. 記得salt鹽也要保存
複製代碼
1. 驗證密碼是否正確(取出salt,對用戶傳過來的密碼+salt再次簽名去批評數據庫保存的密碼是否一致)
2. 正確後建立session對象(userID)存在redis,並設置過時時間
複製代碼
1. 獲取客戶端傳過來的cookie
2. 用cookie+簽名去redis讀取是否有session對象,存在的話取出該用戶id去數據庫查詢用戶信息
複製代碼
node
redis
而且本地啓動mongodb
而且本地啓動note: 下面代碼只是供demo展現, 具體代碼結構設計在生產環境可不能這麼寫, 後面我會總結一篇關於koa最佳實踐文章
。github
這裏就不截圖了,關於GUI推薦使用Robo 3T。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;
複製代碼
注意看返回的Set-Cookie, 接着咱們看下redis
已經存在一條數據, 另外它的有效時間是120S,過了120S該數據會自動清除。
是能夠獲取到用戶信息的,說明一切正常。
redis裏面也確實自動清除了該條數據。
有錯誤的地方歡迎你們斧正, 源碼地址。
最後有興趣的關注一波公衆號。