登录鉴权

登录鉴权

  • cookie
  • session
  • token
  • HTTP 是无状态的协议,每个请求相互独立,服务器端不会保留用户信息。服务器和浏览器为了进行会话跟踪,需要主动去维护访问的状态,用于告知服务端前后两个请求是否来自同一浏览器。这个状态需要 cookie 或 session 实现
  • cookie 存储在客户端,cookie 是服务器发送到用户浏览器并保存在本地的一小块数据,会在浏览器下次向同一服务器发送请求时被携带并发送到服务器上
  • cookie 是不可跨域的,cookie 会绑定单一的域名,无法在别的域名下使用,一级域名与二级域名是可以共享使用 cookie
属性 说明
name = value 键值对,设置 cookie 的名称和对应的值,字符串类型
domain 指定 cookie 的域名,默认是当前域名
path 指定 cookie 在哪个路径下生效,默认是 '/',意味着 '/ab','/ac/aa'都能共享 cookie
maxAge cookie 失效时间,单位为秒,默认为 -1,表示临时 cookie,关闭浏览器失效,如果为正整数,表示几秒后失效,如果为 0,表示删除该 cookie
expires 过期时间,在某个时间点后失效
httpOnly 无法通过 js 获取该信息,但还是可以在浏览器中手动修改 cookie,相对安全

cookie 特点

  • 保存在本地,关闭浏览器还是存在
  • 正常不加密,用户可以看到
  • 用户可以手动修改、删除、禁用 cookie
  • 可以被篡改和攻击
  • 存储量小,4k
  • 发送请求自动带上登陆信息

使用 cookie
使用 cookie-parser

  1. 安装
npm i cookie-parser
  1. 引入
const cookieParser = require('cookie-parser')
  1. 设置中间件
app.use(cookieParser())
  1. 设置 cookie
res.cookie("name":"admin",{maxAge: 60 * 60,httpOnly: true})
  1. 获取 cookie
req.cookie.name

实例:

const cookieParser = require('cookie-parser')
const express = require('express')

const app = express()
app.use(cookieParser())
// 设置 cookie
app.get('/set-cookie', (req, res) => {
    res.cookie('name','liqiang',{maxAge: 60 * 60}) // 保存时间
    res.send('ok')
})
// 删除 cookie
app.get('/delete-cookie', (req, res) => {
    res.clearCookie('name')
    res.send('ok')
})
// 获取 cookie
app.get('/get-cookie', (req, res) => {
    res.send(req.cookies)
})

app.listen(81)

cookie 加密

  • 在保存 cookie 时手动加密,例如使用 MD5 加密
const cookieParser = require('cookie-parser')
const express = require('express')
const md5 = require('md5')

const app = express()
app.use(cookieParser())

app.get('/set-cookie', (req, res) => {
    res.cookie('name',md5('liqiang'),{maxAge: 60 * 60}) // 保存时间
    res.send('ok')
})

  • 在使用 cookieParser 中间件时,添加一个加盐字符串,在设置 cookie 配置项时,将 signed 设置为 true
const cookieParser = require('cookie-parser')
const express = require('express')

const app = express()
app.use(cookieParser('salt'))

app.get('/set-cookie', (req, res) => {
    res.cookie('name','liqiang',{maxAge: 1000 * 60,signed:true}) // 保存时间
    res.send('ok')
})

session

  • session 保存在服务器上。客户端访问服务器时,服务器把客户端信息记录在服务器上,客户端再次访问时只需要从 session 中 查找该客户的状态就可以了,session 是一种特殊的 cookie。cookie 保存在客户端,session 保存在服务端

认证流程

  • 用户第一次请求服务器时,服务器根据用户提交的相关信息,创建 session
  • 请求返回时将 sessionid 返回给浏览器
  • 浏览器接收到 sessionid 后,保存到 cookie 中,同时 cookie 记录 sessionid属于那个域名
  • 当用户第二次访问服务器时,请求自动判断域名下是否存在 cookie 信息,如果存在自动将 cookie 发送给服务端,服务端从 cookie 中获取 sessionid,根据 sessionid 查找 session 信息,如果没找到说明用户没有登陆或者登录失效,如果找到证明用户已经登录可以进行下一步操作

cookie 和 session 的区别

  • 安全性 session 比 cookie 安全,session 存在服务器,cookie 保存在客户端
  • 存取值类型不同 cookie只能存取字符串,session 可以存任意类型
  • 网络传输量不同 cookie 内容设置过多会增大报文体积,影响传输,session 只通过 cookie 传递 id,不影响传输效率
  • 有效期不同 cookie 可以设置为长时间保持,session 失效时间短

session 的使用

  1. 安装 express-session
npm i express-session
  1. 引入
const session = require('express-session')
  1. 设置 session
app.use(session(options))

通过 options 来设置 session 存储,只会记录 sessionid 保存到 cookie 中
options 参数

cookie: {
    // 默认为{ path: '/', httpOnly: true, secure: false, maxAge: null }
    maxAge: 设置给定过期时间的毫秒数(date)
    expires: 设定一个utc过期时间,默认不设置,http>=1.1的时代请使用maxAge代替之(string)
    path: cookie的路径(默认为/)(string)
    domain: 设置域名,默认为当前域(String)
    sameSite: 是否为同一站点的cookie(默认为false)(可以设置为['lax', 'none', 'none']或 true)
    secure: 是否以https的形式发送cookie(false以http的形式。true以https的形式)true 是默认选项。 但是,它需要启用 https 的网站。 如果通过 HTTP 访问您的站点,则不会设置 cookie。 如果使用的是 secure: true,则需要在 express 中设置“trust proxy”。
    httpOnly: 是否只以http(s)的形式发送cookie,对客户端js不可用(默认为true,也就是客户端不能以document.cookie查看cookie)
    signed: 是否对cookie包含签名(默认为true)
    overwrite: 是否可以覆盖先前的同名cookie(默认为true)*/
},
// 默认使用 uid-safe 自动生成 id
genid: req => genuuid(),
// 设置会话的名字,默认是 connect.sid
name: 'value',
// 设置安全 cookies 时信任反向代理,默认未定义
proxy: undefined,
// 是否强制保存会话,未修改也要保存,默认为 true
resave: true,
// 强制将未初始化的绘画保存到存储中,默认为true
saveUninitialized: true,
// 用于生成会话签名的密钥
secret: 'secret',
// 会话存储实例,默认为 new MemoryStore
store: new MemoryStore(),
// 是否保存会话,默认 keep,不保存可以设置 destory
unset: 'keep'

案例:

  • 使用 mongodb 保存 session 信息
const session = require('express-session')
const MongoStore = require('connect-mongo')
const express = require('express')

const app = express()
app.use(session({
    name: 'sessionid', // cookie的name
    secret: 'salt', // 密钥,提高加密等级,加盐
    saveUninitialized: false, // 是否为每个请求都创建新的cookie来存储session的id
    resave: true, // 重新保存,每次请求重新保存session,更新session
    store: MongoStore.create({
        mongoUrl: 'mongodb://localhost:27017/project' // 数据库连接配置
    }),
    cookie: {
        httpOnly: true,
        maxAge: 1000 * 60 * 1 // 设置session过期时间,数据库中也会过期
    }
}))

app.get('/', (req, res) => {
    res.send('home')
})

app.get('/login', (req, res) => {
    // 为了方便,直接在地址中输入登录信息进行演示
    if (req.query.username === 'admin' && req.query.password === 'admin') {
        // 设置 session 信息
        req.session.username = 'admin'
        res.send('成功')
    } else {
        res.send('失败')
    }
})

app.get('/cart', (req, res) => {
    if (req.session.username) {
        res.send('购物车')
    } else {
        res.redirect('/login')
    }
})

app.get('/logout', (req, res) => {
    // 删除 session
    req.session.destroy(() => {
        res.send('logout success')
    })
})

app.listen(3000, () => console.log('running on 3000'))

在地址栏输入信息后,session 会自动保存到数据库中

  • 使用 mysql 存储 session,只有中间件的配置项不一样,展示部分代码
const session = require('express-session')
const Store = require('express-mysql-session')(session)
const express = require('express')

const app = express()

// 数据库配置项
const options = {
    host:"localhost",
    user:"root",
    password:"sql2008",
    port:"3306",
    database:"books"
}
app.use(session({
    name: 'sessionid', // cookie的name
    secret: 'salt', // 密钥,提高加密等级,加盐
    saveUninitialized: false, // 是否为每个请求都创建新的cookie来存储session的id
    resave: true, // 重新保存,每次请求重新保存session,更新session
    store: new Store(options),
    cookie: {
        httpOnly: true,
        maxAge: 1000 * 60 * 1 // 设置session过期时间,数据库中也会过期
    }
}))

在地址栏输入信息后,session 会自动保存到数据库中

session 的问题

  • 每个用户的登录信息会保存到服务器的 session 中,用户增多服务器压力增大
  • session 存在服务器的物理内存中,在分布式系统中将会失效
  • 对于非浏览器的客户端、移动端不适用,session 依赖 cookie,而移动端没有 cookie
  • session 本质基于 cookie,拦截后容易被跨站请求伪造攻击,浏览器禁用 cookie,也会使得 session 失效
  • session 的认证基于 cookie,cookie 无法跨域,所以 session 也不能跨域

token

token 是 访问资源接口所需要的资源凭证

验证流程

  • 客户端使用用户名和密码请求登录
  • 服务端收到请求去验证用户名和密码
  • 验证成功后,服务端签发一个 token 并把 token 发送给客户端
  • 客户端收到 token 后会将它存储起来,放在 cookie 或者 localStorage 中
  • 服务器端每次向请求端请求资源时将 token 一同传输
  • 服务端收到请求后去验证 token 是否正确,验证成功后向客户端返回资源
  • 每一次请求都要携带 token,需要将 token 存放到 HTTP 的 header 中
  • 基于 token 的用户认证是一种服务器端无状态的认证方式,服务器端不用存放 token 数据,用解析 token 的计算时间换取 session 的存储空间,减轻服务器的压力,减少频繁查询数据库

JWT

基于 token 的身份验证

认证流程

  • 前端通过表单将自己的用户名和密码发送到后端的接口,一般是 post 请求,建议使用 ssl 加密的传输(https)
  • 后端核对用户名和密码成功后,将包含用户信息的数据作为 JWT 的 Payload,将其与 JWT Header 分别进行 Base64 编码拼接后签名,形成 JWT Token,大致类似: header.Payload.Signature
  • 后端将 JWT Token 字符串作为登陆成功的结果返回给前端。前端可以将返回的结果保存在浏览器中,退出登录时删除保存的 JWT Token
  • 前端每次请求时将 JWT Token 放入 http 请求头中的Authorization 中(解决 XSS 和 XSRF 问题)
  • 后端检查前端传来的 JWT Token,验证有效性,验证通过后解析出用户信息,返回结果

token 和 jwt比较

相同

  • 访问资源的令牌
  • 记录用户信息
  • 使服务端无状态化
  • 只有验证成功后,客户端才能访问服务端的资源

区别

  • token:服务端验证客户端发送过来的 token 时,需要查询数据库获取用户信息,验证 token 有效性
  • jwt:将 token 和 payload 加密后存储与客户端,服务端只需要使用密钥解密校验即可

简单示例:

const jwt = require('jsonwebtoken')

let token = jwt.sign({
    username:'zhangsan'
},'salt',{
    expiresIn: 100
})
console.log(token)

jwt.verify(token,'salt',(err,data)=>{
    if(err) return console.log('校验失败',err)
    console.log('校验成功',data)
})
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6InpoYW5nc2FuIiwiaWF0IjoxNjg0Mzg5NzUyLCJleHAiOjE2ODQzODk4NTJ9.ejbsTMTinT7PXJ0AoH2g0OizDU5xFu10Ylejd_iWQ4M
// 校验成功 { username: 'zhangsan', iat: 1684389752, exp: 1684389852 }

真实案例

// 封装 jwt
const jwt = require("jsonwebtoken")
const secret = "dselegent"

const JWT = {
    //生成签名
    //expiresIn是过期时间,例'24h'
    //value是要传入的数据
    generate(value,expiresIn){
        return jwt.sign(value,secret,{expiresIn})
    },
    // 解密
    verify(token){
        // 使用 try catch 语法解决 jwt 报错
        try{
            return jwt.verify(token,secret)//返回的是解析后的token,原始数据+自带的数据构成的对象
        }catch(e){
            return false//通过上面按个方法会自动解出是否过期,可是会报错,所以用try-catch
        }
    }
}

module.exports = JWT
// login.js
async login(req,res,next) {
    const { username,password } = req.body
    // 存储数据库
    let data = await ·······
    if(data) {
        // 利用封装的方法设置token和有效时间
        const token = jwt.generate({
            id: data._id,
            username: data._username,
        },'1d')
        // 将token设置到响应头
        res.header("Authorization",token)
        res.send({ok: true})
    }
}
// login.html
// 使用 axios 拦截器
axios.interceptors.response.use(response => {
    const { authorization } = response.headers
    Authorization && localStorage.setItem("token",authorization)
    return response
},error => {
    return Promise.reject(error)
})

// 点击登陆事件
let loginClick = (username, password) => {
    axios.post("/login", {
        username:username,
        password:password
    }).then(res => {
        if(res.data.ok) {
            // 路由跳转
        } else {
            // 错误处理
        }
    })
}
// 需要 token 才能进入的页面

// axios 请求拦截
axios.interceptors.request.use(request =>{
    const token = localStorage.getItem('token')
    request.headers.Authorization = `Bearer ${token}`
}, error => {
    return Promise.reject(error)
})

// 响应拦截
axios.interceptors.response.use(response=>{
    const {authorization} = response.headers
    authotization && localStorage.setItem("token",authorization)
    return response
}, error => {
    if(error.response.status === 401) {
        localStorage.removeItem("token")
        // 重定向到登陆界面
    }
    return Promise.reject(error)
})
// token 处理中间件
app.use((req,res,next) => {
    if(req.url.includes('/login')) {
        next()
        return
    }
    const token = req.headers['authorization'].split(' ')[1] // 这里是小写
    // const token = req.get('Authorization').split(' ')[1] // 或者这样写
    if(token){
        const payload = JWT.verify(token)
        if(payload){
            // 重新生成一个新的token重置时间
            const newToken = JWT.generate({
                id: payload.id,
                username: payload.username,
            }, '1d')
            res.header("Authorization", newToken)
            next()
        } else {
            res.status(401).send({errCode: "-1",errInfo: "Token 过期"})
        }
    }
})
posted @ 2023-05-18 16:35  超重了  阅读(36)  评论(0编辑  收藏  举报