cookies、session和jwt的一些优缺点
先来说说cookies和session实际应用中的一些不足
- 【缺点】cookies的安全性不好,攻击者可以通过获取本地cookies进行欺骗或者利用cookies进行CSRF攻击。
- 【缺点】使用cookies时,在多个域名下,会存在跨域问题。
- 【缺点】session在一定的时间里,需要存放在服务端,因此当拥有大量用户时,也会大幅度降低服务端的性能。
- 【缺点】当有多台机器时,如何共享session也会是一个问题,也就是说,用户第一个访问的时候是服务器A,而第二个请求被转发给了服务器B,那服务器B如何得知其状态。
jwt
- 【优点】使用JWT不需要后端进行记录,减轻服务器压力,每个token都是独立的。
- 【优点】jwt不存在跨域问题,后台生成token返回到前台,前台保存到本地,每次请求在请求头携带token给后台进行验证即可。
- 【缺点】token生成和验证都需要后台计算,而且由于每次向服务器发起请求都要携带token,太大了会造成请求缓慢。
- 【中性】session比JWT好的地方在于在请求时浏览器会自动带http头部带上cookie,并且在用户持续使用时会不断地刷新session的过期时间,当浏览器关闭时自动清除session。相比之下JWT本身没法做到随着用户的使用而更新或手动清除,只能等自动过期
jwt实现
分析
编码:一个token是一串base64字符,大概分成head、payload、sign三部分,这三部分以 . 分割。其中head记录的是加密算法,payload记录的是你定义的一些信息,sign则是head(base64字符) + payload(base64字符) + 秘钥的加密base64字符。
解码:在payload保存过期时间,解码时判断是否过期。head(base64字符) + payload(base64字符) + 秘钥再加密看是否等于sign,不等于则被改动过。
代码实现
首先准备两个转换base64字符的函数
// 普通字符转base64字符
function toBase64(str){
return new Buffer(str).toString('base64');
}
// base64字符转普通字符
function fromBase64(str){
return new Buffer(str, 'base64').toString();
}
签名加密函数
const crypto = require('crypto');
/**
* @param {String} str 要加密的字符串
* @param {String} jwtSecretKey 秘钥
* @method 签名
*/
function sign(str, jwtSecretKey){
return crypto.createHmac('sha256', jwtSecretKey).update(str).digest('base64');
}
token生成函数
函数参数设计:
- 第一个参数是自定义的一些要存储的信息,比如 {username: 'dsa'}
- 第二个参数是秘钥(这是最重要的,不可泄露)
- 第三个参数是一些其他选项,比如token最大时效(必填的){maxAge: 1 * 60 * 1000}
/**
* @param {Object} payload 自定义信息
* @param {String} jwtSecretKey 秘钥
* @param {Object} options 其他选项
* @method 生成token
*/
function encode(payload, jwtSecretKey, options){
// token头部,加密算法
let headPart = toBase64(JSON.stringify({"alg": "sha256","typ": "JWT"}));
// token信息内容部分,自定义的信息
payload.iat = Date.now(); //token生成时间
payload.exp = Date.now() + options.maxAge; //token过期时间
let payloadPart = toBase64(JSON.stringify(payload));
// token签名部分
let signPart = sign(`${headPart}.${payloadPart}`, jwtSecretKey);
return `${headPart}.${payloadPart}.${signPart}`;
}
解析token函数
/**
* @param {String} token 前台传来的token
* @param {String} jwtSecretKey 秘钥
* @method 解析token
* @return {Boolean} 返回布尔值
*/
function decode(token, jwtSecretKey){
// 通常前台传过来的token字符串前面都是加上'Bearer '
let [headPart, payloadPart, signPart] = token.split(' ')[1].split('.');
let payload = JSON.parse(fromBase64(payloadPart));
// 如果token过期了,return false
if(Date.now() > payload.exp) return false;
// 如果token被改过,return false
if(sign(`${headPart}.${payloadPart}`, jwtSecretKey) !== signPart) return false;
return true;
}
后台服务器部分
这里我就不使用任何框架了,直接用Node.js原生撸,前台则使用postman测试。
先来登录获取token
const http = require('http');
const url = require('url');
const jwt = require('./jwt');
// jwt秘钥
const jwtSecretKey = 'lala';
http.createServer((req, res) => {
let {pathname} = url.parse(req.url);
// 前台登录
if(pathname === '/login'){
let chunkArr = [];
req.on('data', chunk => {
chunkArr.push(chunk);
})
req.on('end', () => {
let data = JSON.parse(Buffer.concat(chunkArr).toString());
// 如果登录成功,就给前台返回一个token
if(data.username === 'dsa' && data.pwd === '321'){
// 生成token
let token = jwt.encode({
username: 'dsa'
}, jwtSecretKey, {
maxAge: 1*60*1000 //最大时效1分钟
});
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({
code: 0,
msg: '登录成功',
data: {
token
}
}))
}
})
}
}).listen(8888, () => {
console.log('server listening on port 8888');
})
用postman请求
前台访问 /home 进行token验证
const http = require('http');
const url = require('url');
const jwt = require('./jwt');
// jwt秘钥
const jwtSecretKey = 'lala';
http.createServer((req, res) => {
let {pathname} = url.parse(req.url);
// 前台登录
if(pathname === '/login'){
let chunkArr = [];
req.on('data', chunk => {
chunkArr.push(chunk);
})
req.on('end', () => {
let data = JSON.parse(Buffer.concat(chunkArr).toString());
// 如果登录成功,就给前台返回一个token
if(data.username === 'dsa' && data.pwd === '321'){
// 生成token
let token = jwt.encode({
username: 'dsa'
}, jwtSecretKey, {
maxAge: 1*60*1000 //最大时效1分钟
});
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({
code: 0,
msg: '登录成功',
data: {
token
}
}))
}
})
}else if(pathname === '/home'){ //访问home需要验证token
let token = req.headers.authorization;
console.log(typeof token, token);
// 如果没有token或者token验证错误
if(!token || !jwt.decode(token, jwtSecretKey)){
res.statusCode = 401;
res.setHeader("Content-Type", "application/json");
res.end(JSON.stringify({
code: 1,
msg: 'No authority'
}))
}else{
res.end('Hello World');
}
}
}).listen(8888, () => {
console.log('server listening on port 8888');
})
如果token验证错误
如果token验证通过