第三方接入總結
本文主要講解OAuth2.0協議和github、微博、QQ三個平台提供的接入流程,介紹nodejs下十分好用的認證授權插件passport.js。本文代碼基於nodejs-express。
OAuth2.0介紹
在說OAuth協議之前,想說一下OpenID。在2005年的夏天,一個開源社區為了解決一個其它現有web身份認證技術不容易解決的問題時,制定了OpenID,OpenID是一個認證(Authentication)協議,這個協議讓第三方應用程序在不獲取你密碼的情況下認證你的身份,OpenID和OAuth是兩個經常放在一起比較的web身份認證技術,但OAuth比起OpenID,更像是一個授權(Authorization)協議,是讓第三方應用程序在不獲取你密碼的情況下獲取你的授權去對授權提供商或者說是資源提供商請求你授權的資源,拿到OpenID就像給別人指,這棟房子是我的,我可以拿點東西給你看看,而拿到OAuth授權就像你給別人說,這棟房子是我的,這個鑰匙給你,但只能開大門不能進卧室,哪些可以動哪些可以開都是你說了算,並且你可以隨時方便的收回這把鑰匙。
OAuth2.0是2006年提出來的新一代OAuth版本,比起OAuth1.x,它簡化了認證交互過程,增加了認證機制,使用了SSL,去掉了一個叫做客戶端token secret的東西,這也導致了OAuth升級需要改動代碼,並且沒有了1.0版本的安全bug(1.0a修復),這個安全bug主要是回調地址設定方式導致的。
OAuth2.0協議主要有四個角色,資源所有者即用戶,資源服務器,授權服務器和用戶代理即客戶端,這四個角色主要是完成了這樣一個流程,客戶端與服務器提供商之前,有一個通過OAuth協議完成的授權服務器,客戶端有從服務端獲取的唯一ID,客戶端不能直接登錄服務器提供商,需要先通過用戶在授權服務器取得授權碼,並且拿着獲得用戶授權的授權碼在授權層換取accessToken,通過這個token向資源服務器請求用戶授權信息。
github OAuth2.0登錄接入
OAuth2.0客戶端一共有4種授權模式,授權碼模式(authorization code)、簡化模式(implicit)、密碼模式(resource owner password credentials)和客戶端模式(client credentials),在github提供的OAuth.v3接口中,使用的是授權碼模式,在這種模式中,客戶端需要引導用戶重定向請求到Github的OAuth認證接口,帶上在授權服務器申請的客戶端 client_id
,配置的回調地址 redirect_uri
,申請授權的scope
,你也可以帶上狀態值state
,服務器會在回調中原封不動地返回你這個值,這一步將會換取前面我們說到的授權碼code
,具體流程和代碼實現如下:
1.首先我們需要在github上新建一個應用(記得登錄),獲取客戶端的Client ID和Client Secret並配置好回調地址。
2.重定向到OAuth認證接口 GET https://github.com/login/OAuth/authorize
<a href= '/mygithub'></a>
3.發起重定向請求到授權服務器換取code
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
馬上向資源服務器請求數據,具體流程和代碼實現如下:
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中,你可以打開的門如下:
國內第三方應用商SDK使用
除了用原生的OAuth協議去獲取用戶授權,我們也可以使用一些第三方應用提供商的SDK來實現快速地接入,特別是國內的許多平台都提供了集成OAuth協議的SDK,使用這種SDK可以幫你快速對現有網站進行集成,但是缺點對開發測試很不友好,比如需要你是上線的網站並且在驗證的時候它會默認去訪問服務器80端口,如果你的那台測試服務器剛好http端口在做其他事情,這就很尷尬了。
下面我就微博和騰訊QQ的接入來簡單介紹一下。
微博SDK
使用微博SDK之前,你需要先在微博開放平台驗證網站所有權,微博有一點特別不好的就是在這里接入的地址不能指定端口,再在這里下載相應的SDK。
准備工作做好之后,開發起來就相當便利了。
- 首先在網站的首頁添加一個meta標簽。
meta(property="wb:webmaster" content="YOUR_KEY")
添加一個meta標簽以便微博服務器識別你的身份,微博會對你在驗證網站所有權配置的地址進行http訪問,來查詢是否有正確的meta,不過微博的實現好像不是很好,經常會告訴你驗證失敗,當然QQ也一樣。
- 引入js SDK
在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');
}
}
});
});
- 相關文檔
騰訊QQ SDK
騰訊SDK的使用方式和微博SDK差別不大,最好的一點就是騰訊的SDK如果回調地址是當前頁面,可以不用配置回調地址就可以快速接入。當然你也需要在這里注冊申請appkey
- 首先在網站的首頁添加一個meta標簽。
meta(property="qc:admins" content="YOUR_KEY")
- 引入js SDK
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登錄 注銷成功');
});
- 相關文檔
passport.js插件使用
如果對於每個接入都要去使用這個第三方應用提供商的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('/');
});
返回字段(2016年8月23日獲取)
_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 } }
- github
{ 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和微博申請和審核
最后簡單介紹一下QQ和weibo的應用申請和審核。
微博weibo
- 第一步,新建應用
- 第二步,填寫應用信息,盡量詳細,特別是應用圖標,以方便通過審核。
- 第三步,填寫回調地址,可以配置非80端口
- 第四步,點擊審核,進入審核狀態,等待weibo通過。
騰訊QQ
- 第一步,新建應用,點擊創建應用,選擇網站
- 第二步,填寫應用信息,注意回調地址不能有端口號,默認訪問80。
- 第三步,在信息編輯中,填寫配置協作者賬號,方便開發測試。
- 第四步,點擊審核,進入審核狀態,等待QQ通過。