亚马逊AWS官方博客

利用 Amazon Cognito,安全地验证用户名

本博文由 AWS 社区精英 Larry Ogrodnek 特约发表。Larry 是一名独立顾问,他在 AWS 主要负责云架构、开发运营、无服务器和通用软件开发方面的业务。他总是愿意随时随地谈论 AWS,他热爱开发,也乐于帮助其他开发人员

您的系统是如何识别用户? 通过用户名? 或者电子邮件地址? 用户的唯一性对您而言重要吗?

您可能会跟我一样惊讶地发现在 Amazon Cognito 中,用户名和电子邮件地址都是区分大小写的。如此一来,“JohnSmith”就跟“johnsmith”不同,同样,跟“jOhNSmiTh”也有区别。 电子邮件地址也是如此 – SMTP RFC 中还特别说明,用户“smith”和“Smith”可能会拥有不同的邮箱。这真的太不可思议了!

我最近为 Amazon Cognito 添加了自定义注册验证。在这篇博文中,我将为大家介绍具体的实施方法。

关于唯一性的问题

确定唯一性比处理区分大小写的问题还要困难许多。跟许多人一样,我也收到过与国际化域名同义词攻击有关的电子邮件。一个注册域名是“example.com”的网站,实际上使用的是西里尔字母“a”,其试图以此冒充合法网站并收集信息。同样类型的攻击也可以发生在用户名称注册阶段。如果我不设置检查,那么就可能会有人在我的网站上冒充另一个用户。

您保留名称了吗?

我的应用程序中存在用户生成的消息或内容吗? 除了处理唯一性之外,我可能还想保留某些名称(使其不会被注册)。例如,如果我在 user.myapp.com 或 myapp.com/user.com 上有可供用户编辑的信息,但是如果有人注册了“signup”、“faq”或“support”这样的用户名怎么办? 要是他们注册了“billing”呢?

恶意用户有可能冒充我的站点并用以发动攻击。拥有任何类型的收件箱或进行消息收发的用户,也可能受到类似的攻击。除了保留用户名之外,我还会将用户内容划分到其各自的域中,以避免混淆。我记得 2013 年,在 GitHub 将用户页面从 github.com 移动到 github.io 时,也遇到了类似的问题。

James Bennet 在他的精彩博文 Let’s talk about usernames 中详细描述了这些问题。James 描述了在他的 django-registration 应用程序中执行的验证类型。

与 Amazon Cognito 集成

好的,现在您应该对这些问题已经有了更多的了解,那么我该如何利用 Amazon Cognito 来解决这些问题呢?

我的运气还算不错,因为现在我可以通过 Amazon Cognito 使用 AWS Lambda 触发器自定义大部分身份验证工作流程

在添加用户名或电子邮件验证时,我可以植入预先注册的 Lambda 触发器,然后通过这个触发器执行自定义验证,以及接受或拒绝注册请求。

需要注意的是,我并不能对请求作出修改。要执行任何类型的大小写或名称标准化(例如,强制使用小写),都必须在客户端上进行。我只能验证该过程是在 Lambda 函数中完成的。如果以后可以修改请求,那么将会非常方便。

要声明注册无效,我需要做的就是从 Lambda 函数返回一个错误。在 Python 中,这就和抛出异常一样简单。如果验证通过,则只返回事件,该事件中已经包含了通用成功响应所需的字段。我还可以选择自动验证部分字段。

要强制前端发送标准化的小写用户名,我只需要使用以下代码:

def run(event, context):
  user = event[‘userName’]
  if not user.isLower():
    raise Exception(“Username must be lowercase”)
  return event

添加唯一的约束和保留

我已经从 django-registration 中提取了验证验证结果,并将其输入到名为 username_validator 的 Python 模块中,以使 Lambda 能够更轻松地执行这些类型的唯一性验证:

pip install username-validator

除了检测容易混淆的同形符号外,其还包含一组标准的保留名称,如“www”、“admin”、“root”、“security”和“robots.txt”等等。您可以针对特定应用程序提供额外的保留项,也可以执行单独的检查。

为了添加额外的验证和自定义保留项,我将函数进行了如下更新:

from username_validator import UsernameValidator

MY_RESERVED = [
  "larry",
  "aws",
  "reinvent"
]

validator = UsernameValidator(additional_names=MY_RESERVED)

def run(event, context):
  user = event['userName']

  if not user.islower():
    raise Exception("Username must be lowercase")

  validator.validate_all(user)

  return event

现在,如果我将 Lambda 函数挂载到 Amazon Cognito 用户池作为预先注册的触发器,然后尝试注册“aws”,将会看到 400 错误页面。我还有一些可以包含在注册表单中的文本:其他属性(包括电子邮件(如果使用过))也将可以在事件 [‘request’][‘userAttributes’]} 下使用。例如:

{ "request": {
    "userAttributes": {"name": "larry", "email": "larry@example.com" }
  }
}

那么接下来呢?

我可以用同样的方法验证其他属性。或者,我可以添加其他自定义验证,方法则是添加其他检查并引发异常,如果失败,则会显示自定义消息。

在本文中,我讨论了考虑身份和唯一性的重要性,并演示了如何在 Amazon Cognito 中针对用户注册添加额外的验证。

现在您应该了解了如何使用自定义 Lambda 函数控制注册验证。建议您查看其他用户的池工作流程自定义,这些自定义可以通过 Lambda 触发器实现。