获取我们在 Firebase 峰会上发布的所有信息,了解 Firebase 可如何帮助您加快应用开发速度并满怀信心地运行应用。了解详情

管理用户会话

使用集合让一切井井有条 根据您的偏好保存内容并对其进行分类。

Firebase 身份验证会话是长期存在的。每次用户登录时,用户凭据都会发送到 Firebase 身份验证后端并交换 Firebase ID 令牌 (JWT) 和刷新令牌。 Firebase ID 令牌是短暂的,持续一个小时;刷新令牌可用于检索新的 ID 令牌。刷新令牌仅在发生以下情况之一时过期:

  • 用户被删除
  • 用户被禁用
  • 检测到用户的主要帐户更改。这包括密码或电子邮件地址更新等事件。

Firebase Admin SDK 提供了撤销指定用户的刷新令牌的能力。此外,还提供了用于检查 ID 令牌撤销的 API。借助这些功能,您可以更好地控制用户会话。 SDK 提供了添加限制以防止在可疑情况下使用会话的能力,以及从潜在的令牌盗窃中恢复的机制。

撤销刷新令牌

当用户报告设备丢失或被盗时,您可以撤销用户现有的刷新令牌。同样,如果您发现一般漏洞或怀疑活动令牌大规模泄漏,您可以使用listUsers API 查找所有用户并撤销指定项目的令牌。

密码重置也会撤销用户现有的令牌;但是,在这种情况下,Firebase 身份验证后端会自动处理撤销。撤销时,用户将退出并提示重新进行身份验证。

这是一个使用 Admin SDK 撤销给定用户的刷新令牌的示例实现。要初始化 Admin SDK,请按照设置页面上的说明进行操作。

节点.js

// Revoke all refresh tokens for a specified user for whatever reason.
// Retrieve the timestamp of the revocation, in seconds since the epoch.
getAuth()
  .revokeRefreshTokens(uid)
  .then(() => {
    return getAuth().getUser(uid);
  })
  .then((userRecord) => {
    return new Date(userRecord.tokensValidAfterTime).getTime() / 1000;
  })
  .then((timestamp) => {
    console.log(`Tokens revoked at: ${timestamp}`);
  });

爪哇

FirebaseAuth.getInstance().revokeRefreshTokens(uid);
UserRecord user = FirebaseAuth.getInstance().getUser(uid);
// Convert to seconds as the auth_time in the token claims is in seconds too.
long revocationSecond = user.getTokensValidAfterTimestamp() / 1000;
System.out.println("Tokens revoked at: " + revocationSecond);

Python

# Revoke tokens on the backend.
auth.revoke_refresh_tokens(uid)
user = auth.get_user(uid)
# Convert to seconds as the auth_time in the token claims is in seconds.
revocation_second = user.tokens_valid_after_timestamp / 1000
print('Tokens revoked at: {0}'.format(revocation_second))

client, err := app.Auth(ctx)
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}
if err := client.RevokeRefreshTokens(ctx, uid); err != nil {
	log.Fatalf("error revoking tokens for user: %v, %v\n", uid, err)
}
// accessing the user's TokenValidAfter
u, err := client.GetUser(ctx, uid)
if err != nil {
	log.Fatalf("error getting user %s: %v\n", uid, err)
}
timestamp := u.TokensValidAfterMillis / 1000
log.Printf("the refresh tokens were revoked at: %d (UTC seconds) ", timestamp)

C#

await FirebaseAuth.DefaultInstance.RevokeRefreshTokensAsync(uid);
var user = await FirebaseAuth.DefaultInstance.GetUserAsync(uid);
Console.WriteLine("Tokens revoked at: " + user.TokensValidAfterTimestamp);

检测 ID 令牌撤销

由于 Firebase ID 令牌是无状态的 JWT,因此您只能通过从 Firebase 身份验证后端请求令牌的状态来确定令牌已被撤销。因此,在您的服务器上执行此检查是一项昂贵的操作,需要额外的网络往返。您可以通过设置检查撤销的 Firebase 安全规则来避免发出此网络请求,而不是使用 Admin SDK 进行检查。

在 Firebase 安全规则中检测 ID 令牌吊销

为了能够使用安全规则检测 ID 令牌撤销,我们必须首先存储一些用户特定的元数据。

更新 Firebase 实时数据库中用户特定的元数据。

保存刷新令牌撤销时间戳。这是通过 Firebase 安全规则跟踪 ID 令牌撤销所必需的。这允许在数据库中进行有效检查。在下面的代码示例中,使用上一节中获得的 uid 和吊销时间。

节点.js

const metadataRef = getDatabase().ref('metadata/' + uid);
metadataRef.set({ revokeTime: utcRevocationTimeSecs }).then(() => {
  console.log('Database updated successfully.');
});

爪哇

DatabaseReference ref = FirebaseDatabase.getInstance().getReference("metadata/" + uid);
Map<String, Object> userData = new HashMap<>();
userData.put("revokeTime", revocationSecond);
ref.setValueAsync(userData);

Python

metadata_ref = firebase_admin.db.reference("metadata/" + uid)
metadata_ref.set({'revokeTime': revocation_second})

向 Firebase 安全规则添加检查

要强制执行此检查,请设置一个没有客户端写入权限的规则来存储每个用户的吊销时间。这可以使用上次撤销时间的 UTC 时间戳进行更新,如前面的示例所示:

{
  "rules": {
    "metadata": {
      "$user_id": {
        // this could be false as it is only accessed from backend or rules.
        ".read": "$user_id === auth.uid",
        ".write": "false",
      }
    }
  }
}

任何需要经过身份验证访问的数据都必须配置以下规则。此逻辑仅允许具有未撤销 ID 令牌的经过身份验证的用户访问受保护的数据:

{
  "rules": {
    "users": {
      "$user_id": {
        ".read": "auth != null && $user_id === auth.uid && (
            !root.child('metadata').child(auth.uid).child('revokeTime').exists()
          || auth.token.auth_time > root.child('metadata').child(auth.uid).child('revokeTime').val()
        )",
        ".write": "auth != null && $user_id === auth.uid && (
            !root.child('metadata').child(auth.uid).child('revokeTime').exists()
          || auth.token.auth_time > root.child('metadata').child(auth.uid).child('revokeTime').val()
        )",
      }
    }
  }
}

检测 SDK 中的 ID 令牌撤销。

在您的服务器中,为刷新令牌撤销和 ID 令牌验证实现以下逻辑:

当要验证用户的 ID 令牌时,必须将附加的checkRevoked布尔标志传递给verifyIdToken 。如果用户的令牌被撤销,则应在客户端上注销用户或要求用户使用 Firebase 身份验证客户端 SDK 提供的重新身份验证 API 重新进行身份验证。

要为您的平台初始化 Admin SDK,请按照设置页面上的说明进行操作。检索 ID 令牌的示例在verifyIdToken部分中。

节点.js

// Verify the ID token while checking if the token is revoked by passing
// checkRevoked true.
let checkRevoked = true;
getAuth()
  .verifyIdToken(idToken, checkRevoked)
  .then((payload) => {
    // Token is valid.
  })
  .catch((error) => {
    if (error.code == 'auth/id-token-revoked') {
      // Token has been revoked. Inform the user to reauthenticate or signOut() the user.
    } else {
      // Token is invalid.
    }
  });

爪哇

try {
  // Verify the ID token while checking if the token is revoked by passing checkRevoked
  // as true.
  boolean checkRevoked = true;
  FirebaseToken decodedToken = FirebaseAuth.getInstance()
      .verifyIdToken(idToken, checkRevoked);
  // Token is valid and not revoked.
  String uid = decodedToken.getUid();
} catch (FirebaseAuthException e) {
  if (e.getAuthErrorCode() == AuthErrorCode.REVOKED_ID_TOKEN) {
    // Token has been revoked. Inform the user to re-authenticate or signOut() the user.
  } else {
    // Token is invalid.
  }
}

Python

try:
    # Verify the ID token while checking if the token is revoked by
    # passing check_revoked=True.
    decoded_token = auth.verify_id_token(id_token, check_revoked=True)
    # Token is valid and not revoked.
    uid = decoded_token['uid']
except auth.RevokedIdTokenError:
    # Token revoked, inform the user to reauthenticate or signOut().
    pass
except auth.UserDisabledError:
    # Token belongs to a disabled user record.
    pass
except auth.InvalidIdTokenError:
    # Token is invalid
    pass

client, err := app.Auth(ctx)
if err != nil {
	log.Fatalf("error getting Auth client: %v\n", err)
}
token, err := client.VerifyIDTokenAndCheckRevoked(ctx, idToken)
if err != nil {
	if err.Error() == "ID token has been revoked" {
		// Token is revoked. Inform the user to reauthenticate or signOut() the user.
	} else {
		// Token is invalid
	}
}
log.Printf("Verified ID token: %v\n", token)

C#

try
{
    // Verify the ID token while checking if the token is revoked by passing checkRevoked
    // as true.
    bool checkRevoked = true;
    var decodedToken = await FirebaseAuth.DefaultInstance.VerifyIdTokenAsync(
        idToken, checkRevoked);
    // Token is valid and not revoked.
    string uid = decodedToken.Uid;
}
catch (FirebaseAuthException ex)
{
    if (ex.AuthErrorCode == AuthErrorCode.RevokedIdToken)
    {
        // Token has been revoked. Inform the user to re-authenticate or signOut() the user.
    }
    else
    {
        // Token is invalid.
    }
}

响应客户端上的令牌撤销

如果令牌通过 Admin SDK 被撤销,客户端会收到撤销通知,并且用户需要重新认证或退出:

function onIdTokenRevocation() {
  // For an email/password user. Prompt the user for the password again.
  let password = prompt('Please provide your password for reauthentication');
  let credential = firebase.auth.EmailAuthProvider.credential(
      firebase.auth().currentUser.email, password);
  firebase.auth().currentUser.reauthenticateWithCredential(credential)
    .then(result => {
      // User successfully reauthenticated. New ID tokens should be valid.
    })
    .catch(error => {
      // An error occurred.
    });
}

高级安全:强制 IP 地址限制

用于检测令牌盗窃的常见安全机制是跟踪请求 IP 地址的来源。例如,如果请求总是来自相同的 IP 地址(服务器发出呼叫),则可以强制执行单个 IP 地址会话。或者,如果您检测到用户的 IP 地址突然更改了地理位置,或者您收到来自可疑来源的请求,您可能会撤销用户的令牌。

要基于 IP 地址执行安全检查,请检查每个经过身份验证的请求的 ID 令牌,并检查请求的 IP 地址是否与以前的受信任 IP 地址匹配或在受信任范围内,然后才允许访问受限数据。例如:

app.post('/getRestrictedData', (req, res) => {
  // Get the ID token passed.
  const idToken = req.body.idToken;
  // Verify the ID token, check if revoked and decode its payload.
  admin.auth().verifyIdToken(idToken, true).then((claims) => {
    // Get the user's previous IP addresses, previously saved.
    return getPreviousUserIpAddresses(claims.sub);
  }).then(previousIpAddresses => {
    // Get the request IP address.
    const requestIpAddress = req.connection.remoteAddress;
    // Check if the request IP address origin is suspicious relative to previous
    // IP addresses. The current request timestamp and the auth_time of the ID
    // token can provide additional signals of abuse especially if the IP address
    // suddenly changed. If there was a sudden location change in a
    // short period of time, then it will give stronger signals of possible abuse.
    if (!isValidIpAddress(previousIpAddresses, requestIpAddress)) {
      // Invalid IP address, take action quickly and revoke all user's refresh tokens.
      revokeUserTokens(claims.uid).then(() => {
        res.status(401).send({error: 'Unauthorized access. Please login again!'});
      }, error => {
        res.status(401).send({error: 'Unauthorized access. Please login again!'});
      });
    } else {
      // Access is valid. Try to return data.
      getData(claims).then(data => {
        res.end(JSON.stringify(data);
      }, error => {
        res.status(500).send({ error: 'Server error!' })
      });
    }
  });
});