一篇文章带你了解 cookie+session+jwt+OAuth 四大金刚

在这里插入图片描述

登录鉴权就是判断用户是否有权利登录该网站

其过程👇👇👇:
在这里插入图片描述

Cookie🍪

1.1 Cookie 介绍

由于 HTTP 是无状态的协议,不能保存每一次请求的状态,所以需要给客户端增加 Cookie 来保存客户端的状态。

Cookie 的作用主要用于 用户识别状态管理

// 请求首部字段 
Cookie: key=value; expires=true, 05 Jul 2016 07:23:31 GMT; path=/; domain=.baidu.com 

// 响应首部字段 
Set-Cookie:key = value;
  • key-value : Cookie 值
  • expires=date : Cookie 的有效期(默认的是浏览器关闭之前)
1.2 有效期

Cookie 过期之后,浏览器删除且不再携带 Cookie

  • Expires :具体的过期时间
  • Max-age :过期的时间范围,浏览器接受 Cookie 开始计时
1.3 作用域

浏览器在发送请求之前,检查 Cookie 的 domain 和 path,如果不一致,则不携带 Cookie。

  • path :将服务器的文件目录作为 Cookie 的适用对象
  • domain = 域名 : 作为 Cookie 适用对象的域名。如:naiyou.com,则 www.naiyou.comwww.naiyou2.com 也可以访问。
1.4 安全性

在 Cookie 传输和管理的时候,要确保 Cookie 的安全性,不被窃取。

  • Secure :仅在 HTTPS 安全通信时才会发送 Cookie
  • HttpOnly :设置 Cookie 不能被 Javascript 脚本访问(防止跨站脚本攻击 XSS 对 Cookie 信息的窃取)
  • SameSite :防止跨站伪造 CSRF 的攻击
    • Strict :浏览器完全禁止第三方请求携带 Cookie
    • Lax :只能在 get 方法提交表单情况或者 a 标签发送 get 请求的情况下可以携带 Cookie
    • None :默认,请求会自动携带上 Cookie
1.5 局限性
  • 大小:只有 4KB,只能存储少量信息
  • 性能:如果不设置 Cookie 的 Domain 和 path ,Cookie 就会被发送到各个域名下,随着请求数量的增多,性能也会出现很大的问题
  • 安全: Cookie 明文传输,被攻击劫持之后,服务器的资源会被窃取。且如果不设置 HttpOnly 攻击者会通过 JS 获取到 Cookie
1.6 JS 操作 cookie
设置
/**
  * @param {String} key  属性
  * @param {*} value  值
  * @param { String } expire  过期时间,单位天
*/
function cookieSet(key, value, expire) {
  const d = new Date();
  d.setDate(d.getDate() + expire);
  document.cookie = `${key}=${value};expires=${d.toUTCString()}`
};
获取
/**
 * @param {String} key 属性
 * @returns 
 */
function cookieGet(key) {
  const cookieStr = decodeURIComponent(document.cookie)
  const arr = cookieStr.split('; ')
  let cookieValue = ''
  for (let i = 0; i < arr.length; i++) {
    const temp = arr[i].split('=')
    if (temp[0] === key) {
      cookieValue = temp[1]
      break
    }
  }
  return cookieValue
}
删除
/**
 * @param {String} key 属性
 */
function cookieRemove(key) {
  document.cookie = `${encodeURIComponent(key)}=;expires=${new Date()}`
}

Session🗝️

2.1 什么是 Session

服务器要给每个客户端分配不同的"身份标识",然后客户端每次向服务器发请求的时候,都带上这个”身份标识“,服务器就知道这个请求来自哪里。客户端一般使用 Cookie 来保存这个身份信息

2.2 如何使用

服务端session + 客户端 sessionId

session
  1. 用户向服务器发送用户名和密码

  2. 服务器验证通过后,在当前对话(session)里面保存相关数据,比如用户角色, 登陆时间等

  3. 服务器向用户返回一个session_id, 写入用户的cookie

  4. 用户随后的每一次请求, 都会通过cookie, 将session_id传回服务器

  5. 服务端收到 session_id, 找到前期保存的数据, 由此得知用户的身份

OAuth✔️

认证:你怎么证明你是你自己❓❓❓

在这里插入图片描述

OAuth 是一种行业标准的授权方式。这其中有三个角色:客户端,服务端,授权服务器

在这里插入图片描述

code 认证方式流程

在这里插入图片描述

以 github 授权为例

登录你的 github 账号,点击头像下拉列表的 settings --> 左侧边栏的 Developer settings --> 左侧边栏的 OAuth Apps --> 点击 New OAuth App
在这里插入图片描述

生成的 Client ID 和 Client secrets,会用在后续的请求中。

在这里插入图片描述

// 固定URL
const GITHUB_OAUTH_URL = 'https://github.com/login/oauth/authorize'
// 需要的权限。如果没有赋值,则是最简单的权限,user 则是用户权限
const SCOPE = 'user'

// github 上注册的
const client_id = '7a4**********013866'

module.exports = {
  github: {
    client_id: '7a4**********013866',
    client_secret: 'a9fce********************7620b1f9e717',
    request_token_url: 'https://github.com/login/oauth/access_token'
  },
  GITHUB_OAUTH_URL,
  OAUTH_URL: `${GITHUB_OAUTH_URL}?client_id=${client_id}&scope=${SCOPE}`
}

更多资料请访问:https://www.ruanyifeng.com/blog/2019/04/oauth-grant-types.html

Token🔰

JWT 的 全称是 JSON Web Token ,一个 JWT 由 dot(.) 分隔的三个部分组成 :

  • Header(头部)
  • Payload(载荷)
  • Signature(签名)

xxxxxx.yyyyyyyy.zzzzzzz

const sign = HMACSHA256( base64.encode(header) + "." + base64.encode(payload),secret)
const jwt = base64.encode(header) + "." + base64.encode(payload) + "." + sign
Header (头部)

Header 是一个 JSON 对象

{
  "alg": "HS256", // 表示签名的算法,默认是 HMAC SHA256(写成 HS256)
  "typ": "JWT"  // 表示Token的类型,JWT 令牌统一写为JWT
}
Payload(载荷)

Payload 部分也是一个 JSON 对象,用来存放实际需要传递的数据。包含用户的一些信息,以及 Token 的过期时间

{
  // 7个官方字段
  "iss": "a.com", // issuer:签发人
  "exp": "1d", // expiration time: 过期时间
  "sub": "test", // subject: 主题
  "aud": "xxx", // audience: 受众
  "nbf": "xxx", // Not Before:生效时间
  "iat": "xxx", // Issued At: 签发时间
  "jti": "1111", // JWT ID:编号
  // 可以定义私有字段
  "name": "John Doe",
  "admin": true
}
Signature (签名)

Signature 是对前两部分的签名,防止数据被篡改。

首先,需要指定一个密钥(secret)。这个密钥只有服务器才知道,不能泄露给用户。然后使用Header里面指定的签名算法(默认是 HMAC SHA256),按照下面的公式产生签名。

HMACSHA256(base64UrlEncode(header) + "." + base64UrlEncode(payload), secret)

算出签名后,把 Header、Payload、Signature 三个部分拼成一个字符串,每个部分之间用"点"(.)分隔,就可以返回给用户。

JWT = Base64(Header) + "." + Base64(Payload) + "." + $Signature
jwt.io网站

过程

Token
  1. 用户通过用户名和密码发送请求
  2. 程序验证通过
  3. 返回一个签名的token给客户端
  4. 客户端储存token, 并且每次发送请求都会携带 token
  5. 服务端验证Token通过并返回数据
优缺点

优点

  • 易扩展
  • 支持移动设备
  • 跨应用调用
  • 安全
  • 承载信息丰富
  • 防 CSRF
  • 适合移动端应用
  • 无状态,编码数据

缺点

  • 刷新与过期处理
  • Playload不易过大
  • 中间人攻击

在框架中的应用

koa + jwt

npm install koa-jwt --savenpm install jsonwebtoken --save

const jsonwebtoken = require('jsonwebtoken')

/**
 * 登录
 * @param {Object} ctx 
 */
async login(ctx) {
  // 校验参数(看具体情况)
  ctx.verifyParams({
    name: { type: 'string', required: true },
    password: { type: 'string', required: true }
  })
  const user = await User.findOne(ctx.request.body)
  if (!user) {
    ctx.throw(401, '用户不存在或密码不正确')
  }
  const { _id, name } = user
  // secret =》 秘钥,可以写在一个配置文件中
  // expiresIn 过期时间
  const token = jsonwebtoken.sign({ _id, name }, secret, {
    expiresIn: '12h'
  })
  // 当用户登录时,校验通过,返回一个 token,在请求之后的接口中,通过校验 token 来确定用户的身份。
  ctx.body = { token }
}

config.js

module.exports = {
  connectionStr: 'mongodb://localhost/zhihu',
  secret: 'zhihu-jwt-secret'
}

校验 token

比如有一个评论接口,那么就可以在做真正的数据操作之前,使用中间件进行 token 判断

const jwt = require('koa-jwt')
const { secret } = require('../config.js')

const auth = jwt({secret})

router.post('/', auth, create)
express + jwt

npm install express-jwt --savenpm install jsonwebtoken --save

如果上面每一个接口路由都需要加一个判断,那未免有点麻烦。比如对于一个后台管理系统,只有登录才可以进行操作。此时,可以在全局做登录校验。

例如:

登录接口

const jwt = require('jsonwebtoken')

router.post('/login', function (req, res, next) {
  const username = req.body.username
  const password = req.body.password
  Admin.find({ username }, function (err, doc) {
    if (err) {
      res.json({
        status: '1',
        msg: '管理员不存在'
      })
    } else {
      const dataPassword = doc[0].password
      if (password === dataPassword) {
        // PRIVATE_KEY => 秘钥
        // JWT_EXPIRED => 过期时间
        const token = jwt.sign(
          { username },
          PRIVATE_KEY,
          { expiresIn: JWT_EXPIRED }
        )
        new Result({ token }, '登录成功').success(res)
      }
    }
  })
})

app.js(整个项目的入口主文件)

const router = require('./router')

app.use('/', router)

router / index.js

const jwtAuth = require('./jwt')

// 首先进行登录校验(放在所有 use 的第一个)
router.use(jwtAuth)


// Token验证失败时,使用 use 捕获错误(捕获错误可以放在最后一个)
router.use((err, req, res, next) => {
  console.log(err)
  if (err.name && err.name === 'UnauthorizedError') {
    const { status = 401, message } = err
    new Result(null, 'Token验证失败', {
      error: status,
      errMsg: message
    }).jwtError(res.status(status))
  }
})

jwt.js

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

module.exports = jwt({
  secret: PRIVATE_KEY,
  credentialsRequired: true
}).unless({
  // 以下接口不需要做登录校验
  path: [
    '/',
    '/user/login'
  ]
})
egg + jwt

Controller 控制器

npm install egg-jwt --save

// 设置 jwt
async jwtSign({ id }) {
  const { ctx, app } = this;
  const username = ctx.params('username');
  const token = app.jwt.sign({
    id,
    username
  }, app.config.jwt.secret);
  // app.config.jwt.secret => 秘钥
  return token;
}

// 登录接口
async login() {
  const { ctx, app } = this;
  const { username, password } = ctx.params();
  const user = await ctx.service.user.getUser(username, password);
  if (user) {
    const token = await this.jwtSign({
      id: user.id,
      username: user.username
    });
    this.success({
      ...this.parseResult(ctx, user),
      token
    });
  } else {
    this.error('该用户不存在');
  }
}
koa + jwt + 小程序

npm install jsonwebtoken --savenpm install basic-auth --save

小程序有点区别于平常的 web 开发。在生成的小程序目录中,有一个 app.js 。这是小程序的入口文件。

小程序启动时,会触发 App 中的 onLaunch 函数。

//app.js
App({
  // 是否存在 token
  onLaunch: function () {
    if (!wx.getStorageSync('token')) {
      this.onGetToken()
    }
  },

  // 获取 token
  onGetToken() {
    wx.login({
      success: (res) => {
        if (res.code) {
          wx.request({
            url: 'http://localhost:3000/v1/token',
            method: 'POST',
            data: {
              account: res.code,
              type: 100
            },
            success: (res) => {
              const code = res.statusCode.toString()
              if (code.startsWith('2')) {
                wx.setStorageSync('token', res.data.token)
              }
            }
          })
        }
      }
    })
  },
   
  // 校验 token
  onVerifyToken() {
    wx.request({
      url: 'http://localhost:3000/v1/token/verify',
      method: 'POST',
      data: {
        token: wx.getStorageSync('token')
      },
      success: (res) => {
      }
    })
  },
})

对 http 做一些必要的封装

import { config } from '../config.js'

class HTTP {
  request(params) {
    if (!params.method) {
      params.method = 'GET'
    }
    wx.request({
      url: config.api_base_url + params.url,
      method: params.method,
      data: params.data,
      header: {
        'Authorization': config.Authorization
      },
      success:(res) => {
        let code = res.statusCode.toString()
        if (code.startsWith('2')) {
          params.success && params.success(res.data)
        } else {
          let error_code = res.data.error_code
          this._show_error(error_code)
        }
      },
      fail: (res) => {
        this._show_error(1)
      }
    })
  }
}

export { HTTP }

对使用的 token 做一些处理

import { Base64 } from 'js-base64'

// 获取存储的 token
let token = wx.getStorageSync('token')
let base64 = Base64.encode(token + ':')
let w_base = "Basic " + base64

const config = {
  // 服务器地址
  api_base_url: 'http://localhost:3000/v1/',
  Authorization: w_base
}

module.exports =  {
  config
}

使用

getMyFavor(success) {
  let params = {
    url: 'classic/favor',
    success: success
  }
  this.request(params)
}

后端

获取 token 接口

const { WXManager } = require('../../services/wx')

// 获取 token
router.post('/', async (ctx) => {
  // 校验参数(具体看情况)
  const v = await new TokenValidator().validate(ctx)
  let token = await WXManager.codeToToken(v.get('body.account'))
  ctx.body = {
    token
  }
})

wx.js

const util = require('util')
const axios = require('axios')

const { User } = require('../models/user')
const { generateToken } = require('../../core/util')
const { Auth } = require('../../middlewares/auth')

class WXManager {
  static async codeToToken(code) {
    // code 小程序生成 微信
    // openid 唯一标识

    // 显示注册
    // 唯一标识
    // code
    // appid
    // appsecret
    // url
    const url = util.format(global.config.wx.loginUrl,
      global.config.wx.appId,
      global.config.wx.appSecret,
      code)

    const result = await axios.get(url)

    if (result.status !== 200) {
      throw new global.errs.AuthFailed("openid获取失败")
    }

    const errCode = result.data.errcode
    const errMsg = result.data.errmsg
    if (errCode) {
      throw new global.errs.AuthFailed("openid获取失败: " + errMsg)
    }

    // opedId
    // 建立档案 user uid
    // openId

    // 判断数据库是否存在微信用户 opendid
    let user = await User.getUserByOpenid(result.data.openid)

    // 如果不存在,就创建一个微信小程序用户
    if (!user) {
      user = await User.createUserByOpenid(result.data.openid)
    }

    return generateToken(user.id, Auth.AUSE)
  }
}

module.exports = {
  WXManager
}

generateToken.js

// 颁发令牌
const generateToken = function (uid, scope) {
  const secretKey = global.config.security.secretKey
  const expiresIn = global.config.security.expiresIn
  const token = jwt.sign({
    uid,
    scope
  }, secretKey, {
    expiresIn
  })
  return token
}

module.exports = {
  generateToken
}

auth.js(校验 token)

const basicAuth = require('basic-auth')
const jwt = require('jsonwebtoken')

class Auth {
  constructor() {}

  get m() {
    // token 检测
    // token 开发者 传递令牌
    // token body header
    // HTTP 规定 身份验证机制 HttpBasicAuth
    return async (ctx, next) => {
      const tokenToken = basicAuth(ctx.req)

      let errMsg = "token不合法"

      // 无带token
      if (!tokenToken || !tokenToken.name) {
        throw new global.errs.Forbidden(errMsg)
      }

      try {
        var decode = jwt.verify(tokenToken.name, global.config.security.secretKey)

      } catch (error) {
        // token 不合法 过期
        if (error.name === 'TokenExpiredError') {
          errMsg = "token已过期"
        }

        throw new global.errs.Forbidden(errMsg)
      }

      ctx.auth = {
        uid: decode.uid,
      }

      await next()
    }
  }

  // 验证token是否有效
  static verifyToken(token) {
    try {
      jwt.verify(token, global.config.security.secretKey)

      return true;
    } catch (e) {
      return false
    }
  }

}

module.exports = {
  Auth
}

需要使用登录校验的接口

router.get('/latest', new Auth().m, async (ctx, next) => {
 // .....
})
koa + session

当登录成功的时候,将用户的信息写入到 session 中。

/**
 * 登录
 * @param {Object} ctx koa2 ctx
 * @param {string} userName 用户名
 * @param {string} password 密码
 */
async function login(ctx, userName, password) {
  // 获取用户信息
  const userInfo = await getUserInfo(userName, doCrypto(password))
  if (!userInfo) {
    // 登录失败
    return new ErrorModel(loginFailInfo)
  }

  // 登录成功
  if (ctx.session.userInfo == null) {
    ctx.session.userInfo = userInfo
  }
  return new SuccessModel()
}

需要做登录校验的时候,可为其封装一个中间件,用作登录检查。

/**
 * API 登录验证
 * @param {Object} ctx ctx
 * @param {function} next next
 */
async function loginCheck(ctx, next) {
  if (ctx.session && ctx.session.userInfo) {
    // 已登录,则进行下一步
    await next()
    return
  }
  // 未登录
  ctx.body = new ErrorModel(loginCheckFailInfo)
}

处理路由的时,可以使用中间件进行登录校验

router.post('/create', loginCheck, async (ctx, next) => {
  const { content, image } = ctx.request.body
  const { id: userId } = ctx.session.userInfo
  ctx.body = await create({ userId, content, image })
})

提到了 session,那不妨也了解一下 cookie。二者的设置其实差别不大。

express + cookie
router.post('/login', function (req, res, next) {
  var param = {
    userName: req.body.userName,
    userPwd: req.body.userPwd
  }
  User.findOne(param, function (err, doc) {
    if (err) {
      res.json({
        status: '1',
        msg: err.message
      })
    } else {
      if (doc) {
        res.cookie('userid', doc._id, {
          path: '/',
          maxAge: 1000 * 60 * 60
        })
        res.json({
          status: 0,
          msg: '',
          result: {
            userName: doc.userName
          }
        })
      }
    }
  })
})

app.js(入口主文件)

要放在路由的前面使用

app.use(function (req, res, next) {
  if (req.cookies.userName) {
    next()
  } else {
     // 路由白名单
    if (req.originalUrl === '/users/register' || req.originalUrl === '/users/login' || req.originalUrl === '/users/logout' || req.path === '/goods/list' || req.originalUrl === '/goods/getBannerImage') {
      next()
    } else {
      res.json({
        status: '100001',
        msg: '当前未登录',
        result: ''
      })
    }
  }
})
  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值