Изучение серий внутренней аутентификации: аутентификация на основе файлов cookie, сеансовая аутентификация.

Node.js

Говоря об аутентификации, все должны быть с ней знакомы, но в качестве фронтенд-разработки большая часть процесса аутентификации приходится на бэкенд-брата, но как начинающий разработчик вы должны изучить весь процесс аутентификации и решения. как сотрудничать с бэкендом 😄.

Общие схемы аутентификации

На основе файлов 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.

Примечание

Если есть ошибка, вы можете ее исправить,Адрес источника.

Наконец, обратите внимание на волну публичных аккаунтов с интересом.