如何从 lambda 调用 IAM 授权的 AWS ApiGateway 端点?

How to invoke IAM authorized AWS ApiGateway endpoint from lambda?

我正在尝试从我使用 IAM 授权方保护的 lambda 函数调用 AWS ApiGateway HTTP 端点,但是我终生无法从我的 lambda 函数中获得任何东西。

我已经使用 Postman 测试了端点,并且可以确认当我 select“AWS 签名”作为授权类型并输入我的本地凭据时它可以工作,所以这不是问题端点已设置。这一定是我如何从 Lambda 发送请求的问题。额外的挑战是将 headers 添加到 GraphQL API 请求。

这是我的 lambda 函数的样子:

import { ApolloServer } from 'apollo-server-lambda';
import { APIGatewayProxyEvent, Callback, Context } from 'aws-lambda';
import { ApolloGateway, RemoteGraphQLDataSource } from '@apollo/gateway';
import aws4 from 'aws4';

const userServiceUrl = process.env.USER_SERVICE_URL;
const {hostname, pathname} = new URL(userServiceUrl);

class AuthenticatedDataSource extends RemoteGraphQLDataSource {
  willSendRequest({request}) {
    console.log('request is: ', request);
    const opts: Record<string, any> = {
      service: 'execute-api',
      region: process.env.AWS_REGION,
      host: hostname,
      path: pathname,
      body: JSON.stringify({query: request.query}),
      method: 'POST'
    }
    aws4.sign(opts);
    console.log('opts are: ', opts);
    request.http.headers.set('X-Amz-Date', opts.headers['X-Amz-Date']);
    request.http.headers.set('Authorization', opts.headers['Authorization']);
    request.http.headers.set('X-Amz-Security-Token', opts.headers['X-Amz-Security-Token']);
  }
}

无论我尝试什么,我总是会收到 403 禁止错误,并且请求永远不会到达授权方后面的实际端点。我试过删除 body,我试过将我的本地凭据硬编码到 aws4 调用中,none 有效。我的直觉是我的签名调用不知何故是错误的,但是当我将它与我在互联网上找到的几个例子进行比较时,我看不出有什么明显的错误。

任何可以为我指明正确方向的资源都将不胜感激。我发现的大多数示例都是特定于前端的,所以我知道这可能会误导我。

willSendRequest 函数不是签署请求的最佳位置,因为 apollo-server 可以在调用 willSendRequest 后修改请求对象。 相反,您应该实现自定义提取并将其传递给 RemoteGraphQLDataSource 构造函数,以确保在发送之前签署最终请求。

您的自定义 GraphQLDataSource 和自定义提取将是 一些东西 像这样:

import { Request, RequestInit, Response, fetch, Headers } from "apollo-server-env";
import aws4 from 'aws4';
import { RemoteGraphQLDataSource } from '@apollo/gateway';

class AuthenticatedDataSource extends RemoteGraphQLDataSource {
    public constructor(
        url: string,
    ) {
        super({
            url: url,
            fetcher: doFetch,
        });
    }

    async doFetch(
        input?: string | Request | undefined,
        init?: RequestInit | undefined
    ): Promise<Response> {
        const url = new URL(input as string);
        const opts: Record<string, any> = {
            service: 'execute-api',
            region: process.env.AWS_REGION,
            host: url.hostname,
            path: url.pathname,
            body: init?.body,
            method: init?.method
        }
        aws4.sign(opts);
        init.headers.set('X-Amz-Date', opts.headers['X-Amz-Date']);
        init.headers.set('Authorization', opts.headers['Authorization']);
        init.headers.set('X-Amz-Security-Token', opts.headers['X-Amz-Security-Token']);
        const response = await fetch(input, init);

        return response;
    }
}

为了繁荣,这就是我最终所做的并且工作完美(非常感谢 Glen Thomas!)

import { Request, RequestInit, Response, fetch } from "apollo-server-env";
import { ApolloServer } from 'apollo-server-lambda';
import { APIGatewayProxyEvent, Callback, Context } from 'aws-lambda';
import { ApolloGateway, RemoteGraphQLDataSource } from '@apollo/gateway';
import aws4 from 'aws4';

const userServiceUrl = process.env.USER_SERVICE_URL;

async function doFetch(
  input?: Request | string,
  init?: RequestInit
): Promise<Response> {
  const urlString = typeof input === 'string' ? input : input.url;
  const url = new URL(urlString);
  const opts: Record<string, any> = {
    service: 'execute-api',
    region: process.env.AWS_REGION,
    host: url.hostname,
    path: url.pathname,
    body: init?.body,
    method: init?.method
  }
  aws4.sign(opts);
  init.headers = opts.headers;
  const response = await fetch(input, init);

  return response;
}

class AuthenticatedDataSource extends RemoteGraphQLDataSource {
  constructor(url: string) {
    super({
      url,
      fetcher: doFetch
    })
  }
}

const server = new ApolloServer({
  gateway: new ApolloGateway({
    serviceList: [
      { name: 'users', url: userServiceUrl }
    ],
    buildService({url}) {
      return new AuthenticatedDataSource(url)
    }
  }),
  subscriptions: false,
  introspection: true,
  playground: true,
});

export const handler = (event: APIGatewayProxyEvent, context: Context, callback: Callback) => {
  console.log('event is: ', JSON.stringify(event, null, 2))
  
  return server.createHandler({
    cors: {
      origin: '*'
    }
  })(event, context, callback);
}

实际上,willSendRequest 工作正常,为简单起见,首选。对于您希望通过简单设置对特定数据源的请求进行签名的情况。

记录的尝试失败的原因是因为请求签名需要准确:路径必须同时包含路径和查询参数。 https 也被删除,因为相应的 AWS 签名错误表明主机没有。

GET 请求

  willSendRequest(request: RequestOptions) {
    const query = request.params ? `?${request.params.toString()}` : "";
    const opts: Record<string, any> = {
      service: "execute-api",
      region: process.env.AWS_REGION,
      host: this.baseURL?.replace("https://", ""),
      path: request.path + query,
      method: request.method,
    };

    sign(opts);

    request.headers.set("X-Amz-Date", opts.headers["X-Amz-Date"]);
    request.headers.set("Authorization", opts.headers["Authorization"]);
    request.headers.set(
      "X-Amz-Security-Token",
      opts.headers["X-Amz-Security-Token"]
    );
  }

POST 请求

片段示例,来自另一个具有主体的 JS lambda(不是 apollo-graphql);不确定 apollo 是否会自动解码正文,不管它是一个很好的展示示例。

  if (Object.hasOwnProperty.call(request, 'body')) {
    const { data, encoding } = request.body;
    const buffer = Buffer.from(data, encoding);
    const decodedBody = buffer.toString('utf8');

    if (decodedBody !== '') {
      normalisedRequest.body = decodedBody;
      normalisedRequest.headers = { 'content-type': request.headers['content-type'][0].value };
    }
  }