目錄javascript
本文主要講解OAuth2.0協議和github、微博、QQ三個平臺提供的接入流程,介紹nodejs下十分好用的認證受權插件passport.js。本文代碼基於nodejs-express。php
在說OAuth協議以前,想說一下OpenID。在2005年的夏天,一個開源社區爲了解決一個其它現有web身份認證技術不容易解決的問題時,制定了OpenID,OpenID是一個認證(Authentication)協議,這個協議讓第三方應用程序在不獲取你密碼的狀況下認證你的身份,OpenID和OAuth是兩個常常放在一塊兒比較的web身份認證技術,但OAuth比起OpenID,更像是一個受權(Authorization)協議,是讓第三方應用程序在不獲取你密碼的狀況下獲取你的受權去對受權提供商或者說是資源提供商請求你受權的資源,拿到OpenID就像給別人指,這棟房子是個人,我能夠拿點東西給你看看,而拿到OAuth受權就像你給別人說,這棟房子是個人,這個鑰匙給你,但只能開大門不能進臥室,哪些能夠動哪些能夠開都是你說了算,而且你能夠隨時方便的收回這把鑰匙。html
OAuth2.0是2006年提出來的新一代OAuth版本,比起OAuth1.x,它簡化了認證交互過程,增長了認證機制,使用了SSL,去掉了一個叫作客戶端token secret的東西,這也致使了OAuth升級須要改動代碼,而且沒有了1.0版本的安全bug(1.0a修復),這個安全bug主要是回調地址設定方式致使的。java
OAuth2.0協議主要有四個角色,資源全部者即用戶,資源服務器,受權服務器和用戶代理即客戶端,這四個角色主要是完成了這樣一個流程,客戶端與服務器提供商以前,有一個經過OAuth協議完成的受權服務器,客戶端有從服務端獲取的惟一ID,客戶端不能直接登陸服務器提供商,須要先經過用戶在受權服務器取得受權碼,而且拿着得到用戶受權的受權碼在受權層換取accessToken,經過這個token向資源服務器請求用戶受權信息。node
OAuth2.0客戶端一共有4種受權模式,受權碼模式(authorization code)、簡化模式(implicit)、密碼模式(resource owner password credentials)和客戶端模式(client credentials),在github提供的OAuth.v3接口中,使用的是受權碼模式,在這種模式中,客戶端須要引導用戶重定向請求到Github的OAuth認證接口,帶上在受權服務器申請的客戶端 client_id
,配置的回調地址 redirect_uri
,申請受權的scope
,你也能夠帶上狀態值state
,服務器會在回調中原封不動地返回你這個值,這一步將會換取前面咱們說到的受權碼code
,具體流程和代碼實現以下:git
1.首先咱們須要在github上新建一個應用(記得登陸),獲取客戶端的Client ID和Client Secret並配置好回調地址。
github
2.重定向到OAuth認證接口 GET https://github.com/login/OAuth/authorize
web
<a href= '/mygithub'></a>
3.發起重定向請求到受權服務器換取code
express
var express = require('express'); app.get('/mygithub', function(req, res) { var dataStr = (new Date()).valueOf(); //重定向到認證接口,並配置參數 //注意這裏使用的是node的https模塊發起的請求 var path = "https://github.com/login/OAuth/authorize"; path += '?client_id=' + OAuthConfig.GITHUB_CLIENT_ID; path += '&scope='+OAuthConfig.GITHUB_CLIENT_SCOPE; path += '&state='+ dataStr; //轉發到受權服務器 res.redirect(path); });
經過第一步的請求,若是用戶正確受權,換取的code和上一步傳輸的state會被服務器做爲回調參數,放在你在這裏配置的回調地址上,可是若是受權服務器返回的state和第一步傳入的不一致,根據OAuth2.0協議,開發者應該停止這個請求。拿到了code
後,開發者應該在回調地址處重定向請求到受權服務器利用client_id
、client_secret
和code
換取access_token
,這個access_token
就是你開啓開門的鑰匙,拿到鑰匙後你能夠本地存儲鑰匙或者根據scope
立刻向資源服務器請求數據,具體流程和代碼實現以下:npm
app.get("/OAuth/github_v2/callback", function(req, res){ var code = req.query.code; var state = req.query.state; var headers = req.headers; var path = "/login/OAuth/access_token"; headers.host = 'github.com'; path += '?client_id=' + OAuthConfig.GITHUB_CLIENT_ID; path += '&client_secret='+OAuthConfig.GITHUB_CLIENT_SECRET; path += '&code='+ code; console.log(path); var opts = { hostname:'github.com', port:'443', path:path, headers:headers, method:'POST' }; //注意這裏使用的是node的https模塊發起的請求 var req = https.request(opts, function(res){ res.setEncoding('utf8'); console.log(opts); //解析返回數據 res.on('data', function(data){ var args = data.split('&'); var tokenInfo = args[0].split("="); var token = tokenInfo[1]; console.log(data); //利用access_token向資源服務器請求用戶受權數據 var url = "https://api.github.com/user?access_token="+token&scope="user"; https.get(url, function(res){ res.on('data', function(userInfo){ console.log(userInfo); }); }); }) }); });
至此,你就能夠拿着token獲取用戶受權的信息了,可是token也會過時,須要從新請求。
scope
是OAuth協議提供權限範圍選項,避免讓第三方應用有了一個鑰匙就打開了用戶全部的門,在github中,你能夠打開的門以下:
除了用原生的OAuth協議去獲取用戶受權,咱們也可使用一些第三方應用提供商的SDK來實現快速地接入,特別是國內的許多平臺都提供了集成OAuth協議的SDK,使用這種SDK能夠幫你快速對現有網站進行集成,可是缺點對開發測試很不友好,好比須要你是上線的網站而且在驗證的時候它會默認去訪問服務器80端口,若是你的那臺測試服務器恰好http端口在作其餘事情,這就很尷尬了。
下面我就微博和騰訊QQ的接入來簡單介紹一下。
使用微博SDK以前,你須要先在微博開放平臺驗證網站全部權,微博有一點特別很差的就是在這裏接入的地址不能指定端口,再在這裏下載相應的SDK。
準備工做作好以後,開發起來就至關便利了。
meta(property="wb:webmaster" content="YOUR_KEY")
添加一個meta標籤以便微博服務器識別你的身份,微博會對你在驗證網站全部權配置的地址進行http訪問,來查詢是否有正確的meta,不過微博的實現好像不是很好,常常會告訴你驗證失敗,固然QQ也同樣。
在appkey填入你申請的appkey,而且開啓debug模式方便調試
script(src="http://tjs.sjs.sinajs.cn/open/api/js/wb.js?appkey=YOUR_APPKEY&debug=true" type="text/javascript" charset="utf-8")
完成了上面的資源引用,對於咱們的受權接入,只需簡單放置以下代碼便可
<div id="wb_connect_btn"></div> WB2.anyWhere(function (W) { W.widget.connectButton({ id: "wb_connect_btn",//按鈕id type: '3,2',//按鈕樣式,[這裏](http://open.weibo.com/widget/loginbutton.php)能夠設置 callback: { login: function (o) { //登陸後的回調函數 alert("login: " + o.screen_name) }, logout: function () { //退出後的回調函數 alert('logout'); } } }); });
騰訊SDK的使用方式和微博SDK差異不大,最好的一點就是騰訊的SDK若是回調地址是當前頁面,能夠不用配置回調地址就能夠快速接入。固然你也須要在這裏註冊申請appkey
meta(property="qc:admins" content="YOUR_KEY")
script(type="text/javascript" src="http://qzonestyle.gtimg.cn/qzone/openapi/qc_loader.js" data-appid="YOUR_APPKEY" charset="utf-8")
騰訊的SDK會默認有不少日誌在控制檯輸出。你能夠看到你有什麼錯誤。
<span id="qqLoginBtn"></span>
QC.Login({ btnId: "qqLoginBtn", //插入按鈕的節點id size: "C_S" //按鈕樣式 }, function(reqData, opts) { var dom = document.getElementById('user_login'), _logoutTemplate = [ //頭像 '<span><img src="{figureurl}" class="{size_key}"/></span>', //暱稱 '<span>{nickname}</span>', //退出 '<span><a style="color:white" href="javascript:QC.Login.signOut();">退出</a></span>' ].join(""); dom && (dom.innerHTML = QC.String.format(_logoutTemplate, { nickname: QC.String.escHTML(reqData.nickname), //作xss過濾 figureurl: reqData.figureurl })); $('#user_login').removeClass('hide'); }, function(opts) { //註銷成功 console('QQ登陸 註銷成功'); });
若是對於每一個接入都要去使用這個第三方應用提供商的SDK,那麼對於一個開發者,花在學習調試開發上面的時間會成倍增長,既然如今主流的第三方登陸都適用的OAuth協議,那麼咱們可不能夠根據這個流程封裝一套通用的SDK呢?答案固然是能夠的,而且前人已經爲咱們造好了輪子。
在Node平臺,最出名的認證插件固然非passport.js莫屬了,在它的官網上你能夠很容易的搜索到各類第三方應用登陸策略,只需簡單的npm install,你就能夠進行OAuth認證受權了。點擊官網上面的Strategies
字樣搜索你想要的登陸策略。
passport目前擁有300+的第三方應用接入策略,支持持久化session,擁有簡單易用的回調處理函數,同時支持OpenID
和 OAuth
。
github v3 接口實現、qq、weibo的passport.js插件
npm install passport-github2 npm install passport-qq npm install passport-weibo
咱們使用express來作爲咱們的web框架,使用passport.js作咱們的認證插件,在這個例子中咱們僅僅介紹passport-github2的使用,QQ和weibo插件的使用和passport-github2如出一轍。須要引入以下幾個庫
var express = require('express'); var passport = require('passport'); var util = require('util'); var session = require('express-session'); var bodyParser = require('body-parser'); var methodOverride = require('method-override'); var GitHubStrategy = require('passport-github2').Strategy; var partials = require('express-partials');
第一步,序列化與反序列化session值,以支持持久化登陸,這步之後你能夠方便的在passport的session模塊中根據你序列化的惟一值(通常爲ID),來處理用戶的登陸狀態和登陸信息等
passport.serializeUser(function(user, done) { done(null, user); }); passport.deserializeUser(function(obj, done) { done(null, obj); });
第二步,使用在passport.js中使用GitHub的認證策略GitHubStrategy,形如這樣的策略函數是passport中的一個‘認證’函數,它的回調接受accessToken
, refreshToken
和第三方返回的用戶信息,這裏是GitHub profile
。
passport.use(new GitHubStrategy({ clientID: YOUR_GITHUB_CLIENT_ID, clientSecret: YOUR_GITHUB_CLIENT_SECRET, callbackURL: "http://127.0.0.1/auth/github/callback",//第三方應用申請頁面填寫的回調地址 passReqToCallback: true//會傳輸req對象 }, function(req,accessToken, refreshToken, profile, done) { console.log(profile); process.nextTick(function() { //在這裏你能夠對profile進行處理,好比進行存儲或者方便 if(req.user){ //...綁定 app.db.models.User.findOne({...},function(){ .... }) } else{ //...新建一個用戶 var user = new app.db.models.User(); user.github = profile; user.save(); } //...或者存儲accessToken return done(null, profile); }); } ));
第三步,對express進行配置,和以往的express配置沒有太多不一樣,只須要調用中間價passport的initialize
和session
去初始化passport和它的session模塊。
var app = express(); // configure Express app.set('views', __dirname + '/views'); app.set('view engine', 'ejs'); app.use(partials()); app.use(bodyParser.urlencoded({ extended: true })); app.use(bodyParser.json()); app.use(methodOverride()); app.use(session({ secret: 'keyboard cat', resave: false, saveUninitialized: false })); app.use(passport.initialize()); app.use(passport.session()); app.use(express.static(__dirname + '/public'));
第四步,配置路由
須要引導用戶點擊/auth/github
,使用passport.authenticate()作爲路由中間件去受權認證請求,passport.js插件會重定向用戶到github的受權頁,用戶點擊受權後github會把用戶重定向到你以前申請應用時填寫的回調地址,咱們在回調地址/auth/github/callback
中接受用戶信息,而且利用passport.authenticate()作爲路由中間件去檢查受權是否成功以及引導相應路由。
app.get('/', function(req, res) { res.render('index', { user: req.user }); }); app.get('/account', ensureAuthenticated, function(req, res) { res.render('account', { user: req.user }); }); app.get('/login', function(req, res) { res.render('login', { user: req.user }); }); // GET /auth/github app.get('/auth/github', passport.authenticate('github', { scope:YOUR_GITHUB_CLIENT_SCOPE }), function(req, res) { // The request will be redirected to GitHub for authentication, so this // function will not be called. }); // GET /auth/github/callback app.get('/auth/github/callback', passport.authenticate('github', { failureRedirect: '/login' }), function(req, res) { //這裏能夠重定向到用戶界面等頁面,如今已經拿到用戶信息和存儲了session。 res.redirect('/'); });
_json
值是格式化的raw
值,故全部返回字段皆省略raw
字段
注意QQ的_json字段中並無id值,與github和weibo不一樣
{ provider: 'qq', id: '', nickname: '', _raw: '...', _json: { ret: 0, msg: '', is_lost: 0, nickname: '', gender: '男', province: '四川', city: '成都', year: '1994', figureurl: '', figureurl_1: '', figureurl_2: '', figureurl_qq_1: '', figureurl_qq_2: '', is_yellow_vip: '0', vip: '0', yellow_vip_level: '0', level: '0', is_yellow_year_vip: '0' } }
{ provider: 'weibo', id: '', displayName: '', _raw: { ...}, _json: { id: , idstr: '', class: 1, screen_name: '', name: '', province: '44', city: '3', location: ' ', description: '', url: '', profile_image_url: '', cover_image: '', profile_url: '', domain: '', weihao: '', gender: 'm', followers_count: , friends_count: , pagefriends_count: , statuses_count: , favourites_count: , created_at: '', following: false, allow_all_act_msg: false, geo_enabled: true, verified: true, verified_type: 2, remark: '', status: { created_at: 'Tue Aug 23 15:22:14 +0800 2016', id: , mid: '', idstr: '', text: '', textLength: 60, source_allowclick: 0, source_type: 1, source: '', favorited: false, truncated: false, in_reply_to_status_id: '', in_reply_to_user_id: '', in_reply_to_screen_name: '', pic_urls: [Object], thumbnail_pic: '', bmiddle_pic: '', original_pic: '', geo: null, reposts_count: 0, comments_count: 0, attitudes_count: 0, isLongText: false, mlevel: 0, visible: [Object], biz_feature: 0, page_type: 32, hasActionTypeCard: 0, darwin_tags: [], hot_weibo_tags: [], text_tag_tips: [], userType: 0, positive_recom_flag: 0, gif_ids: '', is_show_bulletin: 2 }, ptype: 0, allow_all_comment: true, avatar_large: '', avatar_hd: '', verified_reason: '', verified_trade: '', verified_reason_url: '', verified_source: '', verified_source_url: '', verified_state: 0, verified_level: 3, verified_type_ext: 0, verified_reason_modified: '', verified_contact_name: '', verified_contact_email: '', verified_contact_mobile: '', follow_me: false, online_status: 0, bi_followers_count: 94, lang: 'zh-cn', star: 0, mbtype: 0, mbrank: 0, block_word: 0, block_app: 0, credit_score: 80, user_ability: 4, urank: 19 } }
{ id: '', displayName: null, username: '', profileUrl: '', emails: [ { value: '' } ], provider: 'github', _raw: '{...}', _json: { login: '', id: , avatar_url: '', gravatar_id: '', url: '', html_url: '', followers_url: '', following_url: '', gists_url: '', starred_url: '', subscriptions_url: '', organizations_url: '', repos_url: '', events_url: '', received_events_url: '', type: 'User', site_admin: false, name: null, company: null, blog: null, location: null, email: '', hireable: null, bio: null, public_repos: 0, public_gists: 0, followers: 0, following: 0, created_at: '2016-07-15T06:17:35Z', updated_at: '2016-08-18T08:06:49Z', private_gists: 0, total_private_repos: 0, owned_private_repos: 0, disk_usage: 0, collaborators: 0, plan: { name: 'free', space: , collaborators: 0, private_repos: 0 } } }
最後簡單介紹一下QQ和weibo的應用申請和審覈。