# Session/Cookie
# cookie 是如何工作的
代码语言:javascript复制// 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
代码语言:javascript复制// 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 进行持久化处理
代码语言:javascript复制// 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 进行传输
- 服务端通过会话标识可以获取到会话相关信息,然后对客户端的请求进行响应;如果找不到有效的会话标识,就判定用户是未登录状态
- 会话有过期时间,也可以通过一些操作(如退出登录)主动删除
# Token 验证
session 不足:
- 服务端有状态需要维护
- 依赖 cookie ,APP 或 跨域处理复杂
# JWT (JSON Web Token)
Bearer token 组成:Header、payload(载荷)、Signature(签名) Header.Payload.Signature
Header:JSON 对象,描述 JWT 的元数据,用 Base64URL 算法转成字符串
代码语言:javascript复制{
"alg": "HS256", // 签名算法,默认是 HMAC SHA256(写成 HS256)
"typ": "JWT" // Token 类型
}
Payload: JSON 对象,用来存放实际需要传递的数据。JWT 规定了7个官方字段,供选用,也可以定义私有字段。JWT 默认是不加密的,任何人都可以读到,所以不要把秘密信息放在这个部分。使用 Base64URL 算法转成字符串
字段名称 | 说明 |
---|---|
iss (issuer) | 签发人 |
exp (expiration time) | 过期时间 |
sub (subject) | 主题 |
aud (audience) | 受众 |
nbf (Not Before) | 生效时间 |
iat (Issued At) | 签发时间 |
jti (JWT ID) | 编号 |
Signature:对前两部分的签名,防止数据篡改。签名密钥只有服务器知道
JWT 特点
- JWT 默认是不加密,但也是可以加密的。生成原始 Token 以后,可以用密钥再加密一次
- JWT 不加密的情况下,不能将秘密数据写入 JWT
- JWT 不仅可以用于认证,也可以用于交换信息。有效使用 JWT,可以降低服务器查询数据库的次数
- JWT 的最大缺点是,由于服务器不保存 session 状态,因此无法在使用过程中废止某个 token,或者更改 token 的权限。也就是说,一旦 JWT 签发了,在到期之前就会始终有效,除非服务器部署额外的逻辑
- JWT 本身包含了认证信息,一旦泄露,任何人都可以获得该令牌的所有权限。为了减少盗用,JWT 的有效期应该设置得比较短。对于一些比较重要的权限,使用时应该再次对用户进行认证
- 为了减少盗用,JWT 不应该使用 HTTP 协议明码传输,要使用 HTTPS 协议传输
JWT 生效过程梳理
代码语言:javascript复制// jwt.js
const jsonwebtoken = require('jsonwebtoken');
// 签名密钥
const secret = 'hello';
// 数据
const user = {
username: 'cell',
password: 'pwd',
};
// 计算token
const token = jsonwebtoken.sign({
data: user,
exp: Math.floor(Date.now() / 1000) (60 * 60),
}, secret);
console.log(token);
// eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJkYXRhIjp7InVzZXJuYW1lIjoiY2VsbCIsInBhc3N3b3JkIjoicHdkIn0sImV4cCI6MTYxNTAzNDk3OCwiaWF0IjoxNjE1MDMxMzc4fQ.iZAHDN6a8eTBcB6a6reeNLgDD-tI9g7CBvfBh9b5Ivs
// 解算 验证
console.log('解码', jsonwebtoken.verify(token, secret));
// 解码 {
// data: { username: 'cell', password: 'pwd' },
// exp: 1615035042,
// iat: 1615031442
// }
# 简单 token 使用流程
前端页面 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 @click="logs=[]">Clear Log</button>
</div>
<ul>
<li v-for="(log, idx) in logs" :key="idx">{{log}}</li>
</ul>
</div>
<script>
axios.interceptors.request.use(config => {
const token = window.localStorage.getItem('token');
if (token) {
// 如果 token 存在,在每个 HTTP header 都加上 token
// Bearer 是 JWT 的认证头部信息
config.headers.common['Authorization'] = `Bearer ${token}`;
}
return config;
}, err => {
return Promise.reject(err);
});
axios.interceptors.response.use(res => {
app.logs.push(JSON.stringify(res.data));
return res;
}, err => {
app.logs.push(JSON.stringify(err.data));
return Promise.reject(err);
});
const app = new Vue({
el: '#app',
data: {
username: 'cell',
password: 'pwd',
logs: [],
},
methods: {
async login() {
const res = await axios.post('/users/login-token', {
username: this.username,
password: this.password
});
localStorage.setItem('token', res.data.token);
},
async logout() {
localStorage.removeItem('token');
},
async getUser() {
await axios.get('/users/getUser-token');
}
}
});
</script>
</body>
</html>
服务端 index.js
const Koa = require('koa');
const router = require('koa-router')();
const jwt = require('jsonwebtoken');
const jwtAuth = require('koa-jwt');
const bodyParser = require('koa-bodyparser');
const static = require('koa-static');
const secret = 'my-secret';
const app = new Koa();
app.keys = ['my secret'];
app.use(static(__dirname '/'));
app.use(bodyParser());
router.post('/users/login-token', async ctx => {
const { body } = ctx.request;
const userinfo = body.username;
ctx.body = {
message: 'Login Successful',
user: userinfo,
// 生成 token 返回给客户端
token: jwt.sign({
data: userinfo,
exp: Math.floor(Date.now() / 1000) 60 * 60, // 一小时后过期
}, secret)
};
});
router.get('/users/getUser-token',
jwtAuth({
secret
}),
async ctx => {
// 验证通过
console.log(ctx.state.user);
// 获取 信息
ctx.body = {
message: 'Get Data Successful',
userinfo: ctx.state.user.data
};
});
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(3000);
# OAuth
使用 github OAuth 实现用户登录 前端页面 index.html
<html>
<head>
<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">
<button @click='oauth()'>Login with Github</button>
<div v-if="userInfo">
Hello {{userInfo.name}}
<img style="width: 50px;height: 50px;" :src="userInfo.avatar_url" />
</div>
<ul>
<li v-for="(log, idx) in logs" :key="idx">{{log}}</li>
</ul>
</div>
<script>
</script>
<script>
axios.interceptors.request.use(
config => {
const token = window.localStorage.getItem("token");
if (token) {
// 判断是否存在 token,如果存在的话,则每个 http header 都加上 token
config.headers.common["Authorization"] = "Bearer " token;
}
return config;
},
err => {
return Promise.reject(err);
}
);
axios.interceptors.response.use(
response => {
app.logs.push(JSON.stringify(response.data));
return response;
},
err => {
app.logs.push(JSON.stringify(err));
return Promise.reject(err);
}
);
const app = new Vue({
el: "#app",
data: {
logs: [],
userInfo: null
},
methods: {
async oauth() {
window.open('/auth/github/login', '_blank')
const intervalId = setInterval(() => {
console.log("等待认证中..");
if (window.localStorage.getItem("authSuccess")) {
clearInterval(intervalId);
window.localStorage.removeItem("authSuccess");
this.getUser();
}
}, 500);
},
async getUser() {
const res = await axios.get("/auth/github/userinfo");
console.log('res:', res.data);
this.userInfo = res.data;
}
}
});
</script>
</body>
</html>
服务端 index.js
const Koa = require('koa')
const router = require('koa-router')()
const static = require('koa-static')
const app = new Koa();
const axios = require('axios')
const querystring = require('querystring')
const jwt = require("jsonwebtoken");
const jwtAuth = require("koa-jwt");
const accessTokens = {}
const secret = "my secret";
app.use(static(__dirname '/'));
// 从 github 注册 Oauth application 后获取 client_id 和 client_secret
const config = {
client_id: '73a4f730f2e8cf7d5fcf',
client_secret: '74bde1aec977bd93ac4eb8f7ab63352dbe03ce48',
}
router.get('/auth/github/login', async (ctx) => {
// 重定向到认证接口,并配置参数
const path = `https://github.com/login/oauth/authorize?${querystring.stringify({ client_id: config.client_id })}`;
// 转发到 github 授权服务器
ctx.redirect(path);
})
// Authorization callback URL
router.get('/auth/github/callback', async (ctx) => {
console.log('callback..')
const code = ctx.query.code;
const params = {
client_id: config.client_id,
client_secret: config.client_secret,
code: code
}
let res = await axios.post('https://github.com/login/oauth/access_token', params)
const access_token = querystring.parse(res.data).access_token
const uid = Math.random() * 99999
accessTokens[uid] = access_token
const token = jwt.sign(
{
data: uid,
// 设置 token 过期时间,一小时后,秒为单位
exp: Math.floor(Date.now() / 1000) 60 * 60
},
secret
)
ctx.response.type = 'html';
console.log('token:', token)
ctx.response.body = ` <script>window.localStorage.setItem("authSuccess","true");window.localStorage.setItem("token","${token}");window.close();</script>`;
})
router.get('/auth/github/userinfo', jwtAuth({
secret
}), async (ctx) => {
// 验证通过,state.user
console.log('jwt playload:', ctx.state.user)
const access_token = accessTokens[ctx.state.user.data]
res = await axios.get('https://api.github.com/user?access_token=' access_token)
console.log('userAccess:', res.data)
ctx.body = res.data
})
app.use(router.routes());
app.use(router.allowedMethods());
app.listen(7001);