验证身份 JWT, JSON web token
interface 用它可以描述一种数据的样子,或者叫形状,就是可以定义一个 interface,起一个名字,然后在里面可以添加一些属性,设置这些属性对应的类型,再把它用在应用里的其他地方。然后在应用的其他地方可以把一个数据的类型设置成这种 interface。所以定义一个 interface 就相当于定义了一种数据类型。
1 2 3 4 interface User{ id:number; name:string; }
加密方法 对称解密:最简单的加密方法就是在加密的时候设置一个密码,只要知道加密的时候用的这个密码,就可以解密这个数据。这种加密方法叫做对称加密。 非对称加密:密钥和公钥。
制作密钥与公钥 生成秘钥:
项目中创建 config 目录,并打开命令行
执行 openssl
执行 genrsa -out private.key 4096
执行 rsa -in private.key -pubout -out public.key
执行 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;