前篇 - 基本認證,用戶名密碼
后篇 - OAuth2 認證
前文使用包passport
實現了一個簡單的用戶名、密碼認證。本文改用oauth2來實現更加安全的認證。全部代碼在這里。
OAUTH2
用戶認證,只使用用戶名、密碼還是非常基礎的認證方式。現在RESTful API認證最多使用的是oauth2。使用oauth2就需要使用https,並hash處理client secret、auth code以及access token。
oauth2需要使用包oauth2orize:
npm install --save oauth2orize
首先看看oauth2的認證時序圖:
仔細看圖發現我們現在的代碼並不足以支撐oauth2認證。我們還需要一個UI界面供用戶輸入用戶名、密碼產生authorization code和access token。
UI界面
目前為止,還沒有使用過任何的界面。我們現在添加一個簡單的頁面。用戶可以允許活拒絕application client訪問他們賬戶的請求。
Express可以使用的界面模板是很多的:jade、handlebars、ejs等。我們使用ejs。安裝ejs:
npm install --save ejs
在server.js中設置Express,讓Express可以解析ejs模板:
var ejs = require('ejs');
...
// 創建一個express的server
var app = express();
app.set('view engine', 'ejs');
...
在目錄petshop/server/下添加一個文件夾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。只有這樣才能完成認證過程。首先安裝session依賴包express-session
。
npm install --save express-session
接下來是如何使用這個包。更新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
首先,我們需要添加一個新的model和一個controller,然后再創建一個application client方便以后使用。一個application client會請求一個用戶的賬戶。比如,有這么一個服務可以替你管理你的寵物。在狗糧不夠的時候通知你。
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就是用來區分不同的application client的。id
和secret
會在后面的oauth2認證過程中使用。這兩個字段的值應該一直都保證是加密的,不過在本文中沒有做加密處理。產品環境必須加密。最后的userId
用來表明哪個用戶擁有這個application client。接下來創建client對應的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
};
這兩個方法可以用來添加新的client和獲取某用戶的全部的client。
修改server.js:
var clientController = require('./controllers/client');
...
// 處理 /clients
router.route('/clients')
.post(authController.isAuthenticated, clientController.postClients)
.get(authController.isAuthenticated, clientController.getClients);
...
下面使用Postman來創建一個application client。
認證Application client
前文中,我們已經可以使用用戶名和密碼來驗證用戶了。下面就來驗證application client。
更新原來的basic認證
在controllers里打開auth.js。更新這個文件, 添加一個新的認證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});
我們新增了一個BasicStrategy
,之所以可以這樣就是應為我們給這個strategy指定了一個名稱client-basic
。
這個strategy的功能是用給定的clientId來查找一個client,並檢查password(client的secret)是否正確。
Authorization code
我們還需要創建一個model來存放authorization code。這個authorizention code用來來獲取access token。
現在我們在models目錄下創建一個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);
很簡單對吧。value
是用來存放authorization code的。redirectUri
用來存放跳轉的uri,稍后會詳細介紹。clientId
和userId
用來存放哪個用戶和哪個application client擁有這個authorization code。為了安全考慮,你可以hash了authorization code。
Access token
這里也需要我們來創建一個model來存放access token。在models目錄下添加一個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的時候使用的token就是value
字段的值。userId
和clientId
就是用來表明哪個用戶和application client擁有這個token。產品環境下最好把token做hash處理,絕對不要想我們的例子一樣使用明文。
使用access token來驗證
我們之前已經添加了第二個BasicStrategy,這樣就可以驗證client發出的請求。現在我們在新建一個BearerStrategy
,這樣我們就可以驗證用戶使用oauth的token發出的請求了。
首先安裝依賴包passport-http-bearer
。
npm install passport-http-bearer --save
更新controllers/auth.js文件。在這個文件中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發出的請求,並使用發送過來的token驗證這些請求。
創建OAuth2 controller
現在正式進入oauth2的開發階段。首先安裝oauth2orize包:
npm install --save oauth2orize
接下來在controllers里創建一個oauth2.js文件。接下來在這個寫代碼。
var oauth2orize = require('oauth2orize'),
User = require('../models/user'),
Client = require('../models/client'),
Token = require('../models/token'),
Code = require('../models/code');
創建OAuth2 server
// 創建一個OAuth 2.0 server
var server = oauth2orize.createServer();
注冊序列化反序列化方法
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);
});
});
注冊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可以訪問哪些被保護的資源。其過程概括起來就是用戶授權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的過程。首先檢查是否存在一個authorization code,如果存在則開始以后的驗證過程。在前面的步驟全部通過的時候,刪除已存在的authorization code,這樣就不能再次使用。並創建一個新的access token。這個token和application client以及用戶綁定在一起。最后存入MongoDB。
用戶給終端授權
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});
}
];
這個終端初始化了一個新的授權事務。這個事務里首先找到訪問用戶賬戶的client,然后渲染我們前面創建的dialog
視圖。
用戶決定是否授權
module.exports.decision = [server.decision()];
無論用戶同意或拒絕授權,都有server.decision()
來處理。之后調用server.grant()
方法。這個方法我們在前面已經創建好。
application client token
module.exports.token = [
server.token(),
server.errorHandler()
];
這段代碼用來處理用戶授權application client之后的請求。
生成唯一編號的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終端添加路由
現在我們可以給oauth2添加路由了。現在來更新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授權
在這一步,oauth2 server需要的全部“工具”都有了。最后一步,需要我們更新一下需要授權的終端(endpoint)。現在我們使用BasicStrategy
來認證的,這主要需要用戶名和密碼。我們現在要換用BearerStrategy
來使用access token認證。
把文件controllers/auth.js中module.exports.isAuthenticated
語句修改為可以使用basic或者bearer策略。
module.exports.isAuthenticated = passport.authenticate(['basic', 'bearer'], {session: false});
這已修改,認證就會使用用戶名、密碼和access token兩個了。
使用OAuth2
代碼好多。趕緊試試效果。在瀏覽器中輸入url:http://localhost:3090/api/oauth2/authorize?client_id=my_id&response_type=code&redirect_uri=http://localhost:3090。注意:client_id的值是我前面用postman添加的一個,你需要改成你自己的client_id。
如果你選擇了allow(同意),那么就會顯示下面的界面:
最后
oauth2orize是一個很強的庫,開發一個oauth2 server簡單了很多。