初識單點登入
最初接觸到單點登入要追溯到3年多以前了,那時候看到的只是passport,當時要做全國所有社群的登入,然後就照著內部文件寫了程式碼,然後就接入了(這裡要提一句是百度與騰訊一旦形成產品的技術專案,文件都很不錯)然後就沒有然後了......
而知識的珍貴程度卻是這樣的:
知識珍貴度排名:
聽過 < demo過 < 實際工作用過 < 實際工作中被坑過< 實際工作中被多次坑過或者深入研究總結過
所以,脫離工作去學習node可能收效不大,脫離專案去說前端優化可能不讓人信服,不被ie坑也認識不到相容,這個時候我們就要抓住工作中的點,一點點深入進去了,也許這一輪的瓶頸就自然而然的突破了呢,這是我最近的想法,也試著朝著相關的方向努力。
今天想要寫passport事實上是工作中用到了這個點了,如果我就照著內部文件接入了似乎意義不大,所以我準備就著這個機會騷騷瞭解下原理,期望將他變成第一類知識沉澱。
文中內容皆是自我總結或者查詢,如果有誤請您提出,文中程式碼比較簡陋,僅幫助理解,勿用作實際專案
什麼是單點登入
單點登入即single sign on,用以解決同一公司不同子產品之間身份認證問題,也就是說,a.baidu.com與b.baidu.com兩個站點之間只需要登入一次即可。
一般來說單點登入實現原理為,首次訪問一個站點時會被引導至登入頁,使用者登入驗證通過,瀏覽器會儲存一個關鍵key(一般採用cookie),使用者訪問其他系統時會帶著這個key,伺服器系統發現具有key標誌後,會對其進行驗證,如果通過便不需要再次登入了。
這裡的關鍵為二,第一是key,其二為系統統一的認證機制,這當中key會有一系列規則保證登入的安全性,而統一的認證機制是單點登入的前提,key是由他提供出去,每次使用者由以這個給出的key來這裡驗證,判斷其有效性,這裡的統一的驗證平臺便是passport,他負責著製作通行令牌,並且對其進行驗證。
這裡舉個例子來說(有誤請您指出):
子系統A統一到passport伺服器鑑權,並且在passport處獲取cookie,並將token加入url,跳轉回子系統A
跳回子系統A後,使用token再次去passport驗證(這裡直接走webservice服務驗證,不再跳轉),驗證通過便在A系統伺服器生成session,以後訪問子系統A,在有效時間內不到passport驗證
進入子系統B,跳到passport鑑權,發現passport cookie已經存在(token),便直接跳回子系統B,url帶有token值,使用token去驗證流程與A一致
實現原理
以騰訊百度等公司產品來說,他們一般會有一個統一的域,比如:
baidu.com => tieba.baidu.com、music.baidu.com、baike.baidu.com......
qq.com => feiji.qq.com、cd.qq.com、music.qq.com
這類共享一個主域,但是這類網站往往還會提供給第三方使用,比如騰訊會給tencent.com與jd.com使用,這個時候一些實現細節又不一樣,但有一點是確定的:
單點登入要求共享一套賬號體系,至少儲存各個子系統的公共資訊
使用者登入態,我們存於session中,這個時候第一類站點的單點登入便比較簡單,以百度為例,直接設定baike.baidu.com與tieba.baidu.com的session為cookie domain為baidu.com便可解決問題。
第二種情況比較普遍,便是music.qq.com與jd.com要實現單點登入,這個便完全跨域了,SSO的做法是將登入態儲存至公用域,一般是passport.xx.com,比如百度為passport.baidu.com。
這個時候tieba.baidu.com或者music.baidu.com的授權檢查與退出操作全部由SSO(passport.baidu.com)來進行
簡單實現
單點登入的呼叫表現一般有兩種:
① 跳轉
② 彈出層回撥
當使用者在子系統未登入時,便會攜帶相關引數,比如tieba.baidu.com去到SSO(passport.baidu.com)進行登入
登入成功SSO會生成ticket key並附加給源網址跳轉回去,這個時候SSO上已經有使用者的登入狀態了,但是各個子系統仍然沒有登入態,所以根據這個ticket設定各個子系統獨立的登入態是需要的。
當在SSO驗證結束後,會回到子系統,子系統會根據得到的ticket(SSO為此次登入生成的使用者基本資訊加密串)獲取使用者的基本資訊,從而在本站設定登入態。
這裡使用程式碼做一個說明,有2年沒有寫伺服器端程式碼了,最後選來選去使用了node實現,才發現自己對node也很不熟悉啊!!!
passport的實現
首先我們看看鑑權程式碼的實現
1 var path = require('path'); 2 var express = require('express'); 3 var router = express.Router(); 4 //偷懶,將token資料寫入檔案 5 var fs = require('fs'); 6 7 /* GET home page. */ 8 router.get('/', function (req, res, next) { 9 if (!req.query.from) { 10 res.render('index', { 11 title: '統一登入passport' 12 }); 13 return; 14 } 15 var from = req.query.from; 16 var token = null; 17 var cookieObj = {}; 18 var token_path = path.resolve() + '/token_user.json'; 19 req.headers.cookie && req.headers.cookie.split(';').forEach(function (Cookie) { 20 var parts = Cookie.split('='); 21 cookieObj[parts[0].trim()] = (parts[1] || '').trim(); 22 }); 23 token = cookieObj.token; 24 //如果url帶有token,則表明已經在passport鑑權過 25 if (token) { 26 //存在token,則在記憶體中尋找之前與使用者的對映關係 27 //非同步的 28 fs.readFile(token_path, 'utf8', function (err, data) { 29 if (err) throw err; 30 if (!data) data = '{}'; 31 data = JSON.parse(data); 32 //如果存在標誌,則驗證通過 33 if (data[token]) { 34 res.redirect('http://' + from + '?token=' + token); 35 return; 36 } 37 //如果不存在便引導至登入頁重新登入 38 res.redirect('/'); 39 }); 40 } else { 41 res.render('index', { 42 title: '統一登入passport' 43 }); 44 } 45 }); 46 47 router.post('/', function (req, res, next) { 48 if (!req.query.from) return; 49 var name = req.body.name; 50 var pwd = req.body.password; 51 var from = req.query.from; 52 var token = new Date().getTime() + '_'; 53 var cookieObj = {}; 54 var token_path = path.resolve() + '/token_user.json'; 55 //簡單驗證 56 if (name === pwd) { 57 req.flash('success', '登入成功'); 58 //passport生成使用者憑證,並且生成令牌入cookie返回給子系統 59 token = token + name; 60 res.setHeader("Set-Cookie", ['token=' + token]); 61 //持久化,將token與使用者的對映寫入檔案 62 fs.readFile(token_path, 'utf8', function (err, data) { 63 if (err) throw err; 64 if (!data) data = '{}'; 65 data = JSON.parse(data); 66 //以token為key 67 data[token] = name; 68 //存回去 69 fs.writeFile(token_path, JSON.stringify(data), function (err) { 70 if (err) throw err; 71 }); 72 }); 73 res.redirect('http://' + from + '?token=' + token); 74 } else { 75 console.log('登入失敗'); 76 } 77 }); 78 module.exports = router;
子系統A的實現
1 var express = require('express'); 2 var router = express.Router(); 3 var request = require('request'); 4 5 /* GET home page. */ 6 router.get('/', function (req, res, next) { 7 var token = req.query.token; 8 var userid = null; 9 //如果本站已經存在憑證,便不需要去passport鑑權 10 if (req.session.user) { 11 res.render('index', { 12 title: '子產品-A-' + req.session.user, 13 user: req.session.user 14 }); 15 return; 16 } 17 //如果沒有本站資訊,又沒有token,便去passport登入鑑權 18 if (!token) { 19 res.redirect('http://passport.com?from=a.com'); 20 return; 21 } 22 //存在token的情況下,去passport主站檢查該token對應使用者是否存在, 23 //存在並返回對應userid 24 //這段程式碼是大坑!!!設定的代理request不起效,調了3小時 25 request( 26 'http://127.0.0.1:3000/check_token?token=' + token + '&t=' + new Date().getTime(), 27 function (error, response, data) { 28 if (!error && response.statusCode == 200) { 29 data = JSON.parse(data); 30 if (data.code == 0) { 31 //這裡userid需要通過一種演算法由passport獲取, 32 //這裡圖方便直接操作token 33 //因為token很容易偽造,所以需要去主戰驗證token的有效性, 34 //一般通過webservers 這裡驗證就簡單驗證即可...... 35 userid = data.userid; 36 //有問題就繼續登入 37 if (!userid) { 38 res.redirect('http://passport.com?from=a.com'); 39 return; 40 } 41 //取得userid後,系統便認為有許可權去資料庫根據使用者id獲取使用者資訊 42 //根據userid運算元據庫流程省略...... 43 // req.session.user = userid; 44 res.render('index', { 45 title: '子產品-A-' + userid, 46 user: userid 47 }); 48 return; 49 } else { 50 //驗證失敗,跳轉 51 res.redirect('http://passport.com?from=a.com'); 52 } 53 } else { 54 res.redirect('http://passport.com?from=a.com'); 55 return; 56 } 57 }); 58 }); 59 module.exports = router;
passport鑑權程式模擬
1 var express = require('express'); 2 var router = express.Router(); 3 var path = require('path'); 4 var fs = require('fs'); 5 6 /* GET users listing. */ 7 router.get('/', function(req, res, next) { 8 var token = req.query.token; 9 var ret = {}; 10 var token_path = path.resolve() + '/token_user.json'; 11 ret.code = 1;//登入失敗 12 if(!token) { 13 res.json(ret); 14 return; 15 } 16 //如果傳遞的token與cookie不等也認為是鑑權失敗 17 // if(token != cookieObj['token']){ 18 // res.json(ret); 19 // return; 20 // } 21 fs.readFile(token_path, 'utf8', function (err, data) { 22 if (err) throw err; 23 if(!data) data = '{}'; 24 data = JSON.parse(data); 25 //如果存在標誌,則驗證通過,未考慮賬號為0的情況 26 if(data[token]) { 27 ret.code = 0;//登入成功 28 ret.userid = data[token]; 29 } 30 res.json(ret); 31 }); 32 }); 33 module.exports = router;
簡單測試
測試之前需要設定host,這裡繼續採用fiddler神器幫助:
這邊直接用node跑了3個伺服器:
然後開啟瀏覽器輸入a.com,直接跳到了,登入頁:
簡單登入後(賬號密碼輸入一致即可):
這個時候直接進入子系統B:
直接便登入了,說明方案大體上是可行的
結語
首先,這裡的程式很簡陋,很多問題,也沒有做統一退出的處理,今天主要目的是瞭解單點登入,後面有實際工作再求深入。
這裡附上原始碼,感興趣的朋友看看吧:http://files.cnblogs.com/files/yexiaochai/web.rar,注意依賴包