前篇 - 用戶名密碼基本認(rèn)證
后篇 - OAuth2認(rèn)證
前文使用包passport實(shí)現(xiàn)了一個(gè)簡(jiǎn)單的用戶名、密碼認(rèn)證。本文改用oauth2來實(shí)現(xiàn)更加安全的認(rèn)證。
代碼在這里。
OAUTH2
用戶認(rèn)證,只使用用戶名、密碼還是非?;A(chǔ)的認(rèn)證方式?,F(xiàn)在RESTful API認(rèn)證最多使用的是oauth2。使用oauth2就需要使用https,并hash處理client secret、auth code以及access token。
oauth2需要使用包oauth2orize:
npm install --save oauth2orize
首先看看oauth2的認(rèn)證時(shí)序圖:

仔細(xì)看圖發(fā)現(xiàn)我們現(xiàn)在的代碼并不足以支撐oauth2認(rèn)證。我們還需要一個(gè)UI界面供用戶輸入用戶名、密碼產(chǎn)生authorization code和access token。
UI界面
目前為止,還沒有使用過任何的界面。我們現(xiàn)在添加一個(gè)簡(jiǎn)單的頁(yè)面。用戶可以允許活拒絕application client訪問他們賬戶的請(qǐng)求。
Express可以使用的界面模板是很多的:jade、handlebars、ejs等。我們使用ejs。安裝ejs:
npm install --save ejs
在server.js中設(shè)置Express,讓Express可以解析ejs模板:
var ejs = require('ejs');
...
// 創(chuàng)建一個(gè)express的server
var app = express();
app.set('view engine', 'ejs');
...
在目錄petshop/server/下添加一個(gè)文件夾views。在目錄中添加文件dialog.ejs。
<!DOCTYPE html>
<html>
<head>
<title>Beer Locker</title>
</head>
<body>
<p>Hi <%= user.username %>!</p>
<p><b><%= client.name %></b> is requesting <b>full access</b> to your account.</p>
<p>Do you approve?</p>
<form action="/api/oauth2/authorize" method="post">
<input name="transaction_id" type="hidden" value="<%= transactionID %>">
<div>
<input type="submit" value="Allow" id="allow">
<input type="submit" value="Deny" name="cancel" id="deny">
</div>
</form>
</body>
</html>
使用Session
oauth2orize需要用到session。只有這樣才能完成認(rèn)證過程。首先安裝session依賴包express-session。
npm install --save express-session
接下來是如何使用這個(gè)包。更新server.js文件:
var session = require('express-session');
...
app.use(bodyParser.urlencoded({
extended: true
}));
app.use(session({
secret: 'a4f8071f-4447-c873-8ee2',
saveUninitialized: true,
resave: true
}));
...
Application client的model和controller
首先,我們需要添加一個(gè)新的model和一個(gè)controller,然后再創(chuàng)建一個(gè)application client方便以后使用。一個(gè)application client會(huì)請(qǐng)求一個(gè)用戶的賬戶。比如,有這么一個(gè)服務(wù)可以替你管理你的寵物。在狗糧不夠的時(shí)候通知你。
model
var mongoose = require('mongoose');
var clientSchema = new mongoose.Schema({
name: {type: String, unique: true, required: true},
id: {type: String, required: true},
secret: {type: String, required: true},
userId: {type: String, required: true}
});
module.exports = mongoose.model('client', clientSchema);
name就是用來區(qū)分不同的application client的。id和secret會(huì)在后面的oauth2認(rèn)證過程中使用。這兩個(gè)字段的值應(yīng)該一直都保證是加密的,不過在本文中沒有做加密處理。產(chǎn)品環(huán)境必須加密。最后的userId用來表明哪個(gè)用戶擁有這個(gè)application client。接下來創(chuàng)建client對(duì)應(yīng)的controller。
controller
var Client = require('../models/client');
var postClients = function(req, res) {
var client = new Client();
client.name = req.body.name;
client.id = req.body.id;
client.secret = req.body.secret;
client.userId = req.user._id;
client.save(function(err) {
if (err) {
res.json({message: 'error', data: err});
return;
}
res.json({message: 'done', data: client});
});
};
var getClients = function(req, res) {
Client.find({userId: req.user._id}, function(err, clients) {
if (err) {
res.json({messag: 'error', data: err});
return;
}
res.json({message: 'done', data: clients});
});
};
module.exports = {postClients: postClients,
getClients: getClients
};
這兩個(gè)方法可以用來添加新的client和獲取某用戶的全部的client。
修改server.js:
var clientController = require('./controllers/client');
...
// 處理 /clients
router.route('/clients')
.post(authController.isAuthenticated, clientController.postClients)
.get(authController.isAuthenticated, clientController.getClients);
...
下面使用Postman來創(chuàng)建一個(gè)application client。
認(rèn)證Application client
前文中,我們已經(jīng)可以使用用戶名和密碼來驗(yàn)證用戶了。下面就來驗(yàn)證application client。
更新原來的basic認(rèn)證
在controllers里打開auth.js。更新這個(gè)文件, 添加一個(gè)新的認(rèn)證strategy:
passport.use('client-basic', new BasicStrategy(
function(username, password, done) {
Client.findOne({id: username}, function(err, client) {
if (err) {
return done(err);
}
if (!client || client.secret !== password) {
return done(null, false);
}
return done(null, client);
});
}
));
module.exports.isClientAuthenticated = passport.authenticate('client-basic', {session: false});
我們新增了一個(gè)BasicStrategy,之所以可以這樣就是應(yīng)為我們給這個(gè)strategy指定了一個(gè)名稱client-basic。
這個(gè)strategy的功能是用給定的clientId來查找一個(gè)client,并檢查password(client的secret)是否正確。
Authorization code
我們還需要?jiǎng)?chuàng)建一個(gè)model來存放authorization code。這個(gè)authorizention code用來來獲取access token。
現(xiàn)在我們?cè)?em>models目錄下創(chuàng)建一個(gè)code.js文件。代碼如下:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var codeSchema = new Schema({
value: {type: String, required: true},
redirectUri: {type: String, required: true},
userId: {type: String, required: true},
clientId: {type: String, required: true}
});
module.exports = mongoose.model('code', codeSchema);
很簡(jiǎn)單對(duì)吧。value是用來存放authorization code的。redirectUri用來存放跳轉(zhuǎn)的uri,稍后會(huì)詳細(xì)介紹。clientId和userId用來存放哪個(gè)用戶和哪個(gè)application client擁有這個(gè)authorization code。為了安全考慮,你可以hash了authorization code。
Access token
這里也需要我們來創(chuàng)建一個(gè)model來存放access token。在models目錄下添加一個(gè)token.js文件:
var mongoose = require('mongoose');
var Schema = mongoose.Schema;
var tokenSchema = new Schema({
value: {type: String, required: true},
userId: {type: String, required: true},
clientId: {type: String, required: true}
});
module.exports = mongoose.model('token', tokenSchema);
用戶訪問api的時(shí)候使用的token就是value字段的值。userId和clientId就是用來表明哪個(gè)用戶和application client擁有這個(gè)token。產(chǎn)品環(huán)境下最好把token做hash處理,絕對(duì)不要想我們的例子一樣使用明文。
使用access token來驗(yàn)證
我們之前已經(jīng)添加了第二個(gè)BasicStrategy,這樣就可以驗(yàn)證client發(fā)出的請(qǐng)求?,F(xiàn)在我們?cè)谛陆ㄒ粋€(gè)BearerStrategy,這樣我們就可以驗(yàn)證用戶使用oauth的token發(fā)出的請(qǐng)求了。
首先安裝依賴包passport-http-bearer。
npm install passport-http-bearer --save
更新controllers/auth.js文件。在這個(gè)文件中require passport-http-bearer包和Token model。
var passport = require('passport'),
BasicStrategy = require('passport-http').BasicStrategy,
BearerStrategy = require('passport-http-bearer').Strategy,
User = require('../models/user'),
Client = require('../models/client'),
Token = require('../models/token');
passport.use(new BearerStrategy(
function(accessToken, done) {
Token.findOne({value: accessToken}, function (err, token) {
if (err) {
return done(err);
}
if (!token) {
return done(null, false);
}
User.findOne({_id: token.userId}, function (err, user) {
if (err) {
return done(err);
}
if (!user) {
return done(null, false);
}
done(null, user, {scope: '*'});
});
});
}
));
...
module.exports.isBearerAuthenticated = passport.authenticate('bearer', {session: false});
新的strategy允許我們接受application client發(fā)出的請(qǐng)求,并使用發(fā)送過來的token驗(yàn)證這些請(qǐng)求。
創(chuàng)建OAuth2 controller
現(xiàn)在正式進(jìn)入oauth2的開發(fā)階段。首先安裝oauth2orize包:
npm install --save oauth2orize
接下來在controllers里創(chuàng)建一個(gè)oauth2.js文件。接下來在這個(gè)寫代碼。
var oauth2orize = require('oauth2orize'),
User = require('../models/user'),
Client = require('../models/client'),
Token = require('../models/token'),
Code = require('../models/code');
創(chuàng)建OAuth2 server
// 創(chuàng)建一個(gè)OAuth 2.0 server
var server = oauth2orize.createServer();
注冊(cè)序列化反序列化方法
server.serializeClient(function(client, callback) {
return callback(null, client._id);
});
server.deserializeClient(function(id, callback) {
Client.findOne({_id: id}, function (err, client) {
if (err) {
return callback(err);
}
return callback(null, client);
});
});
注冊(cè)authorization code許可類型
server.grant(oauth2orize.grant.code(function(client, redirectUri, user, ares, callback) {
var code = new Code({
value: uid(16),
clientId: client._id,
redirectUri: redirectUri,
useId: user._id
});
code.save(function(err) {
if (err) {
return callback(err);
}
callback(null, code.value);
});
}));
使用oauth2.0,用戶可以指定application client可以訪問哪些被保護(hù)的資源。其過程概括起來就是用戶授權(quán)client application,之后client再用用戶許可換取access token。
使用autho code交換access token
server.exchange(oauth2orize.exchange.code(function(client, code, redirectUri, callback) {
Code.findOne({value: code}, function (err, authCode) {
if (err) {return callback(err);}
if (authCode === undefined) {return callback(null, false);}
if (client._id.toString() !== authCode.clientId) {return callback(null, false);}
if (redirectUri !== authCode.redirectUri) {return callback(null, false);}
authCode.remove(function (err) {
if (err) {return callback(err);}
var token = new token({
value: uid(256),
clientId: authCode.clientId,
userId: authCode.userId
});
token.save(function (err) {
if (err) {
return callback(err);
}
callback(null, token);
});
});
});
}));
上面的代碼就完成了authorization code交換access token的過程。首先檢查是否存在一個(gè)authorization code,如果存在則開始以后的驗(yàn)證過程。在前面的步驟全部通過的時(shí)候,刪除已存在的authorization code,這樣就不能再次使用。并創(chuàng)建一個(gè)新的access token。這個(gè)token和application client以及用戶綁定在一起。最后存入MongoDB。
用戶給終端授權(quán)
module.exports.authorization = [
server.authorization(function(clientId, redirectUri, callback) {
Client.findOne({id: clientId}, function(err, client) {
if (err) {return callback(err);}
return callback(null, client, redirectUri);
});
}),
function(req, res) {
res.render('dialog', {transationID: req.oauth2.transactionID, user: req.user, client: req.oauth2.client});
}
];
這個(gè)終端初始化了一個(gè)新的授權(quán)事務(wù)。這個(gè)事務(wù)里首先找到訪問用戶賬戶的client,然后渲染我們前面創(chuàng)建的dialog視圖。
用戶決定是否授權(quán)
module.exports.decision = [server.decision()];
無論用戶同意或拒絕授權(quán),都有server.decision()來處理。之后調(diào)用server.grant()方法。這個(gè)方法我們?cè)谇懊嬉呀?jīng)創(chuàng)建好。
application client token
module.exports.token = [
server.token(),
server.errorHandler()
];
這段代碼用來處理用戶授權(quán)application client之后的請(qǐng)求。
生成唯一編號(hào)的util方法
function uid(len) {
var buf = [],
chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789',
charlen = chars.length;
for (var i = 0; i < len; i++){
buf.push(chars[getRandomInt(0, charlen - 1)]);
}
return buf.join('');
}
function getRandomInt(min, max) {
return Math.floor(Math.random() * (max - min + 1)) + min;
}
給OAuth2終端添加路由
現(xiàn)在我們可以給oauth2添加路由了?,F(xiàn)在來更新server.js代碼,給這些終端添加必要的路由。
var oauth2Controller = require('./controllers/oauth2');
...
router.route('/oauth2/authorize')
.post(authController.isAuthenticated, oauth2Controller.authorization)
.get(authController.isAuthenticated, oauth2Controller.decision);
router.route('/oauth2/token')
.post(authController.isClientAuthenticated, oauth2Controller.token);
給API終端的access token授權(quán)
在這一步,oauth2 server需要的全部“工具”都有了。最后一步,需要我們更新一下需要授權(quán)的終端(endpoint)?,F(xiàn)在我們使用BasicStrategy來認(rèn)證的,這主要需要用戶名和密碼。我們現(xiàn)在要換用BearerStrategy來使用access token認(rèn)證。
把文件controllers/auth.js中module.exports.isAuthenticated語句修改為可以使用basic或者bearer策略。
module.exports.isAuthenticated = passport.authenticate(['basic', 'bearer'], {session: false});
這已修改,認(rèn)證就會(huì)使用用戶名、密碼和access token兩個(gè)了。
使用OAuth2
代碼好多。趕緊試試效果。在瀏覽器中輸入url:http://localhost:3090/api/oauth2/authorize?client_id=my_id&response_type=code&redirect_uri=http://localhost:3090。注意:client_id的值是我前面用postman添加的一個(gè),你需要改成你自己的client_id。
如果你選擇了allow(同意),那么就會(huì)顯示下面的界面:
最后
oauth2orize是一個(gè)很強(qiáng)的庫(kù),開發(fā)一個(gè)oauth2 server簡(jiǎn)單了很多。