基於Node的PetShop,oauth2認證RESTful API


前篇 - 基本認證,用戶名密碼
后篇 - OAuth2 認證

前文使用包passport實現了一個簡單的用戶名、密碼認證。本文改用oauth2來實現更加安全的認證。全部代碼在這里

OAUTH2

用戶認證,只使用用戶名、密碼還是非常基礎的認證方式。現在RESTful API認證最多使用的是oauth2。使用oauth2就需要使用https,並hash處理client secret、auth code以及access token。

oauth2需要使用包oauth2orize:

npm install --save oauth2orize

首先看看oauth2的認證時序圖:
圖片來自oracle

仔細看圖發現我們現在的代碼並不足以支撐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的。idsecret會在后面的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,稍后會詳細介紹。clientIduserId用來存放哪個用戶和哪個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字段的值。userIdclientId就是用來表明哪個用戶和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.jsmodule.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簡單了很多。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM