Google is committed to advancing racial equity for Black communities. See how.
本頁面由 Cloud Translation API 翻譯而成。
Switch to English

管理用戶會話

Firebase身份驗證會話的壽命很長。每次用戶登錄時,用戶憑據都會發送到Firebase身份驗證後端,並交換Firebase ID令牌(JWT)和刷新令牌。 Firebase ID令牌的壽命很短,並且持續一個小時;刷新令牌可用於檢索新的ID令牌。僅當下列情況之一發生時,刷新令牌才會過期:

  • 用戶被刪除
  • 用戶被禁用
  • 檢測到該用戶的主要帳戶更改。這包括密碼或電子郵件地址更新之類的事件。

Firebase Admin SDK提供了撤消指定用戶的刷新令牌的功能。此外,還提供了用於檢查ID令牌吊銷的API。使用這些功能,您可以更好地控制用戶會話。 SDK提供了添加限制以防止會話在可疑情況下使用的功能,以及從潛在的令牌被盜中恢復的機制。

撤消刷新令牌

當用戶報告設備丟失或被盜時,您可能會撤消用戶現有的刷新令牌。同樣,如果發現一般漏洞或懷疑活動令牌發生大規模洩漏,則可以使用listUsers API查找所有用戶並吊銷指定項目的令牌。

密碼重置也會撤消用戶現有的令牌;但是,在這種情況下,Firebase身份驗證後端會自動處理撤銷。吊銷後,用戶將註銷並提示重新認證。

這是使用Admin SDK撤消給定用戶的刷新令牌的示例實現。要初始化Admin SDK,請按照設置頁面上的說明進行操作。

Node.js

// Revoke all refresh tokens for a specified user for whatever reason.
// Retrieve the timestamp of the revocation, in seconds since the epoch.
admin.auth().revokeRefreshTokens(uid)
  .then(() => {
    return admin.auth().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);

蟒蛇

# 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進行檢查,從而避免發出此網絡請求。

在數據庫規則中檢測ID令牌吊銷

為了能夠使用數據庫規則檢測ID令牌吊銷,我們必須首先存儲一些用戶特定的元數據。

在Firebase實時數據庫中更新用戶特定的元數據。

保存刷新令牌吊銷時間戳。這是通過Firebase規則跟踪ID令牌吊銷所必需的。這允許在數據庫內進行有效檢查。在下面的代碼示例中,使用上一節中獲得的uid和吊銷時間。

Node.js

const metadataRef = admin.database().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);

蟒蛇

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

將檢查添加到數據庫規則

要執行此檢查,請設置一個沒有客戶端寫訪問權限的規則來存儲每個用戶的撤銷時間。可以使用上一個撤銷時間的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": "$user_id === auth.uid && auth.token.auth_time > (root.child('metadata').child(auth.uid).child('revokeTime').val() || 0)",
        ".write": "$user_id === auth.uid && auth.token.auth_time > (root.child('metadata').child(auth.uid).child('revokeTime').val() || 0)"
      }
    }
  }
}

在SDK中檢測ID令牌吊銷。

在您的服務器中,實現以下邏輯以進行刷新令牌吊銷和ID令牌驗證:

當要驗證用戶的ID令牌時,必須將額外的checkRevoked布爾標誌傳遞給verifyIdToken 。如果吊銷了用戶的令牌,則應在客戶端上註銷該用戶,或要求用戶使用Firebase身份驗證客戶端SDK提供的重新身份驗證API進行重新身份驗證。

要為您的平台初始化Admin SDK,請按照設置頁面上的說明進行操作。 verifyIdToken部分中提供了檢索ID令牌的verifyIdToken

Node.js

第368章

爪哇

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.
  }
}

蟒蛇

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.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!' })
      });
    }
  });
});