Cookie、Session、JWT在koa中的应用及实现原理
Cookie
- HTTP协议是
无状态
的,但在WEB应用中,在多个请求之间共享会话是非常必要的,所以出现了Cookie - cookie是为了
辩别用户身份
,进行会话跟踪而存储在客户端
上的数据
服务器设置cookie:客户端第一次访问服务器时,会通过响应头向客户端发送Cookie,属性之间用分号空格
分隔
客户端接收并保存cookie:客户端再请求服务器时,会携带Cookie至服务器端,而cookie本身就是一个请求的header
重要属性
通过修改本地hosts文件,模拟两个不同的域名。
# hosts
127.0.0.1 a.echoyya.com
127.0.0.1 b.echoyya.com
属性 | 说明 |
---|---|
name=value |
键值对,可以设置要保存的 Key/Value |
Domain |
针对某个域名生效 可以跨父域和子域,默认是当前域名 |
expires/max-age |
cookie存活时间 ,expires 绝对时间, max-age 相对时间 单位秒 |
secure | 当 secure 值为 true 时,cookie 在 HTTP 中是无效,只在https下生效 |
Path |
表示 cookie 影响到的路径,默认是/ 都能被访问到。若路径不匹配时,浏览器则不发送这个Cookie |
httpOnly |
表示浏览器无法通过代码来获取,防止XSS攻击,但是可以通过手动修改控制台方式进行更改。 |
实现原理
npm install koa koa-router
const Koa = require('koa');
const Router = require('koa-router');
const querystring = require('querystring') // 用于解析和格式化网址查询字符串
const app = new Koa();
const router = new Router();
// koa 操作cookie 实现原理
app.use(async (ctx, next) => {
// 扩展一个设置cookie的方法
let cookieArr = [];
ctx.req.getCookie = function (key) {
let cookies = ctx.req.headers['cookie']; // name=xx; age=yy => name=xx&age=yy
let cookieObj = querystring.parse(cookies,'; ')
return cookieObj[key] || ''
}
ctx.res.setCookie = function (key, value, options={}) {
let args = []; // 每个cookie 属性集合
options.domain && args.push(`domain=${options.domain}`);
options.maxAge && args.push(`max-age=${options.maxAge}`);
options.httpOnly && args.push(`httpOnly=${options.httpOnly}`);
options.path && args.push(`path=${options.path}`);
cookieArr.push(`${key}=${value}; ${args.join('; ')}`);
ctx.res.setHeader('Set-Cookie', cookieArr); // 字符串数组
}
await next();
})
router.get('/read', async (ctx, next) => {
// 自己封装
ctx.body = ctx.req.getCookie('name') || 'empty';
// koa 实现
// ctx.body = ctx.cookies.get('name') || 'empty';
// 原生用法
// ctx.body = ctx.req.headers['cookie'] || 'empty'; // 请求头
})
router.get('/write', async (ctx, next) => {
// 自己封装
ctx.res.setCookie('name', 'nn', {domain: '.echoyya.com'}); // 限制可访问的域名
ctx.res.setCookie('age', '12', {httpOnly: true,path:'/write'}); // 限制可访问的路径
// koa 实现
// ctx.cookies.set('name', 'nn', {domain: '.echoyya.com'});
// ctx.cookies.set('age', '12', {httpOnly: true,path:'/write'});
// 原生用法
// ctx.res.setHeader('Set-Cookie','name=yy');
// ctx.res.setHeader('Set-Cookie','age=15'); // 设置一个cookie,再次set cookie 会将上一次的覆盖
// ctx.res.setHeader('Set-Cookie', ['name=yy; domain=.echoyya.com', 'age=15; path=/; max-age=10; httpOnly=true']); // 设置多个cookie时,可接受一个字符串数组
ctx.body = 'write ok';
})
app.use(router.routes())
app.listen(4000);
cookie签名实现原理
cookie通常由服务器产生,存在客户端,随着每次请求发送至服务器端,而前端存储数据可以被用户手动篡改,
因此可以给cookie签名使其相对安全, 根据cookie的内容产生一个标识,保留原有内容,每次请求检验签名,添加一个配置{ signed: true }
const Koa = require('koa');
const Router = require('koa-router');
const querystring = require('querystring');
const crypto = require('crypto');
const app = new Koa();
const router = new Router();
app.keys = ['ya'];
// base64Url 需要特殊处理 + = /
const sign = value => crypto.createHmac('sha1',app.keys.join('')).update(value).digest('base64').replace(/\+/g,'-').replace(/\=/g,'').replace(/\//g,'_');
app.use(async (ctx, next) => {
let cookieArr = [];
ctx.req.getCookie = function (key, options = {}) {
let cookies = ctx.req.headers['cookie'];
let cookieObj = querystring.parse(cookies,'; ')
if(options.signed){
// 传递过来的签名,和最新计算获得的结果一直,则说明未被修改
if(cookieObj[key + '.sig'] === sign(`${key}=${cookieObj[key]}`)){
return cookieObj[key];
}else {
return ''
}
}
return cookieObj[key] || ''
}
ctx.res.setCookie = function (key, value, options = {}) {
let args = [];
let keyValue = `${key}=${value}`
options.domain && args.push(`domain=${options.domain}`);
options.maxAge && args.push(`max-age=${options.maxAge}`);
options.httpOnly && args.push(`httpOnly=${options.httpOnly}`);
options.path && args.push(`path=${options.path}`);
options.signed && cookieArr.push(`${key}.sig=${sign(keyValue)}`); // 是否开启cookie签名
cookieArr.push(`${keyValue}; ${args.join('; ')}`);
ctx.res.setHeader('Set-Cookie', cookieArr); // 字符串数组
}
await next();
})
// app.keys required for signed cookies
router.get('/visit', async (ctx, next) => {
// Koa 实现
// let count = ctx.cookies.get('visit',{ signed: true }) || 0;
// let visitCount = Number(count) + 1;
// ctx.cookies.set('visit', visitCount, { signed: true });
// ctx.body = `you visit ${visitCount}`
// 自己封装
let count = ctx.req.getCookie('visit', { signed: true }) || 0;
let visitCount = Number(count) + 1;
ctx.res.setCookie('visit', visitCount, { signed: true });
ctx.body = `ya visit: ${visitCount}`
})
app.use(router.routes())
app.listen(3000);
注意事项
- 可能被客户端篡改,
使用前验证签名的合法性
, 不要存储敏感数据
,比如用户密码,账户余额- 每次
请求都会自动携带cookie
,尽量减少cookie的体积
设置正确的domain和path
,减少数据传输
Session
是另一种记录客户状态的机制,不同的是Cookie保存在客户端
浏览器中,而session保存在服务器
上
在服务器存储用户对应的信息,服务器可以存储敏感信息,而session本身是基于cookie的且比cookie安全
同时session 没有持久化功能,需要配合数据库或者redis使用
实现原理
npm install uuid
const Koa = require('koa');
const Router = require('koa-router');
const uuid = require('uuid');
const app = new Koa();
const router = new Router();
app.keys = ['ya']
const session = {}; // 用来存储用户和信息的映射关系,对浏览器不可见
const cardName = 'connect_sig';
router.get('/cut', async (ctx, next) => {
let id = ctx.cookies.get(cardName, {signed:true});
if(id && session[id]){
session[id].mny -= 20;
ctx.body = `mny:` + session[id].mny;
}else{
let cardId = uuid.v4();
session[cardId] = { mny: 500 };
ctx.cookies.set(cardName, cardId,{httpOnly:true,signed:true}); // cookie中只存一个标识,并没有真实的数据
ctx.body = `mny 500`;
}
})
app.use(router.routes())
app.listen(3000);
JWT
JSON Web Token(JWT)
是目前最流行的跨域身份验证解决方案,JWT 默认是不加密的,任何人都可以读到,所以不要把重要信息放在这个部分。
解决问题:session不支持分布式架构,无法支持横向扩展,只能通过数据库来保存会话数据实现共享。如果持久层失败会出现认证失败。
优点:服务器不保存任何会话数据,即服务器变为无状态,使其更容易扩展。
使用方式
- HTTP 请求的头信息Authorization字段里面
Authorization: Bearer <token>
- 如果是post请求也可以放在请求体中,取决于后端采用哪种认证方式
- 通过url传输
http://www.xxx.com/pwa?token=xxxxx
,但是一般不建议这样使用,因为会存在连接分享导致安全隐患
组成
JWT包含了使用.分隔
的三部分 Header.Payload.Signature
1. Header 头部
{ "alg": "HS256", "typ": "JWT"}
// algorithm => HMAC SHA256
// type => JWT
2. Payload内容 JWT 规定了7个官方字段
iss (issuer):签发人
exp (expiration time):过期时间
sub (subject):主题
aud (audience):受众
nbf (Not Before):生效时间
iat (Issued At):签发时间
jti (JWT ID):编号
3. Signature 签名
对前两部分的签名,防止数据篡改 HMACSHA256(base64UrlEncode(header) + "." +base64UrlEncode(payload),secret)
实际应用
npm install koa-bodyparser jwt-simple
const Koa = require('koa');
const Router = require('koa-router');
const bodyParser = require('koa-bodyparser');
const jwt = require('jwt-simple');
const app = new Koa();
const router = new Router();
app.use(bodyParser())
// 登陆
router.post('/login', async (ctx, next) => {
let { username, password } = ctx.request.body;
if (username == 'admin' && password == 'admin') {
// let token = jwt.encode(username,'ya'); // jwt-simple 实现
let token = myJwt.encode(username,'ya'); // 自己实现
ctx.body = {
code: 200,
data: {
token,
username
}
}
}
})
// 验证是否有权限
router.get('/validate', async (ctx, next) => {
let authorization = ctx.get('authorization');
if(authorization){
let [,token] = authorization.split(' ');
try{
// let r = jwt.decode(token,'ya'); // jwt-simple 实现
let r = myJwt.decode(token,'ya'); // 自己实现
ctx.body = {
code:200,
data:{
username:r
}
}
}catch{
ctx.body = {
code:401,
data:'token已失效'
}
}
}
})
app.use(router.routes())
app.listen(3000);
实现原理
// token组成部分为为三段,1,固定格式表示类型 2,内容 3 签名
// 'eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.ImFkbWluIg.xJ0xCP2SXSaJSC-Q1PXuByHdJlBUHCNjdGRU4XW0abU'
const myJwt = {
sign(str,secret){
str = require('crypto').createHmac('sha256',secret).update(str).digest('base64');
return this.toBase64Escape(str);
},
toBase64(content){ // 对象转base64 需要先转为buffer => base64
let source = typeof content === 'string' ? content : JSON.stringify(content);
return this.toBase64Escape(Buffer.from(source).toString('base64'));
},
toBase64Escape(base64){
return base64.replace(/\+/g,'-').replace(/\//g,'_').replace(/\=/g,'');
},
encode(username, secret){ // 转为base64并不是为了安全,只是为了可以在网络中传输
let header = this.toBase64({typ:'JWT',alg:'HS256'});
let content = this.toBase64(username);
let sign = this.sign([header, content].join('.'),secret)
return header + '.' + content + '.' + sign
},
base64urlUnescape(str){
str += new Array(5 - str.length % 4).join('=');
return str.replace(/\-/g, '+').replace(/_/g, '/');
},
// 相同的内容生成的签名相同,可以添加一些过期时间等信息
// 通过内容生成了一个签名,反之通过校验签名。即可得知内容是否发生改变
decode(token,secret){
let [header, content, sign] = token.split('.');
let newSign = this.sign([header, content].join('.'),secret);
if(sign === newSign){ // 此时内容line2中的数据一定是可靠的
return Buffer.from(this.base64urlUnescape(content),'base64').toString();
}else{
throw new Error('token已失效')
}
}
}
前端存储方式 cookie session localStorage sessionStorage token 区别
- cookie特点可以每次请求的时候自动携带,可以实现用户登录功能. 使用cookie来识别用户,1.如果单纯的使用cookie,不建议存放敏感信息,如果被劫持到。(cookie是存在客户端,并不安全,用户可以自行篡改)2.每个浏览器一般对请求头都有大小限制 cookie 不能大于4k,如果cookie过大,会导致页面白屏。 每次访问服务器都会浪费流量(合理设置cookie);
- session:在服务器存储用户对应的信息,服务器可以存储敏感信息,而session本身是基于cookie的且比cookie安全;
- localStorage:关掉浏览器数据依然存在,除非手动清楚,有大小限制约5M,发送请求不会携带;
- sessionStorage:页面不关闭就不会销毁 (用途:如单页应用访问时存储滚动条地址)
- token -> jwt -> jsonwebtoken 不需要服务器存储,没有跨域限制,不建议存储敏感信息
Cookie、Session、JWT在koa中的应用及实现原理的更多相关文章
- Blazor和Vue对比学习(进阶2.2.4):状态管理之持久化保存(2),Cookie/Session/jwt
注:本节涉及到前后端,这个系列的对比学习,还是专注在前端Vue和Blazor技术,所以就不撸码了,下面主要学习概念. 我们知道,Http是无状态协议,客户端请求服务端,认证一次后,如果再次请求,又要重 ...
- 存储机制 cookie session jwt token
cookieCookie的诞生 由于HTTP协议是无状态的,而服务器端的业务必须是要有状态的.Cookie诞生的最初目的是为了存储web中的状态信息,以方便服务器端使用.比如判断用户是否是第一次访问网 ...
- koa 基础(十六)koa 中 session 的使用
1.app.js /** * koa 中 session 的使用 * 1.npm install koa-session --save * 2.const session = require('koa ...
- 一文搞懂Cookie,Session,Token,JWT
HTTP协议是无状态的,无状态意味着,服务器无法给不同的客户端响应不同的信息.这样一些交互业务就无法支撑了.Cookie应运而生. Cookie 通过F12开发者工具,先瞅瞅Cookie的颜值 从图中 ...
- session,cookie,jwt的简单使用
cookie的使用 https://blog.csdn.net/qq_58168493/article/details/122492358 session的使用 https://blog.csdn.n ...
- 在IE浏览器中iframe跨域访问cookie/session丢失的解决办法
单点登录需要在需要进入的子系统B中添加一个类,用于接收A系统传过来的参数: @Action(value = "outerLogin", results = { @Result(na ...
- cookie,session 的概念以及在django中的用法,以及cbv装饰器用法
cookie的由来: 大家都知道HTTP协议是无状态的. 无状态的意思是每次请求都是独立的,它的执行情况和结果与前面的请求和之后的请求都无直接关系,它不会受前面的请求响应情况直接影响,也不会直接影响后 ...
- Cookie和Session在Node.JS中的实践(三)
Cookie和Session在Node.JS中的实践(三) 前面作者写的COOKIE篇.SESSION篇,算是已经比较详细的说明了两者间的区别.机制.联系了.阅读时间可能稍长,因为作者本身作图也做了不 ...
- Cookie和Session在Node.JS中的实践(二)
Cookie和Session在Node.JS中的实践(二) cookie篇在作者的上一篇文章Cookie和Session在Node.JS中的实践(一)已经是写得算是比较详细了,有兴趣可以翻看,这篇是s ...
随机推荐
- cisco交换机端口从errdisable状态恢复
故障描述 经用户反馈,一台cisco2960x接入交换机的一个端口插网线不通,ip电话也没有poe供电. 排查过程 查看交换机端口状态,发现变成了errdisable: ZH_HQN_SW2960X_ ...
- .NET Core添加日志插件
二. 首先控制器的方法中写: private readonly ILogger<fluueController> _logger; public fluueController(ILogg ...
- 一篇文章快速搞懂 Apache SkyWalking 的 OAL
OAL简介 在流模式(Streaming mode)下,SkyWalking 提供了 观测分析语言(Observability Analysis Language,OAL) 来分析流入的数据. OAL ...
- 4、nfs服务器的搭建
4.1.nfs服务介绍: samba服务器一般互联网企业不会使用 nfs服务的端口是不固定的,需要先启动rpc服务对nfs服务端口进行注册 4.2.安装nfs: rpm -qa nfs-utils r ...
- java list 分页
/** * * @param list * @param pageNum * @param pageSize * @param <T> * @return 返回当前页数据 */ publi ...
- 2300+字!在不同系统上安装Docker!看这一篇文章就够了
辰哥准备出一期在Docker跑Python项目的技术文,比如在Docker跑Django或者Flask的网站.跑爬虫程序等等. 在Docker跑Python程序的时候不会太过于细去讲解Docker的基 ...
- MPP大规模并行处理架构详解
面试官:说下你知道的MPP架构的计算引擎? 这个问题不少小伙伴在面试时都遇到过,因为对MPP这个概念了解较少,不少人都卡壳了,但是我们常用的大数据计算引擎有很多都是MPP架构的,像我们熟悉的Impal ...
- ROS2学习之旅(20)——创建一个动作消息
本文用来自定义一个动作消息类型. 以下命令用来创建一个工作空间并建立一个功能包: mkdir -p action_ws/src cd action_ws/src ros2 pkg create act ...
- 嵌入式Redis服务器在Spring Boot测试中的使用
1.概述 Spring Data Redis提供了一种与Redis实例集成的简单方法. 但是,在某些情况下,使用嵌入式服务器比使用真实服务器创建开发和测试环境更方便. 因此,我们将学习如何设置和使用嵌 ...
- Django基础-01篇
一.Django介绍 flask,FastApi是轻量级服务端开发框架 Django是重量级服务端开发框架 ORM:封装了数据库操作 form:校验请求数据 安装Django: pip install ...