Говоря об аутентификации, все должны быть с ней знакомы, но в качестве фронтенд-разработки большая часть процесса аутентификации приходится на бэкенд-брата, но как начинающий разработчик вы должны изучить весь процесс аутентификации и решения. как сотрудничать с бэкендом 😄.
Общие схемы аутентификации
- На основе файлов cookie, аутентификация сеанса
-
JWT-аутентификация,
Token
Сертификация -
OAuth2
Сертификация -
SSO
Единая точка входа -
LDAP
Вход для аутентификации - Отсканируйте код для входа
На основе файлов cookie, аутентификация сеанса
оCookie
Используйте рекомендуемое чтение,HTTP cookies.
Начнем с общегоCookie
, Session
блок-схема.
демонстрационный дисплей
посредством следующихnode
+ koa2
+ redis
+ mongodb
для демонстрации описанного выше процесса.
Идеи реализации:
- Создать пользователя
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最佳实践文章
.
запустить монгодб
Скриншотов здесь нет, Robo 3T рекомендуется для графического интерфейса.
запустить редис
Затем проверьте, сохранил ли ваш Redis данные через терминал.
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
Часть данных уже есть, и ее действительное время составляет 120 с, после 120 с данные будут автоматически очищены.
Доступ к пользовательской информации через другой интерфейс.
Есть возможность получить пользовательскую информацию, свидетельствующую о том, что все в норме.
Через 120 с снова вызовите интерфейс, чтобы проверить, не произошел ли сбой.
Этот фрагмент данных действительно автоматически очищается в Redis.
Проблемы с решениями на основе сеанса
- Сервер должен хранить сеанс
- Поскольку сеанс необходимо часто быстро искать, мы обычно храним его в памяти или на сервере памяти.Когда количество пользователей велико, это занимает много ресурсов сервера.
- Когда вам нужно расшириться, создание сеансового сервера не обязательно является сервером, который аутентифицирует сеанс, поэтому вам необходимо хранить и совместно использовать все сеансы отдельно.
- Поскольку клиент использует файл cookie для хранения SessionID, в междоменных сценариях требуется обработка совместимости, и этот метод также трудно предотвратить атаки CSRF.
Примечание
Если есть ошибка, вы можете ее исправить,Адрес источника.
Наконец, обратите внимание на волну публичных аккаунтов с интересом.