【大前端】認識單點登入

葉小釵發表於2015-04-22

初識單點登入

最初接觸到單點登入要追溯到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,注意依賴包

相關文章