常用鉴权方式

181 阅读6分钟

目前我们所接触到的鉴权的方式有两种实际工作当中也是这两种

  1. JWT
  2. cookie&session

无状态的http

http请求是无状态,也就是说我们每一次的http请求都是临时的,前面的请求和后面的请求每一次请求之间是没有关系的,http无法记录,说白了http不知道你到底是谁,没法记录到底是谁发送的请求不知道客户端是谁!

但是实际情况是我们很多时候需要知道客户端是谁,比如说我们要加购物车、发布评论、留言、修改个人资料!

这个时候该怎么办?

客户端来了请求了,我们服务端给你个“东西”,以后你要来请求请你拿着我给你的这个东西,那么服务端是不是就认识你了呀?

这里的这个“东西”指的就是cookie,在我们的开发中它指的是本地(浏览器)的一小块数据!

cookie

什么是cookie

官方的定义:HTTP Cookie(也叫 Web Cookie 或浏览器 Cookie)是服务器发送到用户浏览器并保存在本地的一小块数据。浏览器会存储 cookie 并在下次向同一服务器再发起请求时携带并发送到服务器上。通常,它用于告知服务端两个请求是否来自同一浏览器——如保持用户的登录状态。Cookie 使基于无状态的 HTTP 协议记录稳定的状态信息成为了可能。

paper.seebug.org/227/

cookie交互流程

3和4后续所有需要鉴权的接口走的都是3和4

  1. 浏览器发送一个请求到服务器
  2. 服务器响应并在httpresponse中增加一个响应头set-cookie: 唯一标识(加密过的)
  3. 浏览器保存此cookie在本地,然后每次请求都会自动携带该cookie在request中的请求头
  4. 服务器收到请求便可读取该cookie的内容,然后做出对应的逻辑处理(这次请求鉴权是否通过、它要的是什么数据?)再返回给前端

cookie是用来保持用户状态的,而这个状态是依赖于浏览器的存储功能

交互案例

  1. 后端代码

前端登录成功,后端返回cookie在response响应头中set-Cookie

// 导入 express 模块
const express = require('express') // node的框架
const bodyParser = require('body-parser') // 解析body体的参数
const cors = require('cors')
const cookieParser = require('cookie-parser') // 获取cookie
const app = express() // 创建服务
app.use(bodyParser())
app.use(cookieParser())
app.use(cors()) // 简单粗暴的解决跨域问题
const port = 3000 // 端口
app.listen(port, () => {
    console.log(`服务运行在:${port}`)
})

app.post('/api/login', (req, res, next) => {
    // 判断客户端提交的用户名和密码是否正确,如果正确我们就给客户端返回一个cookie,错误了就做一个提示
    if (req.body.username !== 'admin' || req.body.password !== '000000') {
        return res.send({
            status: 400,
            msg: '登录失败'
        })
    }
    // 登录成功
    res.cookie('name', req.body.username)
    res.send({
        status: 200,
        msg: '登录成功'
    })
})
  1. 前端代码
<template>
    <button @click="login">登陆</button>
</template>
<script setup>
import { reactive } from 'vue'
import { useRouter } from 'vue-router';
import Axios from 'axios'
const router = useRouter()
function login() {
    Axios.post('http://localhost:3000/api/login', {
        username: 'admin',
        password: '000000'
    }).then(res => {
        console.log(res)
     
    })
}
</script>

后续需要鉴权的接口会自动携带cookie

此案例遇到了跨域的问问题导致cookie携带失效,我们使用代理解决了跨域!(前端来解决的跨域)

如果说cookie的鉴权出现了跨域后端来解决:

  1. https(安全模式)

  2. 服务端需要做配置

  3. 客户端需要的做的就是在axios中配置: withCredentials: true, // // withCredentials 表示跨域请求时是否需要使用凭证

为什么不能只使用cookie鉴权?

通过以上案例我们其实已经实现了只有cookie的鉴权,但是为什么还需要session呢?

其实归根接地是为了安全,我们的案例中第一:把唯一标识给明文存储到了客户端;第二:所有用客户端做安全的都是耍流氓

因为安全的问题在cookie的基础上就诞生了session

伪造cookie绕过登录

前提:我们是为了演示所以用的都是明文的cookie而且是没有加密的cookie

  1. 把application中的cookie清空再刷新需要鉴权的接口会提示“无权限,需要登录”
  2. 伪造cookie在客户端
document.cookie="name=admin"

通过以上代码就可以在客户端写入cookie了,你再刷新需要鉴权的接口就是可以获取到不该获取到的数据了,这就是最简单的cookie篡改

什么是cookie篡改?

但凡是通过cookie攻击了应用的方式都成为cookie篡改

cookie安全

比如cookie可以加密

按时不管你怎么加密总归是在客户端,还是不够安全

所以就有了session和cookie的配合

什么是session?

Session是一种在服务器端保存数据的机制,用来跟踪用户状态的数据结构,可以保存在文件、数据库或者集群中。我的抖音项目是保存在redis

session和cookie的交互流程

  1. 用户第一次请求服务器的时候,服务器校验用户提交的账号和密码,校验通过,服务器生成session和sessionId,服务端会将session存储在数据库中

session里面一般存储的是用户的相关信息(用户名、UserId等等),sessionId相对于session而言,可以把sessionId当做是一把钥匙,session就是数据,只有通过sessionId才能获取到session的数据

  1. 服务端将sessionId存放在响应头中response对象中,set-Cookie发给客户端,客户端收到cookie(sessionId)以后会存放在application(cookie是根据你的域名的)当中。同时cookie会记录次sessionId属于哪个域名
  2. 之后每次的http请求都会自动携带cookie在request中有一个cookie(sessionId)的key
  3. 服务端收到请求时候获取到cookie,也就是cookie中的sessionId,当然是从session的数据库中校验sessionId是否存在,如果存在那就是鉴权成功,然后处理接口相关的逻辑,最后返回给客户端

交互案例

  1. 后端代码
// 导入 express 模块
const express = require('express') // node的框架
const bodyParser = require('body-parser') // 解析body体的参数
const cors = require('cors')
const cookieParser = require('cookie-parser') // 获取cookie
const session = require('express-session') // 使用session
const app = express() // 创建服务
app.use(bodyParser()) // 解析body体的参数
app.use(cookieParser()) // 解析cookie
app.use(cors()) // 简单粗暴的解决跨域问题
app.use(session({
    secret: 'haohaoxuexitiantianxiangshang', // 秘钥
    resave: false, // 会不会覆盖
    saveUninitialized: true // 
}))
const port = 3000 // 端口
app.listen(port, () => {
    console.log(`服务运行在:${port}`)
})

// 登录接口
app.post('/api/login', (req, res, next) => {
    // 判断客户端提交的用户名和密码是否正确,如果正确我们就给客户端返回一个cookie,错误了就做一个提示
    if (req.body.username !== 'admin' || req.body.password !== '000000') {
        return res.send({
            status: 400,
            msg: '登录失败'
        })
    }
    // 登录成功
    req.session.user = req.body // session里面存储的是用户的信息
    req.session.isLogin = true // 用户的登录状态,也就是我们鉴权的依据
    console.log(req.session)
    res.send({
        status: 200,
        msg: '登录成功'
    })
})
// 需要鉴权的接口 - 获取用户名的接口
app.get('/api/username', (req, res, next) => {
    
    // 如果前端request的时候携带了cookie那么就证明这个人以后权限获取username接口的数据
    if (!req.session.isLogin) {
        return res.send({
            status: 401,
            msg: '无权限,请登录'
        })
    }
    res.send({
        status: 200,
        msg: 'sucess'
    })
})

2 客户端代码

<template>
    <h1>首页</h1>
</template>
<script>
import { useRouter } from 'vue-router'
import Axios from 'axios'
const router = useRouter()
Axios.get('/api/username').then(res => {
    console.log(res)
})
</script>
<template>
    <button @click="login">登陆</button>
</template>
<script setup>
import { reactive } from 'vue'
import { useRouter } from 'vue-router';
import Axios from 'axios'
const router = useRouter()
function login() {
    Axios.post('/api/login', {
        username: 'admin',
        password: '000000'
    }).then(res => {
        console.log(res)
     
    })
}
</script>
import { defineConfig } from 'vite'
import vue from '@vitejs/plugin-vue'

// https://vitejs.dev/config/
export default defineConfig({
  plugins: [vue()],
  server: {
        proxy: {
            "/api": {
                target: "http://localhost:3000/",
                changeOrigin: true,
                rewrite: (path) => path.replace(/^/api/, "api"),
            },
        },
    },
})

跨域

cookie鉴权有一个限制:

cookie实现跨域共享要求根域名必须一样才行,比如说www.baidu.com和map.baidu.com,如果你的cookie在他俩的根域名那么这两个域名才能共享cookie,根域名是*.baidu.com(a.baidu.com、b.baidu.com)

我们的项目是:

服务端域名是:http://127.0.0.1:3000/

客户端域名是:http://127.0.0.1:5173/

服务单的域名是:usian.net

客户端的域名是:douyin.usian.net

或者是服务端和客户端在同一个目录

cookie的安全性和劣势

cookie是存在客户端的,客户端的东西都是不安全的!因此敏感的数据(密码)一定不要存在cookie里面

cookie能否提高访问服务端的效率,但是安全性差

cookie劣势:

  1. 每次请求都会自动携带cookie,无形中增加了流量的开销啊
  2. http中的cookie是明文传输的,最好是使用https
  3. cookie存储量较小,只有4kb,对于存储的cookie是非常有限的(大家在回答cookie只有4kb的时候不要说,cookie的存储量是因浏览器而已的,建议大家查资料或者是测试一下developer.mozilla.org/zh-CN/docs/…

由于cookie的不安全以及存储的问题,所有现在常用的鉴权方式就是JWT!

cookie和session的区别

保存的位置 cookie存在客户端 session存在服务端

安全性角度 session的安全性更高

保存内容的类型不同 cookie只能存储字符串 session任意类型

保存内容的大小 cookie有限制4kb session基本无限制

性能角度 session对于服务端压力更大一些

cookie和session的联系

sessionID会被保存在cookie中,请求的时候浏览器自动携带cookie也就是sessionId到服务器,服务器会从存放session的数据库中查找到对应的session信息

1. JWT(现在常用的鉴权方式)

1.1. 什么是JWT

JSON Web Token(缩写 JWT)是目前最流行的跨域认证解决方案

JWT其实就是后端生成一段加密的字符串,通过这个字符串来验证用户的身份信息从而能够让该用户合法的获得需要的数据资源!

1.2. 组成

Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9 // 消息头
.eyJ1aWQiOjUwMCwicmlkIjozMCwiaWF0IjoxNjg3ODMzMTM4LCJleHAiOjE2ODc5MTk1Mzh9 // 消息体(用户Id、角色信息等等)
.BwUo3TlD1oue1uvHLnqG9UWzm-iXFosxt3ca00yHpV4 // 签名,安全的部分防止被篡改

1.3. 作用

鉴权

1.4. 工作原理

通过上述流程我们发现了和cookie&session不同的地方,服务器不需要再开辟session数据的存储用户信息,而是把用户的信息对象加密之后存在了客户端的storage

1.5. 使用

登录案例

  1. 服务端生成token给到客户端
  2. 客户端存储在storage
  3. 后续请求在请求头携带token到服务端
  4. 完成鉴权

1.5.1. express创建项目

express --no-view login627

1.5.2. 下载依赖

cnpm i express-jwt@5.3.3 jsonwebtoken express

1.5.3. 导入需要的包定义秘钥

const jwt = require('jsonwebtoken') 
const expressJWT = require('express-jwt')
// 自定义秘钥
const secretKey = 'caoshichengbuhaohaoxuexileyoujulimobanyuelaiyuejinle'

1.5.4. 生成JWT字符串

// 生成token
let tokenStr = jwt.sign({ username: username }, secretKey, { expiresIn: '0.5h' })

1.5.5. 编写登录接口

var express = require('express');
var router = express.Router();
const jwt = require('jsonwebtoken')
const expressJWT = require('express-jwt')
// 自定义秘钥
const secretKey = 'caoshichengbuhaohaoxuexileyoujulimobanyuelaiyuejinle'

/* GET home page. */
router.get('/', function(req, res, next) {
    res.render('index', { title: 'Express' });
});

// 登录接口
router.post('/login', function(req, res) {
    let { username, password } = req.body
    // 验证数据
    if (username && username === 'admin' && password && password === "123456") {
        // 生成token
        let tokenStr = jwt.sign({ username: username }, secretKey, { expiresIn: '0.5h' })
        // 返回给前端
        res.send({
          status: 200,
          msg: '登录成功',
          data: {
            token: `Bearer ${tokenStr}` // 格式
          }
        })
    } else {
      res.send({
        status: 400,
        msg: '用户名或密码错误'
      })
    }
})

module.exports = router;