14
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

More than 5 years have passed since last update.

ServerlessAdvent Calendar 2018

Day 19

AWS AppSync をシンプルなJavaScriptで試してみる

Last updated at Posted at 2018-12-18

はじめに

こんにちは、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の作成

マネジメントコンソールから「ユーザープールを作成」で作成開始
image.png

適当な名称をつけて、「デフォルトを確認する」で作る
image.png

「属性」を修正する
ユーザのサインインは「メールアドレス」とする(一般的、かつ、Google連携との相性を考慮)
emailは必須属性とする
image.png

「アプリクライアント」を追加する
image.png
※Webから繋ごうとしているので、クライアントシークレットは生成しない
image.png

「確認」からプールを作成する
image.png

以下をメモしておく
「プールID」 ⇨ リージョン_ランダム文字列:us-west-2_xxxxxxxxx
「アプリクライアントID」 ⇨ ランダム文字列:xxxxxxxxxxxxxxxxxxxxxxxxxx

メール認証でユーザを作成する

認証を行うために、Cognitoで用意しているフォームを使う
「アプリの統合」⇨「アプリクライアントの設定」
image.png

image.png
※コールバックURLは適当だが、認証用URLのパラメータで完全一致させる必要があるので注意

認証用のドメインを設定する
image.png

認証用ページに行ってみる
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/

サインアップする

入力したメールアドレスに送信された認証コードを入れる

リダイレクトURLに遷移する
image.png

ユーザープールの「ユーザーとグループ」に追加されている
image.png

Google連携でユーザを作成する

GCPアカウントを作り、プロジェクトを作成し、「認証情報」に行く
image.png

初めての場合は「OAuth同意画面」からドメインを登録しておく

「認証情報」に戻り、OAuthクライアントIDを作成する

image.png

承認済みのリダイレクトURIには、以下を設定する
https://設定したドメイン.auth.リージョン.amazoncognito.com/oauth2/idpresponse

作成するとクライアントIDとシークレットが表示されるので、メモしておく

AWSのマネジメントコンソールに戻り、ユーザープールの「IDプロパイダー」から、Googleを選択する
image.png
image.png
・GoogleアプリID:GCPで取得したクライアントID
・アプリシークレット:GCPで取得したシークレット
・承認スコープ:"profile email openid"

「属性マッピング」の Googleタブでemailを設定し、Googleからメールアドレスを取得できるようにする
image.png

「アプリクライアントの設定」の有効なIDプロパイダにGoogleが追加されているので、チェックを入れる
image.png

認証用URLに遷移すると、Googleが新たに追加されている
image.png

Appsync の設定

マネジメントコンソールから「Create API」で作っていく
image.png

「Create with wizard」を選択してStart
image.png

デフォルトで作成する(モデルは後から変えられる)
image.png

AppSyncが作成されるので「Setting」で設定を確認し、メモしておく
API URL
API ID

AppSyncのAuthorization typeをCognitoに変更し、作成したユーザプールを指定する
image.png

「Queries」からクエリを実行しようとすると、UnauthorizedExceptionとなる
"Login With User Pools"からログインすると、クエリを実行できるようになる

Webサイトからcognitoでログイン&GraphQLでのデータ取得/登録を試す

GraphQLを呼び出すには、jwt(アクセストークン)が必要になる
jwtは、認証用ページで認証をした後にリダイレクトURLに遷移した際のクエリパラメータ「code」を使うことで、cognitoのエンドポイントから取得することができる

↓ ログイン処理までを以下の順で説明する

  • 外部javascriptの読み込み
  • 独自関数群
  • ログイン処理
index.html
<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を実行
image.png
メッセージが通知される
image.png

終わりに

ご覧いただきありがとうございました。
日本語の情報がかなり少なく大変でしたが、シンプルなjavascriptでCognitoへのログイン、
Appsyncを利用したGraphQLの実行(Query/Mutation/Subscription)ができました。
次はルームを作って特定ユーザのみSubscriptionできるようにする仕組みや、オフライン時の動作などを確認したいと思います。

至らぬ点などありましたら、コメントいただけるとありがたいです。

14
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
14
10

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?