在此模块中,您将使用您已部署的应用程序。在此之前,我们来快速回顾一下您在应用程序中使用的 AWS 组件:

我们来看看这些组件时如何联系在一起的。在以下步骤中,您将使用不同的组件逐个了解不同的应用程序终端节点。

第一,您开始使用 Registration 终端节点,新用户在此终端节点注册并创建他们的账户。

第二,您使用 Login 终端节点,用户可在此终端节点使用客户端(例如 Web 应用程序或移动应用程序)进行身份验证和接收 ID 令牌。

第三,您使用 AddUserScore 终端节点记录用户的分数。

第四,您使用 FetchUserScores 终端节点检索特定用户的最高分数。

最后,您使用 FetchTopScores 终端节点检索当天和当月的全球最高分数,以及所有时间的最高分数。

完成模块所需时间:20 分钟


  • 第 1 步:注册新用户

    您查看的第一个工作流程是 Registration 终端节点。在此终端节点,新用户通过提供登录信息(如用户名和密码)进行注册。

    当您查看这些终端节点时,终端节点调用的相关代码片段会显示在此指南中。

    只需向 /users 终端节点发出 POST 请求,您的注册终端节点即可使用。您可以在 application/app.js 文件的中间部分看到此终端节点的逻辑:

    // Create user
    app.post('/users', wrapAsync(async (req, res) => {
      await createCognitoUser(req.body.username, req.body.password, req.body.email)
      res.json({ username: req.body.username })
    }))
    

    您的处理程序将接收请求负载,并在 Amazon Cognito 中创建新用户。

    您已在 Amazon Cognito 模块中了解了 createCognitoUser 函数,我们在此不再赘述。

    尝试调用您的 Registration 终端节点以创建新用户。在终端中运行以下命令:

    curl -X POST ${BASE_URL}/users \
      -H 'Content-Type: application/json' \
      -d '{
    	"username": "puzzlemaster",
    	"password": "Mypassword1",
    	"email": "test@hello.com"
    }'

    您应该能在终端中看到以下输出:

    {"username":"puzzlemaster"}

    很好! 您已成功创建用户。现在,我们来登录并接收 ID 令牌。

  • 第 2 步:登录以获取用户凭证

    您的第二个终端节点是 Login 终端节点。用户将其用户名和密码提交至此终端节点以接收 ID 令牌,该 ID 令牌将用于在之后的请求中对用户进行身份验证。

    要处理此身份验证和令牌分发,您的应用程序有一个 /login 终端节点。此处理程序位于 application/app.js 中,如下所示:

    // Login
    app.post('/login', wrapAsync(async (req, res) => {
      const idToken = await login(req.body.username, req.body.password)
      res.json({ idToken })
    }))

    它要求负载主体具有用户名密码属性。然后会从 auth.js 文件中调用帮助程序的 login 函数。如果登录成功,则会返回用户的 ID 令牌。

    让我们来使用您首次创建的用户进行测试。在终端中运行以下命令:

    curl -X POST ${BASE_URL}/login \
      -H 'Content-Type: application/json' \
      -d '{
    	"username": "puzzlemaster",
    	"password": "Mypassword1"
    }'
    

    您应该会在终端中看到带有 idToken 属性的响应:

    {"idToken":"eyJraWQiO…."}

    此 ID 令牌用于在之后的请求中对用户进行授权。在您的 Shell 中保存 ID 令牌的值,方法是复制引号中间的令牌的值,然后运行以下命令:

    export ID_TOKEN=<idToken>

    注意:此身份令牌是一个临时凭证,一段时间后将失效。如果您在一段时间后返回到本教程,您可能需要重新运行以上步骤以生成新令牌。

  • 第 3 步:记录用户的分数

    现在您的用户可以进行身份验证,我们来记录用户的分数。您可以想象,当用户在其移动设备上完成某个游戏关卡后会发生这种情况。此应用程序会将分数和关卡上传到后端以进行持久性存储。

    创建新游戏的终端节点是 POST /users/:usernameapplication/app.js 中的处理程序代码如下所示:

    // Add new score
    app.post('/users/:username', wrapAsync(async (req, res) => {
      const validated = validateCreateScore(req.body)
      if (!validated.valid) {
        throw new Error(validated.message)
      }
      const token = await verifyToken(req.header('Authorization'))
      if (token['cognito:username'] != req.params.username) {
        throw new Error('Unauthorized')
      }
      const score = await addUserScore(req.params.username, req.body.level, req.body.score)
      res.json(score)
    }))

    此代码执行以下两种操作。首先,验证授权标头中发送的 ID 令牌。如果此令牌有效,并且令牌中的用户名与 URL 路径中的用户名一致,则该用户名将被传递到您的数据包中的 addUserScore 函数中。

    addUserScore 函数位于 application/data/addUserScore.js 中,如下所示:

    const { executeWriteSql } = require('./utils')
    const redis = require('redis')
    
    const client = redis.createClient({
      url: `redis://${process.env.REDIS_ENDPOINT}`
    })
    client.on('error', function(err) {
      console.log('Received Redis error:', err)
    })
    
    const addUserScore = async (username, level, score) => {
      sql = `INSERT INTO games (username, level, score) \
    VALUES (:username, :level, :score)`
      parameters = [
        {
          name: 'username',
          value: { stringValue: username }
        },
        {
          name: 'level',
          value: { longValue: level}
        },
        {
          name: 'score',
          value: { longValue: score}
        }
      ]
      const result = await executeWriteSql(sql, parameters)
      const gametime = new Date()
      const key = `${username}|${gametime}|${game.level}`
      return new Promise((resolve, reject) => {
        client.multi()
          .zadd('Overall Leaderboard', game.score, key)
          .zadd(`Monthly Leaderboard|${gametime.getUTCMonth()}-${gametime.getUTCFullYear()}`, game.score, key)
          .zadd(`Daily Leaderboard|${gametime.getUTCDay()}-${gametime.getUTCMonth()}-${gametime.getUTCFullYear()}`, game.score, key)
          .exec((err) => {
            if (err) {
              reject(err)
            }
            resolve({ username, gametime, level, score })
          })
      })
    }
    
    module.exports = addUserScore

    addUserScore 函数生成当前时间戳,并将游戏得分保存到关系数据库中以进行长期存储。然后,它会将游戏记录保存在相关日期和月份以及全时间段排行榜的排行榜有序集合中。该策略与将所有样本数据加载到 Redis 实例时使用的策略相同。

    我们来试用此终端节点。在终端中运行以下命令:

    curl -X POST ${BASE_URL}/users/puzzlemaster \
     -H 'Content-Type: application/json' \
      -H "Authorization: ${ID_TOKEN}" \
      -d '{
    	"level": 37,
    	"score": 6541
    }'

    注意,您将把第一个用户的 ID 令牌与请求一起传递到授权标头中。

    您应该能在终端中看到以下输出:

    {"username":"puzzlemaster","gametime":"2019-11-12T03:18:51.637Z","level":37,"score":6541}

    已为您的用户记录此新分数!

    再次尝试运行此终端节点,但不使用 ID 令牌。在终端中运行以下命令:

    curl -X POST ${BASE_URL}/users/puzzlemaster \
     -H 'Content-Type: application/json' \
      -d '{
    	"level": "37",
    	"score": "6541"
    }'

    因为您的请求不包括令牌,您应看到以下输出:

    {"message":"jwt must be provided"}

    您已使用 Amazon Cognito 令牌来保护终端节点。

    继续之前,请将另外两个分数加载到您的数据库中,以使下一步更加令人振奋。在终端中运行以下两个请求。

    curl -X POST ${BASE_URL}/users/puzzlemaster \
     -H 'Content-Type: application/json' \
     -H "Authorization: ${ID_TOKEN}" \
      -d '{
    	"level": "42",
    	"score": "7142"
    }'
    
    curl -X POST ${BASE_URL}/users/puzzlemaster \
     -H 'Content-Type: application/json' \
     -H "Authorization: ${ID_TOKEN}" \
      -d '{
    	"level": "48",
    	"score": "9901"
    }'
    
  • 第 4 步:获取用户的最高分数

    在此步骤中,您可以获取用户的所有分数。可在您的应用程序中使用这些分数来显示用户的个人排行榜。您还可以立即使用它来确保您的数据得到正确保存。

    用于获取用户的终端节点是 GET /users/:username,处理程序代码包含在 application/app.js 中,如下所示:

    // Fetch user scores
    app.get('/users/:username', wrapAsync(async (req, res) => {
      const limit = req.query.limit || 10;
      const scores = await fetchUserScores(req.params.username, limit)
      res.json(scores)
    }))

    此终端节点不是经过身份验证的终端节点,每个用户都可以根据需要看到其他用户的详细信息。

    通过在您的终端中输入以下命令,调用终端节点并获取用户:

    curl -X GET ${BASE_URL}/users/puzzlemaster

    您应该能在终端中看到以下输出:

    [{"game_id":303,"username":"puzzlemaster","gamedate":"2019-11-12 03:21:55","score":9901,"level":48},{"game_id":302,"username":"puzzlemaster","gamedate":"2019-11-12 03:21:47","score":7142,"level":42},{"game_id":301,"username":"puzzlemaster","gamedate":"2019-11-12 03:18:51","score":6541,"level":37}]

    成功! 您检索了用户,并且该用户具有您在上一步中输入的三个分数。请注意,分数按从高到低的顺序存储,以便您可以首先向用户显示他们的最高分数。

  • 第 5 步:获取某个日期的最高分数

    在最后一步中,您将获取特定日期的最高分数。此终端节点将返回给定日期和月份的最高分数以及最高总分。

    用于获取用户的终端节点为 GET /scores/:date,其中 :date 是格式为 YYYY-MM-DD 的日期。此处理程序代码包含在 application/app.js 中,如下所示:

    // Fetch top scores
    app.get('/scores/:date', wrapAsync(async (req, res) => {
      const scores = await fetchTopScores(req.params.date)
      res.json(scores)
    }))

    该处理程序非常简单,因为它将日期参数传递到 fetchTopScores 数据方法中。该方法包含在 application/data/fetchTopScores.js 中。该文件的内容如下:

    const redis = require('redis')
    const _ = require('lodash')
    const client = redis.createClient({
      url: `redis://${process.env.REDIS_ENDPOINT}`
    })
    
    client.on('error', function(err) {
      console.log('Received Redis error:', err)
    })
    
    const parseKey = (key) => {
      const parts = key.split('|')
      return {
        username: parts[0],
        gamedate: parts[1],
        level: parts[2]
      }
    }
    
    const parseZRevRangeResponse = (resp) => {
      const result = _.chunk(resp, 2).map(([key, score]) => {
        const obj = parseKey(key)
        return {
          ...obj,
          score
        }
      })
      return result
    }
    
    const fetchTopScores = async (date) => {
      const gametime = new Date(date)
      return new Promise((resolve, reject) => {
        client.multi()
          .zrevrange('Overall Leaderboard', '0', '4', 'WITHSCORES')
          .zrevrange(`Monthly Leaderboard|${gametime.getUTCMonth()}-${gametime.getUTCFullYear()}`, '0', '4', 'WITHSCORES')
          .zrevrange(`Daily Leaderboard|${gametime.getUTCDay()}-${gametime.getUTCMonth()}-${gametime.getUTCFullYear()}`, '0', '4', 'WITHSCORES')
          .exec((err, resp) => {
            if (err) {
              reject(err)
            }
            const overall = parseZRevRangeResponse(resp[0])
            const weekly = parseZRevRangeResponse(resp[1])
            const daily = parseZRevRangeResponse(resp[2])
            resolve ({
              overall,
              weekly,
              daily
            })
          })
      })
    }
    
    module.exports = fetchTopScores

    fetchTopScores 方法将获取日期字符串并将其转换为日期对象。然后,它从 Redis 的相关日期和月份有序集合中获取最高分数,并从总排行榜获取最高分数。它将结果解析为一个结构,在返回结果之前,更便于客户端进行处理。

    尝试通过运行以下命令获取最高分数:

    curl -X GET ${BASE_URL}/scores/2019-11-08

    您应该能在终端中看到以下结果:

    {"overall":[{"username":"puzzlemaster","gamedate":"Tue Nov 12 2019 03:21:55 GMT+0000 (Coordinated Universal Time)","level":"48","score":"9901"},{"username":"debbieschneider","gamedate":"2019-11-09T18:41:27","level":"28","score":"9895"},{"username":"alicia39","gamedate":"2019-11-09T10:39:59","level":"47","score":"9824"},{"username":"rosecolleen","gamedate":"2019-11-10T07:09:51","level":"58","score":"9765"},{"username":"allisonsandra","gamedate":"2019-11-07T22:43:32","level":"62","score":"9760"}],"weekly":[{"username":"puzzlemaster","gamedate":"Tue Nov 12 2019 03:21:55 GMT+0000 (Coordinated Universal Time)","level":"48","score":"9901"},{"username":"debbieschneider","gamedate":"2019-11-09T18:41:27","level":"28","score":"9895"},{"username":"alicia39","gamedate":"2019-11-09T10:39:59","level":"47","score":"9824"},{"username":"rosecolleen","gamedate":"2019-11-10T07:09:51","level":"58","score":"9765"},{"username":"allisonsandra","gamedate":"2019-11-07T22:43:32","level":"62","score":"9760"}],"daily":[{"username":"terriross","gamedate":"2019-11-08T21:31:47","level":"26","score":"9386"},{"username":"alicia39","gamedate":"2019-11-08T15:45:30","level":"60","score":"9323"},{"username":"christopherrichardson","gamedate":"2019-11-08T09:51:28","level":"55","score":"9234"},{"username":"castilloanthony","gamedate":"2019-11-08T15:08:32","level":"77","score":"9175"},{"username":"rodriguezjonathan","gamedate":"2019-11-08T13:56:32","level":"46","score":"9155"}]}

    很好! 如果您仔细观察,会发现 puzzlemaster 用户在每月排行榜和总排行榜中的得分都是最高的。


在此模块中,您使用了正在运行的终端节点来查看各个组件是如何协同运行的。首先,您注册了一个新用户,其中涉及在您的 Amazon Cognito 用户池中创建一个新用户。第二,您使用登录终端节点来获取 ID 令牌。客户端可以使用该令牌对用户进行身份验证。第三,您使用此 ID 令牌授权用户记录一些新分数。第四,您检索了单个用户的最高分数。最后,您检索了全球最高分数。

在下一个模块中,您将清理您创建的资源。