Skip to content

Node-项目:mr-coderhub

[TOC]

项目:mr-coderhub

技术栈:

Node 16

koa 2

项目介绍

coderhub 旨在创建一个程序员分享生活动态的平台

完整的项目接口包括:

  • 面向用户的业务接口
  • 面向后台的管理接口

项目搭建

初始化项目

sh
pnpm init

项目目录

  • 按功能模块划分
  • 按业务模块划分

项目脚本

image-20240207155344621

运行项目

sh
pnpm start

安装 koa2

sh
pnpm add koa @koa/router koa-bodyparser

创建、启动项目

1、基本架构

image-20240207155417382

2、添加 router

image-20240207155616098

封装路由

1、封装路由userRouter

image-20240207161303463

2、导入路由

image-20240207161427438

封装 app

1、封装 app

image-20240207161820802

2、导入 app

image-20240207161742445

配置写入环境变量

1、配置常量到./src/config/目录下

image-20240207155859324

image-20240207155906799

2、优化:将环境变量放入.env文件中

image-20240207160143547

3、加载.env中的配置

依赖包: dotenv

image-20240207160701235

用户注册

image-20240207162045774

postman 创建全局变量

1、添加 coderhub 环境变量

image-20240207162547067

image-20240207162658661

2、在接口路径中使用引用变量

image-20240207162805491

用户注册路由

image-20240207163532476

封装 UserController

1、抽取路由逻辑到UserController

image-20240207164716398

2、导入UserController实例,在 router 中做映射

image-20240207163953373

封装 UserService

1、将数据库操作,抽取到UserService

image-20240207164433482

2、导入UserService实例

image-20240207164655869

创建数据库

image-20240207165224364

数据库连接

依赖包: mysql2

1、使用mysql2连接数据库

image-20240207165426493

2、抽取数据库连接

image-20240207165512575

3、导入数据库连接

image-20240207165552501

4、判断数据库是否连接成功

image-20240207170024337

创建用户表

image-20240207170426610

存入数据库

1、使用预处理语句插入 user 数据

image-20240207171006748

2、异步处理

image-20240207171536247

3、controller 中也改成异步

image-20240207171315167

4、创建用户成功

image-20240207171625027

验证用户

1、验证用户名、密码是否为空

image-20240207172545816

2、判断 name 是否已经存在

UserService

image-20240207172755880

UserController

image-20240207172953113

3、优化: 抽取验证用户的逻辑

抽取中间件

image-20240207173831114

导入中间件

image-20240207173632511

错误处理

1、执行一次错误处理代码

image-20240207174113989

2、在出错的位置发射错误事件

image-20240207174455334

3、监听处理错误

image-20240207174521046

4、优化: 抽取错误常量

image-20240207174656806

使用常量

image-20240207174718833

image-20240207174801145

密码加密

1、在路由中添加加密中间件

image-20240208121244296

2、封装加密中间件

image-20240208121929235

3、封装 md5 加密的工具函数

image-20240208122002081

用户登录

登录凭证

为什么需要登录凭证

web 开发中使用最多的是 HTTP 协议,但是它是一个无状态的协议

无状态协议: HTTP 的每次请求都是一个单独的请求,和之前的请求没有关系。

用户登录

1、创建login.router.js路由文件

image-20240217155323238

2、挂载loginRouter

image-20240217154214276

3、封装 LoginController 并导入 login

js
const KoaRouter = require('@koa/router')

const { login } = require('../controller/login.controller')
const { verifyLogin } = require('../middleware/login.middleware')

const loginRouter = new KoaRouter({ prefix: '/login' }) + loginRouter.post('/', verifyLogin, login)

module.exports = loginRouter

验证用户

0、使用中间件

image-20240217155956793

1、验证用户中间件

  • 判断用户名、密码是否为空

  • 判断用户是否存在

  • 判断密码是否正确

js
const { USERNAME_OR_PASSWORD_REQUIRED, USER_NOT_EXISTS, PASSWORD_IS_NOT_RIGHT } = require('../const/error.const')
const { findUserByUserame, checkPassword } = require('../service/user.service')

/* 验证用户登录 */
const verifyLogin = async (ctx, next) => {
  const { username, password } = ctx.request.body
  // 判断用户名、密码是否为空
  if (!username || !password) {
    return ctx.app.emit('error', USERNAME_OR_PASSWORD_REQUIRED, ctx)
  }

  // 判断用户是否存在
  const users = await findUserByUserame(username)
  const user = users[0]
  if (!users.length) {
    return ctx.app.emit('error', USER_NOT_EXISTS, ctx)
  }

  // 判断密码是否正确
  if (user.password !== encryptMd5(password)) {
    return ctx.app.emit('error', PASSWORD_IS_NOT_RIGHT, ctx)
  }

  // 保存user信息到ctx中
  ctx.user = user

  await next()
}

module.exports = {
  verifyLogin
}

2、添加错误常量

js
const USERNAME_OR_PASSWORD_REQUIRED = 'username_or_password_required'
const USER_ALREADY_EXISTS = 'user_already_exists'
+ const USER_NOT_EXISTS = 'user_not_exists'
+ const PASSWORD_IS_NOT_RIGHT = 'password_is_not_right'

module.exports = {
  USERNAME_OR_PASSWORD_REQUIRED,
  USER_ALREADY_EXISTS,
  USER_NOT_EXISTS,
  PASSWORD_IS_NOT_RIGHT
}

3、添加错误处理

js
app.on('error', (err, ctx) => {
  let code = 0
  let message = ''
  switch (err) {
+    case USERNAME_OR_PASSWORD_REQUIRED:
      code = -1001
      message = '用户名或密码不能为空~'
      break
    case USER_ALREADY_EXISTS:
      code = -1002
      message = '用户已经存在~'
      break
+    case USER_NOT_EXISTS:
      code = -1003
      message = '用户不存在~'
      break
+    case PASSWORD_IS_NOT_RIGHT:
      code = -1004
      message = '用户密码错误~'
      break
  }

4、使用 UserService

js
  /* 验证用户名是否存在 */
  async findUserByUserame(username) {
    const sql = 'SELECT * FROM `user` WHERE `username`=?;'
    const [users] = await connection.execute(sql, [username])
    return users
  }

颁发 token

1、依赖包:jsonwebtoken

sh
pnpm add jsonwebtoken

2、创建 keys

sh
# 开启openssl
openssl

# 创建私钥
genrsa -out private.key 2048

# 根据私钥创建公钥
rsa -in private.key -pubout -out public.key

3、读取并导出私钥和公钥

image-20240217171715299

注意: 这里的路径./keys会有问题,因为readFileSync()是从启动目录nodemon ./src/xx的位置开始计算的。

注意:algorithm: 'RS256' 时,要求生成的私钥长度最少为 2048

要写成如下路径:

image-20240217172205828

或者写成这样:

image-20240217172304286

4、使用私钥和公钥颁发 token

image-20240217171935428

5、接口

image-20240217172409236

image-20240217172347968

验证 token

1、创建/test路由

image-20240217172911087

2、封装 test

image-20240217173751475

3、验证未通过,报错

image-20240217173549214

image-20240217173709283

4、接口

image-20240217173639271

5、封装: 验证 token 的中间件verifyAuth

image-20240217174200358

image-20240217174233138

image-20240217174104218

postman 定义 token 变量

1、在用户登录接口定义 token 全局变量

image-20240217174556987

2、使用 token

image-20240217174631052

动态

发表动态

1、创建动态表moment

image-20240218093514304

2、创建moment.router.js动态路由文件,使用create

image-20240218094450967

3、创建moment.controller.js文件,处理路由逻辑

image-20240218095240949

4、创建moment.service.js文件,处理数据库逻辑

image-20240218094934815

5、在login.middleware.js中判断 token 是否未携带

image-20240218094253413

4、接口

image-20240218095324141

image-20240218095415093

查询动态列表

1、生成动态数据

image-20240218100458321

2、在moment.router.js中,添加查询动态的路由GET: /

image-20240218100612156

3、在moment.controller.js中,处理查询动态的路由逻辑list()

image-20240218101027573

4、在moment.service.js中,处理数据库逻辑queryList()

image-20240218100923738

5、接口

image-20240218101242772

查询动态-分页查询

1、在list()中获取分页参数offset, size

image-20240218101627155

2、在queryList()中,执行分页 SQL 语句

image-20240218102233654

注意: 此处的 offset 和 size 不支持数字类型,需要转成 String

3、接口

image-20240218102310894

4、当接口没有传参时,需要给一个默认值

image-20240218102351137

image-20240218102421375

查询动态-多表查询

1、SQL 语句

image-20240218103019182

2、修改queryList()

image-20240218103127346

3、查询结果

image-20240218103155703

动态详情

1、在router中,添加路由GET /:momentId

image-20240218103359320

2、在controller中,实现detail()

image-20240218104104790

3、在service中,实现queryById()

image-20240218103954607

4、接口

image-20240218104144305

修改动态

1、在router中,添加PATCH /:momentId路由,需要验证 token

image-20240218104532201

2、在controller中,实现update()

image-20240218105008088

3、在service中,实现update()

image-20240218104758670

image-20240218104905063

4、接口

image-20240218104645943

image-20240218105025506

权限验证

在修改动态前需要验证用户身份,用户只能修改自己发表的动态

1、在router中,添加权限验证中间件verifyMomentPermission()

image-20240218105750989

2、创建permission.middleware.js,实现verifyMomentPermission()中间件

image-20240218110406293

3、创建permission.service.js,实现checkMoment()

image-20240218110219017

4、没有权限,处理错误

image-20240218110324122

image-20240218110521252

权限验证-优化

问题: 当前的权限验证只能验证用户是否有操作moment的权限,不能验证用户的其他权限(comment

优化: 让权限验证更具通用性

思路一: 封装一个返回中间件的函数verifyPermission()

1、在permission.middleware.js中,封装一个返回中间件的函数verifyPermission()【未实现】

image-20240218112203142

2、在router中,使用verifyPermission()

image-20240218112312776

思路二: 根据/:momentId获取resourceName,从而动态生成 SQL 语句

1、在router中,使用verifyPermission()

image-20240218113156898

2、在permission.middleware.js中,封装

image-20240218113201406

3、在service中,实现checkResource()

image-20240218113112727

补充: 操作动态前,先查询资源是否存在

[TODO]

删除动态

1、在router中,添加DELETE /:momentId路由,需要验证 token 和权限

image-20240218111159955

2、在controller中,实现remove()

image-20240218111404549

3、在service中,实现remove()

image-20240218111343826

image-20240218111357757

4、接口

image-20240218111447507

image-20240218111507820

评论

发表评论

1、创建评论表comment

image-20240218150844134

image-20240218150857580

2、创建comment.router.js文件,添加POST /路由,需要验证 token

image-20240218152046992

3、创建comment.controller.js文件,实现create()

image-20240218151832004

4、创建comment.service.js文件,实现create()

image-20240218151942004

5、接口

image-20240218152007477

回复评论

1、在router中,添加POST / reply路由,需要验证 token

image-20240218152515848

2、在controller中,实现reply

image-20240218152745468

3、在service中,实现reply

image-20240218152700315

4、接口

image-20240218152321997

image-20240218152759434

删除评论【

修改评论【

查询评论

查询动态时,同时展示评论信息

  • 查询多个动态时,显示评论个数
  • 查询单个动态时,显示评论列表

查询多个动态时,显示评论个数

1、SQL 语句

image-20240218154623938

image-20240218154724822

2、修改queryList()

image-20240218155115590

3、查询结果

image-20240218155143298

查询单个动态时,显示评论列表

1、SQL 语句

image-20240218160425366

image-20240218160433833

2、修改queryById()

image-20240218160839196

3、查询结果

image-20240218160629974

4、在评论对象中添加user对象

image-20240218161143016

image-20240218161225113

动态标签

标签和动态是多对多的关系

image-20240218162039037

标签表

image-20240218162135663

创建标签

1、创建label.router.js文件,添加POST /路由,需要验证 token

image-20240218162459014

2、创建label.controller.js文件,实现create()

image-20240218162748769

3、创建label.service.js文件,实现create()

image-20240218162729311

4、接口

image-20240218162614088

image-20240218162809601

image-20240218162835202

查询标签列表【

1、在router中,添加GET /list路由,不需要验证 token

image-20240218163200584

2、在controller中,实现list()

3、在service中,实现list()

4、接口

image-20240218163228986

多对多关系表

创建一个动态和标签之间的多对多关系表

image-20240218163603893

image-20240218164022741

动态-添加标签

image-20240218165013407

  • 验证是否登录
  • 验证是否具有操作动态的权限
  • 验证 label 是否已经存在于 label 表中
    • 存在:直接使用该 label
    • 不存在:先将 label 添加到 label 表中,再使用该 label
  • 将动态和 labels 的关系添加到关系表中

1、在moment.router.js中,添加POST /:momentId/labels路由

image-20240218165959143

2、创建label.middleware.js文件,实现verifyLabelExsts()中间件

验证 label 是否已经存在于 label 表中

  • 存在:直接使用该 label
  • 不存在:先将 label 添加到 label 表中,再使用该 label

image-20240218171021610

3、在label.service.js中,实现queryLabelByName()

image-20240218170401305

4、在moment.controller.js中,实现addLabels(),为动态添加标签

image-20240218171426932

image-20240218172629383

5、在label.service.js中,实现hasLabel(),判断是否已经存在某个 moment 和 label 的关系

image-20240218172041813

6、在label.service.js中,实现addLabel(),为动态添加标签

image-20240218172225940

7、接口

image-20240218165454012

动态-查询标签

查询多个动态时,显示标签个数

1、SQL

image-20240218175657159

2、修改queryList()

image-20240218175726893

3、查询结果

image-20240218175743413

查询单个动态时,显示标签数据

  • 错误的做法:这种做法查到的 labels 会根据comments的数量,重复查询该数量的次数,因为查询comments时出现了JSON_ARRAYAGG()函数

SQL

image-20240218175839054

  • 正确的做法:使用子查询

1、SQL

image-20240218180545964

2、修改queryById()

image-20240218180753924

3、查询结果

image-20240218180934631

图片上传存储

头像上传

依赖包:

  • multer
  • @koa/multer

1、基本使用

1、创建file.router.js文件,添加POST /avatar路由,需要验证 token

image-20240219162801643

2、封装上传头像中间件

1、创建file.middleware.js文件,封装上传头像中间件

image-20240219163410005

image-20240219163154964

2、抽取uploads常量

image-20240219165954555

image-20240219170006428

3、创建file.controller.js文件,实现create()

image-20240219163358867

image-20240219163346557

5、接口

image-20240219162825384

image-20240219164309220

保存头像信息

保存上传头像的信息到数据表中

1、创建avatar

image-20240219164435587

image-20240219164514897

2、在file.controller.js中,实现create(),保存上传头像的信息

image-20240219164818819

3、在file.service.js中,实现create(),保存上传头像的信息到数据库中

image-20240219164733998

4、上传成功

image-20240219164836867

image-20240219164854289

展示头像

1、在user.router.js中,添加GET /avatar/:userId路由

image-20240219165341278

2、在user.controller.js中,实现showAvatarImage()

image-20240219170127774

3、在file.service.js中,实现queryAvatarByUserId(),获取最新上传的头像信息

image-20240219165737706

4、查看头像

image-20240219170251784

动态-查询头像

1、修改user表,增加头像地址字段

image-20240219170645186

image-20240219170655881

2、在file.controller.jscreate()中,保存用户上传的头像地址

image-20240219171403286

3、在user.service.js中,实现updateUserAvatar(),保存用户上传的头像地址

image-20240219171005375

4、抽取域名常量

image-20240219171315239

image-20240219171341068

image-20240219171445421

5、在moment.service.js中,查询动态信息时,修改 SQL 语句,添加 user 中的 avatar_url

动态列表

image-20240219171829652

动态详情

image-20240219172050272

CMS接口

角色接口

新增角色

1、接口

image-202409111120597492/44

2、在role.router.js中创建路由

image-20240911112457437

3、需要在app/index.js中手动注册路由

image-20240911112430405

4、在路由中添加增删改查

image-20240911112847289

5、在controller/role.controller.js中编写中间件函数

image-20240911113931907

6、在service/role.service.js中操作数据库

image-20240911113838492

7、创建role

image-20240911113604404

查询角色列表

1、接口

image-20240911114708913

2、在controller/role.controller.js中编写中间件函数

注意:

  • 当使用connect.query()时,要求传入的变量必须是number类型
  • 当使用connect.execute()时,要求传入的变量必须是string类型

image-20240911114515818

3、在service/role.service.js中操作数据库

image-20240911114501779

菜单接口

新增菜单

1、接口

image-20240911115941680

2、创建数据库

image-20240911115448761

3、效果

image-20240911121026585

4、在menu.router.js中创建路由,需要验证token

image-20240911115258733

5、在app/index.js中注册路由

image-20240911115142221

6、在menu.controller.js中实现create()

image-20240911120140158

7、在menu.service.js中操作数据库

image-20240911120426149

查询完整菜单树列表

1、接口

image-20240911122054301

2、SQL语句

image-20240911121353958

3、在menu.controller.js中实现list()

image-20240911121943467

4、在menu.service.js中操作数据库

image-20240911121839839

多对多-给角色分配菜单权限

1、接口

image-20240911122844327

2、执行结果

image-20240911123612265

3、创建rolemenu的关系表

image-20240911122321119

4、在role.router.js中,添加路由

image-20240911122754269

5、在role.controller.js中,实现assignMenu(),给角色分配菜单

image-20240911123414234

6、在role.service.js中,操作数据库

image-20240911123322344

多对多-查询角色列表权限

1、查询角色列表的基本信息

2、在role.controller.js中,除了获取角色的基本信息外,还要获取角色的菜单信息

image-20240911124157558

3、在role.service.js中,根据roleId获取所有的menuId

image-20240911124943724

image-20240911125007760

4、在role.service.js中,根据menuId获取菜单树

image-20240911125658006

项目部署

见:C21-服务器部署

知识点

postman

创建全局变量

1、添加 coderhub 环境变量

image-20240207162547067

image-20240207162658661

2、在接口路径中使用引用变量

image-20240207162805491

定义 token 变量

1、在用户登录接口定义 token 全局变量

image-20240217174556987

2、使用 token

image-20240217174631052

封装 app

1、封装 app

image-20240207161820802

2、导入 app

image-20240207161742445

错误处理

1、执行一次错误处理代码

image-20240207174113989

2、在出错的位置发射错误事件

image-20240207174455334

3、监听处理错误

image-20240207174521046

4、优化: 抽取错误常量

image-20240207174656806

使用常量

image-20240207174718833

image-20240207174801145

密码加密

1、在路由中添加加密中间件

image-20240208121244296

2、封装加密中间件

image-20240208121929235

3、封装 md5 加密的工具函数

image-20240208122002081

数据库连接

依赖包: mysql2

1、使用mysql2连接数据库

image-20240207165426493

2、抽取数据库连接

image-20240207165512575

3、导入数据库连接

image-20240207165552501

4、判断数据库是否连接成功

image-20240207170024337

token

颁发 token

1、依赖包:jsonwebtoken

sh
pnpm add jsonwebtoken

2、创建 keys

sh
# 开启openssl
openssl

# 创建私钥
genrsa -out private.key 2048

# 根据私钥创建公钥
rsa -in private.key -pubout -out public.key

3、读取并导出私钥和公钥

image-20240217171715299

注意: 这里的路径./keys会有问题,因为readFileSync()是从启动目录nodemon ./src/xx的位置开始计算的。

注意:algorithm: 'RS256' 时,要求生成的私钥长度最少为 2048

要写成如下路径:

image-20240217172205828

或者写成这样:

image-20240217172304286

4、使用私钥和公钥颁发 token

image-20240217171935428

5、接口

image-20240217172409236

image-20240217172347968

验证 token

1、创建/test路由

image-20240217172911087

2、封装 test

image-20240217173751475

3、验证未通过,报错

image-20240217173549214

image-20240217173709283

4、接口

image-20240217173639271

5、封装: 验证 token 的中间件verifyAuth

image-20240217174200358

image-20240217174233138

image-20240217174104218

自动注册路由

1、在 router 目录下创建index.js文件,并定义registerRouters()

image-20240218092603582

2、在app/index.js文件下,使用registerRouters()

image-20240218092306246

登录凭证

概述

概述: Cookie(复数形态 Cookies),又称为“小甜饼”,类型为“小型文本文件”。

作用: 网站为了辨别用户身份而存储在用户本地终端(Client Side)上的数据。浏览器会在特定的情况下携带上 cookie 来发送请求,可以通过 cookie 来获取一些信息。

Cookie 分类:

Cookie 总是保存在客户端中,按在客户端中的存储位置,Cookie 可以分为内存 Cookie 和硬盘 Cookie。

  • 内存 Cookie: 由浏览器维护,保存在内存中,浏览器关闭时 Cookie 就会消失,其存在时间是短暂的;

  • 硬盘 Cookie: 保存在硬盘中,有一个过期时间,用户手动清理或者过期时间到时,才会被清理;

区分 Cookie:

如何判断一个 cookie 是内存 cookie 还是硬盘 cookie 呢?

  • 内存 Cookie:没有设置过期时间,默认情况下 cookie 是内存 cookie,在关闭浏览器时会自动删除

  • 硬盘 Cookie:有设置过期时间,并且过期时间不为 0 或者负数,是硬盘 cookie,需要手动或者到期时,才会删除

1、客户端设置

  • document.cookie:``,获取 cookie
  • document.cookie = 'key=value;max-age=xxx':``,设置 cookie,同时设置过期时间(单位:s)

注意:

  • 设置 cookie 时,不设置max-agemax-age=0时是内存 cookie,浏览器关闭时 cookie 会消失
  • 设置 cookie 时,设置了max-age时是硬盘 cookie,只有等过期时间到达时,cookie 才会消失

JS 直接设置和获取 cookie:

image-20240208143024930

这个 cookie 会在会话关闭时被删除掉;

设置 cookie,同时设置过期时间(默认单位是秒钟)

image-20240208143042030

2、服务端设置

  • ctx.cookies.set(name, value, options?):``,设置 cookie。允许在服务端创建或更新一个 cookie,并发送给客户端
    • 参数
    • name:string,cookie 的名称
    • value:string,cookie 的值。如果要删除一个 Cookie,通常将值设置为 null
    • opotions?:object
      • maxAge:number,表示从现在开始 Cookie 存活的毫秒数
      • expires:Date,指定了 cookie 的过期日期。如果同时设置了 maxAge,则忽略此属性。
      • domain:string,cookie 的域名。默认没有设置,只对当前域名有效
      • path:string,cookie 的路径。默认是 '/',这意味着 Cookie 对整个站点有效
  • ctx.cookies.get(name):``,从请求中获取指定名称的 cookie 值
    • 参数
    • name:string,要获取的 cookie 的名称
    • 返回值: undefined | value

Koa 中默认支持直接操作 cookie

  • /test 请求中设置 cookie

  • /demo 请求中获取 cookie

~~之前的做法:~~当客户端给服务端发送一个请求后,服务器会在服务端给客户端的电脑设置一个 cookie,下次客户端再来访问时就可以携带上这个 cookie 用户标识自己。

image-20240208143051275

生命周期

cookie 的生命周期:

默认情况下的 cookie 是内存 cookie,也称之为会话 cookie,也就是在浏览器关闭时会自动被删除

我们可以通过设置 expires 或者 max-age 来设置过期的时间

  • expires:设置的是 Date.toUTCString(),设置格式是:expires=date-in-GMTString-format

  • max-age:设置过期的秒钟,max-age=max-age-in-seconds (例如一年为 60*60*24*365)

作用域

~~cookie 的作用域:~~允许 cookie 发送给哪些 URL

  • domain:指定哪些主机可以接受 cookie

    • 如果不指定,那么默认origin,不包括子域名,只能访问如:www.baidu.com,不能访问:map.baidu.com

    • 如果指定,则包含子域名。如果设置 domain=baidu.com,则 cookie 也包含在子域名中,如 map.baidu.com

  • path:指定主机下哪些路径可以接受 cookie

    • 例如,设置 path=/docs,则以下地址都会匹配:

      • /docs

      • /docs/Web/

      • /docs/Web/HTTP

session

koa-session

依赖包: koa-session

Session 是基于 cookie 实现机制

基本使用:

在 koa 中,我们可以借助于 koa-session 来实现 session 认证:

1、创建一个 session,并作为中间件使用

image-20240208155815535

2、使用 session 保存信息

image-20240208155625306

3、通过 session 获取之前保存的信息

image-20240208160639131

4、生成的 sessionid

image-20240208160145515

优化:加密 session

1、设置signed: true,并通过app.keys进行加盐操作

image-20240208160506359

2、生成的 sessionid

image-20240208160557391

3、服务端在获取 value 时,会同时验证sessionidsessionid.sig,只有 2 者都正确时才会通过

其他 session 选项

image-20240208143111843

token

cookie、session 的缺点:

  • Cookie 会被附加在每个 HTTP 请求中,所以无形中增加了流量(事实上某些请求是不需要的);

  • Cookie 是明文传递的,所以存在安全性的问题(可以通过和 session 结合解决该问题);

  • Cookie 的大小限制是 4KB,对于复杂的需求来说是不够的;

  • 对于浏览器外的其他客户端(比如 iOS、Android),必须手动的设置 cookie 和 session;

  • 对于分布式系统和服务器集群中如何可以保证其他系统也可以正确的解析 session?

token 概述:

所以,在目前的前后端分离的开发过程中,使用 token 来进行身份验证的是最多的情况:

  • token可以翻译为令牌

  • 也就是在验证了用户账号和密码正确的情况,给用户颁发一个令牌;

  • 这个令牌作为后续用户访问一些接口或者资源的凭证

  • 我们可以根据这个凭证来判断用户是否有权限来访问;

所以 token 的使用应该分成两个重要的步骤:

  • 1、*生成 token:*登录的时候,颁 token;

  • 2、*验证 token:*访问某些资源或者接口时,验证 token;

JWT 实现 Token

JWT 生成的 Token 由三部分组成,每个部分之间用.间隔:

header:会通过 base64Url 算法进行编码

  • alg:采用的加密算法,默认是 HMAC SHA256(HS256),HS256 是一种对称加密,采用同一个密钥进行加密和解密
  • typ:JWT,固定值,通常都写成 JWT 即可

payload:携带的数据,如我们可以将用户的 id 和 name 放到 payload 中,会通过 base64Url 算法进行编码

  • iat:默认也会携带 iat(issued at),令牌的签发时间
  • exp:我们也可以设置过期时间:exp(expiration time)

signature:用于验证消息在传输过程中未被篡改,并且,对于使用私钥签名的 Token,还可以验证发行者的身份。

  • 设置一个secretKey,通过将前两个的结果合并后进行 HMACSHA256 的算法

  • HMACSHA256(base64Url(header)+.+base64Url(payload), secretKey)

  • 但是如果 secretKey 暴露是一件非常危险的事情,因为之后就可以模拟颁发 token,也可以解密 token

image-20240208143245098

jsonwebtoken

在真实开发中,我们可以直接使用一个库来完成: jsonwebtoken

依赖包: jsonwebtoken

基本使用

1、生成 token

image-20240208180818634

2、验证 token

image-20240208180826928

API
  • jwt.sign():用于生成一个新的 JWT。
  • jwt.verify():验证给定的 JWT,并返回解码后的 payload。
  • jwt.decode():仅解码 JWT 的 payload 部分,而不验证其签名。
jwt.sign()

jwt.sign():用于生成一个新的 JWT。

  • jwt.sign(payload, secretOrPrivateKey, options?, callback?)

  • 参数

  • payload (Object | Buffer | String): 要编码到 JWT 中的数据。如果是对象或 Buffer,则会先进行 JSON 编码。

  • secretOrPrivateKey (String | Buffer): 用于签名的密钥或私钥。

  • options? (Object): 可选参数,用于控制 JWT 的生成方式,例如:

    • algorithm (String)(默认为 HS256): 指定签名算法。
    • expiresIn (单位:s): 设置 Token 的过期时间。
    • notBefore: 定义 Token 何时生效。
    • audience: 设置 Token 的观众(aud)。
    • issuer: 设置 Token 的发行者(iss)。
    • jwtid: 设置 JWT 的 ID。
    • allowInsecureKeySizes:为真则允许当使用 RSA 加密时私钥长度为 2048 以下
  • callback? (Function): 可选的回调函数,用于异步获取 JWT。如果提供了回调函数,jwt.sign() 将异步执行。

注意:algorithm: 'RS256' 时,要求生成的私钥长度最少为 2048byte

示例:

image-20240217173215836

jwt.verify()

jwt.verify():验证给定的 JWT,并返回解码后的 payload。

  • jwt.verify(token, secretOrPublicKey, [options, callback])

  • 参数

  • token (String): 要验证的 JWT 字符串。

  • secretOrPublicKey (String | Buffer): 用于验证签名的密钥或公钥。

  • options (Object): 可选参数,提供验证选项,如:

    • algorithms: (Array)指定接受的签名算法列表。
    • audience: 验证 Token 的观众(aud)值。
    • issuer: 验证 Token 的发行者(iss)值。
    • ignoreExpiration: 是否忽略 Token 的过期时间。
  • callback (Function): 可选的回调函数,用于异步操作。如果提供了回调函数,jwt.verify() 将异步执行。

示例:

image-20240217173322934

jwt.decode()

jwt.decode():仅解码 JWT 的 payload 部分,而不验证其签名。

  • jwt.decode(token, options?)

  • 参数

  • token (String): 要解码的 JWT 字符串。

  • options (Object): 可选参数,目前支持一个选项:

    • complete: 如果设置为 true,返回的将是一个包含 header、payload 和 signature 的完整对象,而不仅仅是 payload。

注意:

  • 使用 jwt.sign() 生成的 Token 应该在客户端安全地存储,例如 HTTP Cookie 或浏览器的 localStorage 中,并且通过 HTTPS 传输以避免被拦截。
  • jwt.verify() 在验证 Token 时非常重要,它确保了 Token 的真实性和完整性。你应该总是在信任的服务器端验证 Token。
  • jwt.decode() 不验证 Token 的有效性,因此不应该用于任何需要安全性的场景。它只是简单地解码 Token,以便查看其内容。

非对称加密

前面我们说过,HS256 加密算法一单密钥暴露就是非常危险的事情:

  • 比如在分布式系统中,每一个子系统都需要获取到密钥;

  • 那么拿到这个密钥后这个子系统既可以发布另外,也可以验证令牌;

  • 但是对于一些资源服务器来说,它们只需要有验证令牌的能力就可以了;

这个时候我们可以使用非对称加密,RS256:

  • 私钥(private key):用于发布令牌;

  • 公钥(public key):用于验证令牌;

我们可以使用 openssl 来生成一对私钥和公钥:

  • Mac 直接使用 terminal 终端即可;

  • Windows 默认的 cmd 终端是不能直接使用的,建议直接使用 git bash 终端;

sh
# 开启openssl
openssl

# 创建私钥
genrsa -out private.key 1024

# 根据私钥创建公钥
rsa -in private.key -pubout -out public.key

使用公钥和私钥签发和验证签名

image-20240208143335215

派发令牌和验证令牌

image-20240208143345212

image-20240208143351439