提及鉴权你们应该都很熟悉, 不过做为前端开发来说, 鉴权的流程大头都在后端小哥那边, 可是做为一个有志气的开发者确定要好好学习整个鉴权流程以及方案, 否则怎么跟后端合做😄。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里面也确实自动清除了该条数据。
有错误的地方欢迎你们斧正, 源码地址。
最后有兴趣的关注一波公众号。