欢迎各位兄弟 发布技术文章

这里的技术是共享的

You are here

宁皓网 Node.js:基于 Token 的身份验证 有大用

介绍了基于 JWT:JSON Web Token 的身份验证方法。
来自 
https://ninghao.net/course/5037#info

介绍与准备
1)身份验证

想要验证用户的身份,我们得在一个地方存储用户的相关信息,比如他的用户名,还有对应的密码。在验证身份的时候,要求用户提供这些信息,再决定是不是可以通过身份的验证。

根据用户在表单或者客户端那里发过来的用户名,查询一下数据库,看看这个用户是不是存在,如果存在,再对比一下用户提供的密码,跟我们的数据库里的这个用户对应的密码是不是匹配 .. 一切都通过以后,就算身份验证成功了。

通过以后我们可以给用户一个 session ,或者发给他一个 token 。下次他们再来访问我们的网站或者应用的时候,可以检查他们的 session ,或者 token 。

下面我们介绍一种基于 token 的身份验证的方法 ... token 的格式可以使用 JSON Web Token ... 我们会用到 Node.js ,MongoDB 还有 jwt 相关的知识,你可以先在宁皓网上找一下相关的课程学习一下 ...

来自 https://ninghao.net/video/5039#info

2)准备

先去准备个项目 .. 在我的桌面上有个 ninghao-node .. 介绍 node.js 的课程,我都是用的这个项目 ..

查看一下分支 ... 这里我们可以基于 RESTful-api 这个分支去创建一个新的分支 .. RESTful api 这个分支是我介绍 Node.js REST 接口的课程里面创建的 ..

新的分支名字可以是 auth ..

再用编辑器打开这个项目 ..

可以先查看一下项目里的主要文件 ... 先打开 package.json .. 在项目的依赖里面,我安装了 body-parser ... 它可以让我们在应用里得到用户发送的请求里面包含的数据 ..

下面是 express ... 应用的服务器,还有接口都是用 express 创建的 ..

mongoose ... 这是我们用的连接 node.js 还有 mongodb 用的方法 ..

在开发依赖里还有个 nodemon .. 它可以监视服务器的变化,有变化会自动给我们重新启动服务器 ..

然后打开 server.js .. 我们在这个文件里创建了一个服务器 ..

routes 目录的下面可以存储应用需要的路由 ...

models 里面是定义的数据模型 ...

controllers 里是一些控制器,我们可以把处理请求的代码放在控制器 ..

config 下面有个 database.js ... 这里就是使用 mongoose 提供的方法,创建了一个对 mongodb 数据库的连接 ...

来自 https://ninghao.net/video/5040#info

用户模型
3)用户模型

我们先得在数据库里存储用户相关的信息 .. 在介绍 Node.js 与 MongoDB 的时候,我们已经学习了怎么样使用 Node.js 处理 MongoDB 数据库 ... 然后在 Node.js RESTful 接口这个课程里,介绍了通过我们创建的接口来处理应用里的数据 ..

现在我们基于这个课程,继续再添加一个用户模型,里面可以存储用户相关的信息 ..

新建一个 js 文件,放在 models 目录的下面,名字是 user.js .. 表示用户模型 .. 在这个文件里,先导入数据库的配置 .. 名字是 db ,位置是 config 下面的 database ..

可以先看一下这个配置 .. 这里我们用了 mongoose 作为连接 node.js 与 mongodb 的方法 .. 这里就是创建了一个数据库的连接 ..

再回到用户模型 .. 再定义一个 schema ... new db.Schema .. 给它一个对象 .. 里面可以描述一下用户模型里的字段 ..

const schema = new db.Schema({
})

先添加一个 userName 表示用户名 .. 给它一个对象,里面再说明一些限制 .. type ,类型是 String .. required 设置成 true,表示这是一个必填字段 ..

再添加一个 password 字段,它可以存储用户对应的登录用的密码 .. type 是 String,也是一个字段串类型的字段 .. required 是 true,密码也是一个必填字段 ..

然后基于这个 schema 去创建一个用户模型 ... 名字是 User .. 用一下 db 的 model 方法,这个 model 方法是 mongoose 提供的 .. 模型的名字是 User .. 再指定一下模型的 schema ..

最后我们可以再导出这里定义好的 User 模型 .. module.exports = User

来自 https://ninghao.net/video/5042#info

4)用户路由与控制器

在 routes 目录的下面,新建一个用户相关的路由 .. 名字是 userRouter.js .. 文件的一开始导入 express .. 再创建一个 router .. 它的值是执行 express 的 Router 返回的东西 ..

然后用这个 router 去创建一个路由 .. 用一下它的 route 方法 ... 路由的地址是 /users .. 下面接着再用一个 post 方法 ..

我们可以给这个方法一个函数参数,在里面去写处理请求的代码 ... 或者也可以把它放在一个控制器的方法里 .. 可以用一下 UserController 控制器里的 store 这个方法 ..

这样如果用户用 HTTP 的 POST 方法请求这个路由地址的时候,就会使用 UserController 里的 store 方法来处理 ..

在上面可以先导入这个控制器 ... 名字是 UserController ,位置可以是上一级目录的 controllers 下面的 UserController .. 一会再去创建这个控制器 ..

最后再导出这个 router ..

然后创建一个控制器,放在 controllers 的下面,名字是 UserController.js .. 里面添加一个 store 方法 .. 两个参数,request 表示请求,response 表示响应 .. 里面可以先响应一个字符 .. 注册用户 ..

最后再导出这个方法 module.exports .. 一个对象 .. 导出 store 方法 ..

打开 server.js .. 先在文件里导入 userRouter ... 位置是当前目录下的 routes 下面的 userRouter ..

改进一下这个 app.use .. 这里加上一个 userRouter ..

app.use('/api', [eventRouter, userRouter])

打开命令行 ... 先启动一下 mongoDB 数据库.. 执行一下 mongod

然后再进入到项目所在的目录 ... 我这里就是桌面上的 ninghao-node 这个目录 ... 执行一下 ./node_modules/.bin/nodemon server.js

启动了服务器以后 ... 再找一个 rest 客户端 .. 可以用 postman .. 或者 insomnia ..

配置一下请求 ... 请求用的方法是 POST ... 请求的地址是 localhost:3000/api/users

这个路由是我们在 userRouter 里定义的 ..

发送一下这个请求 ... 服务器会响应一个 注册用户 .. 这个行为是在 UserController 这个控制器里的 store 方法里定义的 ..

下面我们再改进一下这个方法,让它可以把用户的注册请求发过来的具体的数据存储在数据库里 ..

来自 https://ninghao.net/video/5043#info

注册
5)注册用户

用户通过我们的应用的接口,可以发送注册用户的请求 .. 这个请求是用 UserController 里的 store 方法处理的 .. 我们的服务器里已经用了 body-parser 提供的功能 .. 它可以让我们在应用里得到用户发送过来的数据 ..

回到 UserController .. 先在文件顶部导入 user 这个模型 .. 名字是 User .. 位置是上一级目录下面的 models 下面的 user ..

然后回到 store 这个方法 .. 可以先得到用户送的注册请求里的 userNmae 还有 password ... 添加一个 userName ,它的值就是 request 的 body 里的 userName ..

再添加一个 password .. 它的值是 request.body.password ... 用户的密码我们在后面会特别处理一下 .. 直接把原始密码存储在数据库里会非常的不安全 ..

这里先这样 ... 后面我们会用一下方法改进一下 ..

创建一个新的用户模型 .. 名字是 user .. new User .. 一个对象 .. 把 userName 还有 password 添加进来 ..

然后可以使用模型上的 save 方法,保存一下 ... 接着用一个 then .. 响应一个注册成功的提示 .. 再用一个 catch ,处理下出现的错误 .. 错误会在 error 里面,可以把它响应给用户 ..

打开 rest 客户端,再配置一个请求 ... 方法是 post ,地址是 api/users .. 给它添加一个请体 .. 格式是 json .. 里面添加一个 userName .. 用户名是 wanghao .. 再添加一个 password ... 对应的值就是用户的密码 ..

然后发送一下这个请求 ..

会响应回一个 注册成功 的提示 ... 说明已经成功的把用户信息存储在数据库里了 ..

可以在 mongodb 的图形界面软件下面去查看一下应用的数据库 .. 也可以在 mongodb 客户端执行一些命令,查询一下数据库 ..

新建一个标签 .. 输入 mongo .. use ravent , ravent 是数据库的名字 ..

db.getCollection('users').find()

会给我们返回 ravent 数据库里的 users 集合里所有的文档 .. 在这里你可以找到刚才我们在 rest 客户端创建的用户文档 ...

来自 https://ninghao.net/video/5045#info

6)hash 用户密码

我们不能把用户注册的时候提供的密码原封不动地放到我们的数据库里 .. 这个密码得处理一下 .. 用的方法就是 hash .. 就是我们要存储 hash 之后的密码 .. hash 的时候还得再加点盐 .. 就是 salt .. 这样才会更安全一些 ..

用户在提交验证身份请求的时候,我们可以把他们提供的密码先 hash 一下,然后再跟存储在我们数据库里的 hash 之后的密码进行比较 ..

先了解一下 hash,找个地方去试一下 .. 打开 index.js .. 先用一下 node.js 核心的 crypto ..

再创建一种 hash 方法 .. 用一下 crypto 的 createHash .. hash 的方法可以使用 sha256 ..

然后用一下 hash 的 update 方法,处理一个字符串 .. 比如 password ..

hash.update('password')

在控制台上输出 hash 的结果 ... 用一下 hash 的 digest ,输出的格式是 base64 .. 回到命令行 .. 执行一下 node index.js

这里输出的就是用 sha256 这种 hash 方法 hash 的 password 这个字符串的结果 ... 你在 hash 的时候给他提供同样的东西,他每次都会输出同样的结果 ..

一般没人会告诉这串字符就是 password 这个字符串用 sha256 hash 之后的结果 .. 除非他有个字典可以比较 ...

所以我们需要一种更安全的方法,就是在在 hash 的时候,往里面加点盐。

来自 https://ninghao.net/video/5046#info

7)bcrypt:加 salt 的 hash

给项目安装个包 ... 名字是 bcrypt .. 可以使用 npm 或者 yarn 去安装 ..

yarn add bcrypt

然后再回到项目 .. 打开 index.js ... 先导入刚才安装的 bcrypt 这个包 ..

添加一个 password ... 它表示要 hash 的用户的密码 ..

再用一下 bcrypt 的 genSalt .. 可以先在 hash 之前生成点要加的盐 .. 其实就是一串随机的字符 ..

它的第一个参数是 saltRounds ... 这个值越大就越安全一些 ... 默认它的值是 10 .. 然后是一个回调 .. 第一个参数是出现的错误,可以用 error 表示 .. 第二个参数是 genSalt 生成的 salt ..

可以在控制台上输出生成的 salt ..

在这个回调里,我们可以再用一下 bcrypt 的 hash 方法,去 hash 指定的字符 .. 它的第一个参数就是要 hash 的字符 ... 比如可以是用户提交过来的密码 .. 第二个参数是 salt .. 这个 salt 是用 genSalt 生成的 ... 可以使用 salt 来表示 ..

第三个参数是个回调 .. error 表示错误 .. hash 表示 hash 之后的结果 .. 我们可以把它也输出到控制台上看一下 ..

保存 ... 回到命令行... 执行一下 node index.js

上面输出的是生成的 salt,还有 hash 了指定字符外加 salt 之后的这个 hash ... 再执行几次 ... 你会发现,每次执行输出的结果都不一样 .. 因为 hash 的时候用的 salt 每次都会不一样 ..

这个 hash 的结果里面,有一部分表示的是在 hash 的时候使用的 salt ...

compare

假设我们存储在数据库里的用户的密码是这个 hash ... 现在我们可以比较一下,用户登录的时候输入的密码,跟我们在数据库里存储的 hash 之后的密码是否匹配 ..

复制一下这个 hash ... 回到 index.js ... 添加一个 hashPassword ... 它的值假设就是我们存储在数据库里的 hash 之后的用户密码 ..

再添加一个 userInputPassword ... 它的值表示的是用户请求验证身份的时候提供给我们的密码 ...

下面我们可以使用 bcrypt 的 compare 方法去比较一下 ... 先是用户输入的密码 .. userInputPassword .. 然后是一个 hash,这个 hash 应该是我们存储在数据库里的 hash 之后的用户密码 ..

它会返回 Promise ... 可以用一个 then ... result 表示比较的结果 ... 再把这个结果输出到控制台上 ..

回到命令行 .. 执行一下 node index.js ..

比较的结果是 true ... 回来再修改一下 .. 比如用户输入的是 password1 .. 再回来执行一下 ... 这次会显示 false ... 表示用户输入的密码跟我们存储的 hash 之后的密码不匹配 ...

来自 https://ninghao.net/video/5047#info

8)存储 hash 之后的密码

我们已经学会用 hash 了,不能再存储用户的纯密码了 .. 现在我们去改进一下存储用户注册信息的这个方法 ... 用的是 UserController 里的 store ..

先把 bcrypt 导入进来 ..

在 store 里,用一下 bcrypt 的 hash .. 把要 hash 的密码交给它 .. 这里就是请求的主体里的 password .. 10 表示 saltRounds ,这种用法跟我们之前先用 genSalt 生成 salt ,然后再 hash 是一样的 ..

方法返回 Promise .. 用一个 then ... 处理好的结果可以是 password .. 然后把下面这些代码放在这个方法里面 ..

去掉上面定义的这个 password ... 这样存储的就是 hash 之后的密码了 ..

先把数据库里的 users 集合里的文档删除掉 ... 在 mongodb 的控制台,执行一下 ..

db.getCollection('users').remove({})

然后再打开 rest 客户端 ... 配置一个请求 ... 方法是 post ,地址是 api/users ... 请求的主体是一个 json 格式的数据,里面有用户名,还有密码 ..

发送一下这个请求 .. 返回注册成功 ..

再去查询一下数据库 ..

db.getCollection('users').find()

你会发现,返回的这个用户文档里的 password 字段的值,已经不是原始值了 ... 现在数据库里存储的用户的密码会是 hash 之后的结果 ..

来自 https://ninghao.net/video/5048#info

身份验证
9)身份验证与签发 Token

用户提供它的用户名还有密码,请求验证身份 ... 我们收到请求以后验证用户身份,如果验证通过,就给用户签发一个 Token,这个 Token 用的是 jwt .. 在宁皓网有个专门的课程介绍了 jwt,也就是 JSON Web Token ..

先安装一个签发还有验证 jwt 的包 ... 用 npm 或者 yarn 安装 ... 名字是 jsonwebtoken ..

然后打开 userRouter ,用户路由 .. 添加一个新的路由 .. 用一下 router 的 post 方法 .. 路由地址是 /auth ..

请求用 UserController 的 auth 方法来处理 .. 打开 UserController ,添加一个 auth 方法 .. 两个参数 .. request .. 还有 response ..

下面别忘了把这个方法导出来 .. 添加一个 auth ..

再回到 auth 这个方法 ... 在它里面,可以用一下 User 模型的 findOne 方法,先在我们自己的数据库里查询一下有没有请求的用户 .. 给它设置一个条件 .. 就是 userName .. 对应的值是请求主体里的 userName ..

接着用一个 then .. 如果找到就会返回用户文档,可以使用 user 表示 ..

在里面先判断一下 ... 如果没找到用户 ... 我们就 return 一个 Promise.reject() .. 这样在下面的 catch 方法里可以处理错误 ... reject 的时候带一条信息 ... 没找到用户 ..

下面再用一个 catch .. 错误在 error 里面, 响应一个状态码,400 ,表示 Bad Request .. send 一个 error ...

回到上面的 then .. 如果成功的在数据库里找到了用户 .. 这里我们用一下 bcrypt 的 compare 方法,比较一下用户发送过来的密码,还有存储在数据库里的密码 .. 发送过来的密码是 request.body.password .. 在数据库里查询出来的密码是 user.password ..

这个 compare 也会返回 Promise ... 接着用一个 then .. 结果在 result 里面 .. 然后判断一下 ... 如果结果是 true .. 说明验证通过 .. 也就是用户提供的密码跟存储在我们数据库里的密码是匹配的 ..

这样我们就给用户签发一个 jwt .. 签发可以使用 jsonwebtoken .. 在文件的上面,先导入 jsonwebtoken ..

先添加一个 payload,它是 token 里的具体的数据 .. 里面添加一个 userName ... 对应的值就是用户名 ..

签发 token 还需要用到一个密钥 .. 添加一个 secret ...

然后定义一个 token .. 用一下 jwt 的 sign 方法,token 里的具体的数据就是上面定义的 payload ,再把密钥告诉这个方法 ..

然后再用一下 response 的 send .. 响应一下 .. 一个对象 ... 把 token 放进去 ..

如果验证没通过 ... 一个 else .. response 一个状态码 ... 401 ,表示 Unauthorized , send .. 一条信息 .. 未通过身份验证 ..

演示

下面我们再去测试一下 ... 打开 REST 客户端 .. 配置一个请求 .. 方法是 POST ... 请求的地址是 api/auth ..

请求的主体用 JSON 格式,添加一个 JSON 数据 .. 里面提供一下 userName ... 还有 password ..

发送一下这个请求 .. 提示没找到用户 ...

不过我的数据库里应该有 wanghao 这个用户 ... 回到 UserController 的 auth 方法 .. 这个 findOne 方法的查询条件我写错了 .. 应该是 userName .. Name 的首字母应该是大写的 ..

回到 REST 客户端 .. 再试一下 ..

身份验证通过,就会给我们响应回来一个 token ..

再试一下 ... 改一下这个用户名 .. 发送请求 ... 在数据库里没找到这个用户,会响应一个 400 状态码 .. 还有一条信息,提示没找到用户 ..

这次我们再改一下密码 ... 发送请求 ...

这回虽然找到了用户,但是密码不对 .. 所以会响应 401 状态 ... 表示未通过身份验证 ..

然后再都改成正确的信息 ... 发送一下这个请求 ... 这次又会给我们响应回来一个 token ..

来自 https://ninghao.net/video/5050#info

10)身份验证的 Middleware

我们可以把身份验证的功能放在一个中间件里 .. 这样在想要验证身份的路由上面,可以使用这个中间件去检查用户身份 .. 先添加一个中间件 .. 放在 middlewares 下面,名字可以是 authenticate.js ..

先导入 jwt ... require .. jsonwebtoken .. 用户登录成功以后,我们会给它签发一个 json web token ...

在这个中间件里,主要就是去检验这个 token ..

添加一个方法 .. 名字是 authenticate ... 方法有三个参数 .. request .. response ... 还有一个 next .. 方法的一开始, 我们先要得到 token 的值 ..

假设这个 token 值是在一个自定义的 header 里面 .. const token 等于 .. request.header .. 头部的名字是 X-Access-Token .. 用户对我们的应用发出请求的时候,应该在 header 里面包含 X-Access-Token 这个自定义头部信息 .. 对应的值就是我们签发给他的 Token ..

检查一下 token .. 如果没有 token ... 就 return 一个响应 403 状态 .. 表示 Forbidden .. 带一条信息 ... 没有权限访问 ..

如果有 token .. 我们得检查一下 .. 用的是 jwt 的 verify 方法 .. 检查的是 token .. 再把签发 token 用的密钥告诉这个方法 ..

然后是个回调 .. error 表示错误 .. decoded 表示解码之后的 token 里的数据 .. 这个数据就是签发 token 的时候,payload 里面的东西 .. 这些东西是我们在签发的时候自己放进去的 ..

判断一下 .. 如果有错误 ... 就 return 一个响应的错误 ...

不然的话就是检验合格了 ... 这样我们可以把 request.decoded 的值,设置成 decoded ... 这样可以在其它地方的请求里用到解码之后的 token 里的数据 ..

然后执行一下 next() 方法 ..

最后再导出这个 authenticate ...

使用中间件

下面可以再找一个路由,用一下这个中间件 .. 打开 userRouter.js .. 先导入要使用的中间件 ... 名字是 authenticate .. 位置是上一级目录下面的 middlewares 里的 authenticate ..

这里再添加一个新的路由 ... 用一下 router 的 get 方法 .. 地址是 /me .. 用一下 authenticate 这个中间件 ..

再用一下 UserController 里的 me 这个方法 ..

打开用户控制器 .. 里面添加一个 me 这个方法 .. request .. response ... 方法可以响应一个字符串 .. hello ~ .. 后面加上请求里面的 decoded 里面的 userName ... 也就是用户名 ... 这里的 decoded 是 authenticate 这个中间件在成功验证了用户的 token 以后加进去的 ..

最后别忘了再导入这个 me 方法 ...

测试

然后再试一下 .. 我们可以先 提供正确的用户名还有密码 .. post 一下 api/auth 这个地址,这样可以得到一个 token ..

复制一下这个 token 的值 ... 然后再配置一个请求 .. 方法是 get .. .请求一下 api/me .. 请求不包含主体 ...

再配置一下这个请求的 Header .. 添加一个自定义的 X-Access-Token ... 对应的值就是刚才复制的 token 的值 ..

发送一下这个请求 ... 会响应回来一个 hello ... 后面还包含用户的名字 ..

再去掉这个自定义的 header ... 然后再发送一次这个请求 ... 会响应一个 403 .. 提示没有权限 ..

来自  https://ninghao.net/video/5051#info


普通分类: