作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
保护专有数据, 必须确保通过请求向客户端提供服务的任何API的安全. 一个构建良好的API可以识别入侵者并阻止他们获得访问权 JSON Web令牌 (JWT)允许对客户端请求进行验证和潜在的加密.
在本教程中,我们将演示向控件中添加JWT安全性的过程 Node.js API的实现. 虽然有多种方法可以实现API层安全性, JWT被广泛采用, 开发人员友好的安全实现 Node.js API projects.
JWT是一种开放标准,它允许在空间受限的环境中使用 JSON format. 它简单而紧凑, 支持广泛的应用程序,这些应用程序优雅地结合了许多其他安全标准.
携带我们编码数据的jwt可能被加密和隐藏,也可能被签名并易于读取. 如果令牌是加密的, 它包含所有必需的哈希和算法信息,以支持其解密. 如果令牌已签名, 它的接收者将分析JWT的内容,并且应该能够检测到它是否已被篡改. 通过支持篡改检测 JSON Web签名 (JWS),最常用的签名令牌方法.
JWT由三个主要部分组成,每个部分都由一个名称-值对集合组成:
定义JWT的标头 何塞标准 指定令牌的类型和加密信息. 所需的名称-值对是:
Name | 值描述 |
---|---|
| 内容类型( |
| 令牌签名算法,从 JSON Web算法 (JWA) list |
JWS签名支持对称和非对称算法,以提供令牌篡改检测. (额外的报头名称-值对是由各种算法需要和指定的,但是a 完整的探索 这些标题名称超出了本文的讨论范围.)
JWT所需的有效负载是一方可以发送给另一方的编码(可能加密)内容. 有效载荷是一组 claims,每个都由一个名称-值对表示. 这些声明是消息传输数据的有意义的部分.e.,不包括消息头和元数据). 有效载荷包含在一个安全的通信中,用我们的令牌签名密封.
每个声明可以使用来自JWT保留集的名称, 或者我们可以自己定义一个名称. 如果我们自己定义索赔名称, 最佳实践要求避开下列保留词列表中列出的任何名称, 为了避免混淆.
具体保留名称必须包含在有效载荷中,无论是否存在任何额外的索赔要求:
Name | 值描述 |
---|---|
| 令牌的受众或接受者 |
| 令牌的主题, 在令牌中引用的任何编程实体的唯一标识符.g.,用户ID) |
| 令牌的发行者ID |
| 代币的“发布时间”戳 |
| A token’s “not before” time stamp; the token is rendered invalid before said time |
| A token’s “expiration” time stamp; the token is rendered invalid at said time |
为了安全地实现JWT,一个签名(i.e.(JWS)建议由预期的令牌接收者使用. 签名是一个简单的、url安全的、base64编码的字符串,用于验证令牌的真实性.
签名功能依赖于头文件指定的算法. 头和有效载荷部分都传递给算法,如下所示:
base64_url (fn_signature (base64_url(头)+ base64_url(载荷)))
Any party, 包括收件人, 可以独立运行此签名计算,将其与令牌内的JWT签名进行比较,以查看签名是否匹配.
虽然包含敏感数据的令牌应该加密(例如.e., using JWE), 如果我们的令牌不包含敏感数据, 对于非加密的、因此是公开的,使用JWS是可以接受的, yet encoded, 负载要求. JWS允许我们的签名包含信息,使令牌的接收者能够确定令牌是否已被修改, 因此腐化了, 由第三方提供.
在解释了JWT的结构和意图之后,让我们来探讨使用它的原因. 尽管JWT用例的范围很广,但我们将重点关注最常见的场景.
当客户端使用我们的API进行身份验证时, 返回一个JWT——这个用例在电子商务应用程序中很常见. 然后,客户端将此令牌传递给每个后续API调用. API层将验证授权令牌,验证调用是否可以继续. 客户端可以访问API的路由, services, 以及适合已验证客户端的级别的资源.
JWT通常在联邦身份生态系统中使用, 其中,用户的身份在多个独立的系统中链接, 例如使用Gmail登录的第三方网站. A 集中认证系统 负责验证客户端的标识并生成JWT,以便与连接到联邦标识的任何API或服务一起使用.
而非联邦API令牌则很简单, 联邦身份系统通常使用两种令牌类型: 访问令牌 and refresh令牌. An access token is short-lived; during its period of validity, 访问令牌授权访问受保护的资源. 刷新令牌是长期存在的,允许客户端从授权服务器请求新的访问令牌,而不需要重新输入客户端凭据.
无状态会话身份验证类似于API身份验证, 而是将更多信息打包到JWT中,并随每个请求一起传递给API. A stateless session mainly involves client-side data; for example, 对购物者进行身份验证并存储其购物车商品的电子商务应用程序可能使用JWT来存储它们.
在这个用例中, 服务器避免存储每个用户的状态, 将其操作限制为仅使用传递给它的信息. 在服务器端拥有无状态会话涉及到在客户端存储更多信息, 因此要求JWT包含有关用户交互的信息, 例如购物车或它将重定向到的URL. 这就是为什么无状态会话的JWT比可比的有状态会话的JWT包含更多信息的原因.
为了避免常见的攻击媒介,必须遵循JWT最佳实践:
最佳实践 | Details |
---|---|
始终执行算法验证. | 信任不安全的令牌会让我们容易受到攻击. Avoid trusting security libraries to autodetect the JWT algorithm; instead, 显式设置验证代码的算法. |
选择算法并验证加密输入. | JWA 定义一组可接受的算法和每个算法所需的输入. 对称算法的共享秘密应该很长, complex, random, 不需要对人类友好. |
验证所有索赔. | 只有当签名和内容都有效时,令牌才应该被认为是有效的. 各方之间传递的令牌应该使用一组一致的声明. |
Use the | 当使用多种令牌类型时,系统必须验证每种令牌类型是否被正确处理. 每种令牌类型都应该有自己明确的验证规则. |
要求运输安全. | Use 传输层安全性 (TLS),以减轻不同或相同收件人的攻击. TLS防止第三方访问传输中的令牌. |
依赖可信的JWT实现. | 避免自定义实现. 使用经过最多测试的库,并阅读库的文档,以了解其工作原理. |
生成唯一的 | 从安全的角度来看,存储直接或间接指向用户的信息(例如.g.(如电子邮件地址、用户ID)是不可取的. 不管怎样,考虑到 |
记住这些最佳实践, 让我们转向创建JWT和Node的实际实现.在Js的例子中,我们使用了这些点. 在高水平上, 我们将创建一个新项目,在这个项目中,我们将使用JWT对端点进行身份验证和授权, 以下三个主要步骤.
我们将使用Express,因为它提供了一种快速的方法来创建企业级和业余级的后端应用程序, 使JWT安全层的集成变得简单和直接. 我们将使用Postman进行测试,因为它允许与其他开发人员进行有效的协作以进行标准化 端到端测试.
完整项目的最终、可部署版本 repository 在浏览项目时是否可以作为参考.
创建项目文件夹并初始化Node.js项目:
mkdir jwt-nodejs-security
cd jwt-nodejs-security
npm init -y
接下来,添加项目依赖项并生成一个基本的 tsconfig
文件(在本教程中我们不会对其进行编辑),用于 TypeScript:
NPM install typescript ts-node-dev @types/bcrypt @types/express——save-dev
NPM安装bcrypt body-parser
NPX TSC——init
有了项目文件夹和依赖项,我们现在将定义API项目.
项目将在我们的代码中使用系统环境值. 让我们首先创建一个新的配置文件, src / config /索引.ts
,它从操作系统中检索环境变量,使它们对我们的代码可用:
从'dotenv'导入* as dotenv;
dotenv.config();
//创建一个配置对象来保存这些环境变量.
Const config = {
// JWT重要变量
jwt: {
//秘密用于签名和验证签名.
秘密:过程.env.JWT_SECRET,
//受众和发行者用于验证目的.
观众:过程.env.JWT_AUDIENCE,
发行人:过程.env.JWT_ISSUER
},
//基本API端口和前缀配置值如下:
端口:过程.env.端口|| 3000,
前缀:过程.env.API_PREFIX || 'api'
};
//使确认对象对其他代码可用.
导出默认配置;
The dotenv
类库允许在操作系统或程序中设置环境变量 .env
file. 我们将使用 .env
文件来定义以下值:
JWT_SECRET
JWT_AUDIENCE
JWT_ISSUER
PORT
API_PREFIX
Your .env
文件看起来应该类似于 库的例子. 完成了基本的API配置后,我们现在开始编写API的存储代码.
以避免完全成熟的数据库所带来的复杂性, 我们将把数据本地存储在服务器状态. 让我们创建一个TypeScript文件, src /州/用户.ts
,以容纳存储和 CRUD操作 浏览API使用者资料:
从'bcrypt'导入bcrypt;
导入{NotFoundError}../ /异常notFoundError ';
导入{ClientError}../ /异常clientError ';
//定义用户对象的代码接口.
导出接口IUser {
id: string;
用户名:字符串;
//密码被标记为可选,以允许我们返回这个结构
//没有密码值. 我们将在创建用户时验证它是否为空.
password?: string;
角色:角色;
}
//我们的API支持管理员和普通用户,由角色定义.
导出enum角色{
Admin = ' Admin ',
User = ' User '
}
//让我们用一些用户记录初始化我们的示例API.
//注意:我们使用Node生成密码.js命令行:
// "await require('bcrypt') ".散列(“PASSWORD_TO_HASH”,12)”
让用户:{[id: string]: IUser} = {
'0': {
id: '0',
用户名:“testuser1”,
//明文密码:testuser1_password
密码:2 b 12美元ov6s318JKzBIkMdSMvHKdeTMHSYMqYxCI86xSHL9Q1gyUpwd66Q2e美元,
role: Roles.USER
},
'1': {
id: '1',
用户名:“testuser2”,
//明文密码:testuser2_password
密码:“l0br1winifbfunhaoew 2 b 12美元63美元.55yh8.a3QcpCy7hYt9sfaIDg.rnTAPC',
role: Roles.USER
},
'2': {
id: '2',
用户名:“testuser3”,
//明文密码:testuser3_password
密码:“2 b 12美元美元工联会/ nKtkTsNO91tM7wd5yO6LyY1HpyMlmVUE9SM97IBg8eLMqw4mu评分”,
role: Roles.USER
},
'3': {
id: '3',
用户名:“testadmin1”,
//明文密码:testadmin1_password
密码:“2 b 12美元美元tuzkBzJWCEqN1DemuFjRuuEs4z3z2a3S5K0fRukob / E959dPYLE3i ',
role: Roles.ADMIN
},
'4': {
id: '4',
用户名:“testadmin2”,
//明文密码:testadmin2_password
密码:“2 b 12美元美元.dN3BgEeR0YdWMFv4z0pZOXOWfQUijnncXGz.3 yoychsaeczxqldq’,
role: Roles.ADMIN
}
};
让nextUserId = Object.keys(users).length;
在我们实现特定的API路由和处理函数之前, 让我们把重点放在项目的错误处理支持上,以便在整个项目代码中传播JWT最佳实践.
Express不支持proper 错误处理 with 异步处理程序,因为它不会从异步处理程序中捕获承诺拒绝. 为了捕获这样的拒绝,我们需要实现一个错误处理包装器函数.
让我们创建一个新文件, src /中间件/ asyncHandler.ts
:
import {NextFunction, Request, Response} from 'express';
/**
* Async处理器包装API路由,允许异步错误处理.
* @param fn函数调用的API端点
@用catch语句返回Promise
*/
导出const asyncHandler = (fn: (req:)请求, res:响应, next: NextFunction) => void) => (req: Request, res:响应, next: NextFunction) => {
回报承诺.解析(fn(req, res, next)).抓住(下);
};
The asyncHandler
函数包装API路由,并将承诺错误传播到错误处理程序中. 在编写错误处理程序之前,我们将定义一些自定义异常 src / / customError异常.ts
在我们的应用中使用:
//注意:我们的自定义error扩展自error,所以我们可以抛出这个错误作为一个异常.
导出类CustomError扩展错误{
message!: string;
status!: number;
additionalInfo!: any;
构造函数(message: string, status: number = 500, additionalInfo: any = undefined) {
超级(消息);
this.Message =消息;
this.Status =状态;
this.additionalInfo = additionalInfo;
}
};
导出接口IResponseError {
消息:字符串;
additionalInfo?: string;
}
现在我们在文件中创建错误处理程序 src /中间件/ errorHandler.ts
:
import {Request, Response, NextFunction} from 'express';
导入{CustomError, IResponseError}../ /异常customError ';
导出函数errorHandler(err: any, req: Request, res:响应, next: NextFunction) {
console.错误(错误);
if (!(err instanceof CustomError)) {
res.状态(500).send(
JSON.stringify({
消息:“服务器错误,请稍后再试”
})
);
} else {
const customError = err as customError;
让response = {
信息:customError.message
}作为IResponseError;
//检查是否有更多信息返回.
如果(customError.additionalInfo)响应.additionalInfo = customError.additionalInfo;
res.状态(customError.status).类型(json).send(JSON.stringify(响应));
}
}
我们已经为我们的API实现了一般的错误处理, 但我们还希望支持从API处理程序中抛出丰富的错误. 现在让我们定义这些丰富的错误实用函数,每个函数都在一个单独的文件中定义:
|
导入{CustomError}./ customError”;
导出类ClientError扩展CustomError {
构造函数(message: string) {
超级(消息,400);
}
}
|
导入{CustomError}./ customError”;
导出类UnauthorizedError扩展CustomError {
构造函数(message: string) {
超级(消息,401);
}
}
|
导入{CustomError}./ customError”;
导出类ForbiddenError扩展CustomError {
构造函数(message: string) {
超级(消息,403);
}
}
|
导入{CustomError}./ customError”;
导出类NotFoundError扩展CustomError {
构造函数(message: string) {
超级(消息,404);
}
}
实现了基本的项目和错误处理功能, 让我们定义API端点及其处理程序函数.
让我们创建一个新文件, src/index.ts
,来定义API的入口点:
从“express”中输入express;
从'body-parser'中导入{json};
导入{errorHandler}./中间件/ errorHandler”;
从'导入配置'./config';
//实例化一个Express对象.
Const app = express();
app.使用(json ());
//添加错误处理作为最后一个中间件,就在我们的应用程序之前.listen call.
//这确保所有的错误总是被处理.
app.使用(errorHandler);
//让我们的API监听配置的端口.
app.听(配置.port, () => {
console.日志('服务器正在监听端口${config.port}`);
});
我们需要更新npm生成的 package.json
文件添加默认应用程序入口点. 注意,我们想把这个端点文件引用放在main对象属性列表的顶部:
{
“主要”:“指数.js",
"脚本":{
"start": "ts-node-dev src/index.ts"
...
接下来,我们的API需要定义它的路由,并将这些路由重定向到它们的处理程序. 让我们创建一个文件, src /线路/索引.ts
,将用户操作路由链接到我们的应用程序中. 我们将很快定义路由细节和它们的处理程序定义.
从“express”中导入{Router};
从'导入用户'./user';
const routes = Router();
//所有用户操作都将在"users"路由前缀下可用.
routes.使用(' /用户、用户);
//允许我们的路由器在这个文件之外被使用.
导出默认路由;
我们现在将把这些路线包含在 src/index.ts
导入路由对象,然后要求应用程序使用导入的路由. 作为参考,您可以比较 完整文件版本 使用编辑过的文件.
从'导入路由'./线路/指数”;
//将我们的路由对象添加到Express对象中.
//这必须在应用程序之前.listen call.
app.使用('/' + config.前缀,路线);
// app.listen...
现在我们的API已经准备好让我们实现实际的用户路由及其处理程序定义. 类中定义用户路由 src /线路/用户.ts
文件并链接到即将定义的控制器, 用户控件
:
从“express”中导入{Router};
导入用户控件../控制器/用户控件”;
导入{asyncHandler}../中间件/ asyncHandler”;
const router = router ();
//注意:每个处理函数都被错误处理函数包装.
//获取所有用户.
router.get('/', [], asyncHandler(用户控件.listAll));
//获取一个用户.
router.get (' /: id ([0-9a-z]{24})”,[],asyncHandler(用户控件.getOneById));
//创建新用户.
router.post('/', [], asyncHandler(用户控件.newUser));
//编辑一个用户.
router.补丁(' /:id ([0-9a-z]{24})”,[],asyncHandler(用户控件.editUser));
//删除一个用户.
router.删除(' /:id ([0-9a-z]{24})”,[],asyncHandler(用户控件.deleteUser));
我们的路由将调用的处理程序方法依赖于辅助函数来操作我们的用户信息. 让我们将这些辅助函数添加到 src /州/用户.ts
文件,然后再定义 用户控件
:
//将这些函数放在文件的末尾.
//注意:验证错误直接在这些函数中处理.
//生成一个没有密码的用户副本.
const generateSafeCopy = (user : IUser) : IUser => {
让_user = { ...user };
删除_user.password;
返回_user;
};
//恢复存在的用户.
export const getUser = (id: string): IUser => {
if (!(用户id))抛出新的NotFoundError('用户id ${id}未找到');
(id)返回generateSafeCopy(用户);
};
//如果用户名存在,使用用户名作为查询,基于用户名恢复用户.
export const getUserByUsername = (username: string): IUser | undefined => {
const possibleUsers =对象.值(用户).filter((user) => user.Username === Username);
//如果该用户名不存在,则未定义.
如果(possibleUsers.长度== 0)返回未定义;
返回generateSafeCopy (possibleUsers [0]);
};
export const getAllUsers = (): IUser[] => {
返回对象.值(用户).map((elem) => generateSafeCopy(elem));
};
export const createUser = async (username: string), 密码:字符串, role: Roles): Promise => {
用户名=用户名.trim();
Password = Password.trim();
// Reader:根据您的自定义用例添加检查.
如果用户名.length === 0)抛出new ClientError('无效用户名');
否则如果密码).length === 0)抛出新的ClientError('无效密码');
//检查是否有重复.
如果(getUserByUsername(用户名) != undefined)抛出新的ClientError('Username被占用');
//生成一个用户id.
const id: string = nextUserId.toString();
nextUserId + +;
//创建用户.
Users [id] = {
username,
密码:await bcrypt.散列(密码,12),
role,
id
};
(id)返回generateSafeCopy(用户);
};
export const updateUser = (id: string, username: string, role: Roles): IUser => {
//检查用户是否存在.
if (!(用户id))抛出新的NotFoundError('用户id ${id}未找到');
// Reader:根据您的自定义用例添加检查.
如果用户名.trim().length === 0)抛出new ClientError('无效用户名');
用户名=用户名.trim();
const userIdWithUsername = getUserByUsername(username)?.id;
如果(userIdWithUsername != =未定义 && userIdWithUsername !== id)抛出新的ClientError('Username被占用');
//应用更改.
users[id].Username = Username;
users[id].Role = Role;
(id)返回generateSafeCopy(用户);
};
export const deleteUser = (id: string) => {
if (!(用户id))抛出新的NotFoundError('用户id ${id}未找到');
删除用户(id);
};
export const isPasswordCorrect = async (id: string, 密码:字符串): Promise => {
if (!(用户id))抛出新的NotFoundError('用户id ${id}未找到');
返回等待比特币.比较(密码,用户(id).password!);
};
export const changePassword = async (id: string, 密码:字符串) => {
if (!(用户id))抛出新的NotFoundError('用户id ${id}未找到');
Password = Password.trim();
// Reader:根据您的自定义用例添加检查.
如果密码.length === 0)抛出新的ClientError('无效密码');
//存储加密后的密码
users[id].密码=等待密码.散列(密码,12);
};
最后,可以创建 src /控制器/用户控件.ts
file:
import {NextFunction, Request, Response} from 'express';
导入{getAllUsers, Roles, getUser, createUser, updateUser, deleteUser}../ /用户的状态;
类用户控件 {
static listAll = async (req: Request, res:响应, next: NextFunction) => {
//检索所有用户.
const users = getAllUsers();
//返回用户信息.
res.status(200).类型(json).发送(用户);
};
static getOneById = async (req: Request, res:响应, next: NextFunction) => {
//从URL获取ID.
Const id: string = req.params.id;
//获取请求ID的用户.
const user = getUser(id);
//注意:我们只会到达这里,如果我们找到一个用户与请求的ID.
res.status(200).类型(json).send(user);
};
static newUser = async (req: Request, res:响应, next: NextFunction) => {
//获取用户名和密码.
让{username, password} = req.body;
//我们只能通过这个函数创建普通用户.
const user = await createUser(用户名,密码,角色).USER);
//注意:只有当所有的新用户信息
//是有效的,并且用户已经创建.
//发送一个HTTP "Created"响应.
res.status(201).类型(json).send(user);
};
static editUser = async (req: Request, res:响应, next: NextFunction) => {
//获取用户ID.
Const id = req.params.id;
//从body中获取值.
Const {username, role} = req.body;
if (!Object.值(角色).包括(角色))
抛出新的ClientError('Invalid role');
//检索和更新用户记录.
const user = getUser(id);
const updatedUser = updateUser(id, username ||用户).用户名、角色||用户.role);
//注意:只有当所有的新用户信息
//是有效的,并且用户已经更新.
//发送一个HTTP "No Content"响应.
res.status(204).类型(json).发送(updatedUser);
};
static deleteUser = async (req: Request, res:响应, next: NextFunction) => {
//从URL获取ID.
Const id = req.params.id;
deleteUser (id);
//注意:我们只会到达这里,如果我们找到一个用户的请求ID和
//删除.
//发送一个HTTP "No Content"响应.
res.status(204).类型(json).send();
};
}
导出默认用户控件;
该配置暴露了以下端点:
/ API_PREFIX /用户
获取所有用户./ API_PREFIX /用户
:创建新用户./ API_PREFIX /用户/ {ID}删除
:删除指定用户./ API_PREFIX /用户/ {ID}补丁
:更新指定用户./ API_PREFIX /用户/ {ID}
:获取特定用户.至此,我们的API路由和它们的处理程序就实现了.
现在我们有了基本的 API的实现,但我们仍然需要实现身份验证和授权以保证其安全. 我们将使用jwt来实现这两种目的. 当用户进行身份验证并验证每个后续调用是否使用该身份验证令牌获得授权时,API将发出JWT.
对于每个客户端调用,授权头包含一个 不记名的令牌 将生成的JWT传递给API: Authorization: Bearer
.
为了支持JWT,让我们在项目中安装一些依赖项:
NPM install @types/jsonwebtoken——save-dev
安装jsonwebtoken
在JWT中签名和验证有效负载的一种方法是通过共享秘密算法. 对于我们的设置,我们选择 HS256 这个算法, 因为它是JWT规范中最简单的对称(共享秘密)算法之一. 我们将使用Node CLI,以及 crypto
包生成一个唯一的秘密:
要求(加密).randomBytes (128).toString(十六进制);
我们可以随时更改秘密. 但是,每次更改都会使所有用户的身份验证令牌无效,并强制他们注销.
用于用户登录和更新其密码, 我们的API的身份验证和授权功能需要端点支持这些操作. 为了实现这一目标,我们将创造 src /控制器/ AuthController.ts
,我们的JWT认证控制器:
import {NextFunction, Request, Response} from 'express';
从“jsonwebtoken”中导入{sign};
导入{CustomRequest}../中间件/ checkJwt”;
从'导入配置'../config';
导入{ClientError}../ /异常clientError ';
导入{UnauthorizedError}../ /异常unauthorizedError ';
导入{getUserByUsername, isPasswordCorrect, changePassword}../ /用户的状态;
类AuthController {
static login = async (req: Request, res:响应, next: NextFunction) => {
//确保提供了用户名和密码.
//如果这些值不存在,则向客户端抛出异常.
让{username, password} = req.body;
if (!(username && throw new ClientError('需要用户名和密码');
const user = getUserByUsername(用户名);
//检查提供的密码是否与我们的加密密码匹配.
if (!user || !(等待isPasswordCorrect(用户.id, password))))抛出新的UnauthorizedError("用户名和密码不匹配");
//生成并签名一个JWT,有效期为一小时.
const token = sign({userId:用户名.Id,用户名:user.用户名、角色:user.Role}, config.jwt.secret!, {
expiresIn:‘1 h ',
notBefore: '0', //现在不能使用,可以配置为延迟.
算法:“HS256”,
观众:配置.jwt.audience,
发行人:配置.jwt.issuer
});
//在响应中返回JWT.
res.类型(json).Send ({token: token});
};
static changePassword = async (req: Request, res:响应, next: NextFunction) => {
//从传入的JWT中检索用户ID.
const id = (req = CustomRequest).token.payload.userId;
//从请求体中获取提供的参数.
const {oldPassword, newPassword} = req.body;
if (!(oldPassword && newPassword))抛出新的ClientError("密码不匹配");
//检查旧密码是否与当前存储的密码匹配,然后继续.
//如果旧密码不匹配,则向客户端抛出错误.
if (!(等待isPasswordCorrect (id, 抛出新的UnauthorizedError("旧密码不匹配");
//更新用户密码.
//注意:如果旧密码比较失败,我们将不会击中此代码.
等待changePassword(id, newPassword);
res.status(204).send();
};
}
导出默认AuthController;
我们的身份验证控制器现在完成了, 使用用于登录验证和用户密码更改的单独处理程序.
确保我们的每个API端点都是安全的, 我们需要创建一个公共的JWT验证和角色身份验证挂钩,我们可以将其添加到每个处理程序中. 我们将在中间件中实现这些钩子, 的传入JWT令牌 src /中间件/ checkJwt.ts
file:
import {Request, Response, NextFunction} from 'express';
从“jsonwebtoken”中导入{verify, JwtPayload};
从'导入配置'../config';
// CustomRequest接口使我们能够向控制器提供jwt.
导出接口CustomRequest扩展请求
令牌:JwtPayload;
}
export const checkJwt = (req: Request, res:响应, next: NextFunction) => {
//从请求头中获取JWT.
const token = req.标题(“授权”);
让jwtPayload;
//验证令牌并检索其数据.
try {
//验证有效负载字段.
jwtPayload = verify(token?.分割(' ')[1],配置.jwt.secret!, {
完成:没错,
观众:配置.jwt.audience,
发行人:配置.jwt.issuer,
算法:[' HS256 '),
clockTolerance: 0,
ignoreExpiration:假的,
ignoreNotBefore:假
});
//添加负载到请求中,以便控制器可以访问它.
(请求为CustomRequest).token = jwtPayload;
} catch (error) {
res.status(401)
.类型(json)
.send(JSON.stringify({message: '缺少或无效的令牌'}));
return;
}
//将可编程流传递给下一个中间件/控制器.
next();
};
我们的代码将令牌信息添加到请求中,然后将其转发. 请注意,错误处理程序此时在我们的代码上下文中不可用,因为错误处理程序尚未包含在我们的Express管道中.
接下来我们创建一个JWT授权文件, src /中间件/ checkRole.ts
,以验证用户角色:
import {Request, Response, NextFunction} from 'express';
导入{CustomRequest}./checkJwt';
导入{getUser, Roles}../ /用户的状态;
export const checkRole = (roles: Array) => {
return async (req: Request, res:响应, next: NextFunction) => {
//查找请求ID的用户.
const user = getUser((请求为CustomRequest)).token.payload.userId);
//确保找到一个用户.
if (!user) {
res.status(404)
.类型(json)
.send(JSON.stringify({message: 'User not found'}));
return;
}
//确保用户的角色包含在授权的角色中.
if (roles.indexOf(用户.role) > -1) next();
else {
res.status(403)
.类型(json)
.send(JSON.stringify({message: 'Not enough permissions'}));
return;
}
};
};
注意,我们检索存储在服务器上的用户角色, 而不是JWT中包含的角色. 这允许先前经过身份验证的用户在其身份验证会话中更改其权限. 路由授权将是正确的, 不管存储在JWT中的授权信息是什么.
现在我们更新我们的路由文件. 让我们创建 src /线路/身份验证.ts
授权中间件的文件:
从“express”中导入{Router};
导入AuthController../控制器/ AuthController”;
导入{checkJwt}../中间件/ checkJwt”;
导入{asyncHandler}../中间件/ asyncHandler”;
const router = router ();
//添加认证路由.
router.邮报》(“/登录”,asyncHandler (AuthController.login));
//附加我们的change password路由. 注意,checkJwt强制端点授权.
router.post('/change-password', [checkJwt], asyncHandler(AuthController.changePassword));
导出默认路由器;
为每个端点添加授权和所需的角色, 让我们来更新用户路由文件的内容, src /线路/用户.ts
:
从“express”中导入{Router};
导入用户控件../控制器/用户控件”;
导入{角色}../ /用户的状态;
导入{asyncHandler}../中间件/ asyncHandler”;
导入{checkJwt}../中间件/ checkJwt”;
导入{checkRole}../中间件/ checkRole”;
const router = router ();
//定义我们的路由和它们所需的授权角色.
//获取所有用户.
router.get('/', [checkJwt, checkRole].管理])],asyncHandler(用户控件.listAll));
//获取一个用户.
router.get (' /: id([0 - 9]{1, 24})”,[checkJwt checkRole([角色.用户、角色.管理])],asyncHandler(用户控件.getOneById));
//创建新用户.
router.邮报》(' / ',asyncHandler(用户控件.newUser));
//编辑一个用户.
router.补丁(' /:id([0 - 9]{1, 24})”,[checkJwt checkRole([角色.用户、角色.管理])],asyncHandler(用户控件.editUser));
//删除一个用户.
router.删除(' /:id([0 - 9]{1, 24})”,[checkJwt checkRole([角色.管理])],asyncHandler(用户控件.deleteUser));
导出默认路由器;
方法验证传入的JWT checkJwt
函数,然后使用 checkRole
middleware.
完成认证路由的集成, 我们需要将认证和用户路由附加到API的路由列表中 src /线路/索引.ts
文件,替换其内容:
从“express”中导入{Router};
从'导入用户'./user';
const routes = Router();
//所有auth操作都将在“auth”路由前缀下可用.
routes.用(/认证,认证);
//所有用户操作都将在"users"路由前缀下可用.
routes.使用(' /用户、用户);
//允许我们的路由器在这个文件之外被使用.
导出默认路由;
这个配置现在暴露了额外的API端点:
/ API_PREFIX /身份验证/登录
:登录用户./ API_PREFIX /认证/更改密码
:修改用户密码.使用我们的身份验证和授权中间件, 以及每个请求中可用的JWT有效负载, 下一步是使端点处理程序更加健壮. 我们将添加代码以确保用户只能访问所需的功能.
在端点实现中添加额外的验证,以便定义每个用户可以访问和/或修改的数据, 我们会更新 src /控制器/用户控件.ts
file:
import {NextFunction, Request, Response} from 'express';
导入{getAllUsers, Roles, getUser, createUser, updateUser, deleteUser}../ /用户的状态;
import {ForbiddenError} from../ /异常forbiddenError ';
导入{ClientError}../ /异常clientError ';
导入{CustomRequest}../中间件/ checkJwt”;
类用户控件 {
static listAll = async (req: Request, res:响应, next: NextFunction) => {
//检索所有用户.
const users = getAllUsers();
//返回用户信息.
res.status(200).类型(json).发送(用户);
};
static getOneById = async (req: Request, res:响应, next: NextFunction) => {
//从URL获取ID.
Const id: string = req.params.id;
//新代码:限制USER请求者检索他们自己的记录.
//允许ADMIN请求者检索任何记录.
if ((请求为CustomRequest).token.payload.role === Roles.USER && req.params.id !== (req作为CustomRequest).token.payload.userId) {
抛出新的ForbiddenError('Not enough permissions');
}
//获取请求ID的用户.
const user = getUser(id);
//注意:我们只会到达这里,如果我们找到一个用户与请求的ID.
res.status(200).类型(json).send(user);
};
static newUser = async (req: Request, res:响应, next: NextFunction) => {
//注意:这个函数没有变化.
//获取用户名和密码.
让{username, password} = req.body;
//我们只能通过这个函数创建普通用户.
const user = await createUser(用户名,密码,角色).USER);
//注意:只有当所有的新用户信息
//是有效的,并且用户已经创建.
//发送一个HTTP "Created"响应.
res.status(201).类型(json).send(user);
};
static editUser = async (req: Request, res:响应, next: NextFunction) => {
//获取用户ID.
Const id = req.params.id;
//新代码:限制USER请求者编辑自己的记录.
//允许ADMIN请求者编辑任何记录.
if ((请求为CustomRequest).token.payload.role === Roles.USER && req.params.id !== (req作为CustomRequest).token.payload.userId) {
抛出新的ForbiddenError('Not enough permissions');
}
//从body中获取值.
Const {username, role} = req.body;
//新代码:不允许用户将自己更改为ADMIN.
//如果你是一个用户,验证你不能将自己设置为ADMIN.
if ((请求为CustomRequest).token.payload.role === Roles.USER && role === Roles.ADMIN) {
抛出新的ForbiddenError('Not enough permissions');
}
//验证角色是否正确.
else if (!Object.值(角色).包括(角色))
抛出新的ClientError('Invalid role');
//检索和更新用户记录.
const user = getUser(id);
const updatedUser = updateUser(id, username ||用户).用户名、角色||用户.role);
//注意:只有当所有的新用户信息
//是有效的,并且用户已经更新.
//发送一个HTTP "No Content"响应.
res.status(204).类型(json).发送(updatedUser);
};
static deleteUser = async (req: Request, res:响应, next: NextFunction) => {
//注意:这个函数没有变化.
//从URL获取ID.
Const id = req.params.id;
deleteUser (id);
//注意:我们只会到达这里,如果我们找到一个用户的请求ID和
//删除.
//发送一个HTTP "No Content"响应.
res.status(204).类型(json).send();
};
}
导出默认用户控件;
有了一个完整且安全的API,我们就可以开始了 测试我们的代码.
为了测试我们的API,我们必须首先启动我们的项目:
NPM运行启动
Next, we’ll 安装邮差,然后创建一个请求来验证测试用户:
{
"username": “testadmin1”,
"password": “testadmin1_password”
}
现在我们有了测试用户的JWT, 我们将创建另一个请求来测试其中一个端点并获得可用的请求 USER
records:
GET
请求用户身份验证.localhost: 3000 / api /用户
.不记名的令牌
.这些例子只是许多可能的测试中的一小部分. 以全面探索API调用并测试我们的授权逻辑, 按照演示的模式创建其他测试.
当我们将JWT组合成一个Node时.js API, 我们利用行业标准的库和实现来最大化我们的结果并最小化开发人员的工作. JWT功能丰富且对开发人员友好, 对于开发者来说,它很容易在我们的应用中实现,而且学习曲线也很短.
不过, 开发人员在向他们的项目中添加JWT安全性时仍然必须谨慎,以避免常见的陷阱. 遵循我们的指导, 开发人员应该感到有能力在Node中更好地应用JWT实现.js. JWT的可信安全性与Node的多功能性相结合.Js为开发人员提供了创建解决方案的极大灵活性.
Toptal工程博客的编辑团队向 Abhijeet Ahuja and 默罕默德·哈立德 查看本文中提供的代码示例和其他技术内容.
JSON Web令牌 (JWT)是一个开放的标准,用于在空间受限的环境中安全地交换信息. JWT包含消息验证所需的所有信息. 所提供的签名用于验证消息内容是否真实且未经掺假.
JWT使用base64编码它的三个主要组件:报头、有效负载和签名. JWT提供了一个简洁且url安全的令牌.
JWT定义了一个与行业无关的开放标准,用于紧凑地传输数据.
而JWT可以在结构上有所不同, 它的主要组成部分是:header, containing information used for validation; payload, holding custom data from the issuing server; and signature, 确认令牌的内容没有被篡改.
JWT安全性是遵循JWT标准的令牌的实现. 它定义了算法、加密和字段,以允许安全令牌身份验证和授权.
JWT提供了一个安全层,可以与新的或现有的Node无缝集成.js api来验证和授权用户请求. JWT也可以作为无状态会话的基础.
Node.js是一个JavaScript运行时环境,旨在构建可扩展的网络应用程序. 它通常用于创建服务器和api.
鉴于Node的可伸缩性.Js,它可以用于业余爱好和企业解决方案. Node的多功能性.Js允许它在任何行业中使用.
世界级的文章,每周发一次.
世界级的文章,每周发一次.