【架构师(第四十二篇)】 服务端开发之常用的登录鉴权方式

2,233 阅读3分钟

内容概括

主要产出

  • 了解 Web 常用的登录鉴权方式

主要内容

  • cookiesession
  • JWT
  • SSOOAuth2

关于短信验证码

  • 用户体验好,无需注册,无需记住密码
  • 验证码需要收费,所以需要防止恶意共计,恶意刷接口

介绍 Session 登录

cookie 做登录校验的过程

  • 前端输入用户名密码,传给后端
  • 后端验证成功,返回信息时 set-cookie
  • 接下来的所有接口访问,都自动带上 cookie

为何会有 Session

  • cookie 只存储 userId,不暴露用户信息
  • 用户信息存储在 session

Session 的优点

  • 原理简单,易于学习
  • 用户信息存储在服务端,可以快速封禁某个登录的用户

Session 的缺点

  • 占用服务端内存,有硬件成本
  • 多进程,多服务器时,不好同步,一般使用第三方 redis 存储,成本高
  • 跨域传递 cookie,需要特殊配置

介绍 JWT 登录

JWT 的全称是 JSON Web Token

JWT 的过程

  • 前端输入用户名密码,传递给后端
  • 后端验证成功,返回一段 token 字符串,将用户信息加密之后得到的结果
  • 前端获取 token 之后,存储下来
  • 以后访问接口,都在 header 中携带这段 token

JWT 的优点

  • 不占用服务器内存
  • 多进程,多服务器,不受影响
  • 不受跨域限制

JWT 的缺点

  • 无法快速封禁登录的用户

JWT 和 Session 的重要区别

  • JWT 用户信息存储在客户端
  • Session 用户信息存储在服务器端

为何选择 JWT

  • 没有快速封禁登录用户的需求

  • JWT 成本低,维护简单

  • 需要考虑跨域的扩展性

代码演示

  • 安装 npm 插件,koa-jwtjsonwebjson

  • 封装 jwt 中间件,并使用

  • 封装 loginCheck 中间件

  • 相关的配置项,构造函数等

安装插件

npm i koa-jwt jsonwebtoken -S

封装 jwt 中间件

增加配置

// src\config\constant.js

module.exports = {
  // jwt 秘钥
  JWT_SECRET: 'warbler_for-json#web$token',

  // jwt 忽略默认验证的 path:全部忽略即可,需要登录验证的,用自己封装的 loginCheck
  JWT_IGNORE_PATH: [/\//],
}

封装中间件

//  src\middlewares\jwt.js

const jwtKoa = require('koa-jwt')
const { JWT_SECRET, JWT_IGNORE_PATH } = require('../config/constant')

const jwt = jwtKoa({
  secret: JWT_SECRET, // jwt 秘钥
  cookie: 'jwt_token', // 使用 cookie 存储 token
}).unless({
  // 定义哪些路由忽略 jwt 验证
  path: JWT_IGNORE_PATH,
})

module.exports = jwt

使用

// src\app.js

const jwt = require('./middlewares/jwt')
// 配置 jwt 中间件
app.use(jwt)

封装 JWT 工具

// src\utils\jwt.js

const util = require('util')
const jwt = require('jsonwebtoken')

// jwt 密钥
const { JWT_SECRET } = require('../config/constant')

// jwt 过期时间
const { jwtExpiresIn } = require('../config/index')

const verify = util.promisify(jwt.verify)

/**
 * jwt verify  解密
 * @param {string} token token
 */
async function jwtVerify(token) {
  const data = await verify(token.split(' ')[1], JWT_SECRET) // 去掉前面的 Bearer
  return data
}

/**
 * jwt sign  加密
 * @param {Object} data data
 */
function jwtSign(data) {
  const token = jwt.sign(data, JWT_SECRET, { expiresIn: jwtExpiresIn })
  return token
}

// 导出解密,加密两个方法
module.exports = {
  jwtVerify,
  jwtSign,
}

封装 loginCheck 中间件

封装数据模型

// src\res-model\index.js

/**
 * 基础模型,包括 errno data 和 message
 */
class BaseRes {
  constructor({ errno, data, message }) {
    this.errno = errno
    if (data) {
      this.data = data
    }
    if (message) {
      this.message = message
    }
  }
}

/**
 * 执行失败的数据模型
 */
class ErrorRes extends BaseRes {
  constructor({ errno = -1, message = '', data }, addMessage = '') {
    super({
      errno,
      message: addMessage
        ? `${message} - ${addMessage}` // 有追加信息
        : message,
      data,
    })
  }
}

/**
 * 执行成功的数据模型
 */
class SuccessRes extends BaseRes {
  constructor(data = {}) {
    super({
      errno: 0,
      data,
    })
  }
}

module.exports = {
  ErrorRes,
  SuccessRes,
}

封装错误信息集合

// src\res-model\failInfo\error.js

module.exports = {
  // 统一错误处理
  serverErrorFailInfo: {
    errno: -1,
    message: '运行错误',
  },
  // 404
  notFoundFailInfo: {
    errno: -2,
    message: '404 Not Found',
  },
}

封装中间件

// src\middlewares\loginCheck.js

// 解密
const { jwtVerify } = require('../utils/jwt')
// 执行失败的数据模型
const { ErrorRes } = require('../res-model/index')
// 错误信息集合
const { loginCheckFailInfo } = require('../res-model/failInfo/index')

/**
 * 登录校验
 * @param {Object} ctx ctx
 * @param {function} next next
 */
module.exports = async function loginCheck(ctx, next) {
  // 失败信息
  const errRes = new ErrorRes(loginCheckFailInfo)

  // 获取 token
  const token = ctx.header.authorization
  if (!token) {
    ctx.body = errRes
    return
  }

  let flag = true
  try {
    const userInfo = await jwtVerify(token)
    delete userInfo.password // 屏蔽密码

    // 验证成功,获取 userInfo
    ctx.userInfo = userInfo
  } catch (ex) {
    flag = false
    ctx.body = errRes
  }

  if (flag) {
    // 继续下一步
    await next()
  }
}

介绍 SSO 和 OAuth2

  • SSO 单点登录
  • OAuth2 第三方鉴权

使用 cookie 实现单点登录

简单的,如果业务系统都在同一主域名下,比如 wenku.baidu.comtieba.baidu.com ,就可以直接把 cookie domain 设置为主域名 baidu.com ,百度也就是这么干的

SSO

image.png

OAuth2

SSOOAuth2 的实际案例,其他常见的还有微信登录,github 登录等,当涉及到第三方用户登录校验时,都会使用 OAuth2 标准。