9. Node.js:基于 Token 的身份验证

本文由清尘发表于2019-04-01 17:18最后修改于2020-05-26属于Node.js分类

相关文章:

1.Node.js 起步
2.Node.js测试 Mocha框架
3.Express基础
4.MongoDB 基础
5.Node.js 与 MongoDB
6.Node.js:REST 接口
7. Socket.io
8. JWT:JSON Web Token
9. Node.js:基于 Token 的身份验证
10. Node.js:上传文件
===========================
github:https://github.com/shine130/shine-node

Tgit私有仓库(auth分支):

HTTP: https://git.code.tencent.com/testpro/shine-node.git
SSH:git@git.code.tencent.com:testpro/shine-node.git

身份验证

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

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

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

用户模型

新建文件 models/user.js

const db = require('../config/database')

const schema = new db.Schema({
  userName:{
    type:String,
    required:true
  },
  password:{
    type:String,
    required:true
  }
})

const User = db.model('User',schema)

module.exports = User

用户路由与控制器

新建routes/userRouter.js

const express = require('express')
const router = express.Router()
const UserController = require('../controllers/UserController')

router.route('/users')
  .post(UserController.store)

module.exports = router

新建 controllers/UserController.js

const store = (request,response) => {
  response.send('注册用户')
}

module.exports = {
  store
}

server.js使用user的路由

const express = require('express')
const eventRouter = require('./routes/eventRouter')
const userRouter = require('./routes/userRouter')
const app = express()
const port = process.env.PORT || 3000
const bodyParser = require('body-parser')

app.use(bodyParser.json())

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

app.get('/',(request,response) => {
    response.send('hello~')
})

app.listen(port,() => console.log(`监听端口:${port}`))

用REST客户端发送POST 请求 http://localhost:3000/api/users 可以看到返回的数据

注册用户

修改controllers/UserController.js

const User = require('../models/user')

const store = (request,response) => {
  const userName = request.body.userName
  const password = request.body.password

  const user = new User({
    userName,
    password
  })

  user.save()
    .then( () => response.send({ message: '注册成功'}))
    .catch(error => response.send(error))

}

module.exports = {
  store
}

使用REST客户端发送POST请求 http://localhost:3000/api/users 传递数据

{
	"userName":"shine",
	"password":"password"
}

返回注册成功

hash 用户密码

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

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

const crypto = require('crypto')
const hash = crypto.createHash('sha256')

hash.update('password')
console.log(hash.digest('base64'))

bcrypt:加 salt 的 hash

安装bcrypt

npm install bcrypt --save

使用

const bcrypt = require('bcrypt')
const password = 'password'

bcrypt.genSalt(10,(error,salt) => {
  console.log('salt:',salt)
  bcrypt.hash(password,salt,(error,hash) => {
    console.log('hash:',hash)
  })
})

验证

const bcrypt = require('bcrypt')
const password = 'password'

bcrypt.genSalt(10,(error,salt) => {
  console.log('salt:',salt)
  bcrypt.hash(password,salt,(error,hash) => {
    console.log('hash:',hash)
  })
})

// 验证
const hashPassword = '$2b$10$ZqXjl.tj/cf4b55Iy4fVRem11N7Oih2s6JaStI66RN8T8dCJ5lPSe'
const userInputPassword = 'password'

bcrypt.compare(userInputPassword,hashPassword)
  .then(result => console.log(result))

验证通过返回 true 否则返回 false

存储 hash 之后的密码

修改 controllers/UserController.js

const User = require('../models/user')
const bcrypt = require('bcrypt')

const store = (request,response) => {
  const userName = request.body.userName

  bcrypt.hash(request.body.password,10)
    .then(password => {
      const user = new User({
            userName,
            password
      })
    
      user.save()
        .then( () => response.send({ message: '注册成功'}))
        .catch(error => response.send(error))
    })
}

module.exports = {
  store
}

用户端POST请求一下 http://localhost:3000/api/users 发送用户名密码

{
	"userName":"shine",
	"password":"password"
}

再查看数据库里面的密码已经是hash之后的密码了

身份验证与签发 Token

安装jwt

npm install jsonwebtoken --save

修改routes/userRouter.js 增加一个用户登陆验证的路由

const express = require('express')
const router = express.Router()
const UserController = require('../controllers/UserController')

router.route('/users')
  .post(UserController.store)

router.post('/auth',UserController.auth)

module.exports = router

修改controllers/UserController.js 增加auth方法为用户签发Token

const User = require('../models/user')
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')

const store = (request,response) => {
  const userName = request.body.userName

  bcrypt.hash(request.body.password,10)
    .then(password => {
      const user = new User({
            userName,
            password
      })
    
      user.save()
        .then( () => response.send({ message: '注册成功'}))
        .catch(error => response.send(error))
    })
}

const auth = (request,response) => {
  User.findOne({userName: request.body.userName})
    .then(user => {
      if(!user){
        return Promise.reject({message:'没找到用户'})
      }

      bcrypt.compare(request.body.password,user.password)
        .then(result => {
          if(result){
            const payload = {
              userName:user.userName
            }
            const secret = 'I_LOVE_SHINE'
            const token = jwt.sign(payload,secret)

            response.send({token})
          }else {
            response.status(401).send({message:'未通过身份验证'})
          }
        })

    })
    .catch(error => response.status(400).send(error))
}

module.exports = {
  store,
  auth
}

用REST客户端POST请求 http://localhost:3000/api/auth 发送用户名密码

{
	"userName":"shine",
	"password":"password"
}

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

身份验证的 Middleware

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

新建文件 middlewares/authenticate.js

const jwt = require('jsonwebtoken')

const authenticate = (request,response,next) => {
  const token = request.header('X-Access-Token')

  if(token){
    jwt.verify(token,'I_LOVE_SHINE',(error,decoded) => {
      if(error){
        return response.send(error)
      }else{
        request.decoded = decoded
        next()
      }
    })

  }else{
    return response.status(403).send({message:'没有权限访问'})
  }

}

module.exports = authenticate

修改routes/userRouter.js 添加一个新的路由,使用这个中间件

const express = require('express')
const router = express.Router()
const UserController = require('../controllers/UserController')
const authenticate = require('../middlewares/authenticate')

router.route('/users')
  .post(UserController.store)

router.post('/auth',UserController.auth)

router.get('/me',authenticate,UserController.me)

module.exports = router

在controllers/UserController.js 里面增加me方法

const User = require('../models/user')
const bcrypt = require('bcrypt')
const jwt = require('jsonwebtoken')

const store = (request,response) => {
  const userName = request.body.userName

  bcrypt.hash(request.body.password,10)
    .then(password => {
      const user = new User({
            userName,
            password
      })
    
      user.save()
        .then( () => response.send({ message: '注册成功'}))
        .catch(error => response.send(error))
    })
}

const auth = (request,response) => {
  User.findOne({userName: request.body.userName})
    .then(user => {
      if(!user){
        return Promise.reject({message:'没找到用户'})
      }

      bcrypt.compare(request.body.password,user.password)
        .then(result => {
          if(result){
            const payload = {
              userName:user.userName
            }
            const secret = 'I_LOVE_SHINE'
            const token = jwt.sign(payload,secret)

            response.send({token})
          }else {
            response.status(401).send({message:'未通过身份验证'})
          }
        })

    })
    .catch(error => response.status(400).send(error))
}

const me = (request,response) => {
  response.send(`hello ~ ${request.decoded.userName}`)
}

module.exports = {
  store,
  auth,
  me
}

先post用户名和密码到http://localhost:3000/api/auth 得到token 。复制一下token的值

{
	"userName":"shine",
	"password":"password"
}

使用REST客户端发起一个GET请求 http://localhost:3000/api/me ,添加Header头 “X-Access-Token” 值为上一步复制的token 。
发送一下这个请求 … 会响应回来一个 hello … 后面还包含用户的名字 ..
再去掉这个自定义的 header … 然后再发送一次这个请求 … 会响应一个 403 .. 提示没有权限 ..