Koa 增加用户身份认证 Auth (1)

266 阅读4分钟

预备知识

导引

通过一步步的练习,最后你可以通过本文对以下方面有基本的了解。

  • 实现注册功能:用户名 + 密码
  • 了解用户会话存储方式之一:session 是什么
  • 实现用户名和密码方式的登录

概要

  • 技术栈:koa + mysql
  • 必要环境:nodejs,docker,docker compose,nodemon
  • 非必要: git
  • package 版本
    "dependencies": {
        "bcryptjs": "^2.4.3",
        "knex": "^2.4.2",
        "koa": "^2.14.2",
        "koa-bodyparser": "^4.4.0",
        "koa-passport": "^6.0.0",
        "koa-router": "^12.0.0",
        "koa-session": "^6.4.0",
        "mysql2": "^3.3.5",
        "passport-local": "^1.0.0"
      }
    

0. 初始化

基于已经构建好的环境进行后续操作

git clone https://github.com/egolink0/koa-tutorial.git
git checkout v1.1.0 
npm install

1. 注册

流程图

流程图 (5).jpg

注册需要的部分:

  • 注册界面:register.html
  • 注册接口:/auth/register
  • 密码加密处理

注册界面:register.html

增加注册界面

<!-- src/server/views/register.js -->
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>Register</title>
</head>

<body>
  <h1>Register</h1>
  <form action="/auth/register" method="post">
    <!-- username & password -->
    <p><label>Username: <input type="text" name="username" /></label></p>
    <p><label>Password: <input type="password" name="password" /></label></p>
    <p><button type="submit">Register</button></p>
  </form>
</body>

</html>

创建 /auth/register 接口

// src/server/routes/auth.js
const Router = require("koa-router");
const fs = require("fs");

const router = new Router();

router.get("/auth/register", async (ctx) => {
  ctx.type = "html";
  ctx.body = fs.createReadStream("./src/server/views/register.html");
});

module.exports = router;

此时,启动服务访问 http://localhost:4000/auth/register 可以看到如下界面

image.png

增加用户表:user table

用户界面有了,但是还需要 user 表来存储用户的注册信息。

  • 创建 users 数据表:knex migrate:make users
// src/server/db/migrations/xx_users.js
exports.up = (knex, Promise) => {
 return knex.schema.createTable("users", (table) => {
   table.increments();
   table.string("username").unique().notNullable();
   table.string("password").notNullable();
 });
};

exports.down = (knex, Promise) => {
 return knex.schema.dropTable("users");
};

  • 创建字段,初始化表 knex migrate:latest --env development
  • 创建初始化数据的结构:knex seed:make users_seed
  • 安装加密 package : npm i bcryptjs
// src/server/db/seeds/users_seed.js

const bcrypt = require("bcryptjs"); // 密码加密

exports.seed = (knex, Promise) => {
  return knex("users")
    .del()
    .then(() => {
      const salt = bcrypt.genSaltSync();
      const hash = bcrypt.hashSync("johnson", salt);
      return knex("users").insert({
        username: "jeremy",
        password: hash,
      });
    });
};

  • 注入数据:knex seed:run --env development

增加注册接口 /auth/register

增加注册接口,写入 user 信息,用户密码需要加密处理

// src/server/db/queries/users.js

const knex = require("../connection");
const bcrypt = require("bcryptjs"); // 加密库

function addUser(user) {
  const salt = bcrypt.genSaltSync();
  const hash = bcrypt.hashSync(user.password, salt); // 密码加密
  return knex("users").insert({
    username: user.username,
    password: hash,
  });
}

module.exports = { addUser };

创建 /auth/register 路由

// src/server/db/queries/users.js

const queries = require("../db/queries/users");

router.post("/auth/register", async (ctx) => {
  const user = await queries.addUser(ctx.request.body); // 加入数据库用户信息
 if (user) {
      ctx.status = 200;
      ctx.body = { status: "success" };
    } else {
      ctx.status = 400;
      ctx.body = { status: "error" };
    }
});

此时访问 register 页面,输入用户名和密码就可以看到成功返回了数据 {"status":"success"}

2. 登录

登录我们先使用 session 的方式,不了解的可以先看这里:session 机制

image.png

流程图

登录整体逻辑如下:

流程图 (7).jpg

需要构建的内容:

  • 登录页面 + 获取路由: login.html + /auth/login (get)
  • 登录成功页面 + 获取路由: success.html + /auth/success (get)
  • 登录接口: /auth/login (post)
  • 登出接口:/auth/logout (get)
  • passport local 机制

增加 login.html

<!-- src/server/views/login.html -->
<!DOCTYPE html>
<html>

<head>
  <meta charset="utf-8">
  <title>Login</title>
</head>

<body>
  <h1>Login</h1>
  <form action="/auth/login" method="post">
    <p><label>Username: <input type="text" name="username" /></label></p>
    <p><label>Password: <input type="password" name="password" /></label></p>
    <p><button type="submit">Log In</button></p>
  </form>
</body>
</html>

路由部分

router.get("/auth/login", async (ctx) => {
  ctx.type = "html";
  ctx.body = fs.createReadStream("./src/server/views/login.html");
});

增加 success.html

 <!-- src/server/views/success.html -->
<!DOCTYPE html>
<html>
<head>
  <meta charset="utf-8">
  <title>Status</title>
</head>
<body>
  <p>You are authenticated.</p>
  <p><a href="/auth/logout">Logout</a>?</p>
</body>
</html>

路由部分

router.get("/auth/success", async (ctx) => {
  ctx.type = "html";
  ctx.body = fs.createReadStream("./src/server/views/success.html");
});

添加 passport 中间件

passport 是 node 的一个处理身份认证的中间件,支持扩展各种插件(也可以称为策略),通过策略验证请求以验证用户身份。
这里我们使用的是为 koa 封装的 koa-passport,创建本地 passport 部分,采用 local 策略。
先安装必要的包: npm i koa-passport passport-local koa-session

// src/server/passport.js

const passport = require("koa-passport");
const knex = require("./db/connection");
const LocalStrategy = require("passport-local").Strategy;
const bcrypt = require("bcryptjs");

// 密码比较
function comparePass(userPassword, databasePassword) {
  return bcrypt.compareSync(userPassword, databasePassword);
}

passport.serializeUser((user, done) => {
  done(null, user.id);
});

passport.deserializeUser((id, done) => {
  return knex("users")
    .where({ id })
    .first()
    .then((user) => {
      done(null, user);
    })
    .catch((err) => {
      done(err, null);
    });
});

// serializing and de-serializing the user information to the session.
const options = {};
passport.use(
  new LocalStrategy(options, (username, password, done) => {
     // 查询数据库,找到相应的用户名和密码
    knex("users")
      .where({ username })
      .first()
      .then((user) => {
        if (!user) return done(null, false);
        // 匹配校验
        if (!comparePass(password, user.password)) {
          return done(null, false);
        } else {
          return done(null, user);
        }
      })
      .catch((err) => {
        return done(err);
      });
  })
);

module.exports = passport;

引入中间件,加入 session 机制部分,使用 koa-session 中间件

// src/server/index.js

const passport = require("./passport");
const session = require("koa-session");

// sessions
app.keys = ["koa:session-secret"];
app.use(session({ maxAge: 60 * 1000 }, app)); // 过期时间 1 分钟

// 在路由之前引入 passport 
app.use(passport.initialize());
app.use(passport.session());

创建登录路由 /auth/login

const passport = require("koa-passport");

router.post("/auth/login", async (ctx) => {
  // local 策略
  return passport.authenticate("local", (err, user, info, status) => {
    if (user) {
      // 登录成功
      ctx.login(user);
      ctx.redirect("/auth/success"); // 成功页面
    } else {
      ctx.status = 400;
      ctx.body = { status: "error" };
    }
  })(ctx); // 传入 context
});

增加登出接口 /auth/logout

router.get("/auth/logout", async (ctx) => {
  if (ctx.isAuthenticated()) {
    // 这里需要 return ,否则报错(Error [ERR_HTTP_HEADERS_SENT]: Cannot set headers after they are sent to the client)
    return ctx.logout().then((r) => {
      ctx.redirect("/auth/login");
    });
  } else {
    ctx.redirect("/auth/login");
  }
});

完善 /auth/success 路由

router.get("/auth/success", async (ctx) => {
  // 判断是否已经登录,成功才跳转
  if (ctx.isAuthenticated()) {
    ctx.type = "html";
    ctx.body = fs.createReadStream("./src/server/views/success.html");
  } else {
  // 没有成功就跳转到登录界面
    ctx.redirect("/auth/login");
  }
});

完善 /auth/login 路由

router.get("/auth/login", async (ctx) => {
  // 判断是否已经登录,没有登录才跳转 login.html
  if (!ctx.isAuthenticated()) {
    ctx.type = "html";
    ctx.body = fs.createReadStream("./src/server/views/login.html");
  } else {
   // 登录了跳转 success 界面
    ctx.redirect("/auth/success");
  }
});

此时可以根据流程图验证整体的登录流程。

结束语

至此,你实现了基本的用户名和密码的注册功能,其次,实现了用 session(cookie)的方式 存储用户登录状态的用户身份认证方式,接下来你还可以做:

  • 加入 redis,使用 redis 的方式存 session
  • 切换策略方式:换成 Token(jwt) 方式存储用户登录状态

或者关注我,后续会陆续发相应的教程,欢迎关注~