这篇文章关于使用 Serverless 框架和 AWS IoT 进行 Serverless 通知的教程。
Demo 地址:serverless-notifications.zanon.io
Code:GitHub
实时通知是现代应用程序的重要用例。例如,您可能需要通知用户他的社交 Feed 中有其他帖子,或者其他人在他的照片中添加了评论。
当你使用 WebSockets 并拥有专用服务器时,实现通知是一项简单的任务。您可以在用户和服务器之间建立永久链接,并使用发布/订阅(publish/subscribe)模式来共享消息。浏览器将订阅自动接收新消息,而不需要轮询机制来不断检查更新。
但是,如果我们用了 Serverless,我们没有专用的服务器。相反的,我们需要一个云服务,它将解决这个问题,并为我们提供可扩展性、高可用性,并且是按每个消息来收费,而不是每小时。
在这篇文章中,我将介绍如何使用 Serverless Framework 和 AWS IoT 进行浏览器的未经身份验证的用户实现通知系统。我知道 “物联网” 在网站上使用起来很奇怪,但它支持 WebSockets,非常易于使用。此外,Amazon SNS(Simple Notification Service,简单通知服务)具有更好的名称,但不支持 WebSockets。
IoT 由于其简单的消息系统而被使用。您创建一个 “主题” 并让用户订阅它。发送到此主题的邮件将自动与所有订阅的用户共享。一个常见的用例是聊天系统。
如果你想要私人消息,你只需要创建私人主题并限制访问。只要有一个用户订阅到此主题,您可以使您的系统(Lambda 函数)向本主题发送更新以通知此特定用户。
在本玩法中,我们将实现以下架构。
注意:您可以使用 AWS Cognito 来代替 API Gateway 和 Lambda 来检索 IAM 凭据。
配置 Route 53 和 Amazon S3 来提供静态文件是一个常见的使用方式。我已经在另一篇博文中介绍了如何做到这一点。如果你愿意,你可以看看这里。
在我们的前端代码中,我们开始为我们的布局创建 index.html。
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="utf-8"/>
<title>Serverless Notifications</title>
<link href="bootstrap.min.css" rel="stylesheet"/>
</head>
<body>
<div class="container">
<div class="row">
<h2>Serverless Notifications</h2>
</div>
<div class="row">
<input class="btn btn-primary" id="btn-keys" type="button" value="Retrieve Keys"/>
<input class="btn btn-primary" id="btn-connect" type="button" value="Connect"/>
</div>
<div class="row">
<input id="message" type="text"/>
<input class="btn btn-primary" id="btn-send" type="button" value="Send Message"/>
</div>
<div class="row">
<h3>Log Messages</h3>
<ul id="log"></ul>
</div>
</div>
<script src="jquery.min.js"></script>
<script type="text/javascript">
window.lambdaEndpoint = 'https://abcdefghij.execute-api.us-east-1.amazonaws.com/dev/iot/keys';
</script>
<script src="bundle.js"></script> <!-- IoT -->
</body>
</html>
注意
:我创建了一个 window.lambdaEndpoint 的变量。稍后,您需要使用无服务器框架的输出来更改此值。该地址将是您的函数结点(PS,即 Lambda API 地址)。
HTML 的结果如下所示:
在此演示中,用户将点击 “Retrieve Keys”,它将发一个 Ajax 请求来调用 Lambda。其目的是检索临时密钥,以用于与我们的 IoT 消息系统连接。 通过这些 keys,“Connect” 按钮将创建一个频道并订阅消息。 “Send Message” 按钮将与其他用户共享消息(打开另一个浏览器选项卡进行测试)。
接着,我们需要通过 Ajax 请求来发起对 Lambda 函数的调用,以检索临时的 AWS 密钥。
注意
:我使用的是 ES6 语法,因为当我打包 IoT 客户端时,这个文件将由 Babel 处理。
$(document).ready(() => {
let iotKeys;
$('#btn-keys').on('click', () => {
$.ajax({
url: window.lambdaEndpoint,
success: (res) => {
addLog(`Endpoint: ${res.iotEndpoint},
Region: ${res.region},
AccessKey: ${res.accessKey},
SecretKey: ${res.secretKey},
SessionToken: ${res.sessionToken}`);
iotKeys = res; // save the keys
}
});
});
});
const addLog = (msg) => {
const date = (new Date()).toTimeString().slice(0, 8);
$("#log").prepend(`<li>[${date}] ${msg}</li>`);
}
为了构建我们的通知系统,我们需要使用一个 Node 模块 AWS IoT SDK,并打包到浏览器中运行。
在这个项目中,我创建了另一个名为 iot
的文件夹来开发 IoT 客户端代码。它有一个 package.json
,所以运行 npm install
来安装 aws-iot-device-sdk
依赖。
这个 IoT 对象具有 connect
和 send
的功能。它还为其他功能(如 onConnect
和 onMessage
)提供处理程序。
const awsIot = require('aws-iot-device-sdk');
let client, iotTopic;
const IoT = {
connect: (topic, iotEndpoint, region, accessKey, secretKey, sessionToken) => {
iotTopic = topic;
client = awsIot.device({
region: region,
protocol: 'wss',
accessKeyId: accessKey,
secretKey: secretKey,
sessionToken: sessionToken,
port: 443,
host: iotEndpoint
});
client.on('connect', onConnect);
client.on('message', onMessage);
client.on('close', onClose);
},
send: (message) => {
client.publish(iotTopic, message);
}
};
const onConnect = () => {
client.subscribe(iotTopic);
addLog('Connected');
};
const onMessage = (topic, message) => {
addLog(message);
};
const onClose = () => {
addLog('Connection failed');
};
正如我们开发的 IoT 客户端,我们现在可以完成 JavaScript 浏览器代码。添加以下内容:
$('#btn-connect').on('click', () => {
const iotTopic = '/serverless/pubsub';
IoT.connect(iotTopic,
iotKeys.iotEndpoint,
iotKeys.region,
iotKeys.accessKey,
iotKeys.secretKey,
iotKeys.sessionToken);
});
$('#btn-send').on('click', () => {
const msg = $('#message').val();
IoT.send(msg);
});
现在,我们来创建一个 bundle
。 似乎目前这个模块不能与 webpack 捆绑在一起,但是通过 Browserify 它可以正常工作。我创建了一个名为 make-bundle.js 的文件,有助于此。你可以在这里查看这个文件。
只需在此文件夹中运行 npm
安装,并运行node make-bundle
来创建更新的版本。这个 bundle.js
将在我们的前端代码中使用,并且必须位于 index.html
文件的同一个文件夹中。
我们的 Lambda 函数将负责创建临时的 AWS 密钥。然而,它需要一个角色来定义这些密钥将提供哪些访问权限。
您可以使用 IAM 控制台创建此角色,也可以执行 create-role 文件夹中的 index.js
文件来您创建一个。此软件包使用 AWS SDK,并且在使用之前需要 npm install
。
我使用角色名称是 serverless-notifications
。如果需要,您可以在此处更改此名称,但您还需要更改 Lambda 函数中的名称。
创造角色:
const AWS = require('aws-sdk');
const iam = new AWS.IAM();
const sts = new AWS.STS();
const roleName = 'serverless-notifications';
// get the account id
sts.getCallerIdentity({}, (err, data) => {
if (err) return console.log(err, err.stack);
const createRoleParams = {
AssumeRolePolicyDocument: `{
"Version":"2012-10-17",
"Statement":[{
"Effect": "Allow",
"Principal": {
"AWS": "arn:aws:iam::${data.Account}:root"
},
"Action": "sts:AssumeRole"
}
]
}`,
RoleName: roleName
};
// create role
iam.createRole(createRoleParams, (err, data) => {
if (err) return console.log(err, err.stack);
const attachPolicyParams = {
PolicyDocument: `{
"Version": "2012-10-17",
"Statement": [{
"Action": ["iot:Connect", "iot:Subscribe", "iot:Publish", "iot:Receive"],
"Resource": "*",
"Effect": "Allow"
}]
}`,
PolicyName: roleName,
RoleName: roleName
};
// add iot policy
iam.putRolePolicy(attachPolicyParams, (err, data) => {
if (err) console.log(err, err.stack);
else console.log(`Finished creating IoT Role: ${roleName}`);
});
});
});
要完成剩下的内容,我们创建一个 Lambda 函数,它将生成临时密钥(有效期为1小时)以连接到 IoT 服务。我们将使用 Serverless Framework 来帮助您。如果你不知道如何使用它,你可以看看我创建的另一个教程。
该 serverless.yml
必须为 iot:DescribeEndpoint
(找到您的帐户端点)添加 Lambda 权限和 sts:AssumeRole
(创建临时密钥)。
service: serverless-notifications
provider:
name: aws
runtime: nodejs4.3
stage: dev
region: us-east-1
iamRoleStatements:
- Effect: "Allow"
Action:
- 'iot:DescribeEndpoint'
Resource: "*"
- Effect: "Allow"
Action:
- 'sts:AssumeRole'
Resource: "*"
functions:
auth:
handler: handler.auth
events:
- http: GET iot/keys
memorySize: 128
timeout: 10
package:
exclude:
- .git/**
- create-role/**
- frontend/**
- iot/**
Lambda 函数:
'use strict';
const AWS = require('aws-sdk');
const iot = new AWS.Iot();
const sts = new AWS.STS();
const roleName = 'serverless-notifications';
module.exports.auth = (event, context, callback) => {
// get the endpoint address
iot.describeEndpoint({}, (err, data) => {
if (err) return callback(err);
const iotEndpoint = data.endpointAddress;
const region = getRegion(iotEndpoint);
// get the account id which will be used to assume a role
sts.getCallerIdentity({}, (err, data) => {
if (err) return callback(err);
const params = {
RoleArn: `arn:aws:iam::${data.Account}:role/${roleName}`,
RoleSessionName: getRandomInt().toString()
};
// assume role returns temporary keys
sts.assumeRole(params, (err, data) => {
if (err) return callback(err);
const res = buildResponseObject(iotEndpoint,
region,
data.Credentials.AccessKeyId,
data.Credentials.SecretAccessKey,
data.Credentials.SessionToken);
callback(null, res);
});
});
});
};
const buildResponseObject = (iotEndpoint, region, accessKey, secretKey, sessionToken) => {
return {
statusCode: 200,
headers: {
'Access-Control-Allow-Origin': '*'
},
body: JSON.stringify({
iotEndpoint: iotEndpoint,
region: region,
accessKey: accessKey,
secretKey: secretKey,
sessionToken: sessionToken
})
};
};
const getRegion = (iotEndpoint) => {
const partial = iotEndpoint.replace('.amazonaws.com', '');
const iotIndex = iotEndpoint.indexOf('iot');
return partial.substring(iotIndex + 4);
};
// Get random Int
const getRandomInt = () => {
return Math.floor(Math.random() * Number.MAX_SAFE_INTEGER);
};
此示例认为只有一个主题。没关系,每个人都订阅了同一个频道。但是,如果要向特定用户发送私人通知,则需要为每个用户创建一个新主题。
为了处理权限,我建议您继续创建一个角色,以访问所有主题(就像我们在 create-role
项目中所做的那样),并使用 putsRole
命令(Lambda 内部)创建具有特定主题的受限访问的密钥。 这个限制是将策略文档作为一个将来的参数传递给参数(参见 docs)。
对于经过身份验证的用户,我建议您使用 Cognito 凭据。
1).创建 Identity Pool
const AWS = require('aws-sdk');
const cognitoidentity = new AWS.CognitoIdentity();
const params = {
AllowUnauthenticatedIdentities: true,
IdentityPoolName: 'serverless-notifications'
};
cognitoidentity.createIdentityPool(params, (err, data) => {
if (err) console.log(err, err.stack);
else console.log(data.IdentityPoolId); // save the IdentityPoolId
});
2).为我们以前做过的这个身份池创建一个角色。但是,对于 AssumeRolPolicyDocument
,请使用以下 JSON(将 IdentityPoolId 设置为上一个函数返回的值)。
{
"Version": "2012-10-17",
"Statement": [
{
"Effect": "Allow",
"Principal": {
"Federated": "cognito-identity.amazonaws.com"
},
"Action": "sts:AssumeRoleWithWebIdentity",
"Condition": {
"StringEquals": {
"cognito-identity.amazonaws.com:aud": ${IdentityPoolId}
},
"ForAnyValue:StringLike": {
"cognito-identity.amazonaws.com:amr": "unauthenticated"
}
}
}
]
}
3).使用 Identity Pool 设置新的 Role。
const AWS = require('aws-sdk');
const cognitoidentity = new AWS.CognitoIdentity();
const params = {
IdentityPoolId: 'IDENTITY_POOL_ID',
Roles: {
unauthenticated: 'ROLE_ARN',
authenticated: 'ROLE_ARN'
}
};
cognitoidentity.setIdentityPoolRoles(params, (err, data) => {
if (err) console.log(err, err.stack);
else console.log(data); // successful response returns an empty object
});
4).在浏览器里,请求 Congnito 凭证
const AWS = require('aws-sdk');
AWS.config.region = 'YOUR_COGNITO_REGION';
AWS.config.credentials = new AWS.CognitoIdentityCredentials({
IdentityPoolId: 'IDENTITY_POOL_ID',
});
AWS.config.credentials.get(() => {
// Use these credentials with IoT
const accessKeyId = AWS.config.credentials.accessKeyId;
const secretAccessKey = AWS.config.credentials.secretAccessKey;
const sessionToken = AWS.config.credentials.sessionToken;
});
观光\评论区