登录鉴权
登录鉴权
- cookie
- session
- token
cookie
- 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
- 安装
npm i cookie-parser
- 引入
const cookieParser = require('cookie-parser')
- 设置中间件
app.use(cookieParser())
- 设置 cookie
res.cookie("name":"admin",{maxAge: 60 * 60,httpOnly: true})
- 获取 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 的使用
- 安装 express-session
npm i express-session
- 引入
const session = require('express-session')
- 设置 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 过期"})
}
}
})