验证身份

JWT, JSON web token

interface

用它可以描述一种数据的样子,或者叫形状,就是可以定义一个 interface,起一个名字,然后在里面可以添加一些属性,设置这些属性对应的类型,再把它用在应用里的其他地方。然后在应用的其他地方可以把一个数据的类型设置成这种 interface。所以定义一个 interface 就相当于定义了一种数据类型。

1
2
3
4
interface User{
id:number;
name:string;
}

加密方法

对称解密:最简单的加密方法就是在加密的时候设置一个密码,只要知道加密的时候用的这个密码,就可以解密这个数据。这种加密方法叫做对称加密。
非对称加密:密钥和公钥。

制作密钥与公钥

生成秘钥:

  1. 项目中创建 config 目录,并打开命令行
  2. 执行 openssl
  3. 执行 genrsa -out private.key 4096
  4. 执行 rsa -in private.key -pubout -out public.key
  5. 执行 exit 退出

结束后在 gitignore 中忽略掉这两个文件。

准备密钥和公钥配置

由于环境变量不支持添加多行,需要先用 base64 处理一下密钥和公钥文件后才能添加至环境变量。
config 中新建文件 convert.key.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
const fs = require('fs');
const path = require('path');

/**
* 读取密钥文件
*/
const privateKey = fs.readFileSync(path.join('config', 'private.key'));
const publicKey = fs.readFileSync(path.join('config', 'public.key'));

/**
* 转换成base64格式
*/
const privateKeyBase64 = Buffer.from(privateKey).toString('base64');
const publicKeyBase64 = Buffer.from(publicKey).toString('base64');

/**
* 输出转换结果
*/
console.log('\nPrivate.key:')
console.log(privateKeyBase64);
console.log('\nPublic.key:')
console.log(publicKeyBase64);

执行 node config/private.key  就可以看到 base64 处理后的密钥和公钥了
这时可以将处理后的密钥和公钥添加到环境变量中了。
添加完成后可以在 app.config 中配置一下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import dotenv from "dotenv";

dotenv.config(); //会默认载入根目录下的env文件

/**
* 应用配置
*/

export const { APP_PORT } = process.env;

/**
* 数据仓库配置
*/
export const {
MYSQL_HOST,
MYSQL_PORT,
MYSQL_USER,
MYSQL_PASSWORD,
MYSQL_DATABASE,
} = process.env;

/**
* 密钥配置
*/
export let { PRIVATE_KEY, PUBLIC_KEY } = process.env;
//将base64处理的密钥还原成之前的
PRIVATE_KEY = Buffer.from(PRIVATE_KEY, "base64").toString();
PUBLIC_KEY = Buffer.from(PUBLIC_KEY, "base64").toString();

定义签发 JWT 令牌的服务

安装一个包,名字叫做 jsonwebtoken,借助它的功能去完成。

1
npm install jsonwebtoken

如果使用了 typescript 需要安装类型定义:

1
npm install @types/jsonwebtoken --save-dev

在 auth.service.ts 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import jwt from "jsonwebtoken";
import { PRIVATE_KEY } from "../app/app.config";

/**
* 签发令牌
*/
interface SignTokenOptions {
payload?: any;
}

export const signToken = (options: SignTokenOptions) => {
//准备选项
const { payload } = options;

//签发 JWT
const token = jwt.sign(payload, PRIVATE_KEY, { algorithm: "RS256" });

//提供 JWT
return token;
};

签发 JWT 令牌

auth.middleware.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
import { Request, Response, NextFunction } from "express";
import * as userService from "../user/user.service";
import bcrypt from "bcrypt";

/**
* 验证用户登录数据
*/
export const validateLoginData = async (
request: Request,
response: Response,
next: NextFunction
) => {
console.log("验证用户登录数据");

//准备数据
const { name, password } = request.body;

//验证必填数据
if (!name) return next(new Error("NAME_IS_REQUIRED"));
if (!password) return next(new Error("PASSWORD_IS_REQUIRED"));

//验证用户名
const user = await userService.getUserByName(name, { password: true });
if (!user) return next(new Error("USER_DOES_NOT_EXIST"));

//验证用户密码
const matched = await bcrypt.compare(password, user.password);
if (!matched) return next(new Error("PASSWORD_DOES_NOT_MATCH"));

// 在请求主体里添加用户
request.body.user = user; //这里添加个body.user

//下一步
next();
};

auth.controller.ts 中:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
import { Request, Response, NextFunction } from "express";
import { signToken } from "./auth.service";

/**
* 用户登录
*/
export const login = async (
request: Request,
response: Response,
next: NextFunction
) => {
// 准备数据
const {
user: { id, name },
} = request.body;

const payload = { id, name };

try {
//签发令牌
const token = signToken({ payload });

//做出响应
response.send({ id, name, token });
} catch (error) {
next(error);
}
};

理解令牌

可以在 jwt.io 这个网站的 debugger 中验证。左边粘贴 token,右边会显示一些信息。底部红色字体是说签名未验证,在右边框中把公钥粘贴进去,然后红色字体就变成签名已验证了。


定义验证用户身份中间件

auth.middleware.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
import { Request, Response, NextFunction, request } from "express";
import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";
import * as userService from "../user/user.service";
import { PUBLIC_KEY } from "../app/app.config";

/**
* 验证用户身份
*/
export const authGuard = (
request: Request,
response: Response,
next: NextFunction
) => {
console.log("验证用户身份");

try {
// 提取Authorization
const authorization = request.header("Authorization");
if (!authorization) throw new Error();

// 提取JWT令牌
const token = authorization.replace("Bearer ", "");
if (!token) throw new Error();

//验证令牌
jwt.verify(token, PUBLIC_KEY, {
algorithms: ["RS256"],
});

//下一步
next();
} catch (error) {
next(new Error("UNAUTHORIZED"));
}
};

在 app.middleware.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
/**
* 默认异常处理器
*/
export const defaultErrorHandler = (
error: any,
request: Request,
response: Response,
next: NextFunction
) => {
if (error.message) {
console.log(error.message);
}
let statusCode: number, message: string;

/**
* 处理异常
*/
switch (error.message) {
case "NAME_IS_REQUIRED":
statusCode = 400;
message = "请提供用户名";
break;
case "PASSWORD_IS_REQUIRED":
statusCode = 400;
message = "请提供用户密码";
break;
case "USER_ALREADY_EXIST":
statusCode = 409;
message = "用户名已被占用";
break;
case "USER_DOES_NOT_EXIST":
statusCode = 400;
message = "用户不存在";
break;
case "PASSWORD_DOES_NOT_MATCH":
statusCode = 400;
message = "密码不对";
break;
case "UNAUTHORIZED":
statusCode = 401;
message = "请先登录";
break;
default:
statusCode = 500;
message = "服务暂时出了点问题~";
break;
}
response.status(statusCode).send({ message });
};

测试验证用户登录状态

auth.controller.ts:

1
2
3
4
5
6
7
8
9
10
/**
* 验证登录
*/
export const validate = (
request: Request,
response: Response,
next: NextFunction
) => {
response.sendStatus(200);
};

auth.router.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import express, { request } from "express";
import * as authController from "./auth.controller";
import { authGuard, validateLoginData } from "./auth.middleware";

const router = express.Router();

/**
* 用户登录
*/
router.post("/login", validateLoginData, authController.login);

/**
* 定义验证登录接口
*/
router.post("/auth/validate", authGuard, authController.validate);

/**
* 导出路由
*/
export default router;

然后使用 insomnia 测试一下,显示请先登录,复制一下 token,配置在 bearer token 中,再请求一下,显示 OK。


在请求里添加当前用户

新建 auto.interface.ts 文件

1
2
3
4
5
export interface TokenPayload {
id?: number;
name?: string;
iat?: number;
}

auth.middleware.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { Request, Response, NextFunction, request } from "express";
import jwt from "jsonwebtoken";
import bcrypt from "bcrypt";
import * as userService from "../user/user.service";
import { PUBLIC_KEY } from "../app/app.config";
import { TokenPayload } from "./auto.interface";

/**
* 验证用户身份
*/
export const authGuard = (
request: Request,
response: Response,
next: NextFunction
) => {
console.log("验证用户身份");

try {
// 提取Authorization
const authorization = request.header("Authorization");
if (!authorization) throw new Error();

// 提取JWT令牌
const token = authorization.replace("Bearer ", "");
if (!token) throw new Error();

//验证令牌
const decoded = jwt.verify(token, PUBLIC_KEY, {
algorithms: ["RS256"],
});

// 在请求里添加当前用户
request.user = decoded as TokenPayload;

//下一步
next();
} catch (error) {
next(new Error("UNAUTHORIZED"));
}
};

auth.controller.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import { Request, Response, NextFunction } from "express";
import { signToken } from "./auth.service";

/**
* 用户登录
*/
export const login = async (
request: Request,
response: Response,
next: NextFunction
) => {
// 准备数据
const {
user: { id, name },
} = request.body;

const payload = { id, name };

try {
//签发令牌
const token = signToken({ payload });

//做出响应
response.send({ id, name, token });
} catch (error) {
next(error);
}
};

/**
* 验证登录
*/
export const validate = (
request: Request,
response: Response,
next: NextFunction
) => {
console.log(request.user);
response.sendStatus(200);
};

auth.router.ts:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
import express from "express";
import * as authController from "./auth.controller";
import { authGuard, validateLoginData } from "./auth.middleware";

const router = express.Router();

/**
* 用户登录
*/
router.post("/login", validateLoginData, authController.login);

/**
* 定义验证登录接口
*/
router.post("/auth/validate", authGuard, authController.validate);

/**
* 导出路由
*/
export default router;