許可權處理 - 用redis實現分散式session~ (cookie && session )

渣渣的生存之道發表於2018-07-27

如何進行許可權管理? 首先我們要了解一下cookie和session

cookie

為什麼有cookie?我們說http請求其實是無狀態的,也就是第一次和第二次訪問瀏覽器都無法識別,於是我們在瀏覽器第一次訪問伺服器的時候,伺服器會通過set-cookie響應頭給瀏覽器種個cookie(標識),cookie是為了辯別使用者身份,進行會話跟蹤而儲存在客戶端上的資料,如圖:

許可權處理 - 用redis實現分散式session~ (cookie && session  )

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分佈用法,寶寶我啃了兩天,大家耐心一點哦

相關文章