單點登錄

初識單點登錄

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

原文:http://www.cnblogs.com/yexiaochai/p/4422460.html

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章