IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> JavaScript知识库 -> 用户登录【下】 -> 正文阅读

[JavaScript知识库]用户登录【下】

后端 API 处理流程

搭建 https 服务

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

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

const privateKey = fs.readFileSync('./https/book.llmysnow.top.key', 'utf8')
const pem = fs.readFileSync('./https/book.llmysnow.top.pem', 'utf8')
const credentials = {
  key: privateKey,
  cert: pem,
}
const httpsServer = https.createServer(credentials, app)

httpsServer.listen(18082, () => {
  console.log('running on https://127.0.0.1:%s', 18082)
})

创建 /user/login API

router/user.js 中添加如下代码:

router.post('/login', (req, res) => {
  res.json({
    code: 0,
    msg: '登录成功',
  })
})

这里我使用 Postman 测的接口,也可以使用

  • curl http://127.0.0.1:3003/user/login -X POST -d "username=ll&password=123"

    简写为:curl http://127.0.0.1:3003/user/login -d "username=ll&password=123"

这里我们通过 req.body 获取 POST 请求参数无果,我们需要通过 body-parser 中间件来解决这个问题,新版本 express 内置了 POST 参数解析

app.use(express.urlencoded({ extended: true }))
app.use(express.json())

app.use('/', router)

Nodejs进阶:Express常用中间件body-parser实现解析

body-parser 主要实现如下:

  1. 处理不同类型的请求体,比如 textjsonurlencoded 等,对应的报文主体的格式不同
  2. 处理不同的编码,比如 utf8gbk
  3. 处理不同的压缩类型:比如 gzipdeflare
  4. 其他边界、异常的处理

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

Access to XMLHttpRequest at 'http://127.0.0.1:3003/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 ,后端部署在 http://localhost:3003,所以导致了跨域错误,我们需要在 Node 中添加跨域中间件 cors

npm i cors

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

app.use(cors())

什么时候会发送options请求

响应结果封装

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

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

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

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,
  DEBUG: false
}

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

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

有了 Result 类后,我们可以将登陆 API 改为:

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

登陆用户数据库查询

响应过程封装完毕后,我们需要在数据库中查询用户信息来验证用户名和密码是否准确

安装 mysql 库:

npm i mysql

创建 db 目录,创建两个文件 index.jsconfig.js,在 config.js 中添加如下代码:

  • 因为我电脑装了多个 mysql,所以我需要指定端口号,默认是 3306
module.exports = {
  host: '127.0.0.1',
  user: 'root',
  password: 'root',
  database: 'book',
  port: 3308
}

连接数据库:

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,
    port: config.port,
    multipleStatements: true,
  })
}

查询时调用 connection 对象的 query 方法

  • 注意 conn 对象使用完毕后需要调用 end 进行关闭,否则会导致内存泄露
function querySql(sql) {
  const conn = connect()
  DEBUG && console.log(sql)
  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 (err) {
      reject(err)
    } finally {
      conn.end()
    }
  })
}

可以把 sql 语句直接写在 router/user.js,更好的做法是封装一层 sevice,用来协调业务逻辑和数据库查询,创建 service/user.js

  • 密码采用了 MD5 + SALT 加密

    当然加密方法还有很多,可以看我这篇文章:前后端 JS 加密常用方法(非对称加密、对称加密)

    npm i crypto
    

    创建在 utils/index.js ,添加如下内容:

    const crypto = require('crypto')
    
    function md5(s) {
      return crypto.createHash('md5').update(String(s)).digest('hex')
    }
    
    module.exports = { md5 }
    

utils/constant.js 中添加如下内容:

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

再次输入正确的用户名密码 admin admin,即可查询成功

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

express-validator 表单验证

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

express-validator

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

  • 安装
npm i express-validator
  • 验证
router.post(
  '/login',
  [
    body('username').isString().withMessage('用户名必须为字符'),
    body('password').isString().withMessage('密码必须为字符'),
  ],
  (req, res, next) => {
    const err = validationResult(req)
    if (!err.isEmpty()) {
      const [{ msg }] = err.errors
      next(boom.badRequest(msg))
    } else {
      let { username, password } = req.body
      password = md5(`${password}${PWD_SALT}`)
      login(username, password).then(user => {
        console.log(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)) 抛出异常,交给我们自定义的异常处理方法进行处理

JWT

Token

Token 本质是字符串,用于请求时附带请求头中,校验请求是否合法及判断用户身份

Token、Session、Cookie 的区别

  • Session 保存在服务端,用于客户端与服务端连接时,临时保存用户信息,当用户释放连接后,Session 将被释放
  • Cookie 保存在客户端,当客户端发起请求时,Cookie 会附带在 http header 中,提供给服务端辨识用户身份
  • Token 请求时提供用于校验用户是否具备访问接口的权限

其他方面的区别,可以参考我这篇文章: HTTP网络层性能优化

Token 用途

  • 拦截无效请求,降低服务器处理压力

  • 实现第三方 API 授权,无需每次都输入用户名密码鉴权

    比如:微信、微博第三方授权登录都是使用 Token 实现

  • 身份校验,防止 CSRF 攻击

JWT

JSON Web Token(JWT)是非常流行的跨域身份验证解决方案。jwt 官网

上面 Encoded 那段 JWT 字符串,他被解析成以下三部分:

  • HEADER : ALGORITHM & TOKEN TYPE

    header 是描述 JWT 元数据的 JSON 对象:

    • alg:表示加密算法,HS256HMAC SHA256 的缩写
    • type:Token 类型
    {
      "alg": "HS256",
      "typ": "JWT"
    }
    
  • PAYLOAD : DATA

    payloadJWT 的主体内容部分,也是一个 JSON 字符串,包含需要传递的数据,注意 payload 部分不要存储隐私数据,防止信息泄露

    {
      "sub": "1234567890",
      "name": "John Doe",
      "iat": 1516239022
    }
    
  • VERIFY SIGNATURE

    JWT 签名部分是对上面两部分数据加密后生成的字符串,通过 header 指定的算法生成加密字符串,以确保数据不会被篡改

    生成签名时需要使用秘钥,秘钥只保存在服务端,不能向用户公开,它是一个字符串,我们可以自由设定

    生成签名时需要根据 header 中指定的签名算法,并根据下方的公式,即将 headerpayload 的数据通过 BASE64 加密后采用 . 进行连接,然后通过秘钥进行 SHA256 加密,由于加入了秘钥,所以生成的字符串将无法被破译和篡改,只有在服务端才能还原

    HMACSHA256(
      base64UrlEncode(header) + "." +
      base64UrlEncode(payload),
      your-256-bit-secret
    ) 
    

生成 JWT Token

安装

npm i jsonwebtoken

使用

const jwt = require('jsonwebtoken')
const { PRIVATE_KEY, JWT_EXPIRED } = require('../utils/constant')

login(username, password).then(user => {
  if (!user || user.length === 0) {
    new Result('登录失败').fail(res)
  } else {
    const token = jwt.sign({ username }, PRIVATE_KEY, { expiresIn: JWT_EXPIRED })
    new Result({ token }, '登录成功').success(res)
  }
})

utils/constant.js,这里需要定义 jwt 的私钥和过期时间,过期时间不宜过短,也不宜过长,根据业务场景来把控

module.exports = {
  // ...
  PRIVATE_KEY: 'admin_imooc_node_private_key',
  JWT_EXPIRED: 60 * 60, // token失效时间 1小时
}

前端返回结果如下:

{
  code: 0,
  msg: '登录成功',
  data: {
    token:
      'eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImFkbWluIiwiaWF0IjoxNjM3MDMwNjcyLCJleHAiOjE2MzcwMzQyNzJ9.Rc6KMMjs025lT4aVMMfk2bUcJnlSwVGBWaDQm-hVTY8',
  },
}

我们可以将该 Tokenjwt 官网 进行验证,可以得到如下结果:

  • 鼠标划在 iatexp 上即可看到生成时间和过期时间
{
  "username": "admin",
  "iat": 1637030672,
  "exp": 1637034272
}

可以看到 username 被正确解析,说明 Token 生成成功

JWT 认证

主要功能:检查所有路由是否有过期的 Token,如果没有过期则验证为通过

安装

npm i express-jwt

创建 router/jwt.js

const expressJwt = require('express-jwt')
const { PRIVATE_KEY } = require('../utils/constant')

const jwtAuth = expressJwt({
  secret: PRIVATE_KEY,
  // 设置为false就不进行校验了,游客可以访问
  credentialsRequired: true,
}).unless({
  // 设置jwt认证白名单
  path: ['/', '/user/login'],
})

module.exports = jwtAuth

启动 Node 时会发现报错:

  • if (!options.algorithms) throw new Error('algorithms should be set');Error: algorithms should be set

    2020.7.7 之后,安装的 express-jwt 模块会默认为 6 版本的最新版,更新后的 jwt 需要在配置中加入 algorithms 属性,即设置 jwt 算法

通过 express-jwt 中间件进行验证,通过 unless 设置白名单,并在所有请求前使用

const expressJwt = require('express-jwt')
const { PRIVATE_KEY } = require('../utils/constant')

const jwtAuth = expressJwt({
  secret: PRIVATE_KEY,
  algorithms: ['HS256'],
  // 设置为false就不进行校验了,游客可以访问
  credentialsRequired: true,
}).unless({
  // 设置jwt认证白名单
  path: ['/', '/user/login'],
})

module.exports = jwtAuth

router/index.js 中使用中间件

  • 注意:需要在所有路由之前使用
const jwtAuth = require('./jwt')

router.use(jwtAuth)

重新启动,点击登录后端返回如下内容

为了使 Token 过期报错区别于其他错误,在 utils/constant.js

module.exports = {
  // ...
  CODE_TOKEN_EXPIRED: -2
}

修改自定义异常

  • 修改 /model/Result.js

    jwtError(res) {
      this.code = CODE_TOKEN_EXPIRED
      this.json(res)
    }
    
  • UnauthorizedError 进行单独处理(Token 验证失败),并对其他情况用 Result 方法进行简化

router.use((err, req, res, next) => {
  if (err.name && err.name === 'UnauthorizedError') {
    const { status = 401, message } = err
    new Result(null, 'Token验证失败', {
      error: status,
      errorMsg: message,
    }).jwtError(res.status(status))
  } else {
    const msg = (err && err.message) || '系统错误'
    const statusCode = (err.output && err.output.statusCode) || 500
    const errorMsg = (err.output && err.output.payload && err.output.payload.error) || err.message
    new Result(null, msg, {
      error: statusCode,
      errorMsg,
    }).fail(res.status(statusCode))
  }
})

重新启动,点击登录后端返回如下内容

前端处理 JWT Token

前端登录请求改造,修改 src/utils/request.js 中的响应拦截器

  • error 对象进行解构,取其中的 response.data.msg ,并对其进行提示
service.interceptors.response.use(
  response => {
    const res = response.data
    if (res.code !== 0) {
      const errMsg = res.msg || '请求失败'
      Message({
        message: errMsg,
        type: 'error',
        duration: 5 * 1000
      })

      if (res.code === 2) {
        MessageBox.confirm('Token已失效,请重新登录', '确认退出登录', {
          confirmButtonText: '重新登录',
          cancelButtonText: '取消',
          type: 'warning'
        }).then(() => {
          store.dispatch('user/resetToken').then(() => {
            location.reload()
          })
        })
      }
      return Promise.reject(new Error(errMsg))
    } else {
      return res
    }
  },
  error => {
    console.log({ error }) // for debug
    const { msg } = error.response.data
    Message({
      message: msg || '请求失败',
      type: 'error',
      duration: 5 * 1000
    })
    return Promise.reject(error)
  }
)

后端添加路由的 jwt 认证后,再次请求 /user/info 将抛出 401 错误,这是由于前端未传递合理的 Token 导致的,需要对其请求拦截器中的 headers 进行修改

  • 修改 headers 中为 Authorization
  • getToken() 之前加一个 Bearer(后面有一个空格)
service.interceptors.request.use(
  config => {
    if (store.getters.token) {
      config.headers['Authorization'] = `Bearer ${getToken()}`
    }
    return config
  },
  error => {
    return Promise.reject(error)
  }
)

前端去掉 /user/info 请求时传入的 Token,因为我们已经从 Token 中传入,修改 src/api/user.js

export function getInfo() {
  return request({
    url: '/user/info',
    method: 'get'
  })
}

用户查询 /user/info

查询用户只有一个人,querySql 查询的是多个人,返回的是一个数组,需要增加一个 queryOne 方法

  • /db/index.js 中添加
function queryOne(sql) {
  return new Promise((resolve, reject) => {
    querySql(sql)
      .then(results => {
        if (results && results.length > 0) {
          resolve(results[0])
        } else {
          resolve(null)
        }
      })
      .catch(err => {
        reject(err)
      })
  })
}
  • /services/user.js 中添加:
function findUser(username) {
  const sql = `select * from admin_user where username='${username}'`
  return queryOne(sql)
}

请求 /user/info 接口会得到如下内容:

  • 这里 password 为敏感字段,需要对其进行删除

修改后代码如下(当然也可以在获取 user 以后进行处理,不过不推荐这么做):

function findUser(username) {
  return queryOne(`select id, username, nickname, role, avatar from admin_user
  where username='${username}'`)
}

前端在 HTTP header 中传入了 Token ,通过 Token 获取 username 就需要对 jwt token 进行解析,在 utils/index.js 中添加如下内容

const jwt = require('jsonwebtoken')
const { PRIVATE_KEY } = require('./constant')

function decoded(req) {
  let token = req.get('Authorization')
  if (token.indexOf('Bearer') >= 0) {
    token = token.replace('Bearer ', '')
  }
  return jwt.verify(token, PRIVATE_KEY)
}

修改 router/user.js

  • 重新在前端进行登录,就可以登录进去了
router.get('/info', (req, res) => {
  const decode = decoded(req)
  if (decode && decode.username) {
    findUser(decode.username).then(user => {
      if (user) {
        user.roles = [user.role]
        new Result(user, '用户信息查询成功').success(res)
      } else {
        new Result(user, '用户信息查询失败').fail(res)
      }
    })
  }else{
    new Result('用户信息查询失败').fail(res)
  }
})

logout 方法

  • 修改 src/store/modules/user.js
logout({ commit, state, dispatch }) {
  return new Promise((resolve, reject) => {
    try {
      commit('SET_TOKEN', '')
      commit('SET_ROLES', [])
      removeToken()
      resetRouter()
      dispatch('tagsView/delAllViews', null, { root: true })
      resolve()
    } catch (e) {
      reject(e)
    }
  })
}

RefreshToken

场景:需要授权给第三方 APP

通常我们需要再增加一个 RefreshToken 的 API,该 API 的用途是根据现有的 Token 获取用户名,然后生成一个新的 Token,这样做的目的是为了防止 Token 失效后退出登录,所以 APP 一般都会在打开时刷新一次 Token

  JavaScript知识库 最新文章
ES6的相关知识点
react 函数式组件 & react其他一些总结
Vue基础超详细
前端JS也可以连点成线(Vue中运用 AntVG6)
Vue事件处理的基本使用
Vue后台项目的记录 (一)
前后端分离vue跨域,devServer配置proxy代理
TypeScript
初识vuex
vue项目安装包指令收集
上一篇文章      下一篇文章      查看所有文章
加:2021-11-17 12:40:05  更:2021-11-17 12:41:14 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/24 4:44:03-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码