使用 token 验证用户登录

简介

日常开发中经常需要对用户的登录状态进行记录和验证,让用户登录后能保持一段时间的登录状态,让用户能访问自己的用户信息等。

这篇文章主要讲解使用 token 的方式如何做到用户登录验证,文章案例的后端使用了nodejsexpress,前端使用了fetch请求。

服务端

依赖

npm i express mongoose bcrypt jsonwebtoken

用户模型

定义一个mongoose的用户模型user model,在MongoDB中存用户数据。

const mongoose = require('mongoose')
const bcrypt = require('bcrypt')
mongoose.connect('mongodb://localhost/manage', {
  useNewUrlParser: true,
  useUnifiedTopology: true,
  useCreateIndex: true
})

const requiredString = {
  type: String,
  required: true
}

const userSchema = mongoose.Schema({
  username: {
    unique: true,
    ...requiredString
  },
  password: {
    set(value) {
      // 存密码时做加密
      // 参数:(散列值, 强度)
      return bcrypt.hashSync(value, 10)
    },
    ...requiredString
  }
})

module.exports = mongoose.model('User', userSchema)

Express 结构

const express = require('express')
// bcrypt 用于用户密码加密
const bcrypt = require('bcrypt')
// jsonwebtoken 用于颁发token
const jwt = require('jsonwebtoken')
// User 是 mongoose 的一个 model,用于存用户信息
const User = require('./models/user')
const app = express()
const port = 8000
// secret 是后面用于token的加密密钥
// 这只是案例,实际项目别放这
const secret = 'dflglrsgiv879grejh'

app.use(express.json())

// 案例后面的路由抽离于此处

app.listen(port, () => {
  console.log(`http://localhost:${port}`)
})

CORS 跨域

为了方便我们就做一个简单的方法解决跨域问题,实际开发中会用到cors这个express的中间件。

对所有OPTINOS请求进行处理,OPTIONS是预检请求

app.options('*', (req, res, next) => {
  // 允许所有来源(跨域请求)
  res.setHeader('Access-Control-Allow-Origin', '*')
  // 允许携带的请求头
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization')
  next()
})

const cors = (req, res, next) => {
  res.setHeader('Access-Control-Allow-Origin', '*')
  next()
}

用户注册

/register路由往数据库中存放用户信息。前端发送POST请求,传入用户名和密码。

app.post('/register', cors, async (req, res) => {
  const { username, password } = req.body
  try {
    // 添加用户
    const user = await User.create({ username, password })
    res.json({
      ok: true,
      data: user
    })
  } catch (err) {
    // 错误处理
    res.json({
      ok: false,
      message: '该用户已存在'
    })
  }
})

用户登录

/login路由进行用户登录。前端发送POST请求,传入用户名和密码。

用户登录需要做的操作:

  1. 判断这个用户是否存在
  2. 判断用户输入的密码是否正确
  3. 颁发 token,这里把token返回给前端自己进行处理
app.post('/login', cors, async (req, res) => {
  const { username, password } = req.body
  // 1. 用户存在?
  const user = await User.findOne({ username })
  if (!user) {
    return res.json({
      ok: false,
      message: '用户名不存在'
    })
  }
  // 2. 密码正确
  // 进行bcrypt密文比对,这里用了同步。参数:(明文, 密文)
  const isPasswordValid = bcrypt.compareSync(
    password,
    user.password
  )
  if (!isPasswordValid) {
    return res.json({
      ok: false,
      message: '密码错误'
    })
  }
  // 3. 生成token
  // 用户的id唯一标识进行token颁发
  // 参数:(加密的字符串/对象,自定义加密的密钥)
  const token = jwt.sign(
    { id: user._id },
    secret
  )
  res.json({
    ok: true,
    data: { user, token }
  })
})

验证用户登录

比如:/profile这个路由需要用户验证才能获得数据。前端发送GET请求,添加Authorization请求头放入前面用户登录时返回的token

auth中间件

将用户验证抽离成一个express中间件,这样在之后需要对用户登录状态进行验证的路由添加auth这个中间件就行了。

const auth = async (req, res, next) => {
  // 从前端发来的请求头中拿到 Authorization
  const { authorization } = req.headers
  // 没传?
  if (authorization) {
    // 拿到前端传来的token
    const token = authorization.split(' ').pop()
    // 使用 jsonwebtoken 进行验证拿到之前颁发token时传入的用户id
    const { id } = jwt.verify(token, secret)
    // 使用id查询用户
    try {
      req.user = await User.findById(id)
      // 拿到了user放到request对象里,并使用next()方法转接到下一个中间件进行处理
      next()
    } catch (error) {
      res.json({
        ok: false,
        message: 'token无效'
      })
    }
  } else {
    res.json({
      ok: false,
      message: '请设置token'
    })
  }
}

/profile路由

在处理前传了两个中间件,cors用于跨域处理,auth做了用户验证。

app.get('/profile', cors, auth, async (req, res) => {
  res.json({
    ok: true,
    data: req.user
  })
})

客户端

这里使用fetch方法来发送请求,同样的你可以使用XMLHttpRequestAxios等都可以。

后端 API

const serverURL = 'http://localhost:8000'
const registerURL = `${serverURL}/register`
const loginURL = `${serverURL}/register`
const profileURL = `${serverURL}/register`

注册用户

registerURL后端的登录API发送POST请求,传了json格式用户名和密码。

fetch(registerURL, {
  method: 'POST',
  body: JSON.stringify({
    username: 'user',
    password: '1234'
  }),
  headers: new Headers({
    'Content-Type': 'application/json'
  })
})
  .then(res => res.json())
  .then(json => console.log(json))

登录用户

loginURL后端的登录API发送POST请求,传了json格式用户名和密码。

fetch(loginURL, {
  method: 'POST',
  body: JSON.stringify({
    username: 'user',
    password: '1234'
  }),
  headers: new Headers({
    'Content-Type': 'application/json'
  })
})
  .then(res => res.json())
  .then(json => console.log(json))

用户信息

后端的对/profile做了用户登录验证,我们就需要按照要求在请求头中添加Authorization字段来传token给后端去做验证。

Authorization请求头的type类型BearerOAuth 2.0中进行了相应的说明。

const token = ''
fetch(profileURL, {
  method: 'GET',
  headers: new Headers({
    'Authorization': `Bearer ${token}`
  })
})
  .then(res => res.json())
  .then(json => console.log(json))

补充:服务端直接设置 Cookie

通常情况后端都不会把token交给前端来处理,让前端自己去存放和删除。下面就使用简单的例子说明通过后端来设置cookie并且从cookie中做用户登录验证。

服务端

CORS 跨域

在跨域的情况下,前端的请求不会自动带上cookie传递给后端,那么前端需要设置Credentials字段做相应的处理。

这时后端同样需要添加额外的相应头Access-Control-Allow-Credentialstrue,而且Access-Control-Allow-Origin不允许再设为*所有域,这里假设前端运行在3000端口上。

app.options('*', (req, res, next) => {
  // 允许的源不能再是 *
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000')
  // 允许携带Cookie
  res.setHeader('Access-Control-Allow-Credentials', true)
  res.setHeader('Access-Control-Allow-Headers', 'Content-Type,Authorization')
  next()
})

const cors = (req, res, next) => {
  res.setHeader('Access-Control-Allow-Credentials', true)
  res.setHeader('Access-Control-Allow-Origin', 'http://localhost:3000')
  next()
}

用户登录

这时用户登录返回到前端的结果中就不要有token字段了,我们直接把token通过响应头Set-Cookie设置到cookie中。

express中提供了Respone.cookie()这个方法去设置cookie,这样就变得十分方便。

你仍然可以使用res.setHeader('Set-Cookie', 'user=dssfrgsrgh; path=/; HttpOnly')这样的方式在express中设置响应头去设置Cookie

app.post('/login', cors, async (req, res) => {
  // ...省略部分代码
  // 3. 生成token
  const token = jwt.sign(
    { id: user._id },
    secret
  )

  // => 不再返回给客户端token让客户端处理
  // res.json({
  //   ok: true,
  //   data: { user, token }
  // })

  // => 把token颁发到cookie
  res.cookie('user', token, {
    httpOnly: true
  })
  res.json({
    ok: true,
    data: user
  })
})

用户信息

我们就不再需要从Authorization请求头中拿token了,现在就要从cookie中获得token

const auth = async (req, res, next) => {
  const { cookie } = req.headers
  // 没有任何 Cookie?
  if (cookie) {
    // 使用一个简单的正则拿到token
    let matchReg = /user=(.*?);/
    // 只有一个cookie就贪婪匹配
    if (cookie.split(';').length === 1) {
      matchReg = /user=(.*)/
    }
    const matchResult = cookie.match(matchReg)
    // 没有token?
    if (matchResult) {
      const token = matchResult[1]
      const { id } = jwt.verify(token, secret)
      // 查询用户
      try {
        req.user = await User.findById(id)
        next()
      } catch (err) {
        res.json({
          ok: false,
          message: 'token无效'
        })
      }
    } else {
      res.json({
        ok: false,
        message: '请先登录'
      })
    }
  } else {
    res.json({
      ok: false,
      message: '没有Cookie'
    })
  }
}

app.get('/profile', cors, auth, async (req, res) => {
  res.json({
    ok: true,
    data: req.user
  })
})

客户端

用户登录

现在前端的登录请求不会拿到token,也就不再需要对token进行存放、删除等操作了。

发送POST请求,跨域携带cookie

fetch方法在跨源请求中携带cookie需要设置credentials字段。

另外,对于axios就是withCredentials字段。

fetch(loginURL, {
  method: 'POST',
  body: JSON.stringify({
    username: 'user',
    password: '1234'
  }),
  credentials: 'include'
})

需要授权的请求

发送GET请求,跨域携带cookie

fetch(profileURL, { credentials: 'include' })

版权声明:

Anand's Blog文章皆为站长Anand Zhang原创内容,转载请注明出处。

包括商业转载在内,注明下方要求的文章出处信息即可,无需联系站长授权。

请尊重他人劳动成果,用爱发电十分不易,谢谢!

请注明出处:

本文出自:Anand's Blog

本文永久链接:https://anandzhang.com/posts/backend/2