# 前后端的身份认证
# Web 开发模式
- 基于服务端渲染的传统 Web 开发模式
- 基于前后端分离的新型 Web 开发模式
一些页面可能会采用首屏服务端渲染 + 其他页面前后端分离的开发模式
# 服务端渲染的 Web 开发模式
服务端渲染的概念:服务器发送给客户端的 HTML 页面,是在服务器通过字符串的拼接,动态生成的。因此,客户端不需要使用 Ajax 这样的技术额外请求页面的数据。代码示例如下:
app.get("/index.html", (req, res) => {
// 要渲染的数据
const user = { name: "zs", age: 20 };
// 服务器通过字符串的拼接,动态生成 HTML 内容
const html = `<h1>姓名${user.name},年龄${user.age}</h1>`;
// 把生成好的页面内容响应给客户端,因此,客户端拿到的是带有真实数据的 HTML 页面
res.send(html);
});
-
优点:
- 前端耗时少。因为服务器端负责动态生成 HTML 内容,浏览器只需要直接渲染页面即可。尤其是移动端,更省电。
- 有利于 SEO。因为服务器端响应的是完整的 HTML 页面内容,所以爬虫更容易爬取获得信息,更有利于 SEO。
-
缺点:
- 占用服务器端资源。即服务器端完成 HTML 页面内容的拼接,如果请求较多,会对服务器造成一定的访问压力。
- 不利于前后端分离,开发效率低。使用服务器端渲染,则无法进行分工合作,尤其对于前端复杂度高的项目,不利于项目高效开发。
# 前后端分离的 Web 开发模式
前后端分离的概念:前后端分离的开发模式,依赖于 Ajax 技术的广泛应用。简而言之,前后端分离的 Web 开发模式,就是后端只负责提供 API 接口,前端使用 Ajax 调用接口的开发模式。
-
优点:
- 开发体验好。前端专注于 UI 页面的开发,后端专注于 api 的开发,且前端有更多的选择性。
- 用户体验好。Ajax 技术的广泛应用,极大的提高了用户的体验,可以轻松实现页面的局部刷新。
- 减轻了服务器端的渲染压力。因为页面最终是在每个用户的浏览器中生成的。
-
缺点:
- 不利于 SEO。因为完整的 HTML 页面需要在客户端动态拼接完成,所以爬虫对无法爬取页面的有效信息。(解决方案:利用 Vue、React 等前端框架的 SSR (server side render)技术能够很好的解决 SEO 问题!)
# 身份认证
# 什么是身份认证
身份认证(Authentication)又称 “身份验证”、“鉴权”,是指 通过一定的手段,完成对用户身份的确认。
- 日常生活中的身份认证随处可见,例如:高铁的验票乘车,手机的密码或指纹解锁,支付宝或微信的支付密码等。
- 在 Web 开发中,也涉及到用户身份的认证,例如:各大网站的
手机验证码登录、邮箱密码登录、二维码登录等。
# 为什么需要身份认证
身份认证的目的,是为了 确认当前所声称为某种身份的用户,确实是所声称的用户 。例如,你去找快递员取快递,你要怎么证明这份快递是你的。
在互联网项目开发中,如何对用户的身份进行认证,是一个值得深入探讨的问题。例如,如何才能保证网站不会错误的将 “马云的存款数额” 显示到 “马化腾的账户” 上。
# 不同开发模式下的身份认证
对于 服务端渲染 和 前后端分 离这两种开发模式来说,分别有着不同的身份认证方案:
服务端渲染推荐使用Session 认证机制前后端分离推荐使用JWT 认证机制
# Session 认证机制
# HTTP 协议的无状态性
了解 HTTP 协议的无状态性是进一步学习 Session 认证机制的必要前提。
HTTP 协议的无状态性,指的是客户端的 每次 HTTP 请求都是独立的 ,连续多个请求之间没有直接的关系, 服务器不会主动保留每次 HTTP 请求的状态
# 如何突破 HTTP 无状态的限制
对于超市来说,为了方便收银员在进行结算时给 VIP 用户打折,超市可以为每个 VIP 用户发放会员卡。
注意:现实生活中的 会员卡身份认证方式 ,在 Web 开发中的专业术语叫做 Cookie 。
# 什么是 Cookie
Cookie 是 存储在用户浏览器中的一段不超过 4 KB 的字符串 。它由一个 名称 (Name)、一个 值 (Value)和其它几个用于控制 Cookie 有效期、安全性、使用范围的可选属性 组成。
不同域名下的 Cookie 各自独立 ,每当客户端发起请求时,会 自动 把 当前域名下 所有 未过期的 Cookie 一同发送到服务器。
Cookie 的几大特性:
- 自动发送
- 域名独立 (百度的不能去访问腾讯的)
- 过期时限
- 4KB 限制
# Cookie 在身份认证中的作用
客户端第一次请求服务器的时候,服务器 通过响应头的形式 ,向客户端发送一个身份认证的 Cookie,客户端会自动将 Cookie 保存在浏览器中。
随后,当客户端浏览器每次请求服务器的时候,浏览器会 自动 将身份认证相关的 Cookie, 通过请求头的形式 发送给服务器,服务器即可验明客户端的身份。
# Cookie 不具有安全性
由于 Cookie 是存储在浏览器中的,而且 浏览器也提供了读写 Cookie 的 API ,因此 Cookie 很容易被伪造 ,不具有安全性。因此不建议服务器将重要的隐私数据,通过 Cookie 的形式发送给浏览器。
注意 :千万不要使用 Cookie 存储重要且隐私的数据!比如用户的身份信息、密码等。
# 提高身份认证的安全性
为了防止客户伪造会员卡,收银员在拿到客户出示的会员卡之后,可以 在收银机上进行刷卡认证 。只有收银机确认存在的会员卡,才能被正常使用。
这种 “会员卡 + 刷卡认证” 的设计理念,就是 Session 认证机制的精髓。
# Session 的工作原理
# 在 Express 中使用 Session 认证
安装 express-session 中间件 npm install express-session
-
案例
const express = require("express"); // 创建 express 的服务器实例 const app = express(); // TODO_01:请配置 Session 中间件 const session = require("express-session"); app.use( session({ secret: "giao", // secret 属性的值可以为任意字符串 resave: false, // 固定写法 saveUninitialized: true, // 固定写法 }) ); // 托管静态页面 app.use(express.static("./pages")); // 解析 POST 提交过来的表单数据 app.use(express.urlencoded({ extended: false })); // 登录的 API 接口 app.post("/api/login", (req, res) => { // 判断用户提交的登录信息是否正确 if (req.body.username !== "admin" || req.body.password !== "000000") { return res.send({ status: 1, msg: "登录失败" }); } // TODO_02:请将登录成功后的用户信息,保存到 Session 中 // 注意:只有成功配置了 express-session 这个中间件之后,才能够通过 req 点出来 session 这个属性 req.session.user = req.body; // 将用户的信息,存储到 Session 中 req.session.islogin = true; // 将用户的登录状态,存储到 Session 中 res.send({ status: 0, msg: "登录成功" }); }); // 获取用户姓名的接口 app.get("/api/username", (req, res) => { // TODO_03:请从 Session 中获取用户的名称,响应给客户端 if (!req.session.islogin) { return res.send({ status: 1, msg: "fail" }); } res.send({ status: 0, msg: "success", username: req.session.user.username, }); }); // 退出登录的接口 app.post("/api/logout", (req, res) => { // TODO_04:清空 Session 信息 // 调用 req.session.destroy() 函数,即可清空服务器保存的该用户所对应的 session 信息,而不会清空其他用户的 Session 信息。 req.session.destroy(); res.send({ status: 0, msg: "退出登录成功", }); }); // 调用 app.listen 方法,指定端口号并启动web服务器 app.listen(80, function () { console.log("Express server running at http://127.0.0.1:80"); });
# JWT 认证机制
# 了解 Session 认证的局限性
Session 认证机制 需要配合 Cookie 才能实现 。由于 Cookie 默认不支持跨域访问,所以,当涉及到 前端跨域请求后端接口 的时候, 需要做很多额外的配置 ,才能实现跨域 Session 认证。
注意:
-
当前端请求后端接口
不存在跨域问题的时候,推荐使用 Session身份认证机制。 -
当前端需要跨域请求后端接口的时候,不推荐使用 Session 身份认证机制,推荐使用 JWT 认证机制。
-
JWT(英文全称:JSON Web Token)是目前
最流行的跨域认证解决方案。
# JWT 的工作原理
总结:用户的信息通过 Token 字符串的形式,保存在客户端浏览器中。服务器通过还原 Token 字符串的形式来认证用户的身份。
# JWT 的组成部分
JWT 通常由三部分组成,分别是 Header (头部)、 Payload (有效荷载)、 Signature (签名)。
三者之间使用英文的 “.” 分隔,格式如下:
Header.Payload.Signature;
下面是 JWT 字符串的示例:
eyJhbGc101JIUzI1N1IsInR5cCI6IkpXVCJ9.eyJpZCI6MSw1dXN1cm5hbWU101JhZG1 pb1I sInBhc3N3b3JkIjo1Iiwibm1ja25 hbWUi01Lms6X1t7T1t7QiLCJ1bWFpbCI6Im5pYmF1YUBpdGNhc3QuY241LCJ1c2VyX3BpYyI6IiIs Im1hdCI6MTU30DAzNjY4Miw1ZXhwI JoxNTc4MDcyNj8yfQ.Mwq7GqCxJPK-EA8LN tMG0411KdZ33S9KBL3XeuBxuI
# JWT 的三个部分各自代表的含义
JWT 的三个组成部分,从前到后分别是 Header、Payload、Signature。
其中:
Payload部分才是真正的用户信息,它是用户信息经过加密之后生成的字符串。- Header 和 Signature 是
安全性相关的部分,只是为了保证 Token 的安全性。
# JWT 的使用方式
客户端收到服务器返回的 JWT 之后,通常会将它储存在 localStorage 或 sessionStorage 中。
此后,客户端每次与服务器通信,都要带上这个 JWT 的字符串,从而进行身份认证。推荐的做法是 把 JWT 放在 HTTP 请求头的 Authorization 字段中 ,格式如下:
Authorization: Bearer <token>
// 举例:Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjU5ODg2ODUwLCJleHAiOjE2NTk4ODY4ODB9.M_0N3CoyVPDe6DmbZmj5JLzD4MRjxtL4_bGozh6YLIA
# 在 Express 中使用 JWT 认证
安装两个 JWT 相关的包 npm install jsonwebtoken express-jwt
jsonwebtoken 用于生成 JWT 字符串
express-jwt 用于将 JWT 字符串解析还原成 JSON 对象
-
定义 secret 密钥
为了保证 JWT 字符串的安全性,防止 JWT 字符串在网络传输过程中被别人破解,我们需要专门定义一个用于加密和解密的 secret 密钥:- 当生成 JWT 字符串的时候,需要使用 secret 密钥对用户的信息
进行加密,最终得到加密好的 JWT 字符串 - 当把 JWT 字符串解析还原成 JSON 对象的时候,需要使用 secret 密钥
进行解密
// secret 密钥的本质:就是一个字符串 const secretKey = "giao No1 ^_^"; - 当生成 JWT 字符串的时候,需要使用 secret 密钥对用户的信息
-
案例
const express = require("express"); // 创建 express 的服务器实例 const app = express(); // TODO_01:安装并导入 JWT 相关的两个包,分别是 jsonwebtoken 和 express-jwt const jwt = require("jsonwebtoken"); const expressJWT = require("express-jwt"); // 允许跨域资源共享 const cors = require("cors"); app.use(cors()); // 解析 post 表单数据的中间件 const bodyParser = require("body-parser"); app.use(bodyParser.urlencoded({ extended: false })); // TODO_02:定义 secret 密钥,建议将密钥命名为 secretKey const secretKey = "giao No1 ^_^"; // TODO_04:注册将 JWT 字符串解析还原成 JSON 对象的中间件 // expressJWT({ secret: secretKey }) 就是用来解析 Token 的中间件 // .unless({ path: [/^\/api\//] }) 用来指定哪些接口不需要访问权限 // 注意:只要配置成功了 express-jwt 这个中间件,就可以把解析出来的用户信息,挂载到 req.user 属性上 app.use(expressJWT({ secret: secretKey }).unless({ path: [/^\/api\//] })); // 登录接口 app.post("/api/login", function (req, res) { // 将 req.body 请求体中的数据,转存为 userinfo 常量 const userinfo = req.body; // 登录失败 if (userinfo.username !== "admin" || userinfo.password !== "000000") { return res.send({ status: 400, message: "登录失败!", }); } // 登录成功 // TODO_03:在登录成功之后,调用 jwt.sign() 加密方法生成 JWT 字符串。并通过 token 属性发送给客户端 // 参数1:用户信息对象 // 参数2:加密秘钥 // 参数3:配置对象,可以配置当前 token 的有效期 // 记住:千万不要把密码加密到 token 字符中 const tokenStr = jwt.sign({ username: userinfo.username }, secretKey, { expiresIn: "30s", }); res.send({ status: 200, message: "登录成功!", token: tokenStr, // 要发送给客户端的 token 字符串 }); }); // 这是一个有权限的 API 接口 app.get("/admin/getinfo", function (req, res) { // TODO_05:使用 req.user 获取用户信息,并使用 data 属性将用户信息发送给客户端 console.log(req.user); res.send({ status: 200, message: "获取用户信息成功!", data: req.user, // 要发送给客户端的用户信息 }); }); // TODO_06:使用全局错误处理中间件,捕获解析 JWT 失败后产生的错误 app.use((err, req, res, next) => { // 这次错误是由 token 解析失败导致的 if (err.name === "UnauthorizedError") { return res.send({ status: 401, message: "无效的token", }); } res.send({ status: 500, message: "未知的错误", }); }); // 调用 app.listen 方法,指定端口号并启动web服务器 app.listen(8888, function () { console.log("Express server running at http://127.0.0.1:8888"); });
# 实战
大事件后台 API 项目,API 接口文档请参考:https://www.showdoc.com.cn/escook/3707158761215217
大事件项目 api_server:http://escook.cn:8088/#/
# 实战代码 api_server
-
package.json
{ "name": "api_server", "version": "1.0.0", "description": "", "main": "index.js", "scripts": { "test": "echo \"Error: no test specified\" && exit 1" }, "keywords": [], "author": "", "license": "ISC", "dependencies": { "@escook/express-joi": "^1.1.1", "@hapi/joi": "^17.1.0", "bcryptjs": "^2.4.3", "cors": "^2.8.5", "express": "^4.17.1", "express-jwt": "^5.3.3", "jsonwebtoken": "^8.5.1", "mysql": "^2.18.1" } } -
config.js
// 这是一个全局的配置文件 module.exports = { // 加密和解密的 token jwtSecretKey: "giao", // token 的有效期 expiresIn: "10h", }; -
app.js
const express = require("express"); const app = express(); const joi = require("joi"); // 配置 cors 跨域 const cors = require("cors"); app.use(cors()); // 配置解析表单数据的中间件 // 这个中间件只能解析 application/x-www-form-urlencoded 格式的表单数据 app.use(express.urlencoded({ extended: false })); // 优化 res.send() 代码 // 一定要在路由之前,封装 res.cc 函数,如果放在了路由之后,那么在路由里面就访问不了 res.cc 函数了 app.use((req, res, next) => { // status 默认值为 1,表示失败的情况 // err 的值,可能是一个错误对象,也可能是一个错误的描述字符串 res.cc = function (err, status = 1) { res.send({ status, message: err instanceof Error ? err.message : err }); }; next(); }); // 一定要在路由之前配置解析 Token 的中间件 const expressJWT = require("express-jwt"); const config = require("./config"); // 使用 .unless({ path: [/^\/api\//] }) 指定哪些接口不需要进行 Token 的身份认证 app.use( expressJWT({ secret: config.jwtSecretKey }).unless({ path: [/^\/api/] }) ); // 导入并使用用户路由模块 const userRouter = require("./router/user"); // 访问userRouter模块的接口时,必须加上 /api 前缀 app.use("/api", userRouter); // 导入并使用用户信息路由模块 const userinfoRouter = require("./router/userinfo"); // 注意:以 /my 开头的接口,都是有权限的接口,需要进行 Token 身份认证 app.use("/my", userinfoRouter); // 导入并使用文章分类的路由模块 const artCateRouter = require("./router/artcate"); app.use("/my/article", artCateRouter); // 定义错误级别的中间件 app.use((err, req, res, next) => { // 这是 /api/reguser 这种类似接口导致的Joi参数错误 // Joi验证失败导致的错误 if (err instanceof joi.ValidationError) { return res.cc(err); } // 身份认证失败后的错误 // 如果请求 /giao/test 这个接口会报 身份认证token 失败的错误,原因是因为 /giao/test 不是 unless 方法中不需要验证的接口,他需要验证身份,但是他的请求头当中没添加 Authorization 这个字段,如果这个字段的值不是服务端返回的正确的token也会报错 if (err.name === "UnauthorizedError") { return res.cc("身份认证token失败"); } // 未知的错误 res.cc(err); // 在 Express 中不允许连续两次调用 res.send() }); app.listen(3007, () => { console.log("server running at:http://127.0.0.1:3007"); }); -
db 文件夹
-
index.js
const mysql = require("mysql"); const db = mysql.createPool({ host: "127.0.0.1", // 数据库的 IP port: "3307", // 端口号 user: "root", // 登录数据库的账号 password: "admin123", // 登录数据库的密码 database: "my_db_01", // 指定要操作哪个数据库 }); module.exports = db;
-
-
schema 文件夹
-
user.js
// 为表单中携带的每个数据项,定义验证规则 const joi = require("joi"); /** * string() 值必须是字符串 * alphanum() 值只能是包含 a-zA-Z0-9 的字符串 * min(length) 最小长度 * max(length) 最大长度 * required() 值是必填项,不能为 undefined * pattern(正则表达式) 值必须符合正则表达式的规则 */ // 定义用户名的验证规则 const username = joi.string().alphanum().min(1).max(10).required(); // 定义密码的验证规则 // \S 表示非空字符,非空字符6-12位之间 const password = joi .string() .pattern(/^[\S]{6,12}$/) .required(); // 定义 id, nickname, emial 的验证规则 const id = joi.number().integer().min(1).required(); const nickname = joi.string().required(); const email = joi.string().email().required(); // dataUri() 指的是如下格式的字符串数据: // 举例:data:image/png;base64,VE9PTUFOWVNFQ1JFVFM= const avatar = joi.string().dataUri().required(); // 定义注册和登录表单的验证规则对象 // 具体的规则去 npm 中搜索 @escook/express-joi 的用法 exports.reg_login_schema = { // 表示需要对 req.body 中的数据进行验证 body: { username, password, }, }; // 规则验证对象 - 更新用户基本信息 exports.update_userinfo_schema = { body: { id, nickname, email, }, }; // 验证规则对象 - 重置密码 exports.update_password_schema = { body: { // 使用 password 这个规则,验证 req.body.oldPwd 的值 oldPwd: password, // 使用 joi.not(joi.ref('oldPwd')).concat(password) 规则,验证 req.body.newPwd 的值 // 解读: // 1. joi.ref('oldPwd') 表示 newPwd 的值必须和 oldPwd 的值保持一致 // 2. joi.not(joi.ref('oldPwd')) 表示 newPwd 的值不能等于 oldPwd 的值 // 3. .concat() 用于合并 joi.not(joi.ref('oldPwd')) 和 password 这两条验证规则 newPwd: joi.not(joi.ref("oldPwd")).concat(password), }, }; // 验证规则对象 - 更新头像 exports.update_avatar_schema = { body: { avatar, }, }; -
artcate.js
// 为表单中携带的每个数据项,定义验证规则 const joi = require("joi"); // 定义 分类名称 和 分类别名 的校验规则 const name = joi.string().required(); const alias = joi.string().alphanum().required(); // 定义 分类Id 的校验规则 const id = joi.number().integer().min(1).required(); exports.add_cate_schema = { // 表示需要对 req.body 中的数据进行验证 body: { name, alias, }, }; // 校验规则对象 - 删除分类 exports.delete_cate_schema = { params: { id, }, }; // 校验规则对象 - 根据 Id 获取分类 exports.get_cate_schema = { params: { id, }, }; // 校验规则对象 - 更新分类 exports.update_cate_schema = { body: { Id: id, name, alias, }, };
-
-
router 文件夹
-
user.js
const express = require("express"); const router = express.Router(); // 导入用户路由处理函数模块 const userHandler = require("../router_handler/user"); // 导入验证表单数据的中间件 const expressJoi = require("@escook/express-joi"); // 导入需要的验证规则对象 const { reg_login_schema } = require("../schema/user"); // 注册新用户 // 在注册新用户的路由中,声明局部中间件,对当前请求中携带的数据进行验证 // 数据验证通过后,会把这次请求流转给后面的路由处理函数 // 数据验证失败后,终止后续代码的执行,并抛出一个全局的 Error 错误,进入全局错误级别中间件中进行处理 // 请求 /api/reguser 接口的时候,会先判断 expressJoi(reg_login_schema) 是否执行成功,如果成功才会往下走,如果不成功就会走到自己定义的全局错误中间件。 router.post("/reguser", expressJoi(reg_login_schema), userHandler.regUser); // 登录 router.post("/login", expressJoi(reg_login_schema), userHandler.login); module.exports = router; -
userinfo.js
const express = require("express"); const router = express.Router(); // 导入验证数据合法性的中间件 const expressJoi = require("@escook/express-joi"); // 导入需要验证的规则对象 const { update_userinfo_schema, update_password_schema, update_avatar_schema, } = require("../schema/user"); // 导入用户信息的处理函数模块 const userinfo_handler = require("../router_handler/userinfo"); // 获取用户的基本信息的路由 router.get("/userinfo", userinfo_handler.getUserInfo); // 更新用户信息的路由 router.post( "/userinfo", expressJoi(update_userinfo_schema), userinfo_handler.updateUserInfo ); // 重置密码的路由 router.post( "/updatepwd", expressJoi(update_password_schema), userinfo_handler.updatePassword ); // 更改用户头像路由 router.post( "/update/avatar", expressJoi(update_avatar_schema), userinfo_handler.updateAvatar ); module.exports = router; -
artcate.js
const express = require("express"); const router = express.Router(); // 导入验证数据的中间件 const expressJoi = require("@escook/express-joi"); // 导入文章分类的验证模块 const { add_cate_schema, delete_cate_schema, get_cate_schema, update_cate_schema, } = require("../schema/artcate"); // 导入文章分类的路由处理函数模块 const artcate_handler = require("../router_handler/artcate"); // 获取文章分类列表数据的路由 router.get("/cates", artcate_handler.getArticleCates); // 新增文章分类的路由 router.post( "/addcates", expressJoi(add_cate_schema), artcate_handler.addArticleCates ); // 根据id删除文章分类的路由 router.get( "/deletecate/:id", expressJoi(delete_cate_schema), artcate_handler.deleteCateById ); // 根据id获取文章分类数据的路由 router.get( "/cates/:id", expressJoi(get_cate_schema), artcate_handler.getArtCateById ); // 根据 id 更新文章分类的路由 router.post( "/updatecate", expressJoi(update_cate_schema), artcate_handler.updateCateById ); module.exports = router;
-
-
router_handler 文件夹
-
user.js
// 导入数据库操作模块 const db = require("../db/index"); // 导入 bcryptjs 这个包 const bcrypt = require("bcryptjs"); // 导入生成 token 的包 const jwt = require("jsonwebtoken"); // 导入全局的配置文件 const conFig = require("../config"); // 注册新用户的处理函数 exports.regUser = (req, res) => { // 获取客户端提交到服务器的用户信息 const userinfo = req.body; // 对表单中的数据,进行合法性的校验 // if (!userinfo.username || !userinfo.password) { // // return res.send({ status: 1, message: "用户名或密码不合法!" }); // return res.cc("用户名或密码不合法!"); // } // 定义 SQL 语句,查询用户名是否被占用 const sqlStr = `select * from ev_users where username=?`; db.query(sqlStr, userinfo.username, (err, results) => { // 执行 sql 语句失败 if (err) { // return res.send({ status: 1, message: err.message }); return res.cc(err); } // 判断用户名是否已被占用 if (results.length > 0) { // return res.send({ // status: 1, // message: "用户名已被占用,请更换其他用户名!", // }); return res.cc("用户名已被占用,请更换其他用户名!"); } // 用户名可以使用 // 调用 bcrypt.hashSync(明文密码, 随机盐的长度) 对密码进行加密 // 对用户的密码,进行 bcrype 加密,返回值是加密之后的密码字符串 userinfo.password = bcrypt.hashSync(userinfo.password, 10); // 定义插入新用户的 SQL 语句 const sql = "insert into ev_users set ?"; db.query( sql, { username: userinfo.username, password: userinfo.password }, (err, results) => { // 判断 SQL 语句是否执行成功 // if (err) { // return res.send({ status: 1, message: err.message }); // } if (err) return res.cc(err); // 判断影响行数是否为 1 // if (results.affectedRows !== 1) { // return res.send({ status: 1, message: "注册用户失败,请稍后重试" }); // } if (results.length > 0) { return res.cc("注册用户失败,请稍后重试"); } // 注册用户成功 // res.send({ status: 0, message: "注册用户成功!" }); res.cc("注册用户成功!", 0); } ); }); }; // 登录的处理函数 exports.login = (req, res) => { // 接收表单的数据 const userinfo = req.body; // 定义 SQL 语句 const sql = "select * from ev_users where username = ?"; // 执行 SQL 语句,根据用户名查询用户的信息 db.query(sql, userinfo.username, (err, results) => { if (err) { return res.cc(err); } // 执行 SQL 语句成功,但是获取到的数据条数不等于 1 if (results.length !== 1) { return res.cc("登录失败!"); } // 判断密码是否正确 // 调用 bcrypt.compareSync(用户提交的密码, 数据库中的密码) 方法比较密码是否一致 const compareResult = bcrypt.compareSync( userinfo.password, results[0].password ); if (!compareResult) { return res.cc("登陆失败!"); } // 在服务器端生成 Token 字符串 const user = { ...results[0], password: "", user_pic: "" }; // 对用户的信息进行加密,生成 Token 字符串 const tokenStr = jwt.sign(user, conFig.jwtSecretKey, { expiresIn: conFig.expiresIn, }); // 将 token 响应给客户端 res.send({ status: 0, message: "登陆成功", token: "Bearer " + tokenStr, }); }); }; -
userinfo.js
// 导入数据库操作模块 const db = require("../db/index"); // 可使用 bcrypt.compareSync(提交的密码,数据库中的密码) 方法验证密码是否正确 // compareSync() 函数的返回值为布尔值,true 表示密码正确,false 表示密码错误 const bcrypt = require("bcryptjs"); // 获取用户信息 exports.getUserInfo = (req, res) => { // 根据用户的 id,查询用户的基本信息 // 注意:为了防止用户的密码泄露,需要排除 password 字段 const sqlStr = `select id, username, nickname, email, user_pic from ev_users where id=?`; // 注意:req 对象上的 user 属性,是 Token 解析成功,express-jwt 中间件帮我们挂载上去的 db.query(sqlStr, req.user.id, (err, results) => { // 执行 SQL 语句失败 if (err) return res.cc(err); // 执行 SQL 语句成功,但是查询到的数据条数不等于 1 if (results.length !== 1) return res.cc("获取用户信息失败!"); res.send({ status: 0, message: "获取用户基本信息成功!", data: results[0], }); }); }; // 更新用户信息 exports.updateUserInfo = (req, res) => { const sqlStr = "update ev_users set ? where id = ?"; db.query(sqlStr, [req.body, req.body.id], (err, results) => { if (err) return res.cc(err); if (results.affectedRows !== 1) return res.cc("修改用户基础信息失败"); return res.cc("修改用户基本信息成功!", 0); }); }; // 重置密码 exports.updatePassword = (req, res) => { // 根据 id 查询用户的信息 const sql = "select * from ev_users where id = ?"; // 执行根据 id 查询用户信息的 SQL 语句 db.query(sql, req.user.id, (err, results) => { if (err) return res.cc(err); // 检查指定 id 的用户是否存在 if (results.length !== 1) return res.cc("用户不存在!"); // 判断密码是否正确 const compareResult = bcrypt.compareSync( req.body.oldPwd, results[0].password ); if (!compareResult) return res.cc("旧密码错误!"); // 定义更新用户密码的 SQL 语句 const sqlStr = `update ev_users set password=? where id=?`; // 对新密码进行 bcrypt 加密处理 const newPwd = bcrypt.hashSync(req.body.newPwd, 10); // 执行 SQL 语句,根据 id 更新用户的密码 db.query(sqlStr, [newPwd, req.user.id], (err, results) => { if (err) return res.cc(err); if (results.affectedRows !== 1) return res.cc("更新密码失败!"); res.cc("更新密码成功!", 0); }); }); }; // 更新用户头像 exports.updateAvatar = (req, res) => { const sql = "update ev_users set user_pic = ? where id = ?"; db.query(sql, [req.body.avatar, req.user.id], (err, results) => { if (err) return res.cc(err); if (results.affectedRows !== 1) return res.cc("更新头像失败"); return res.cc("更新头像成功!", 0); }); }; -
artcate.js
// 导入数据库操作模块 const db = require("../db/index"); // 获取用户信息 exports.getArticleCates = (req, res) => { // 根据分类的状态,获取所有未被删除的分类列表数据 // is_delete 为 0 表示没有被标记为删除 的数据 const sqlStr = `select * from ev_article_cate where is_delete = 0 order by id asc`; db.query(sqlStr, (err, results) => { if (err) return res.cc(err); res.send({ status: 0, message: "获取文章分类列表成功!", data: results, }); }); }; // 新增文章分类 exports.addArticleCates = (req, res) => { // 定义查询 分类名称 与 分类别名 是否被占用的 SQL 语句 const sql = `select * from ev_article_cate where name=? or alias=?`; db.query(sql, [req.body.name, req.body.alias], (err, results) => { if (err) return res.cc(err); // 分类名称 和 分类别名 都被占用 if (results.length === 2) { return res.cc("分类名称与别名被占用,请更换后重试!"); } if ( results.length === 1 && results[0].name === req.body.name && results[0].alias === req.body.alias ) { return res.cc("分类名称与别名被占用,请更换后重试!"); } // 分类名称 或 分类别名 被占用 if (results.length === 1 && results[0].name === req.body.name) { return res.cc("分类名称被占用,请更换后重试!"); } if (results.length === 1 && results[0].alias === req.body.alias) { return res.cc("分类别名被占用,请更换后重试!"); } const sqlStr = `insert into ev_article_cate set ?`; db.query(sqlStr, req.body, (err, results) => { if (err) return res.cc(err); if (results.affectedRows !== 1) return res.cc("新增文章分类失败!"); res.cc("新增文章分类成功!", 0); }); }); }; // 删除文章分类的处理函数 exports.deleteCateById = (req, res) => { // 标记删除的 SQL 语句 const sql = `update ev_article_cate set is_delete=1 where id=?`; db.query(sql, req.params.id, (err, results) => { if (err) return res.cc(err); if (results.affectedRows !== 1) return res.cc("删除文章分类失败!"); res.cc("删除文章分类成功!", 0); }); }; // 根据 id 获取文章分类的处理函数 exports.getArtCateById = (req, res) => { const sql = `select * from ev_article_cate where id=?`; db.query(sql, req.params.id, (err, results) => { if (err) return res.cc(err); if (results.length !== 1) return res.cc("获取文章分类数据失败!"); res.send({ status: 0, message: "获取文章分类数据成功!", data: results[0], }); }); }; // 根据 Id 更新文章分类数据 exports.updateCateById = (req, res) => { // 定义查询 分类名称 与 分类别名 是否被占用的 SQL 语句 const sql = `select * from ev_article_cate where Id!=? and (name=? or alias=?)`; db.query( sql, [req.body.Id, req.body.name, req.body.alias], (err, results) => { if (err) return res.cc(err); if (results.length === 2) { return res.cc("分类名称与别名被占用,请更换后重试!"); } if ( results.length === 1 && results[0].name === req.body.name && results[0].alias === req.body.alias ) { return res.cc("分类名称与别名被占用,请更换后重试!"); } // 分类名称 或 分类别名 被占用 if (results.length === 1 && results[0].name === req.body.name) { return res.cc("分类名称被占用,请更换后重试!"); } if (results.length === 1 && results[0].alias === req.body.alias) { return res.cc("分类别名被占用,请更换后重试!"); } const sqlStr = `update ev_article_cate set ? where Id=?`; db.query(sqlStr, [req.body, req.body.Id], (err, results) => { if (err) return res.cc(err); if (results.affectedRows !== 1) return res.cc("更新文章分类失败!"); res.cc("更新文章分类成功!", 0); }); } ); };
-