zl程序教程

您现在的位置是:首页 >  前端

当前栏目

【笔记】Vue Element+Node.js开发企业通用管理后台系统——用户登录(下)(搭建https、拦截器、跨域、数据库)

2023-09-27 14:26:51 时间


用户登录(下) | 「小慕读书」管理后台


一、后端 API 处理流程

login_api.png

二、搭建 https 服务

在这里插入图片描述

首先需要将 https 证书拷贝到 node 项目中,然后添加下列代码:

const fs = require('fs')
const https = require('https')

const privateKey = fs.readFileSync('https/book_youbaobao_xyz.key', 'utf8')
const certificate = fs.readFileSync('https/book_youbaobao_xyz.pem', 'utf8')
const credentials = { key: privateKey, cert: certificate }
const httpsServer = https.createServer(credentials, app)
const SSLPORT = 18082
httpsServer.listen(SSLPORT, function() {
  console.log('HTTPS Server is running on: https://localhost:%s', SSLPORT)
})

启动 https 服务需要证书对象 credentials,包含了私钥和证书,重新启动 node 服务:

node app.js

在浏览器中输入:

https://book.aimooc.top:18082

可以看到:
欢迎学习小慕读书管理后台
说明 https 服务启动成功

三、创建 /user/login API

router/user.js 中填入以下代码:

router.post('/login', function(req, res, next) {
  console.log('/user/login', req.body)
  res.json({
    code: 0,
    msg: '登录成功'
  })
})

在控制台测试:

$ curl https://book.aimooc.top:18082/user/login -X POST -d "username=sam&password=123456"

{"code":0,"msg":"登录成功"}

上述命令可以简写为:

curl https://book.aimooc.top:18082/user/login -d "username=sam&password=123456"

这里通过 req.body 获取 POST 请求中的参数,但是没有获取成功,需要通过 body-parser 中间件来解决这个问题:

npm i -S body-parser

在 app.js 中加入:

const bodyParser = require('body-parser')

// 创建 express 应用
const app = express()

app.use(bodyParser.urlencoded({ extended: true }))
app.use(bodyParser.json())
app.use('/', router)

这样body里的参数就被正确解析出来了:
在这里插入图片描述

TIP
关于 body-parser 的实现原理与细节可以参考这篇文档,说得非常明白:https://juejin.im/post/59222c5d2f301e006b1616ae

返回前端使用登录按钮请求登录接口,发现控制台报错:

Access to XMLHttpRequest at 'https://book.youbaobao.xyz:18082/user/login' from origin 'http://localhost:9527' has been blocked by CORS policy: Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource.

在这里插入图片描述
这是由于前端部署在 http://localhost:9527 而后端部署在 https://book.aimooc.top:18082,所以导致了跨域错误,我们需要在 node 服务中添加跨域中间件 cors

npm i -S cors

然后修改 app.js:

const cors = require('cors')

// ...
app.use(cors())

再次请求即可成功,这里我们在 Network 中会发现发起了两次 https 请求,这是因为由于触发跨域,所以会首先进行 OPTIONS 请求,判断服务端是否允许跨域请求,如果允许才能实际进行请求,这是由浏览器决定的

TIP:

  • 注意,这个代码一定要,写在注册路由的前面。此模块也可以,当做路由中间件,指定某一个,或者某一部分路由,拥有跨域功能。
  • 关于为什么要发起 OPTIONS 请求,大家可以参考这篇文档:> https://juejin.im/post/5cb3eedcf265da038f7734c4

在前端登录时,检查请求信息发现如下:
在这里插入图片描述
需要修改vue-element-admin\src\api\user.js为:
在这里插入图片描述
此时就正常了:
在这里插入图片描述
在这里插入图片描述

CORS:是一个W3C标准,全称"跨域资源共享"(Cross-origin resource sharing)。CORS需要浏览器和服务器同时支持,才可以实现跨域请求,目前几乎所有浏览器都支持CORS,IE不能低于IE10。CORS的整个过程都由浏览器自动完成,前端无需做任何设置,跟平时发送ajax请求并无差异。so,实现CORS的关键在于服务器,只要服务器实现CORS接口,就可以实现跨域通信。详细可见:

四、响应结果封装

/user/login 我们看到返回值是:

res.json({
  code: 0,
  msg: '登录成功'
})

之后我们还要定义错误返回值,但如果每个接口都编写以上代码就显得非常冗余,而且不易维护,比如我们要将 code 默认值从 0 改为 1,就要修改每个接口,所以我们创建一个 Result 类来解决这个问题

创建 /models/Result.js 文件:

const {
  CODE_ERROR,
  CODE_SUCCESS
} = require('../utils/constant')

class Result {
  constructor(data, msg = '操作成功', options) {
    this.data = null
    if (arguments.length === 0) {
      this.msg = '操作成功'
    } else if (arguments.length === 1) {
      this.msg = data
    } else {
      this.data = data
      this.msg = msg
      if (options) {
        this.options = options
      }
    }
  }

  createResult() {
    if (!this.code) {
      this.code = CODE_SUCCESS
    }
    let base = {
      code: this.code,
      msg: this.msg
    }
    if (this.data) {
      base.data = this.data
    }
    if (this.options) {
      base = { ...base, ...this.options }
    }
    console.log(base)
    return base
  }

  json(res) {
    res.json(this.createResult())
  }

  success(res) {
    this.code = CODE_SUCCESS
    this.json(res)
  }

  fail(res) {
    this.code = CODE_ERROR
    this.json(res)
  }
}

module.exports = Result

修改 /utils/constant.js

module.exports = {
  CODE_ERROR: -1,
  CODE_SUCCESS: 0
}

Result 使用了 ES6 的 Class,使用方法如下:

// 调用成功时
new Result().success(res)
new Result('登录成功').success(res)
// 调用成功时,包含参数
new Result({ token }, '登录成功').success(res)
// 调用失败时
new Result('用户名或密码不存在').fail(res)

有了 Result 类后,我们可以将登录 API (router\user.js)改为:

router.post('/login', function(req, res, next) {
  const { username, password } = req.body
  if (username === 'admin' && password === '123456') {
    new Result('登录成功').success(res)
  } else {
    new Result('登录失败').fail(res)
  }
})

如果在响应前抛出 Error,此时 Error 将被我们自定义的异常处理捕获,并返回 500 至前端(throw new Error('ERROR...')

五、登录用户数据库查询

响应过程封装完毕后,我们需要在数据库中查询用户信息来验证用户名和密码是否准确,首先需要知道一些在node中使用mysql的基本操作:

1.安装

安装 mysql 库:

 cnpm i -S mysql

2.配置

创建 db 目录,新建两个文件:

  • config.js
module.exports = {
  host: 'localhost',
  user: 'root',
  password: '123456',
  database: 'book'
}
  • index.js
const mysql = require('mysql')
const config = require('./config')

// 连接
function connect() {
  return mysql.createConnection({
    host: config.host,
    user: config.user,
    password: config.password,
    database: config.database,
    multipleStatements: true
  })
}

// 查询
function querySql(sql) {
  const conn = connect()
  return new Promise((resolve, reject) => {
    try {
      conn.query(sql, (err, results) => {
        if (err) {
          // debug && console.log('查询失败,原因:' + JSON.stringify(err))
          reject(err)
        } else {
          // debug && console.log('查询成功', JSON.stringify(results))
          resolve(results)
        }
      })
    } catch (e) {
      reject(e)
    } finally {
      conn.end() // 注意一定要加,否则会造成内存泄漏
    }
  })
}

module.exports = {
  querySql
}

multipleStatements:允许每条 mysql 语句有多条查询.使用它时要非常注意,因为它很容易引起 sql 注入(默认:false)

router\user.js中添加如下语句测试连接:

const { querySql } = require('../db') // new
const router = express.Router()

router.post('/login', function(req, res, next) {
    console.log('req.body', req.body)
    const { username, password } = req.body
    querySql('select * from admin_user').then(res => { console.log(res) }) // new
    if (username === 'admin' && password === '111111') {
        new Result('登录成功').success(res)
    } else {
        new Result('登陆失败').fail(res)
    }
})

登录,看到node控制台显示查询结果即成功。

我们在 constant.js 创建一个 debug 参数控制日志打印:
utils\constant.js中新增debug: true
修改db\index.js

const mysql = require('mysql')
const config = require('./config')
const { debug } = require('../utils/constant') // new

// 连接
function connect() {
  return mysql.createConnection({
    host: config.host,
    user: config.user,
    password: config.password,
    database: config.database,
    multipleStatements: true
  })
}

// 查询
function querySql(sql) {
  const conn = connect()
  debug && console.log(sql) // new
  return new Promise((resolve, reject) => {
    try {
      conn.query(sql, (err, results) => {
        if (err) {
          debug && console.log('查询失败,原因:' + JSON.stringify(err)) // new
          reject(err)
        } else {
          debug && console.log('查询成功', JSON.stringify(results)) // new
          resolve(results)
        }
      })
    } catch (e) {
      reject(e)
    } finally {
      conn.end() // 注意一定要加,否则会造成内存泄漏
    }
  })
}

module.exports = {
  querySql
}

这时如有查询错误就会在控制台显示,方便debug
在这里插入图片描述
测试完毕,移除router\user.js中的:

const { querySql } = require('../db')
querySql('select * from admin_user').then(res => { console.log(res) }) 

这里我们需要基于 mysql 查询库封装一层 service,用来协调业务逻辑和数据库查询,我们不希望直接把业务逻辑写在 router 中,创建 /service/user.js

const { querySql } = require('../db')

function login(username, password) {
  const sql = `select * from admin_user where username='${username}' and password='${password}'`
  return querySql(sql)
}

module.exports = {
  login
}

改造 /user/login API:

const express = require('express')
const Result = require('../models/Result')
const { login } = require('../services/user')

const router = express.Router()

router.post('/login', function(req, res, next) {
    const { username, password } = req.body
    login(username, password).then(user => {
        if (!user || user.length === 0) {
            new Result('登录失败').fail(res)
        } else {
            new Result('登录成功').success(res)
        }
    })
})
...

此时即使我们输入正确的用户名和密码仍然无法登录,这是因为密码采用了 MD5 + SALT 加密,所以我们需要对密码进行对等加密,才能查询成功。
在这里插入图片描述
/utils/constant.js 中加入 SALT:

module.exports = {
  // ...
  PWD_SALT: 'admin_imooc_node',
}

安装 crypto 库:

cnpm i -S crypto

新建/utils/index.js

const crypto = require('crypto')

function md5(s) {
  // 注意参数需要为 String 类型,否则会出错
  return crypto.createHash('md5')
    .update(String(s)).digest('hex');
}

module.exports = {
  md5
}

修改/user/login

const express = require('express')
const Result = require('../models/Result')
const { login } = require('../services/user')
const { md5 } = require('../utils/index') // new
const { PWD_SALT } = require('../utils/constant') // new

const router = express.Router()

router.post('/login', function(req, res, next) {
    let { username, password } = req.body
    password = md5(`${password}${PWD_SALT}`) // new
    login(username, password).then(user => {
        if (!user || user.length === 0) {
            new Result('登录失败').fail(res)
        } else {
            new Result('登录成功').success(res)
        }
    })
})

再次输入正确的用户名和密码,查询成功:
在这里插入图片描述

六、express-validator

express-validator 是一个功能强大的表单验证器,它是 validator.js 的中间件

源码地址:https://github.com/express-validator/express-validator

使用 express-validator 可以简化 POST 请求的参数验证,使用方法如下:

安装

cnpm i -S express-validator

验证

修改/user/login

const express = require('express')
const Result = require('../models/Result')
const { login } = require('../services/user')
const { md5 } = require('../utils/index')
const { PWD_SALT } = require('../utils/constant')
const { body, validationResult } = require('express-validator')
const boom = require('boom')

const router = express.Router()

router.post(
    '/login',
    [
        body('username').isString().withMessage('username类型不正确'),
        body('password').isString().withMessage('password类型不正确')
    ],
    function(req, res, next) {
        const err = validationResult(req)
        if (!err.isEmpty()) {
            const [{ msg }] = err.errors
            next(boom.badRequest(msg)) // 400错误
        } else {
            const username = req.body.username
            const password = md5(`${req.body.password}${PWD_SALT}`)

            login(username, password).then(user => {
                if (!user || user.length === 0) {
                    new Result('登录失败').fail(res)
                } else {
                    new Result('登录成功').success(res)
                }
            })
        }
    })

在这里插入图片描述
express-validator 使用技巧:

  • router.post 方法的第二个参数中使用 body 方法判断参数类型,并指定出错时的提示信息
  • 使用 const err = validationResult(req) 获取错误信息,err.errors 是一个数组,包含所有错误信息,如果 err.errors 为空则表示校验成功,没有参数错误
  • 如果发现错误我们可以使用 next(boom.badRequest(msg)) 抛出异常,交给我们自定义的异常处理方法进行处理

boom是一个兼容 HTTP 的错误对象,他提供了一些标准的 HTTP 错误,比如 400(参数错误)等。