预备知识
导引
通过一步步的练习,最后你可以通过本文对以下方面有基本的了解。
- 实现注册功能:用户名 + 密码
- 了解用户会话存储方式之一: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. 注册
流程图
注册需要的部分:
- 注册界面: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 可以看到如下界面
增加用户表: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 机制
流程图
登录整体逻辑如下:
需要构建的内容:
- 登录页面 + 获取路由: 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) 方式存储用户登录状态
或者关注我,后续会陆续发相应的教程,欢迎关注~