Session/Cookie
cookie 是如何工作的
// cookie.js
const http = require('http');
http
.createServer((req, res) => {
if (req.url === '/favicon.ico') {
res.end('favicon.ico');
return;
}
// 获取 Cookie
console.log(`cookies: ${req.headers.cookie}`);
// 设置 Cookie
res.setHeader('Set-Cookie', 'cookie1=a');
res.end('Hello Cookie');
})
.listen(3000);
cookie 缺点
- 空间太小
- 不安全
简易 session 实现
session 会话机制是一种服务器端机制,使用类似于哈希表的结构来保存信息
实现原理:
- 服务器在接受客户端首次访问时在服务器端创建 session,然后保存 session (保存在内存或 redis 中),然后给这个 session 生成一个唯一的标识字符串(uuid),然后在响应头中设置该 uuid
- 签名,通过密钥对 sid 进行签名处理,避免客户端修改 sid (非必需步骤)
- 浏览器中收到请求响应的时候解析响应头,然后将 sid 保存在本地 cookie 中,浏览器下次发起 http 请求时会带上该域名下的 cookie 信息
- 服务器在接受客户端请求时会解析请求头 cookie 中的 sid,然后根据这个 sid 去找服务器端保存的该客户端的 session,然后判断请求是否合法
// cookie.js
const http = require('http');
const session = {};
http
.createServer((req, res) => {
if (req.url === '/favicon.ico') {
res.end('favicon.ico');
return;
}
// 获取 Cookie
console.log(`cookies: ${req.headers.cookie}`);
const sessionKey = 'sid';
const cookie = req.headers.cookie;
if (cookie && cookie.indexOf(sessionKey) > -1) {
res.end('Second Request');
// 获取 Cookie 中的信息
const pattern = new RegExp(`${sessionKey}=([^;]+);?\s*`);
const sid = pattern.exec(cookie)[1];
console.log(`session: ${sid}, ${JSON.stringify(session[sid])}`);
} else {
const sid = (Math.random() * 999999).toFixed();
// 设置 Cookie
res.setHeader('Set-Cookie', `${sessionKey}=${sid}`);
session[sid] = { name: 'cell' };
res.end('Hello Session');
}
})
.listen(3000);
在 koa 中使用 session
// app.js
const koa = require('koa');
const session = require('koa-session');
const app = new koa();
// 签名 key
app.keys = ['my secret'];
// 配置项
const SESS_CONFIG = {
key: 'cilab:sess', // cookie 键名
maxAge: 24 * 60 * 60 * 1000, // 1 day
httpOnly: true, // 仅限服务器修改
signed: true // 对 cookie 进行签名
};
app.use(session(SESS_CONFIG, app));
app.use(ctx => {
if (ctx.path === '/favicon.ico') return;
// 获取
let n = ctx.session.count || 0;
// 设置
ctx.session.count = ++n;
ctx.body = `request ${n} times`;
});
app.listen(3000);
使用 redis 进行持久化处理
// app.js
const koa = require('koa');
const session = require('koa-session');
const redis = require('redis');
const redisStore = require('koa-redis');
const wrapper = require('co-redis');
const app = new koa();
const redisClient = redis.createClient(6379, 'localhost');
const client = wrapper(redisClient);
// 签名 key
app.keys = ['my secret'];
app.use(session({
key: 'cilab:sess',
store: redisStore({ client })
}, app));
app.use(async (ctx, next) => {
const keys = await client.keys('*');
keys.forEach(async key => {
console.log(await client.get(key));
});
await next();
});
app.use(ctx => {
if (ctx.path === '/favicon.ico') return;
// 获取
let n = ctx.session.count || 0;
// 设置
ctx.session.count = ++n;
ctx.body = `request ${n} times`;
});
app.listen(3000);
session-cookie 方案在 Koa 中实践
前端页面 index.html
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta http-equiv="X-UA-Compatible" content="IE=edge">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Document</title>
<script src="https://cdn.jsdelivr.net/npm/vue/dist/vue.js"></script>
<script src="https://unpkg.com/axios/dist/axios.min.js"></script>
</head>
<body>
<div id="app">
<div>
<input v-model="username">
<input v-model="password">
</div>
<div>
<button @click="login">Login</button>
<button @click="logout">Logout</button>
<button @click="getUser">Get User</button>
</div>
<div>
<button onclick="document.getElementById('log').innerHTML = ''">Clear Log</button>
</div>
<h6 id="log"></h6>
</div>
<script>
axios.defaults.widthCredentials = true;
axios.interceptors.response.use(res => {
document.getElementById('log').append(JSON.stringify(res.data));
return res;
});
const app = new Vue({
el: '#app',
data: {
username: 'cell',
password: 'pwd',
},
methods: {
async login() {
await axios.post('/users/login', {
username: this.username,
password: this.password
});
},
async logout() {
await axios.post('/users/logout');
},
async getUser() {
await axios.get('/users/getUser');
}
}
});
</script>
</body>
</html>
服务端 index.js
const Koa = require('koa');
const router = require('koa-router')();
const session = require('koa-session');
const cors = require('koa2-cors');
const bodyParser = require('koa-bodyparser');
const static = require('koa-static');
const app = new Koa();
app.use(cors({
credentials: true,
}));
app.keys = ['my secret'];
app.use(static(__dirname + '/'));
app.use(bodyParser());
app.use(session(app));
app.use((ctx, next) => {
if (ctx.url.indexOf('login') > -1) {
next();
} else {
console.log('session', ctx.session.userinfo);
if (!ctx.session.userinfo) {
ctx.body = {
message: 'Login Failed',
};
} else {
next();
}
}
});
router.post('/users/login', async ctx => {
const { body } = ctx.request;
console.log('body', body);
// 设置 session
ctx.session.userinfo = body.username;
ctx.body = {
message: 'Login Successful'
};
});
router.post('/users/logout', async ctx => {
// 设置 session
delete ctx.session.userinfo;
ctx.body = {
message: 'Logout Successful'
};
});
router.get('/users/getUser', async ctx => {
ctx.body = {
message: 'Get Data Successful',
userinfo: ctx.session.userinfo,
};
});
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);
过程描述:
- 用户登录时,服务端生成一个唯一的会话标识,并以该标识作为 key 存储相关数据
- 会话标识在客户端和服务端之间通过 cookie 进行传输
- 服务端通过会话标识可以获取到会话相关信息,然后对客户端的请求进行响应;如果找不到有效的会话标识,就判定用户是未登录状态
- 会话有过期时间,也可以通过一些操作(如退出登录)主动删除