使用推送订阅

Pub/Sub 支持推送消息传送和拉取消息传送。如需大致了解拉取订阅和推送订阅以及两者的对比情况,请参阅订阅者概览。本文档介绍了推送传送。如需了解有关拉取传送的讨论,请参阅拉取订阅者指南

如果订阅使用推送传送,Pub/Sub 服务会将消息传送到推送端点。推送端点必须是可公开访问的 HTTPS 地址。推送端点的服务器必须具有由证书授权机构签署的有效 SSL 证书。

Pub/Sub 服务会将同一 Google Cloud 地区中的消息传送至 Pub/Sub 服务存储消息所在的推送端点。Pub/Sub 服务会尽最大努力传送来自同一 Google Cloud 地区的消息。

此外,推送订阅可以配置为提供授权标头,以允许端点对请求进行身份验证。自动身份验证和授权机制可用于托管在与订阅相同的项目中的 App Engine 标准和 Cloud Functions 端点。

接收消息

当 Pub/Sub 将消息传送到推送端点时,Pub/Sub 会在 POST 请求的正文中发送消息。请求的正文是 JSON 对象,消息数据位于 message.data 字段中。消息数据采用 base64 编码。

以下示例展示了向推送端点发出的 POST 请求的正文:

{
    "message": {
        "attributes": {
            "key": "value"
        },
        "data": "SGVsbG8gQ2xvdWQgUHViL1N1YiEgSGVyZSBpcyBteSBtZXNzYWdlIQ==",
        "messageId": "136969346945"
    },
   "subscription": "projects/myproject/subscriptions/mysubscription"
}

如需接收来自推送订阅的消息,请使用网络钩子并处理 Pub/Sub 发送到推送端点的 POST 请求。例如,以下 Cloud Functions 函数是用于对消息数据进行解码并输出的网络钩子:

Go


// Package helloworld provides a set of Cloud Functions samples.
package helloworld

import (
	"context"
	"log"
)

// PubSubMessage is the payload of a Pub/Sub event.
type PubSubMessage struct {
	Data []byte `json:"data"`
}

// HelloPubSub consumes a Pub/Sub message.
func HelloPubSub(ctx context.Context, m PubSubMessage) error {
	name := string(m.Data) // Automatically decoded from base64.
	if name == "" {
		name = "World"
	}
	log.Printf("Hello, %s!", name)
	return nil
}

Java


import com.google.cloud.functions.BackgroundFunction;
import com.google.cloud.functions.Context;
import functions.eventpojos.PubSubMessage;
import java.nio.charset.StandardCharsets;
import java.util.Base64;
import java.util.logging.Level;
import java.util.logging.Logger;

public class HelloPubSub implements BackgroundFunction<PubSubMessage> {
  private static final Logger logger = Logger.getLogger(HelloPubSub.class.getName());

  @Override
  public void accept(PubSubMessage message, Context context) {
    String name = "world";
    if (message != null && message.getData() != null) {
      name = new String(
          Base64.getDecoder().decode(message.getData().getBytes(StandardCharsets.UTF_8)),
          StandardCharsets.UTF_8);
    }
    logger.info(String.format("Hello %s!", name));
    return;
  }
}

Node.js

/**
 * Background Cloud Function to be triggered by Pub/Sub.
 * This function is exported by index.js, and executed when
 * the trigger topic receives a message.
 *
 * @param {object} message The Pub/Sub message.
 * @param {object} context The event metadata.
 */
exports.helloPubSub = (message, context) => {
  const name = message.data
    ? Buffer.from(message.data, 'base64').toString()
    : 'World';

  console.log(`Hello, ${name}!`);
};

Python

def hello_pubsub(event, context):
    """Background Cloud Function to be triggered by Pub/Sub.
    Args:
         event (dict):  The dictionary with data specific to this type of
         event. The `data` field contains the PubsubMessage message. The
         `attributes` field will contain custom attributes if there are any.
         context (google.cloud.functions.Context): The Cloud Functions event
         metadata. The `event_id` field contains the Pub/Sub message ID. The
         `timestamp` field contains the publish time.
    """
    import base64

    print("""This Function was triggered by messageId {} published at {}
    """.format(context.event_id, context.timestamp))

    if 'data' in event:
        name = base64.b64decode(event['data']).decode('utf-8')
    else:
        name = 'World'
    print('Hello {}!'.format(name))

收到推送请求后,返回 HTTP 状态代码。要确认该消息,请返回以下状态代码之一:

  • 102
  • 200
  • 201
  • 202
  • 204

要发送此消息的否定确认,请返回其他任何状态代码。如果您发送否定确认或确认时限到期,则 Pub/Sub 会重新发送该消息。您无法修改从推送订阅收到的个别消息的确认时限。

身份验证和授权

如果推送订阅使用身份验证,则 Pub/Sub 服务会签署 JSON Web 令牌(JWT)并在推送请求的授权标头中发送 JWT。JWT 包含声明和签名。

订阅者可以解码 JWT并验证以下内容:

  • 声明准确无误。
  • Pub/Sub 服务签署了声明。

如果订阅者使用防火墙,则无法接收推送请求。要接收推送请求,您必须关闭防火墙并验证 JWT。

JWT 格式

JWT 是一个 OpenIDConnect JWT,包含标头、声明集和签名三个部分。Pub/Sub 服务将 JWT 编码为具有英文句点分隔符的 base64 字符串。

例如,以下授权标头包含已编码的 JWT:

"Authorization" : "Bearer
eyJhbGciOiJSUzI1NiIsImtpZCI6IjdkNjgwZDhjNzBkNDRlOTQ3MTMzY2JkNDk5ZWJjMWE2MWMzZDVh
YmMiLCJ0eXAiOiJKV1QifQ.eyJhdWQiOiJodHRwczovL2V4YW1wbGUuY29tIiwiYXpwIjoiMTEzNzc0M
jY0NDYzMDM4MzIxOTY0IiwiZW1haWwiOiJnYWUtZ2NwQGFwcHNwb3QuZ3NlcnZpY2VhY2NvdW50LmNvb
SIsImVtYWlsX3ZlcmlmaWVkIjp0cnVlLCJleHAiOjE1NTAxODU5MzUsImlhdCI6MTU1MDE4MjMzNSwia
XNzIjoiaHR0cHM6Ly9hY2NvdW50cy5nb29nbGUuY29tIiwic3ViIjoiMTEzNzc0MjY0NDYzMDM4MzIxO
TY0In0.QVjyqpmadTyDZmlX2u3jWd1kJ68YkdwsRZDo-QxSPbxjug4ucLBwAs2QePrcgZ6hhkvdc4UHY
4YF3fz9g7XHULNVIzX5xh02qXEH8dK6PgGndIWcZQzjSYfgO-q-R2oo2hNM5HBBsQN4ARtGK_acG-NGG
WM3CQfahbEjZPAJe_B8M7HfIu_G5jOLZCw2EUcGo8BvEwGcLWB2WqEgRM0-xt5-UPzoa3-FpSPG7DHk7
z9zRUeq6eB__ldb-2o4RciJmjVwHgnYqn3VvlX9oVKEgXpNFhKuYA-mWh5o7BCwhujSMmFoBOh6mbIXF
cyf5UiVqKjpqEbqPGo_AvKvIQ9VTQ" 

标头和声明集是 JSON 字符串。解码后,它们将采用以下格式:

{"alg":"RS256","kid":"7d680d8c70d44e947133cbd499ebc1a61c3d5abc","typ":"JWT"}

{
   "aud":"https://example.com",
   "azp":"113774264463038321964",
   "email":"gae-gcp@appspot.gserviceaccount.com",
   "sub":"113774264463038321964",
   "email_verified":true,
   "exp":1550185935,
   "iat":1550182335,
   "iss":"https://accounts.google.com"
  }

附加到发送到推送端点的请求的令牌可具有最长达一个小时的生命周期。

为推送身份验证设置 Pub/Sub

订阅的身份验证配置包含以下两个参数:

  • 服务帐号:与推送订阅关联的 GCP 服务帐号。 推送请求使用此服务帐号的身份。 例如,它可以是角色为 roles/run.invoker 且绑定到特定 Cloud Run(全托管式)服务的服务帐号;使用此服务帐号配置的推送订阅可以调用该 Cloud Run(全托管式)服务。
  • 令牌受众群体(可选):一个不区分大小写的字符串,可供 Webhook 用于验证此特定令牌的目标受众群体。

除了配置以上字段之外,您还必须授予 Pub/Sub 为您的服务帐号创建令牌所需的权限。Pub/Sub 会为您的项目创建并保留一个特殊服务帐号:service-PROJECT_NUMBER@gcp-sa-pubsub.iam.gserviceaccount.com.。此服务帐号需要 Service Account Token Creator 角色。如果您使用 Cloud Console 为推送身份验证设置订阅,则系统会自动授予该角色。否则,您必须向此帐号明确授予该角色。

命令行

# grant Cloud Pub/Sub the permission to create tokens
PUBSUB_SERVICE_ACCOUNT="service-${PROJECT_NUMBER}@gcp-sa-pubsub.iam.gserviceaccount.com"
gcloud projects add-iam-policy-binding ${PROJECT_ID} \
 --member="serviceAccount:${PUBSUB_SERVICE_ACCOUNT}"\
 --role='roles/iam.serviceAccountTokenCreator'

# configure the subscription push identity
gcloud pubsub subscriptions (create|update|modify-push-config) ${SUBSCRIPTION} \
 --topic=${TOPIC} \
 --push-endpoint=${PUSH_ENDPOINT_URI} \
 --push-auth-service-account=${SERVICE_ACCOUNT_EMAIL} \
 --push-auth-token-audience=${OPTIONAL_AUDIENCE_OVERRIDE}

控制台

  1. 转到 Pub/Sub 主题页面。

    转到“主题”页面

  2. 点击主题名称。

  3. 创建或更新订阅。

  4. 输入身份和(可选)受众群体。

使用推送端点执行身份验证和授权

声明

JWT 可用于验证由 Google 签名的声明(包括 emailaud 声明)。如需详细了解 Google 的 OAuth 2.0 API 如何用于身份验证和授权,请参阅 OpenID Connect

可通过两种机制使声明变得有意义。首先,Pub/Sub 要求,用于将服务帐号身份与推送订阅关联的用户或服务帐号具有项目或服务帐号的 Service Account User 角色(roles/iam.serviceAccountUser)。

其次,严格控制对用于为令牌签名的证书的访问。要创建令牌,Pub/Sub 必须使用单独的签名服务帐号身份调用内部 Google 服务。该签名服务帐号必须获得授权才能为已声明的服务帐号或此帐号所属的项目创建令牌。 您可以使用 iam.serviceAccounts.getOpenIdToken 权限或 Service Account Token Creator 角色(roles/iam.serviceAccountTokenCreator)执行此操作。

此角色或权限可以授予任何帐号。但是,您可以使用 IAM 服务来确保 Pub/Sub 签名帐号具有此权限。具体来说,Pub/Sub 使用如下所示的服务帐号:

service-{project_number}@gcp-sa-pubsub.iam.gserviceaccount.com
  • {project_number}:订阅所属的 GCP 项目。
  • gcp-sa-pubsub:签名服务帐号所属的 Google 拥有的项目。

验证令牌

以下示例展示了如何对发送到 App Engine 应用的推送请求进行身份验证。

协议

请求:

GET https://oauth2.googleapis.com/tokeninfo?id_token={BEARER_TOKEN}

响应:

200 OK
{
    "alg": "RS256",
    "aud": "example.com",
    "azp": "104176025330667568672",
    "email": "{SERVICE_ACCOUNT_NAME}@{YOUR_PROJECT_NAME}.iam.gserviceaccount.com",
    "email_verified": "true",
    "exp": "1555463097",
    "iat": "1555459497",
    "iss": "https://accounts.google.com",
    "kid": "3782d3f0bc89008d9d2c01730f765cfb19d3b70e",
    "sub": "104176025330667568672",
    "typ": "JWT"
}

Java

@WebServlet(value = "/pubsub/authenticated-push")
public class PubSubAuthenticatedPush extends HttpServlet {
  private final String pubsubVerificationToken = System.getenv("PUBSUB_VERIFICATION_TOKEN");
  private final MessageRepository messageRepository;
  private final GoogleIdTokenVerifier verifier =
      new GoogleIdTokenVerifier.Builder(new NetHttpTransport(), new JacksonFactory())
          /**
           * Please change example.com to match with value you are providing while creating
           * subscription as provided in @see <a
           * href="https://github.com/GoogleCloudPlatform/java-docs-samples/tree/master/appengine-java8/pubsub">README</a>.
           */
          .setAudience(Collections.singletonList("example.com"))
          .build();
  private final Gson gson = new Gson();
  private final JsonParser jsonParser = new JsonParser();

  @Override
  public void doPost(HttpServletRequest req, HttpServletResponse resp)
      throws IOException, ServletException {

    // Verify that the request originates from the application.
    if (req.getParameter("token").compareTo(pubsubVerificationToken) != 0) {
      resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
      return;
    }
    // Get the Cloud Pub/Sub-generated JWT in the "Authorization" header.
    String authorizationHeader = req.getHeader("Authorization");
    if (authorizationHeader == null
        || authorizationHeader.isEmpty()
        || authorizationHeader.split(" ").length != 2) {
      resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
      return;
    }
    String authorization = authorizationHeader.split(" ")[1];

    try {
      // Verify and decode the JWT.
      // Note: For high volume push requests, it would save some network overhead
      // if you verify the tokens offline by decoding them using Google's Public
      // Cert; caching already seen tokens works best when a large volume of
      // messsages have prompted a singple push server to handle them, in which
      // case they would all share the same token for a limited time window.
      GoogleIdToken idToken = verifier.verify(authorization);
      messageRepository.saveToken(authorization);
      messageRepository.saveClaim(idToken.getPayload().toPrettyString());
      // parse message object from "message" field in the request body json
      // decode message data from base64
      Message message = getMessage(req);
      messageRepository.save(message);
      // 200, 201, 204, 102 status codes are interpreted as success by the Pub/Sub system
      resp.setStatus(102);
      super.doPost(req, resp);
    } catch (Exception e) {
      resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
    }
  }

  private Message getMessage(HttpServletRequest request) throws IOException {
    String requestBody = request.getReader().lines().collect(Collectors.joining("\n"));
    JsonElement jsonRoot = jsonParser.parse(requestBody);
    String messageStr = jsonRoot.getAsJsonObject().get("message").toString();
    Message message = gson.fromJson(messageStr, Message.class);
    // decode from base64
    String decoded = decode(message.getData());
    message.setData(decoded);
    return message;
  }

  private String decode(String data) {
    return new String(Base64.getDecoder().decode(data));
  }

  PubSubAuthenticatedPush(MessageRepository messageRepository) {
    this.messageRepository = messageRepository;
  }

  public PubSubAuthenticatedPush() {
    this(MessageRepositoryImpl.getInstance());
  }
}

Node.js

app.post('/pubsub/authenticated-push', jsonBodyParser, async (req, res) => {
  // Verify that the request originates from the application.
  if (req.query.token !== PUBSUB_VERIFICATION_TOKEN) {
    res.status(400).send('Invalid request');
    return;
  }

  // Verify that the push request originates from Cloud Pub/Sub.
  try {
    // Get the Cloud Pub/Sub-generated JWT in the "Authorization" header.
    const bearer = req.header('Authorization');
    const [, token] = bearer.match(/Bearer (.*)/);
    tokens.push(token);

    // Verify and decode the JWT.
    // Note: For high volume push requests, it would save some network
    // overhead if you verify the tokens offline by decoding them using
    // Google's Public Cert; caching already seen tokens works best when
    // a large volume of messages have prompted a single push server to
    // handle them, in which case they would all share the same token for
    // a limited time window.
    const ticket = await authClient.verifyIdToken({
      idToken: token,
      audience: 'example.com',
    });

    const claim = ticket.getPayload();
    claims.push(claim);
  } catch (e) {
    res.status(400).send('Invalid token');
    return;
  }

  // The message is a unicode string encoded in base64.
  const message = Buffer.from(req.body.message.data, 'base64').toString(
    'utf-8'
  );

  messages.push(message);

  res.status(200).send();
});

Python

@app.route('/push-handlers/receive_messages', methods=['POST'])
def receive_messages_handler():
    # Verify that the request originates from the application.
    if (request.args.get('token', '') !=
            current_app.config['PUBSUB_VERIFICATION_TOKEN']):
        return 'Invalid request', 400

    # Verify that the push request originates from Cloud Pub/Sub.
    try:
        # Get the Cloud Pub/Sub-generated JWT in the "Authorization" header.
        bearer_token = request.headers.get('Authorization')
        token = bearer_token.split(' ')[1]
        TOKENS.append(token)

        # Verify and decode the JWT. `verify_oauth2_token` verifies
        # the JWT signature, the `aud` claim, and the `exp` claim.
        # Note: For high volume push requests, it would save some network
        # overhead if you verify the tokens offline by downloading Google's
        # Public Cert and decode them using the `google.auth.jwt` module;
        # caching already seen tokens works best when a large volume of
        # messages have prompted a single push server to handle them, in which
        # case they would all share the same token for a limited time window.
        claim = id_token.verify_oauth2_token(token, requests.Request(),
                                             audience='example.com')
        CLAIMS.append(claim)
    except Exception as e:
        return 'Invalid token: {}\n'.format(e), 400

    envelope = json.loads(request.data.decode('utf-8'))
    payload = base64.b64decode(envelope['message']['data'])
    MESSAGES.append(payload)
    # Returning any 2xx status indicates successful receipt of the message.
    return 'OK', 200

Go

// receiveMessagesHandler validates authentication token and caches the Pub/Sub
// message received.
func (a *app) receiveMessagesHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != "POST" {
		http.Error(w, http.StatusText(http.StatusMethodNotAllowed), http.StatusMethodNotAllowed)
		return
	}

	// Verify that the request originates from the application.
	// a.pubsubVerificationToken = os.Getenv("PUBSUB_VERIFICATION_TOKEN")
	if token, ok := r.URL.Query()["token"]; !ok || len(token) != 1 || token[0] != a.pubsubVerificationToken {
		http.Error(w, "Bad token", http.StatusBadRequest)
		return
	}

	// Get the Cloud Pub/Sub-generated JWT in the "Authorization" header.
	authHeader := r.Header.Get("Authorization")
	if authHeader == "" || len(strings.Split(authHeader, " ")) != 2 {
		http.Error(w, "Missing Authorization header", http.StatusBadRequest)
		return
	}
	token := strings.Split(authHeader, " ")[1]
	// Verify and decode the JWT.
	// If you don't need to control the HTTP client used you can use the
	// convenience method idtoken.Validate instead of creating a Validator.
	v, err := idtoken.NewValidator(r.Context(), option.WithHTTPClient(a.defaultHTTPClient))
	if err != nil {
		http.Error(w, "Unable to create Validator", http.StatusBadRequest)
		return
	}
	// Please change http://example.com to match with the value you are
	// providing while creating the subscription.
	payload, err := v.Validate(r.Context(), token, "http://example.com")
	if err != nil {
		http.Error(w, fmt.Sprintf("Invalid Token: %v", err), http.StatusBadRequest)
		return
	}
	if payload.Issuer != "accounts.google.com" && payload.Issuer != "https://accounts.google.com" {
		http.Error(w, "Wrong Issuer", http.StatusBadRequest)
	}

	var pr pushRequest
	if err := json.NewDecoder(r.Body).Decode(&pr); err != nil {
		http.Error(w, fmt.Sprintf("Could not decode body: %v", err), http.StatusBadRequest)
		return
	}

	a.messagesMu.Lock()
	defer a.messagesMu.Unlock()
	// Limit to ten.
	a.messages = append(a.messages, pr.Message.Data)
	if len(a.messages) > maxMessages {
		a.messages = a.messages[len(a.messages)-maxMessages:]
	}

	fmt.Fprint(w, "OK")
}

您可以在面向网站的 Google 登录指南中找到其他有关如何验证不记名 JWT 的示例。如需详细了解 OpenID 令牌,请参阅 OpenID Connect 指南

Cloud Run 和 App Engine

Cloud Run 和 App Engine 通过验证 Pub/Sub 生成的令牌来自动对 HTTP 调用进行身份验证。用户所需的唯一配置是向调用者帐号授予必要的 IAM 角色。例如,您可以授予或撤消用于为帐号调用特定 Cloud Run 端点的权限。如需了解详情,请参阅以下教程:

停止和恢复传送

要暂时阻止 Pub/Sub 将请求发送到推送端点,请将订阅更改为拉取。请注意,此转换可能需要几分钟才能生效。

要恢复推送传送,请再次将网址设置为有效端点。要永久停止传送,请删除订阅

配额、限制和传送速率

请注意,推送订阅受一组配额资源限制的约束。

推送退避算法

如果推送订阅者发送否定确认,Pub/Sub 可能会使用推送退避算法传送消息。当 Pub/Sub 使用推送退避算法时,它会停止传送 100 毫秒到 60 秒的消息,然后再次开始传送消息。

推送退避算法是一种指数退避算法,可防止推送订阅者接收无法处理的消息。Pub/Sub 停止传送消息的时间取决于推送订阅者发送的否定确认条数。

例如,如果推送订阅者每秒接收 5 条消息,并且每秒发送一条否定确认,则 Pub/Sub 大约每 500 毫秒传送一次消息。如果推送订阅者每秒发送 5 条否定确认,Pub/Sub 会每隔 30-60 秒传送一次消息。

传送速率

Pub/Sub 使用慢启动算法来调整并发推送请求的数量。推送窗口是允许的最大并发推送请求数。 推送窗口在传送成功时会增大,在传送失败时会减小。系统会从一个小窗口开始:3 x N,其中 N 是发布地区的数量。

当订阅者确认消息时,窗口大小以指数方式增加,最多为 3000 x N 条未完成消息。对于订阅者确认的消息超过 99% 且平均推送请求延迟时间少于一秒的订阅,推送窗口大小最多会增加到 30000 x 未完成的消息。

推送请求延迟时间包含以下内容:

接收 3000 多条未完成消息后,窗口大小会以线性方式增加,以防止推送端点接收过多消息。如果平均延迟时间超过一秒,或订阅者确认的请求少于 99%,则窗口会减少到 3000 条未完成消息的下限。

如需详细了解可用于监控推送传送的指标,请参阅监控推送订阅