权限处理 - 用redis实现分布式session~ (cookie && session )

2,540 阅读7分钟

如何进行权限管理? 首先我们要了解一下cookie和session

cookie

为什么有cookie?我们说http请求其实是无状态的,也就是第一次和第二次访问浏览器都无法识别,于是我们在浏览器第一次访问服务器的时候,服务器会通过set-cookie响应头给浏览器种个cookie(标识),cookie是为了辩别用户身份,进行会话跟踪而存储在客户端上的数据,如图:

express对cookie做了封装,我们先看原生cookie是如何实现通信的

cookie原生

let http = require('http');
let server = http.createServer(function (req,res) {
    if(req.url === '/read'){
      res.end(req.headers['cookie']);
    }else if(req.url === '/write'){
      res.setHeader('Set-Cookie','name=zdl');
      res.end('oks');
    }else{
      res.end('Not Found');
    }
});

server.listen(3000);

express封装的cookie

express-cookie,cookie一般存没用的东西,只是个标识,签名

let express = require('express');
let app = express();

app.get('/read',function (req,res) {
    res.send(req.cookies); // 取没有签名的cookie
});
app.get('/write',function (req,res) {
  //设置cookie方法res.cookie
  res.cookie('name','zdl'); 
  res.cookie('age','9');
  res.send('ok')
})

app.listen(3000);

设置多个cookie我们可能需要以对象的形式展示出来,这是我们需要用到querystring模块

let express = require('express');
let app = express();

let querystring = require('querystring');
app.get('/read',function (req,res) {
    res.send(querystring.parse(req.headers.cookie, '; ', '='));
});
app.get('/write',function (req,res) {
  //设置cookie方法res.cookie
  res.cookie('name','zdl'); 
  res.cookie('age','9');
  res.send('ok')
})

app.listen(3000);

签名(cookie)

document.cookie= 'name=zdl'再刷新之后就会被串改,所以我们会加个签名(加密),需要引用一个模块(cookie-parser)(需要安装)

let express = require('express');
let app = express();

let cookieParser = require('cookie-parser')
app.use(cookieParser('zfpx'));
app.get('/read',function (req,res) {
  res.send(req.signedCookies); // 签名可以防止cookie的篡改
});
app.get('/write',function (req,res) {
  //设置cookie方法res.cookie
  res.cookie('name','zdl1',{signed:true}); 
  res.cookie('age','9');
  res.send('ok')
})

app.listen(3000);

其他选项

cookie将缓存放在硬盘上了,session放在浏览器上的浏览器关掉就消失了

let express = require('express');
let app = express();


app.get('/write',function (req,res) {
  //仅访问read的时候有,且10s之后过期
  res.cookie('name','zdl',{path:'/read',expires:new Date(Date.now() + 10*1000),maxAge: 10 *1000,httpOnly:true}); 
  res.end('ok')
})
app.get('/read',function (req,res) {
  res.end(JSON.stringify(req.cookies));
});
app.get('/read2',function (req,res) {
  res.end(JSON.stringify(req.cookies));
});

app.listen(3000);

实现cookie-parser中间件

let querystring = require('querystring');

module.exports = function(){
    return function(req,res,next) {
        let cookie = req.headers.cookie;
        if(cookie) {
            req.cookies = querystring.parse(cookie,'; ');
            next();
        }else{
            req.cookies ={};
            next();
        }
    }
}

实现req.cookies中间件

app.use(function(req,res,next){
  res.cookie = function(key ,val, options){
    let pairs = [`${key} = ${encodeURIComponent(val)}`];
    if(options.domain){
      pairs.push(`Domain=${options.domain}`)
    }
    if(options.path){
      pairs.push(`Path=${options.path}`)
    }
    if(options.expires){
      pairs.push(`Expires=${options.expires.toUTCString()}`)
    }
    if(options.maxAge){
      pairs.push(`max-Age=${options.maxAge}`)
    }
    if(options.httpOnly){
      pairs.push(`httpOnly=true`)
    }
  }
  let cookie = pairs.jion('; ');
  res.setHeader('Set-Cookie',cookie)
})

统计某个客户端访问的次数

let express = require('express');
let cookieParser = require('cookie-parser');
let app = express();
app.use(cookieParser());
app.get('/visit',function(req,res){
    let visit = req.cookies.visit;
    console.log(visit);
    
    if(visit){
        visit = isNaN(visit) ? 0 : Number(visit) + 1;
    }else{
        visit = 1;
    }
    res.cookie('visit',visit,{path:'/visit',httpOnly:true});
    res.send(`这是你第${visit}次访问`)
})
app.listen(8080);
//test:localhost:8080/visit

为了防止服务器串改,我们使用{signed:true}也就是加密,现在对req.cookies中间件做加密优化(加密请查看加密详解文章


let express = require('express');
let querystring = require('querystring')
// let cookieParser = require('cookie-parser');
let app = express();
function signed(val ,secret){ //值,秘钥 (secret在cookieparser的时候传进来)
    return 's:' + val + '.' + require('crypto')
    .createHmac('sha256',secret)
    .update(val)
    .digest('base64')
    .replace(/\=+$/,'') //生成秘钥后去掉等号就是最后的结果
}
function unsign(val,secret){
    let value = val.slice(2,val.indexOf('.'));
    console.log('value=' + value);
    console.log(val.indexOf('.'));
    
    console.log(val);
    console.log(signed(value,secret));
    
    //signed值没有编码的,包含+的
    return signed(value,secret) == val ? value : false;
}
function cookieParser(secret){
    return function(req,res,next){
        req.secret = secret;//将秘钥付给req,加密需要此对象
        let cookie = req.headers.cookie;//name='s:9.0000
        if(cookie){ //如果cookie存在则进行对比,这里的cookie拿到的是签名后的值
            let cookies = querystring.parse(cookie,'; ')//{name:'s:9.0000'}
            let signedCookies = {};
            if(secret){//是否解密
                for(let key in cookies){
                    signedCookies[key] = unsign(cookies[key],secret);
                }
            }
            req.signedCookies = signedCookies;
            req.cookies = cookies;
            next();
        }else{//否则
            req.cookies = req.signedCookies = {};
            next();
        }
    }
}
app.use(cookieParser('zdl'));  //传过去秘钥
app.use(function(req,res,next){
    res.cookie = function(key ,val, options){
        encodeURIComponent
        //req.res可以拿到响应对象,res.req可以拿到请求对象
        let pairs = [`${key} = ${encodeURIComponent(signed(String(val),this.req.secret))}`];//值,密码

        if(options.domain){
            pairs.push(`Domain=${options.domain}`)
        }
        if(options.path){
            pairs.push(`Path=${options.path}`)
        }
        if(options.expires){
            pairs.push(`Expires=${options.expires.toUTCString()}`)
        }
        if(options.maxAge){
            pairs.push(`max-Age=${options.maxAge}`)
        }
        if(options.httpOnly){
            pairs.push(`httpOnly=true`)
        }
        let cookie = pairs.join('; '); //57hang
        res.setHeader('Set-Cookie',cookie)
    }
    next();
})

app.get('/visit',function(req,res){ //63hang
    let visit = req.signedCookies.visit;  // let visit = req.cookies.visit;
    if(visit){
        visit = isNaN(visit) ? 0 : Number(visit) + 1;
    }else{
        visit = 1;
    }
    //this.req.secret
    res.cookie('visit',String(visit),{signed:true});
    res.send(`${visit}`)
})
app.listen(8080);

使用cookie注意事项

  • 可能被客户端串改,使用钱验证合法性
  • 不要存储敏感数据,比如用户密码,账户余额
  • 使用httpOnly保证安全
  • 尽量减少cookie的体积
  • 设置正确的domain和path,减少数据传输

由于cookie存储在硬盘上很容易被用户串改,我们一般采用session,session是存在服务器上的

session

每次访问服务器同一个卡号对应服务器端存储信息

let express = require('express');
let cookiepaser = require('cookie-parser');
let app = express();
app.use(cookiepaser())
let sessions = {};
const SESSION_KEY = 'connect.sid';
app.get('/',function(req, res){
    let sessionId = req.cookies[SESSION_KEY];
    if(sessionId){
        let sessionObj = sessions[sessionId];
        if(sessionObj){
            sessionObj.balance -= 10;
            res.send(`欢迎新顾客,送你一张卡,余额${sessionObj.balance}`)
        }else{
            genId();
        }
    }else{
        genId();
    }
    function genId(){
        //第一次来发会员卡 卡号唯一,不容易被猜到
        let sessionId = Date.now() + Math.random() + '';
        //在服务器端开辟内存,记录信息
        sessions[sessionId] = {balance:100, name:req.query.name} ; //session对象
        //把卡号通过cookie发给客户端
        res.cookie(SESSION_KEY , sessionId)
        res.send('欢迎新顾客,送你一张卡,余额100')
    }
})
app.listen(3000)

express-session中间件使用

需要安装$ npm install express-session

  • 在服务器端生成全局唯一标识符session_id
  • 在服务器内存里开辟此session_id对应的数据存储空间
  • 将session_id作为全局唯一标示符通过cookie发送给客户端
  • 以后客户端再次访问服务器时会把session_id通过请求头中的cookie发送- 给服务器
  • 服务器再通过session_id把此标识符在服务器端的数据取出

express-session是用来获取和绑定session的,当你使用此之后在req商会多一个req.session


let express = require('express');
let querystring = require('querystring')
let session = require('express-session');
let app = express();
//secret, resave,saveUninitialized
app.use(session({
    secret: 'zdl',
    resave: true,
    saveUninitialized: true
})); 
app.get('/visit',function(req,res){
    let visit = req.session.visit; 
    if(visit){
        visit = isNaN(visit) ? 0 : Number(visit) + 1;
    }else{
        visit = 1;
    }
    req.session.visit = visit;
    res.send(`${visit}`)
})
app.listen(8080);

//test:localhost:8080/visit
 

express-session 中间件的实现

  • 浏览器通过cookie把sessionId发送给服务器
  • express拿到ID,从而拿到session对象,session有个store(仓库),可能是cookie,memory,connet-mencached
  • user 返回 new user
  • 拿到ID返回
name detailed
name 设置 cookie 中,保存 session 的字段名称,默认为 connect.sid
store session 的存储方式,默认存放在内存中,也可以使用 redis,mongodb 等
secret 通过设置的 secret 字符串,来计算 hash 值并放在 cookie 中,使产生的 signedCookie 防篡改
cookie 设置存放 session id 的 cookie 的相关选项,默认为 (default: { path: '/', httpOnly: true, secure: false, maxAge: null })
genid 产生一个新的 session_id 时,所使用的函数, 默认使用 uid2 这个 npm 包
rolling 每个请求都重新设置一个 cookie,默认为 false
saveUninitialized 是指无论有没有session cookie,每次请求都设置个session cookie ,默认给个标示为 connect.sid
resave 是指每次请求都重新设置session cookie,假设你的cookie是10分钟过期,每次请求都会再设置10分钟

需要安装部分中间件,这里就不一一列举了


let express = require('express');
let path = require('path');
let querystring = require('querystring')
let bodyPaser = require('body-parser')
let session = require('express-session');
let app = express();
app.set('view engine','html');
app.use(bodyPaser.urlencoded({extended:true}))
app.set('views',path.resolve(__dirname,'views'));
app.engine('.html',require('ejs').__express);
//secret, resave,saveUninitialized
app.use(session({
    secret: 'zdl',
    resave: true,
    saveUninitialized: true
})); 
let users = [];
//注册
app.get('/reg',function(req,res){
    res.render('reg',{title: '注册'})
})
app.post('/reg',function(req,res){
    let user = req.body;
    users.push(user);
    res.redirect('/login');//重定向到login页面
})
//登录
app.get('/login',function(req,res){
    res.render('login',{title: '登录'})
})
app.post('/login',function(req,res){
    let user = req.body;
    let oldUser = users.find(item =>  user.username == item.username && user.password == item.password);
    if(oldUser){
        req.session.user = oldUser; //跳转之前记录下用户
        res.redirect('/user')
    }else{
        res.render('back')
    }
})
function checkuser(req,res,next){
    if(req.session.user){
        next()
    }else{
        res.redirect('/login')   
    }
}
app.get('/user',checkuser,function(req,res){
    //在没有登录的情况下直接访问会报错,需要给她配置一个检查,写个中间件
    res.render('user',{username:req.session.user.username,title:'用户中心'})
})
//退出
app.get('/logout',function(req,res){
    delete  req.session.user; 
    res.redirect('/login')

})
app.listen(8080);

redis实现分布式session

session会话和seesionsttorge不一样的

客户端访问服务器,通常在浏览器访问量很大的时候,我们会有很多台服务器,这首我们一般会随机分配(其实有些策略像nginx负载均衡,例如轮询等等,后面的文行坑能会更新这一部分内容),大概原理就是我们请求不同的服务器,不同的服务器将数据统一在统一台数据库做处理,这边其实不是服务器,其实是集群,所有服务器读取一体,下面我们用redis实现一下

安装connect-redis中间件使用 需要下载此中间件+redis数据库

//在上面js基础上添加
...
//通过redis存储session
const ResiStore = require('connect-redis')(session);
...
app.use(session({
    ...
    //store制定把会话数据放在哪个地方
    store:new ResiStore({ // 制定redis放在哪个地方
        host: 'localhost',
        port: 6379
    })
})); 
let users = [];
//注册
app.get('/reg',function(req,res){...})
...
app.listen(8080);

同样将seesion存到文件,类似

//自己实现一个模块将session数据放在文件系统里
const FileStore = require('./connect-file')(session);
...
app.use(session({
    ...
    store:new FileStore({ // 制定redis放在哪个地方
         dir : path.resolve(__dirname,'sessions')
    })
})); 
let users = [];
//注册
...
app.listen(8080);

connect-redis中间件实现

connect-file

const FileStore = require('./connect-file')(session);

store:new FileStore({dir : path.resolve(__dirname,'sessions') })

上述两行代码可以看出

  • ./connect-file 导出的FileStore 是个函数
  • 传进去session参数
  • new FileStor像是个类,也像是个构造函数
  • express-session 提供一组接口 实现了一组基类的实现,可以扩展,扩展存储位置,如果实现自定义存储类,需要定义三个方法 get获取sessionset设置sessiondestory销毁session,也就是读,写,销毁
let util = require('util');
let path = require('path');
let mkdirp = require('mkdirp');//集群创建目录的模块 需要安装
let fs = require('fs');

//返回函数传进去参数session
function createFileStore(session){
    //store类是所有自定义存储的基类
    let Store = session.Store;
    util.inherits(FileStore,Store);//FileStore继承Store类
    //此目录存放着所有的session对象,每个对象都是json文件
    function FileStore(options = {}){
   // new FileStore({dir : path.resolve(__dirname,'sessions') })
        let {dir = path.resolve(__dirname,'session')} = options;
        this.dir = dir; //保存到实例上,在portotype可以使用
        mkdirp(this.dir);//级联创建文件,fs.mkdir父目录不存在不能创建子目录,此模块此需要安装
    }
    //FileStore是个类
    //通过sessonid拿到对应的文件名
    FileStore.prototype.resolve = function(sid){
        return path.resolve(this.dir,`${sid}.json`)//文件名,每个session对象对应这样一个文件 
    }
    //通过sessonid保存session到文件中去
    FileStore.prototype.set = function(sid , session ,  callback){
        fs.writeFile(this.resolve(sid),JSON.stringify(session),callback)
    }
    //sid卡号
    //通过sessonid获取文件系统中存放的session对象
    FileStore.prototype.get = function(sid , callback){
        fs.readFile(this.resolve(sid),'utf8',function(err,data){
            if(err) callback(err);
            else callback(err, JSON.parse(data))
        })
    }
    FileStore.prototype.destroy = function(sid , callback){
        fs.unlink(this.resolve(sid),callback)
    }
    return FileStore;
}
module.exports = createFileStore;

最后会生成一个session文件夹,下面存在一个json文件存放session

connect-redis

let util = require('util');
const redis = require('redis');//redis文件需要下载

function createRedisStore(session){
    let Store = session.Store;
    util.inherits(RedisStore,Store);//子,父类
    function RedisStore(options = {}){
        let {} = options;
        //创建客户端
        this.client = redis.createClient(options.port|| 6379,options.host||'localhost'); 
    }
    RedisStore.prototype.set = function(sid , session ,  callback){
        this.client.set(sid,JSON.stringify(session),callback)
    }
    RedisStore.prototype.get = function(sid , callback){
        this.client.get(sid,function(err,data){
            if(err) callback(err);//第一次文件不存在
            callback(err,JSON.parse(data))
        })
    }
    RedisStore.prototype.destroy = function(sid , callback){
        this.client.unset(sid,callback)
    }
    return RedisStore;
}
module.exports = createRedisStore;

这就是我们的cookie,session,利用齐权限管理以及中间件的实现,和redis分布用法,宝宝我啃了两天,大家耐心一点哦