http 请求是没有状态的,每一次发请求,对服务器来说,都是单独的一个请求,服务器端无法判断你是谁,这个时候就需要一个登录凭证
cookie
cookie
cookie 又称为 “小甜饼” 类型为 “小型文本文件”,某些网站为了辨别用户身份而存储在用户本地终端上的数据
浏览器会在特定的情况下携带上 cookie 来发送请求,我们可以通过 cookie 来获取一些信息
- cookie 总是保存在客户端中,按在客户端中的存储位置,cookie 可以分为内存 cookie 和 硬盘 cookie
- 内存 cookie 由浏览器保护,保护在内存中,浏览器关闭时 cookie 就会消失,其存在时间是短暂的
- 硬盘 cookie 保存在硬盘中,有一个过期时间,用户手动清理或者过期时间到时,才会被清理
- 如果判断一个 cookie 是内存 cookie 还是硬盘 cookie 呢?
- 没有设置过期时间,默认情况下 cookie 是 内存 cookie ,在关闭浏览器时,会自动删除
- 有设置过期时间,并且过期时间不为0 或者 负数的 cookie ,是硬盘 cookie 需要手动或者到期时,才会删除
cookie 生命周期
- 默认情况下 cookie 是内存 cookie 也称之为 会话 cookie 也就是在浏览器关闭时会自动删除
- 我们可以通过 expires 或者 max-age 来设置过期的时间
- expires:设置的是 Date.toUTCString(),设置格式是 expires=data-in-GMTString-format
- max-age:设置过期的秒钟,max-age=max-age-in-seconds (例如一年为60X60X24X365)
cookie 作用域 (允许 cookie 发送给那些 URL)
- Domain:指定那些域名可以接受 cookie
- 如果不指定,那些默认是 origin 不包括子域名 ,比如 域名 是 ww.ddg.com,那么 music.ddg.com、video.ddg.com 就是它的子域名,但 ddg.com/ xx 下的,都可以携带
- 如果指定,则包含子域名。如,设置 Domain=ddg.com 则 cookie 也包含在 子域名中 (如music.ddg.com、video.ddg.com)
- Path:指定主机下那些路径可以接受 cookie
- 例如,设置 Path=/docs,则以下地址都会匹配
- /docs
- /docs/web/
- /docs/web/http
- 例如,设置 Path=/docs,则以下地址都会匹配
设置cookie
一般来说,是服务器设置 cookie,,客户端删除 cookie。但 客户端也可以设置 cookie
浏览器端设置cookie
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
</head>
<body>
<button id="btn">添加cookie</button>
<script>
document.getElementById('btn').onclick = function () {
// document.cookie = 'name=kobe';
// 通过 js 设置 cookie
// age值为18 , 过期时间为 5秒
document.cookie = 'age=18;max-age=5;';
}
</script>
</body>
</html>
服务器端设置 cookie
const Koa = require('koa')
const Router = require('koa-router')
const app = new Koa()
const testRouter = new Router()
app.use(testRouter.routes())
testRouter.get('/test', (ctx, next) => {
ctx.cookies.set("name", "lilei", {
// maxAge: 是毫秒
maxAge: 500 * 1000,
})
ctx.body = "牛逼"
})
testRouter.get('/demo', (ctx, next) => {
// 读取 cookie
const value = ctx.cookies.get('name')
ctx.body = "你的cookie是" + value
})
app.listen(4000, () => {
console.log('4000');
})
session
session 其实是基于 cookies 的,,但是一般不会 cookie 或者 session 单独来使用,一般是 cookie+session 结合使用
const Koa = require('koa');
const Router = require('koa-router');
// 安装 koa-session
const Session = require('koa-session');
const app = new Koa();
const testRouter = new Router();
// 创建Session的配置
const session = Session({
// 起个名 叫 sessionid
key: 'sessionid',
maxAge: 10 * 1000, // 1单位也是毫秒
signed: true, // 是否使用加密签名,防止客户端伪造
}, app);
// 设置签名以后,在浏览器 cookies 里面查看 ,会有一个 sessionid.sig 的一个属性,这个就是签名
// 设置签名, 通过 "aaaa" 来 加密
app.keys = ["aaaa"];
app.use(session);
// 登录接口
testRouter.get('/test', (ctx, next) => {
// name/password -> id/name
const id = 110;
const name = "coderwhy";
// sessionid = { id,name} 会把这个对象转换成 base64
ctx.session.user = { id, name };
ctx.body = "test";
});
testRouter.get('/demo', (ctx, next) => {
console.log(ctx.session.user);
ctx.body = 'demo';
});
app.use(testRouter.routes());
app.use(testRouter.allowedMethods());
app.listen(8080, () => {
console.log("服务器启动成功~");
})
cookie 和 session 的缺点
- cookie 会被附加在每个http 请求中,所以 无形中增加了流量(事实上某些请求是不需要的)
- cookie 是明文传递的,所以存在安全问题
- cookie 的大小限制是 4KB 有的还会有个数限制,对于复杂的需求来说是不够的
- 对于浏览器外的其他客户端(比如 ios 、Android),必须手动的设置 cookie 和 session
- 对于分布式系统和服务器集群中如何可以保证其他系统也可以正确的解析 session?
后面两个才是 最致命的缺点
在浏览器端,我们可以通过 js 设置cookie ,但是在其他端,cookie 需要你手动设置,每次发一个请求,你就需要设置一次。浏览器客户端和其他客户端,是不统一的
服务器集群,对于高并发的一些东西,比如有几万人访问这个接口,一个服务器是承受不了的,所以,需要多个服务器,这些服务器的代码可能一样,他们组成服务器集群,有 nginx来做一个反向代理,看看那个服务器空闲,就把这个请求分发给那个服务器。。。比如A服务器设置了sessionid,发过去了, 客户端请求的时候,被nginx分发到了B服务器,B服务器怎么解析。。
分布式,就是把系统分开,分成好多个子系统,session由用户管理系统,但我访问其他系统,其他系统怎么解析?
以上这两个缺点,都有解决方案,但是 使用 cookie+session 的越来越少
token
-
token可以翻译为令牌;
-
也就是在验证了用户账号和密码正确的情况,给用户颁发一个令牌;
-
这个令牌作为后续用户访问一些接口或者资源的凭证;
-
我们可以根据这个凭证来判断用户是否有权限来访问;
步骤:
- 生成 token 登录的时候,颁发token
- 验证 token 访问某些资源或者接口时,验证token
JWT 实现 token 机制
JWT 生成 token 的组成
-
header
-
alg:采用的加密算法,默认时 HMAC SHA256(HS256),采用同一个密钥进行加密和解密
-
typ:JWT ,固定值
-
会通过 base64Url 算法进行编码
-
-
payload
- 携带的数据,比如,我们可以将用户的 id 和 name 放到 payload 中
- 默认也会携带 iat (issued at),令牌的签发时间
- 我们也可以设置过期时间:exp
- 会通过 base64Url 算法进行编码
-
signature (是签名的意思)
- 设置一个 secretKey(密钥) 通过将 前两个的结果合并后进行 HMAC SHA256(HS256) 的算法
- HMAC SHA256(base64Url(header)+.+base64Url(payload),secretKey )
- 但是如果 secretKey 暴露是一件非常危险的事情,因为之后就可以模拟颁发 token,也可以解密 token
在真实开发中,我们可以直接使用一个库来完成: jsonwebtoken;
一般来说,都会在请求头设置, 一个 Authorization 字段, 属性值是 :Bearer token……
const Koa = require('koa');
const Router = require('koa-router');
const jwt = require('jsonwebtoken');
const app = new Koa()
const testRouter = new Router()
// 设置一个签名
const SERCET_KEY = 'daidaigou'
// 登录接口
testRouter.get('/test', (ctx, next) => {
// 登陆成功以后
// 令牌 包括 id name
// 设置 payload
const user = { id: 123, name: "呆呆狗" }
// 第一个参数,就是你要传递什么数据
// 第二个参数,密钥
// 第三个参数,额外的参数
const token = jwt.sign(user, SERCET_KEY, {
// 设置过期时间 单位秒
expiresIn: 50 * 1000,
})
ctx.body = token;
})
// 验证接口
testRouter.get('/demo', (ctx, next) => {
// 一般来说, token都是设置在请求头中, 名为:Authorization,值为 Bearer token
const authorization = ctx.headers.authorization;
const token = authorization.replace("Bearer ", "");
// JWT 验证失败,会直接抛出异常的 可以用 try catch 捕获
try {
// 验证 token
// 第一个参数 token
// 第二个颁发的签名
const result = jwt.verify(token, SERCET_KEY);
ctx.body = result;
} catch (error) {
console.log(error.message);
ctx.body = "token是无效的~";
}
// 解析成功则会返回
/*
{
"id": 123,
"name": "呆呆狗",
"iat": 1625559910, 颁发的时间
"exp": 1625609910 过期的时间
}
*/
})
app.use(testRouter.routes())
app.listen(4000, () => {
console.log('服务器开启 4000');
})
非对称加密
HS256 加密算法一旦密钥暴露,就会非常危险,
这个时候,我们可以使用非对称加密 RS256
- 私钥(private key)用户发布令牌
- 公钥(public key)用于验证令牌
# 打开 git bash
// 进入 openssl
openssl
// 生成私钥
genrsa -out private.key 1024
// genrsa 表示生成 -out 表示输出 1024 表示位数
// 生成公钥
rsa -in private.key -pubout -out public.key
// rsa -in private.key 把 private.key 作为一个输入,要依靠 私钥生成公钥
// -pubout 表示生成公钥
// -out public.key 输出位置
const Koa = require('koa');
const Router = require('koa-router');
const jwt = require('jsonwebtoken');
const fs = require('fs');
const { pathToFileURL } = require('url');
// fs 读取文件的时候,有时候可以使用相对路径,有时候不可以
// 我们可以在 这个项目的 上一个 文件夹,来启动这个项目,这个时候,使用相对路径就会有问题
// 在项目中的任何一个地方的相对路径,都是相对于 process.cwd
// 就是你在那个文件夹下启动这个项目 process.cwd 就是那个文件夹
// 解决方案
// 1. 使用 path.resolve(__dirname,拼接路径)
// 2.
const app = new Koa();
const testRouter = new Router();
// 在项目中的任何一个地方的相对路径, 都是相对于process.cwd()
console.log(process.cwd());
const PRIVATE_KEY = fs.readFileSync('./keys/private.key');
const PUBLIC_KEY = fs.readFileSync('./keys/public.key');
// 登录接口
testRouter.get('/test', (ctx, next) => {
// 登陆成功以后
// 令牌 包括 id name
const user = { id: 110, name: 'why' };
// 第一个参数,就是你要传递什么数据
// 第二个参数,密钥
// 第三个参数,额外的参数
const token = jwt.sign(user, PRIVATE_KEY, {
// 设置过期时间 单位秒
expiresIn: 50 * 1000,
// 设置 非对称 加密 算法
algorithm: "RS256"
});
ctx.body = token;
});
// 验证接口
testRouter.get('/demo', (ctx, next) => {
const authorization = ctx.headers.authorization;
const token = authorization.replace("Bearer ", "");
// JWT 验证失败,会直接抛出异常的 可以用 try catch 捕获
try {
// 验证 token
// 第一个参数 token
// 第二个颁发的签名
// 第三个参数 设置其他参数
const result = jwt.verify(token, PUBLIC_KEY, {
// 设置 验证方式,是一个数组
algorithms: ["RS256"]
});
ctx.body = result;
} catch (error) {
console.log(error.message);
ctx.body = "token是无效的~";
}
});
app.use(testRouter.routes());
app.use(testRouter.allowedMethods());
app.listen(8080, () => {
console.log("服务器启动成功~");
})