前后端分离用firebase实现手机号认证功能经过实践,这

做项目的时候经常会遇到需要发短信验证手机号的功能,大部分都是直接在前端集成相应的 SDK 去实现的。但不可避免的是,有时候我们需要在后端提供发短信及短信验证码验证的功能,比如有 web,小程序,native App 都需要复用这个功能。

经过调研,目前国外常见的几个支持后端认证的 provider 有以下几个:

经常简单的调研,我们发现 Facebook Account Kit 必须使用他们的 UI 来发送验证码,不适用于我们项目的场景。Twilio 倒是可以用,不过我们最后还是选了 Firebase,主要是我们本来就用的 Google Cloud Platform, 使用 firebase 就很方便,而且它的免费计划支持每个月 10,000 次电话验证,非常适合我们的需求。

image.png
图片来源

不过坑又来了,仔细看了 firebase 的官方文档,我们发现几乎找不到在后端实现它的方式,文档里写的都是在前端用 SDK 的形式集成的。又经过一番折腾,才终于找到了我们能用来发短信和验证的 Google 服务 identify platform。

发送短信

首先,我们需要发短信,可以调用下面的 Google API:

curl -X POST \
'https://www.googleapis.com/identitytoolkit/v3/relyingparty/sendVerificationCode?key=api_key' \
-H 'content-type: application/json' \
-d '{
"phoneNumber": "phone_number",
"recaptchaToken": "recaptcha_token"
}'
复制代码

如果访问成功,会得到如下的结果:

{
"sessionInfo": "session_info_token"
}
复制代码

用 Node 简单实现一个 API:

const FIREBASE_API_KEY = 'AIzaSyBi0MBSiB88xCvbti1T8plNTreX-bzZfAw'
const baseUrl = 'https://www.googleapis.com/identitytoolkit/v3/relyingparty/'
router.post('/otp', async (req: Request, res: Response) => {
  const { recaptchaToken, phoneNumber } = req.body
  const response = await axios.post(
    `${baseUrl}sendVerificationCode?key=${FIREBASE_API_KEY}`,
    {
      phoneNumber,
      recaptchaToken,
    },
  )
  return res.json({
    sessionInfo: response.data.sessionInfo,
  })
})
复制代码

后面验证的时候需要用到这个 sessionInfo,我们可以保存在后端的数据库里。

验证 OTP

接下来收到短信后,就可以调用另一个 Google API 是用来验证 OTP(One Time Pin)了:

curl -X POST \
'https://www.googleapis.com/identitytoolkit/v3/relyingparty/verifyPhoneNumber?key=api_key' \
-H 'content-type: application/json' \
-d '{
"sessionInfo": "sessionInfo",
"code":"otp_code"
}'
复制代码

成功后得到的 response 如下:

{
    "idToken": "idToken",
    "refreshToken": "refreshToken",
    "expiresIn": "3600",
    "localId": "localId",
    "isNewUser": false,
    "phoneNumber": "phone_number"
}
复制代码

示例代码如下:

router.post('/verification', async (req: Request, res: Response) => {
  const { code, sessionInfo } = req.body
  const response = await axios.post(
    `${baseUrl}verifyPhoneNumber?key=${FIREBASE_API_KEY}`,
    {
      sessionInfo,
      code,
    },
  )
  return res.json(response.data)
})
复制代码

刷新 Token

另外一个很常用的功能就是自动刷新 token,也有相应的 Google API 可以使用。

curl -X POST \
'https://securetoken.googleapis.com/v1/token?key=api_key' \
-H 'content-type: application/json' \
-d '{
"grant_type": "refresh_token",
"refresh_token": "refresh_token (验证 OTP 后得到的刷新 token)"
}'
复制代码

这样前端就可以在 idToken 过期后调用刷新 token 的 API 来获得新的 idToken 而不用重新输入验证码。

示例代码如下:

router.post('/token', async (req: Request, res: Response) => {
  const { refreshToken } = req.body
  const response = await axios.post(
    `https://securetoken.googleapis.com/v1/token?key=${FIREBASE_API_KEY}`,
    {
      grant_type: 'refresh_token',
      refresh_token: refreshToken,
    },
  )
  return res.json(response.data)
})
复制代码

经过实践,这个方案是可行的,不过有以下的一些坑:

  1. 发短信需要用 reCAPTCHA token,这需要在前端生成。虽然 reCAPTCHA 在前端可以设成 invisible 模式,但是如果检测到可疑活动 (开发和测试人员在测试的时候不断请求就可能触发),隐形的 reCAPTCHA 有时会触发挑战 (Full challenge),页面上会出现下面的 UI:

image.png

  1. 发出的短信的格式是固定的,不能改。
xxxxxx is your verification code for domain_name.
复制代码

其中 domain_name 是可以在 firebase 控制台配置的。如果这个格式不能满足需求的话,就只能换其他 provider 了。