はじめに
こんにちは、Serverless Advent Calendar 2018 の 19日目 です。
今まで見ることしかしていなかったAdvent Calendarですが、
大好きなServerlessについて、初めて記事を書くことを決意しました。
私と Serverless Advent Calendar 2018
・ケチかつ飽き性なので、従量課金のServerlessに興味をもつ
・モバイルアプリを作ろうとしたが、認証/ソケット/Push通知あたりで手こずる
・DynamoDBの設計がイマイチでAPIの数も増える一方…(この辺りでやる気なくなってきた)
・DevDayTokyo2018のServerlessDayに参加
・その中のセッションでAWS AppSyncの素晴らしさ を感じ、 DynamoDBの設計指針も固まった
・この熱が冷める前にAdventCalendarに参加!
私の経験
・10年目のシステムエンジニア
・業務ではAWSは利用していない
・APIGateway+Lambda+DynamoDBは業務外で少し経験あり(SAM)
・AWS AppSync初(Cognito初、GraphQL初)
さっそくチュートリアル
以下をゴールとし、理解を深めるために「なるべく」手作りする
・Cognitoでユーザ認証(メール認証、Google連携)
・GraphQL経由でのデータアクセス、更新(認証ユーザでのデータアクセス制御)
・GraphQLのサブスクリプションによる通知
順序
・Cognito UserPoolの作成(AppSyncの設定に必要)
・Appsync でAPIの作成
・WebサイトからGraphQLでのデータアクセス、サブスクリプションを試す
Cognito UserPoolの作成
マネジメントコンソールから「ユーザープールを作成」で作成開始
「属性」を修正する
ユーザのサインインは「メールアドレス」とする(一般的、かつ、Google連携との相性を考慮)
emailは必須属性とする
「アプリクライアント」を追加する
※Webから繋ごうとしているので、クライアントシークレットは生成しない
以下をメモしておく
「プールID」 ⇨ リージョン_ランダム文字列:us-west-2_xxxxxxxxx
「アプリクライアントID」 ⇨ ランダム文字列:xxxxxxxxxxxxxxxxxxxxxxxxxx
メール認証でユーザを作成する
認証を行うために、Cognitoで用意しているフォームを使う
「アプリの統合」⇨「アプリクライアントの設定」
↓
※コールバックURLは適当だが、認証用URLのパラメータで完全一致させる必要があるので注意
認証用ページに行ってみる
https://設定したドメイン.auth.リージョン.amazoncognito.com/login?response_type=code&client_id=クライアントID&redirect_uri=http://リダイレクトURL/
Ex. https://test.auth.us-west-2.amazoncognito.com/login?response_type=code&client_id=xxxxxxxxxxxxxxxxxxxxxxxxxx&redirect_uri=https://aws.amazon.com/jp/
Google連携でユーザを作成する
GCPアカウントを作り、プロジェクトを作成し、「認証情報」に行く
初めての場合は「OAuth同意画面」からドメインを登録しておく
承認済みのリダイレクトURIには、以下を設定する
https://設定したドメイン.auth.リージョン.amazoncognito.com/oauth2/idpresponse
作成するとクライアントIDとシークレットが表示されるので、メモしておく
AWSのマネジメントコンソールに戻り、ユーザープールの「IDプロパイダー」から、Googleを選択する
・GoogleアプリID:GCPで取得したクライアントID
・アプリシークレット:GCPで取得したシークレット
・承認スコープ:"profile email openid"
「属性マッピング」の Googleタブでemailを設定し、Googleからメールアドレスを取得できるようにする
「アプリクライアントの設定」の有効なIDプロパイダにGoogleが追加されているので、チェックを入れる
認証用URLに遷移すると、Googleが新たに追加されている
Appsync の設定
マネジメントコンソールから「Create API」で作っていく
「Create with wizard」を選択してStart
AppSyncが作成されるので「Setting」で設定を確認し、メモしておく
API URL
API ID
AppSyncのAuthorization typeをCognitoに変更し、作成したユーザプールを指定する
「Queries」からクエリを実行しようとすると、UnauthorizedExceptionとなる
"Login With User Pools"からログインすると、クエリを実行できるようになる
Webサイトからcognitoでログイン&GraphQLでのデータ取得/登録を試す
GraphQLを呼び出すには、jwt(アクセストークン)が必要になる
jwtは、認証用ページで認証をした後にリダイレクトURLに遷移した際のクエリパラメータ「code」を使うことで、cognitoのエンドポイントから取得することができる
↓ ログイン処理までを以下の順で説明する
- 外部javascriptの読み込み
- 独自関数群
- ログイン処理
<html>
<head>
<meta charset="UTF-8">
<!-- 外部javascriptの読み込み -->
<script
src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script type="module" src="src/aws-exports.js"></script>
<script src="src/aws-sdk.js"></script>
<script src="src/config.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript"></script>
<!-- 独自関数群 -->
<script>
var _resolve = function(x){ return $.Deferred().resolve(x).promise(); };
var _reject = function(x){ return $.Deferred().reject(x).promise(); };
var _getStorageItem = function(x){
var _item = JSON.parse(localStorage.getItem(x));
return _item ? _item : {};
};
_param={};
location.search.slice(1).split("&").forEach(v=>{
key = v.split("=")[0];
_param[key] = v.replace(key+"=","");
});
var _cognito = {
storage : {
setTokens : _tokens => {
localStorage.setItem("tokens", JSON.stringify(_tokens));
_cognito.storage.jwt = _tokens.access_token;
return _resolve(_tokens);
},
jwt : _getStorageItem("tokens").access_token
},
api : {
getTokens : _code => {
return $.ajax({
url: opt.aws_cognito_tokenEndpoint,
method: "POST",
contentType: "application/x-www-form-urlencoded",
data: {
grant_type: "authorization_code",
client_id: opt.aws_cognito_clientid,
redirect_uri: opt.aws_cognito_redirecturl,
code: _code,
},
})
},
getUser : () => {
return $.ajax({
url: opt.aws_cognito_userEndpoint,
method: "GET",
contentType: "application/json",
headers: {
Authorization: "Bearer "+_cognito.storage.jwt
},
})
},
graphql : (_query) => {
return $.ajax({
url: opt.aws_appsync_graphqlEndpoint,
method: "POST",
contentType: "application/json",
headers: {
Authorization: _cognito.storage.jwt
},
data: JSON.stringify(_query),
})
},
},
};
//ログイン処理
typeof _cognito.storage.jwt === "undefined" && !_param.code ? location.href = opt.aws_cognito_loginEndpoint : ""; //token, codeなし
(_cognito.storage.jwt
? _resolve(_cognito.storage.jwt)
: _cognito.api.getTokens(_param.code) //token取得
.then(x=>_cognito.storage.setTokens(x)) //storageに保存
.then(x=>location.href=opt.aws_cognito_redirecturl)
)
.then(() =>
_cognito.api.graphql({
query: `
{
getMyInfo {
id
email
}
}
`})
)
.then(x=>{
return x.data.getMyInfo
? _resolve(console.log(x.data.getMyInfo))
: _cognito.api.getUser() //ユーザ情報取得
.then(x=>_cognito.api.graphql({ //ユーザ作成
operationName: "myInfo",
query: `
mutation myInfo($input: CreateMyInfoInput!){
createMyInfo(input: $input){
email
}
}`,
variables: {
"input": {
"email": x.email
}
}
}));
})
.fail(x=>_cognito.storage.setTokens({}).then(x=>location.href = opt.aws_cognito_loginEndpoint));
</script>
</head>
<body>
</body>
</html>
外部javascriptの読み込み
・CDNから取得したjQuery
・amplifyコマンドで作成した aws-exports.js
・aws-sdk.js
・定数定義などをした config.js(これだけイチから作成したjsファイル)
・Paho Javascript Clientからダウンロードした mqttws31.min.js
<!-- 外部javascriptの読み込み -->
<script
src="https://code.jquery.com/jquery-3.3.1.min.js"
integrity="sha256-FgpCb/KJQlLNfOu91ta32o/NMZxltwRo8QtmkMRdAu8="
crossorigin="anonymous"></script>
<script type="module" src="src/aws-exports.js"></script>
<script src="src/aws-sdk.js"></script>
<script src="src/config.js"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/paho-mqtt/1.0.1/mqttws31.min.js" type="text/javascript"></script>
独自関数群
・Promise用の _resolve, _reject関数
・ローカルストレージアクセス用の _getStorageItem関数
・Getパラメータの取得結果を格納する _paramオブジェクト
var _resolve = function(x){ return $.Deferred().resolve(x).promise(); };
var _reject = function(x){ return $.Deferred().reject(x).promise(); };
var _getStorageItem = function(x){
var _item = JSON.parse(localStorage.getItem(x));
return _item ? _item : {};
};
_param={};
location.search.slice(1).split("&").forEach(v=>{
key = v.split("=")[0];
_param[key] = v.replace(key+"=","");
});
・Cognito/GraphQL周りのアクセス用の _cognitoオブジェクト
関数名 | 説明 |
---|---|
_cognito.storage.setTokens | cognitoから取得したアクセストークンをローカルストレージに保存する関数 |
_cognito.storage.jwt | アクセストークンを格納する変数 |
_cognito.api.getTokens | cognitoからアクセストークンを取得する関数 |
_cognito.api.getUser | cognitoからログイン中のユーザ情報を取得する関数 |
_cognito.api.graphql | AppsyncのGraphQL APIを呼び出す関数 |
var _cognito = {
storage : {
setTokens : _tokens => {
localStorage.setItem("tokens", JSON.stringify(_tokens));
_cognito.storage.jwt = _tokens.access_token;
return _resolve(_tokens);
},
jwt : _getStorageItem("tokens").access_token
},
api : {
getTokens : _code => {
return $.ajax({
url: opt.aws_cognito_tokenEndpoint,
method: "POST",
contentType: "application/x-www-form-urlencoded",
data: {
grant_type: "authorization_code",
client_id: opt.aws_cognito_clientid,
redirect_uri: opt.aws_cognito_redirecturl,
code: _code,
},
})
},
getUser : () => {
return $.ajax({
url: opt.aws_cognito_userEndpoint,
method: "GET",
contentType: "application/json",
headers: {
Authorization: "Bearer "+_cognito.storage.jwt
},
})
},
graphql : (_query) => {
return $.ajax({
url: opt.aws_appsync_graphqlEndpoint,
method: "POST",
contentType: "application/json",
headers: {
Authorization: _cognito.storage.jwt
},
data: JSON.stringify(_query),
})
},
},
};
ログイン処理
・jwtがストレージに保存されていない、かつ、パラメータにcodeが無い場合は、認証用ページに遷移させる
//ログイン処理
typeof _cognito.storage.jwt === "undefined" && !_param.code ? location.href = opt.aws_cognito_loginEndpoint : ""; //token, codeなし
1.jwtがローカルストレージにある場合は2に進み、jwtが無い場合はcognitoから取得してローカルストレージに保存
2.jwtを取得したら、GraphQL(DynamoDB)から自身の情報を取得する
3.自身の情報が取得できたらコンソールに出力して終了する
4.自身の情報が取得できなかった場合は、DynamoDBにデータを作成する(cognitoからemailの情報を取得し、GraphQLで実行する)
(_cognito.storage.jwt
? _resolve(_cognito.storage.jwt)
: _cognito.api.getTokens(_param.code) //1.cognitoからtoken取得
.then(x=>_cognito.storage.setTokens(x)) //1.storageに保存
.then(x=>location.href=opt.aws_cognito_redirecturl)
)
.then(() =>
_cognito.api.graphql({ //2.自身の情報を取得する
query: `
{
getMyInfo {
id
email
}
}
`})
)
.then(x=>{
return x.data.getMyInfo
? _resolve(console.log(x.data.getMyInfo)) //3. コンソールに出力して終了
: _cognito.api.getUser() //4. cognitoからユーザ情報取得
.then(x=>_cognito.api.graphql({ //4. GraphQLでユーザ作成
operationName: "myInfo",
query: `
mutation myInfo($input: CreateMyInfoInput!){
createMyInfo(input: $input){
email
}
}`,
variables: {
"input": {
"email": x.email //4. cognitoから取得したユーザ情報のemailを利用する
}
}
}));
})
.fail(x=>_cognito.storage.setTokens({}).then(x=>location.href = opt.aws_cognito_loginEndpoint)); //エラー時はtokenを全て消してリダイレクトする(最初からやり直し)
getMyInfoのResolverは以下の通り
IDにcognitoのsubを使う
#request mapping template
{
"version": "2017-02-28",
"operation": "GetItem",
"key": {
"id": { "S" : "${context.identity.sub}" }
}
}
#response mapping template
$util.toJson($ctx.result)
createMyInfoのResolverは以下の通り
IDにcognitoのsubを使う
attributeValuesでinputのemailが設定される
#request mapping template
{
"version": "2017-02-28",
"operation": "PutItem",
"key": {
"id": $util.dynamodb.toDynamoDBJson($ctx.identity.sub),
},
"attributeValues": $util.dynamodb.toMapValuesJson($ctx.args.input),
"condition": {
"expression": "attribute_not_exists(#id)",
"expressionNames": {
"#id": "id",
},
},
}
#response mapping template
$util.toJson($ctx.result)
これで、ログインまでを実装することができた!
APIの呼び出しについてもう少し詳しく解説
_cognito.api.getTokens
cognitoからアクセストークンを取得する関数
・urlは「https://設定したドメイン.auth.リージョン.amazoncognito.com/oauth2/token」を設定する
・client_idはユーザープールで設定した「アプリクライアントID」を設定する
・redirect_uriはアプリクライアントの設定の「コールバックURL」と一致させる
・codeは認証用ページからリダイレクトされた時に付与されるGetパラメータcodeが設定される
getTokens : _code => {
return $.ajax({
url: opt.aws_cognito_tokenEndpoint,
method: "POST",
contentType: "application/x-www-form-urlencoded",
data: {
grant_type: "authorization_code",
client_id: opt.aws_cognito_clientid,
redirect_uri: opt.aws_cognito_redirecturl,
code: _code,
},
})
}
_cognito.api.getUser
cognitoからログイン中のユーザ情報を取得する関数
・urlは「https://設定したドメイン.auth.リージョン.amazoncognito.com/oauth2/userInfo」を設定する
・Authorizationヘッダに、"Bearer " + jwt を設定する
getUser : () => {
return $.ajax({
url: opt.aws_cognito_userEndpoint,
method: "GET",
contentType: "application/json",
headers: {
Authorization: "Bearer "+_cognito.storage.jwt
},
})
}
_cognito.api.graphql
AppsyncのGraphQL APIを呼び出す関数
・urlはAppsyncの「Settings」にあるAPI URLを設定する
・Authorizationヘッダに、jwt を設定する
graphql : (_query) => {
return $.ajax({
url: opt.aws_appsync_graphqlEndpoint,
method: "POST",
contentType: "application/json",
headers: {
Authorization: _cognito.storage.jwt
},
data: JSON.stringify(_query),
})
}
WebサイトからAppsyncのSubscriptionを試す
以下の順序でSubscriptionを確認する
1.GraphQLのsubscriptionを実行する
2.戻り値でMQTT接続情報が返って来るため、Paho.MQTT.Clientを作成し、接続する
3.接続後、subscribeをしてメッセージを受け付ける
4.メッセージを受け取ると、コンソールにログを出力する
var onConnect = function(x){ //3.接続後、subscribeをしてメッセージを受け付ける関数
console.log("onConnect!");
console.log(x);
client.subscribe(sub.topics);
};
var onMessageArrived = function(x){ //4.メッセージを受け取ると、コンソールにログを出力する関数
console.log("onMessageArrived!");
console.log(x.payloadString);
};
var onConnectionLost = function(x){
console.log("onConnectionLost!");
console.log(x);
};
//subscription開始
_cognito.api.graphql({ //1.GraphQLのsubscriptionを実行する
query: `
subscription NewPostSub {
onCreatePost {
id
postDate
message
}
}
`
})
.then(x=>{ //2.戻り値でMQTT接続情報が返って来るため、Paho.MQTT.Clientを作成し、接続する
sub = {
wssUrl: x.extensions.subscription.mqttConnections[0].url,
client: x.extensions.subscription.mqttConnections[0].client,
topics: x.extensions.subscription.mqttConnections[0].topics[0],
}
client = new Paho.MQTT.Client(sub.wssUrl, sub.client);
var connectOptions = {
onSuccess: onConnect, //3.接続後、subscribeをしてメッセージを受け付ける関数の登録
useSSL: true,
timeout: 3,
mqttVersion: 4,
onFailure: function(x) {
console.log("connect failed!");
console.log(x);
}
};
client.onMessageArrived = onMessageArrived; //4.メッセージを受け取ると、コンソールにログを出力する関数の登録
client.onConnectionLost = onConnectionLost;
client.connect(connectOptions);
})
.fail(x=>console.log(x));
上記を実行後、onCreatePostの発火を促すcreatePostを実行すると、メッセージが出力される!
AppsyncのQueryでmutationを実行
メッセージが通知される
終わりに
ご覧いただきありがとうございました。
日本語の情報がかなり少なく大変でしたが、シンプルなjavascriptでCognitoへのログイン、
Appsyncを利用したGraphQLの実行(Query/Mutation/Subscription)ができました。
次はルームを作って特定ユーザのみSubscriptionできるようにする仕組みや、オフライン時の動作などを確認したいと思います。
至らぬ点などありましたら、コメントいただけるとありがたいです。