在开发应用程序时,您可能发现需要让用户将文件存储在服务器上。为了解决这个问题,您可能只是简单地将文件存储在数据库中,甚至可能将它们直接存储在文件系统上。不幸的是,这些传统方法存在一些缺陷。如果直接将文件存储在文件系统上,则需要额外的工作来确保稳定性和安全性,并创建备份。将大型文件存储在数据库中会让性能变得糟糕,备份变得耗时,还原变得缓慢。
一些 Object Storage 服务已开发了出来,它们通过提供高度可用、基于云的存储来减轻这些问题,并为您扩展、保护和维护这些存储。
Bluemix 现在支持通过所谓的 Object Storage 服务来免费访问 Softlayer 的 Object Storage 。Object Storage 服务可通过 Bluemix 服务目录获取,由 OpenStack 的 Swift Object Store 提供支持。您可以在 OpenStack 网站上 阅读该 API 的概述。
可通过 Swift 管理 3 种类型的资源:
在通过 Bluemix 创建 Object Storage 服务的实例时,会为您分配一个帐户,您的所有容器和对象都存储在该帐户下。您帐户的凭据可通过 VCAP_SERVICES 提供。
{ "objectstorage": [{ "name": "instance_basic", "label": "objectstorage", "plan": "plan_name", "credentials": { "auth_uri": "https://host:port/auth/instance_id/binding_id", "username": "user_name", "password": "password" } }] }
在创建任何对象或从服务读取任何对象之前,您必须从服务器获取一个授权令牌。该令牌使用包含上述凭据的基本身份验证机制从 auth_uri
端点获取。然后,获取的令牌可用在处理容器和对象的所有后续请求上。
您还能够在容器上创建访问控制列表 (ACL) 来增加对其他对象存储帐户访问限制和其他访问条件。尽管可使用 ACL 在帐户之间共享数据,但应该认识到这些帐户是特定于项目,而不是特定于最终用户。
此身份验证模式具有两大挑战:
每种挑战都可通过以下方式克服:
本文通过一个应用程序展示如何解决这些限制。
该应用程序是一个基于 Web 的简单的镜像管理应用程序。用户通过 LinkedIn 执行身份验证,能够上传新镜像,还能够查看、更新和删除已存储的镜像。
运行应用程序
获取代码
点击查看大图
关闭 [x]
如果已理解 Object Storage 服务如何验证用户,可跳过这一步。 Object Storage 文档页面 上也介绍了这一步。
点击查看大图
关闭 [x]
GET
请求。包含上述凭据的 cURL
命令类似于: 点击查看代码清单
关闭 [x]
curl -i --user da938ab5c18601a38c45b3ad6c97cb8d1e7441da:29b6eb028cfdd3e1fec0cc8fa2066fea4a169cb352819e3dfa11e3f0efde https://swift.ng.bluemix.net/auth/45947227-d694-4e3b-beb4-66bfe895752d/bf587adc-10b8-40d8-aeb5-805f73f13844/da938ab5c18601a38c45b3ad6c97cb8d1e7441da
X-Storage-URL
和 X-Auth-Token
标头(表示您的对象存储所在的 URL)和您必须包含在所有请求上的授权令牌。 点击查看代码清单
关闭 [x]
HTTP/1.1 200 OK X-Backside-Transport: OK OK Connection: Keep-Alive Transfer-Encoding: chunked Content-Type: application/json:charset=utf-8 Date: Fri, 30 Jan 2015 18:33:12 GMT Set-Cookie: connect.sid=s%3AnPo52qjvdynxlvCGEAmJfs7d.HuYPqYVmtQqeImm73iUnubk2r9T0S5WIQVOL5edX08U; Path=/; HttpOnly --> X-Auth-Token: AUTH_tk924cd97c2af7475c8e6cb25d2adaccf8 X-Cf-Requestid: 969d6f00-8b85-471e-5ab5-80c36031f9fb X-Powered-By: Express --> X-Storage-Url: https://dal05.objectstorage.softlayer.net/v1/AUTH_0c6bf5d5-b5e4-43aa-93f1-ea7065837fb8 X-Client-IP: 129.42.208.182 X-Global-Transaction-ID: 770597619
X-Storage-Url
标头中提供的 URL 上,而且必须包含 X-Auth-Token
标头。例如,要为您的存储创建一个新容器,可以发出以下请求: 点击查看代码清单
关闭 [x]
curl -i -H "X-Auth-Token: AUTH_tk924cd97c2af7475c8e6cb25d2adaccf8" -X PUT https://dal05.objectstorage.softlayer.net/v1/AUTH_0c6bf5d5-b5e4-43aa-93f1-ea7065837fb8/myNewContiner
如果一切运行正常,您会从服务器收到 201 响应代码。您可以在同一个 URL 上使用已设置的 X-Auth-Token
标头执行一次 GET
命令,获取与您的帐户有关联的所有容器:
点击查看代码清单
关闭 [x]
curl -i -H "X-Auth-Token: AUTH_tk924cd97c2af7475c8e6cb25d2adaccf8" https://dal05.objectstorage.softlayer.net/v1/AUTH_0c6bf5d5-b5e4-43aa-93f1-ea7065837fb8/
服务器响应应类似于以下内容,其中包含您创建的一个容器列表:
HTTP/1.1 200 OK Content-Length: 44 Content-Type: text/plain; charset=utf-8 X-Account-Object-Count: 0 X-Timestamp: 1422642699.36706 X-Account-Meta-Cdn-Id: 21904 X-Account-Bytes-Used: 0 X-Account-Container-Count: 3 X-Account-Meta-Nas-Id: 4359794 Accept-Ranges: bytes X-Trans-Id: tx0284889f9e2b47dca6c39-0054cbea86 Date: Fri, 30 Jan 2015 20:33:10 GMT myNewContiner myNewContiner2 myNewContiner3
点击查看大图
关闭 [x]
备注:您将在第 6 步绑定该服务。
配备 Single Sign On 服务后,您的信息将发送到服务控制台。也可以从仪表板中的服务部分中打开此控制台。
备注:单击 Continue 后,可能需要花一分钟来配备服务,在此期间控制台将会无响应。
是 SSO 服务器在完成身份验证后将响应的 URL。在本地测试应用程序时,可以使用:http://localhost:3000/sso/redirect。托管在 Bluemix 上时,该 URL 必须更新为:https://<your-app-domain>.mybluemix.net/sso/redirect。
请注意协议上的区别。对于本地托管的应用程序,使用的协议为 http,但对于 Bluemix 托管的应用程序,则应使用安全 HTTP,也就是 https。
至于现在,使用 http://localhost:3000/sso/redirect URL 并单击 Save 。
npm install
。 node app.js
。 测试所有功能都能在本地运行后,您可以将应用程序部署到 Bluemix 上:
cf push <Your App Name> -p .
,以便将应用程序部署到 Bluemix 在解压项目时,您会看到以下文件:
npm install
时获取。 客户端部分是一个使用 AngularJS(一种基于 JavaScript 的框架)编写的单页 Web 应用程序。服务器是使用 NodeJS 编写的,它提供了 REST API,使用 SSO 服务处理授权流,代表客户端向 Object Storage 服务发出请求,还提供了 Web 应用程序的静态内容(HTML、CSS 和 JavaScript)。来自客户端的通信通过 Ajax 请求传输到服务器。
服务器有两个主要的用途:
服务器是使用 NodeJS 编写的。以下库是我们用来完成此任务的主要的库:
服务器有 3 个主要的根端点:
下一节将分析代码。
首先,我们在 /swift
路由上创建一个 express 路由器:
如果不熟悉 express 路由器,可以在 Express 文档页面 上查阅有关的更多信息。
var proxyRouter = express.Router(); app.use('/swift', proxyRouter); proxyRouter.use('/:container', validateProxyRequest);
发送给 https://<app-domain>.mybluemix.net/swift 的所有请求都被转发给对象缓存存储。这通过两个步骤来完成:
proxyRouter.use(function (req, res, next) { var swiftStorageCredentials = cache.get('swiftStorageCredentials') if(!swiftStorageCredentials === undefined){ var parsedUrl = url.parse(swiftConfig.credentials.auth_uri); var options = { hostname: parsedUrl.hostname, path: parsedUrl.path + '/' + swiftConfig.credentials.username, method: 'GET', auth: swiftConfig.credentials.username + ':' + swiftConfig.credentials.password }; var swiftRequest = https.request(options, function(swiftResponse) { var storageURL = swiftResponse.headers["x-storage-url"]; var storageToken = swiftResponse.headers["x-auth-token"]; swiftStorageCredentials = new SwiftStorageCredentials(storageURL, storageToken) cache.set('swiftStorageCredentials', swiftStorageCredentials ) ; next(); }); swiftRequest.end(); } else{ next(); } });
此中间件首先检查 URL 缓存,查看我们是否已在最近 15 分钟内获得了该令牌。如果没有,则会使用基本的 auth
令牌向授权端点发出一个安全请求。您可以在 swiftRequest
响应回调中看到,我们获得了存储 URL 和 auth
令牌,因此我们可以在后续代理请求中使用它们,并将它们存储在缓存中。
使用缓存中存储的凭据来发出代理请求。
为此,我们定义了将请求重定向到代理的中间件:
proxyRouter.use(function (clientRequest, clientResponse, next){ //Obtain Object Storage service endpoint from stored credentials. //This will be the target of the proxy. var swiftStorageCredentials = cache.get('swiftStorageCredentials'); var proxyURL = swiftStorageCredentials.storageURL ; var options = {target : proxyURL}; if(!clientRequest.header('expect')){ proxy.web(clientRequest, clientResponse, options); } else{ clientResponse.writeContinue(); clientResponse.setHeader("SkipProxyWeb", "true"); clientResponse.end(); } });
这会获取我们存储在缓存中的存储 URL,并使用指定的同一个路径来调用该代理。换句话说,对 http://hostname/swift/mycontainer/myimage.jpg 的请求被转发到 https://storageURL/mycontainer/myimage.jpg。
在代理自身上,我们必须定义一个函数,以便所有传出的请求都包含 auth
令牌,该令牌是在我们的第一个中间件所发出的身份验证请求中获取的。
proxy.on('proxyReq', function(proxiedReq, req, res, options) { var swiftStorageCredentials = cache.get('swiftStorageCredentials'); if(!req.header(‘expect')){ proxiedReq.setHeader(‘X-Auth-Token', swiftStorageCredentials.storageToken); } else{ res.setHeader("ProxyRequestEventFoundExpect", "true"); } });
有了此信息后,向 https://<app-domain>.mybluemix.net/swift 发出的所有请求都会被重定向到对象存储服务,使用帐户凭据来授权请求,而不会向客户端把暴露凭据。
Single Sign On 文档的 配置 Node.js 应用程序 一节详细介绍了如何使用 Passport.js 节点库和 Single Sign On(passport-idaas-openidconnect) 将第三方用户安全集成到应用程序中。在这一节中,我们将介绍应用程序中的相关更改。
要持久保存用户信息,必须向 express 应用程序注册 express-session 和 cookieParser 中间件。
app.use(cookieParser()); app.use(session({ secret: 'SOME SECRET HERE', resave: true, saveUninitialized: true })); app.use(passport.initialize()); app.use(passport.session()); //Serializes the user into the session passport.serializeUser(function(user, done) { done(null, user); }); //Deserializes the user into an object and makes it available on req.user passport.deserializeUser(function(obj, done) { done(null, obj); });
上述代码初始化并注册了 session 和 passport 中间件。 serializeUser
和 deserializeUser
方法调用描述了如何序列化和去序列化会话中的用户信息。参见 Passport 配置文档 中的 “会话” 部分,了解有关的更多信息。
接下来,我们将设置 Single Sign On 文档中所介绍的 Passport(护照)战略。
//Retrieve SSO credentials from VCAP_SERVICES when running on Bluemix, //or config.json when running locally var services = isBluemix() ? JSON.parse(process.env.VCAP_SERVICES) : config.VCAP_SERVICES, ssoConfig = services.SingleSignOn[0]; //Create Passport Strategy with SSO credentials to handle authentication flows passport.use(new OpenIDConnectStrategy({ authorizationURL: ssoConfig.credentials.authorizationEndpointUrl, tokenURL: ssoConfig.credentials.tokenEndpointUrl, clientID: ssoConfig.credentials.clientId, scope: 'openid', response_type: 'code', clientSecret: ssoConfig.credentials.secret, callbackURL: "/sso/redirect", skipUserProfile: true, issuer: ssoConfig.credentials.issuerIdentifier }, function(accessToken, refreshToken, profile, done) { process.nextTick(function() { profile.accessToken = accessToken; profile.refreshToken = refreshToken; done(null, profile); }) }));
来自 VCAP_SERVICES 的 SSO 凭据用于实例化 OpenIDConnectStrategy
。此战略会告诉护照,我们希望发出何种授权请求和在何处发出这些请求(使用 SSO 服务)。 authorizationURL
、 tokenURL
、 clientId
、 clientSecret
和 issuer
身份都由 SSO 服务提供。我们还在这里指定,我们希望在完成登录操作后调用我们的 /sso/redirect 端点。
为了简化登录请求,我们创建了一个 authRouter
,并将它与发送给 /sso/* 的所有请求相关联。
var authRouter = express.Router(); app.use('/sso', authRouter);
然后,为了处理登录请求,我们创建了一个 /sso/login 端点,并让护照来处理该请求。
authRouter.get('/login', passport.authenticate('openidconnect', {}));
护照和 SSO 服务将为我们处理实际的身份验证流。完成身份验证后,用户会被重定向到之前提到的 /sso/redirect。
authRouter.get('/redirect',function(req,res,next) { passport.authenticate('openidconnect',{ successRedirect: "/web", failureRedirect: '/sso/failure' })(req,res,next); });
您可以看到,这个端点表明成功的身份验证将重定向个到 /web/ 端点,用户将能够在其中查看实际的网站。失败的身份验证将被重定向到 '/sso/failure'
,这会通知用户登录失败:
authRouter.get('/failure', function(req, res) { res.send('login failed'); });
要保护端点,必须检查用户是否已在所有请求上进行了验证。为此,我们可以检查 req.isAuthenicated()
是否为 true,也可以检查 req.user
是否允许用户访问请求的容器。
为了保护根 (/) 端点,我们创建了 ensureAuthenticated
函数:
function ensureAuthenticated(req, res, next) { //If user isn't authenticated, redirect to login if (!req.isAuthenticated()) { res.redirect('/sso/login'); } else { //If authenticated, continue to next middleware return next(); } }
它的使用方式是:
app.get('/', ensureAuthenticated, function(req,res,next){ res.redirect('/web'); });
借助代理请求,我们执行一个额外的步骤,确保经过验证的用户仅对允许他们访问的容器发出代理请求。在我们的案例中,我们要求用户 ID(他们的电子邮件地址)与容器的名称匹配。为此,我们创建下面这个函数:
点击查看代码清单
关闭 [x]
function validateProxyRequest(req, res, next) { if (!req.user) { console.log("Invalid proxy request. User is not logged in."); res.send(401).send("Not logged in."); } else if (req.user.id != req.params.container) { res.send(401).send("Container and user id don't match. Container: %s, User id: %s”, req.params.container, req.user.id); } else { return next(); } }
在代理端点之前向 proxyRouter
注册,将它注册为中间件。
proxyRouter.use('/:container', validateProxyRequest);
对于 validateProxyRequest
函数,有两点需要注意:
req.user
,而不是 req.isAuthenticated()
。因此我们可确保 req.user.id
与 :container
端点参数 ( req.params.container
) 相匹配。这可以确保用户无法访问其他用户的文件。 ensureAuthenticated
中那样执行重定向,我们发送了 401。这是因为代理请求将使用 Ajax 代理来处理,因此服务器端重定向无法正确处理。如果我们发送 401,我们可登录到存在错误的客户端,通知用户他们需要登录(为了简便起见,我们在应用程序中执行了客户端重定向)。 客户端部分通过流行的 Angular 库 来使用 HTML、CSS 和 JavaScript 编写。Angular 是一个基于 MVC 的框架,用于编写基于 JavaScript 的 Web 应用程序。可以从 Angular 的 在线文档 获取它的概念性概述。出于本文的目的,我们坚持只介绍用于与我们刚演示的服务器通信的控制器的内容。
/web/index.html 中的控制逻辑包含在 HTML 后的脚本标记中。首先,我们定义了一个名为 swiftResourceApp
的模块,它充当着所有 Web 应用程序的逻辑的容器模块。随后,我们定义了 swiftListController
,而且它位于应用程序的大部分逻辑所在的控制器中。
您会注意到,此控制器中定义了多个方法。以下方法用于与服务器通信:
getContainer
:此方法调用我们在应用程序中定义的一个特殊端点(简言之,就是 /container),该端点将告诉我们此用户有权访问的容器名称。容器名称应与从 Passport 的已验证用户获取的用户 ID 或电子邮件地址相同。 备注: 这不是代理端点。所有其他 Ajax 调用都是对代理发出的。 listResources
:在 /swift/<container name> 上执行一次 GET
,获取用户的容器中存储的所有文件。如果这是用户第一次登录,服务器会使用一个 404 页面作为响应。此问题的处理方式是,在 /swift/<container name> 上执行一次 PUT
,创建一个具有相同名称的新容器。 createResource
:在 /swift/<container name>/<resource name> 上使用在请求中作为数据上传的文件的内容执行一次 PUT
。 onSaveUpdate
:类似于 createResource
,但更新了现有资源。 您会注意到,在对我们的服务器的每次调用中,如果服务器使用 401 作为响应,我们会通过将 window.location.href
更改为登录端点来执行一次客户端重定向:
error(function(response, status){ if (status == 401) { window.location.href = window.location.origin+"/sso/login"; } else { console.error("Error updating resource. Error code: %s, Error message: %s", status, response); } });
IBM Bluemix 是一个创建和部署以云为中心的应用程序的重要平台。在本文中,我们演示了开发人员如何使用与 Node.js 和 Angular.js 相关的前沿技术(都受 OAUTH 保护),安全地将资源存储在 IBM SoftLayer 的 Object Storage 服务中。通过设计一种多层架构,我们可确保开发人员能够使用 IBM Bluemix 平台开发一个高度可扩展、可靠、高性能的解决方案。Bluemix 上的服务目录非常庞大,而且提供了许多不同的功能,它们对开发人员很有吸引力,可以缩短新一代应用程序的上市准备时间。
BLUEMIX SERVICES USED IN THIS TUTORIAL: