我在AWS CDK v2中有一个带有自定义授权器的ApiGateway RestApi。现在我想创建一个带有授权器的WebSocket。
我开始遵循这个指南Stack 3: Api网关Websocket Api AWS CDK堆栈演练,这已经让我到目前为止创建ApiGatewayV2 Websocket。我正在努力弄清楚如何为此创建一个自定义authoriser。
我的一些问题:
- 我可以使用与ApiGatewayV2 CfnAuthoriser相同的authoriser功能吗? 授权需要以某种websocket风格发生吗?
- 我如何从前端应用程序与WebSocket授权?它只是HTTP请求中的认证头吗?
我有一个艰难的时间谷歌它,继续得到cdkv1文章。如果有人有时间给我指出正确的方向,我将非常感激。
主要堆栈
export class ThingCdkStack extends Stack {
private authoriserLogicalId: string;
constructor(scope: Construct, id: string, props: StackProps, private envs: Environment) {
super(scope, id, props);
const api = new RestApi(this, 'ThingApi');
const role = new Role(this, 'ThingRole', {
roleName: 'thing-role',
assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),
inlinePolicies: {
allowLambdaInvocation: PolicyDocument.fromJson({
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: ['lambda:InvokeFunction', 'lambda:InvokeAsync'],
Resource: `arn:aws:lambda:${envs.REGION}:${envs.ACCOUNT}:function:*`,
},
],
}),
},
});
const authorizerHandler = new NodejsFunction(this, 'ThingCustomAuthorizer', {
entry: 'lambda/handlers/auth/auth0-authoriser.ts',
runtime: Runtime.NODEJS_18_X,
environment: {
AUTH0_ISSUER: 'https://my-auth.eu.auth0.com/',
AUTH0_AUDIENCE: 'https://my-demo.com',
REGION: envs.REGION,
ACCOUNT: envs.ACCOUNT,
}
});
const authorizer = new CfnAuthorizer(this, 'ThingAuthoriser', {
restApiId: api.restApiId,
type: 'TOKEN',
name: 'thing-authoriser',
identitySource: 'method.request.header.Authorization',
authorizerUri: `arn:aws:apigateway:${envs.REGION}:lambda:path/2015-03-31/functions/${authorizerHandler.functionArn}/invocations`,
authorizerCredentials: role.roleArn
});
this.authoriserLogicalId = authorizer.logicalId;
const createThingHandler = new NodejsFunction(this, 'CreateThingLambda', {
entry: 'lambda/handlers/thing/create-thing.ts',
runtime: Runtime.NODEJS_18_X,
});
this.addAuthMethod('post', api.addResource('thing'), createThingHandler);
this.addWebsocket(envs, authorizer);
}
private addAuthMethod(method: string, resource: Resource, handler: NodejsFunction, integrationOptions?: LambdaIntegrationOptions) {
const route = resource.addMethod(
method,
new LambdaIntegration(handler, integrationOptions),
{
authorizationType: AuthorizationType.CUSTOM,
}
);
const childResource = route.node.findChild('Resource');
(childResource as CfnResource).addPropertyOverride('AuthorizationType', AuthorizationType.CUSTOM);
(childResource as CfnResource).addPropertyOverride('AuthorizerId', {Ref: this.authoriserLogicalId});
}
private addWebsocket(environment: Environment, authorizer: CfnAuthorizer) {
const connectionsTable = new Table(this, 'ConnectionsTable', {
partitionKey: {name: 'connectionId', type: AttributeType.STRING},
readCapacity: 2,
writeCapacity: 1,
timeToLiveAttribute: "ttl"
});
const commonHandlerProps: NodejsFunctionProps = {
bundling: {minify: true, sourceMap: true, target: 'es2019'},
runtime: Runtime.NODEJS_18_X,
logRetention: RetentionDays.THREE_DAYS
};
const connectHandler = new NodejsFunction(this, 'ConnectHandler', {
...commonHandlerProps,
entry: 'lambda/websocket/connect.ts',
environment: {
CONNECTIONS_TBL: connectionsTable.tableName
}
});
const defaultHandler = new NodejsFunction(this, 'defaultHandler', {
...commonHandlerProps,
entry: 'lambda/websocket/default.ts',
environment: {
CONNECTIONS_TBL: connectionsTable.tableName
}
});
const disconnectHandler = new NodejsFunction(this, 'DisconnectHandler', {
...commonHandlerProps,
entry: 'lambda/websocket/disconnect.ts',
environment: {
CONNECTIONS_TBL: connectionsTable.tableName
}
});
const websocketApi = new WebsocketApi(this, "CompletionWebsocketApi", {
apiName: "completions-api",
apiDescription: "Web Socket API for Completions",
stageName: environment.STAGE,
connectHandler,
disconnectHandler,
defaultHandler,
connectionsTable
});
const CONNECTION_URL = `https://${websocketApi.api.ref}.execute-api.${Aws.REGION}.amazonaws.com/${environment.STAGE}`;
const completionHandler = new NodejsFunction(this, 'CompletionHandler', {
...commonHandlerProps,
entry: 'lambda/websocket/completions.ts',
environment: {
CONNECTION_TBL: connectionsTable.tableName,
CONNECTION_URL: CONNECTION_URL
},
});
websocketApi.addLambdaIntegration(completionHandler, 'completions', 'CompletionsRoute')
const managementApiPolicyStatement = new PolicyStatement({
effect: Effect.ALLOW,
actions: ["execute-api:ManageConnections"],
resources: [`arn:aws:execute-api:${Aws.REGION}:${Aws.ACCOUNT_ID}:${websocketApi.api.ref}/*`]
})
defaultHandler.addToRolePolicy(managementApiPolicyStatement);
completionHandler.addToRolePolicy(managementApiPolicyStatement);
new CfnOutput(this, 'WebsocketConnectionUrl', {value: CONNECTION_URL});
const websocketApiUrl = `${websocketApi.api.attrApiEndpoint}/${environment.STAGE}`
new CfnOutput(this, "websocketUrl", {
value: websocketApiUrl
});
}
}
WebsocketApi构建
import { ITable } from "aws-cdk-lib/aws-dynamodb";
import { IFunction } from "aws-cdk-lib/aws-lambda";
import { CfnApi, CfnIntegration, CfnRoute, CfnStage, CfnDeployment } from "aws-cdk-lib/aws-apigatewayv2";
import { ServicePrincipal } from "aws-cdk-lib/aws-iam";
import { Aws, Stack } from 'aws-cdk-lib';
import { Construct } from 'constructs';
export interface WebsocketApiProps {
readonly apiName: string;
readonly apiDescription: string;
readonly stageName: string;
readonly connectHandler: IFunction;
readonly disconnectHandler: IFunction;
readonly connectionsTable: ITable;
readonly defaultHandler?: IFunction;
}
export class WebsocketApi extends Construct {
readonly props: WebsocketApiProps;
readonly api: CfnApi;
readonly deployment: CfnDeployment;
constructor(parent: Stack, name: string, props: WebsocketApiProps) {
super(parent, name);
this.props = props;
this.api = new CfnApi(this, 'CompletionsWebSocketApi', {
name: props.apiName,
description: props.apiDescription,
protocolType: "WEBSOCKET",
routeSelectionExpression: "$request.body.action",
});
this.deployment = new CfnDeployment(this, "WebsocketDeployment", {
apiId: this.api.ref,
});
new CfnStage(this, "WebsocketStage", {
stageName: props.stageName,
apiId: this.api.ref,
deploymentId: this.deployment.ref,
});
props.connectionsTable.grantWriteData(props.connectHandler);
props.connectionsTable.grantWriteData(props.disconnectHandler);
this.addLambdaIntegration(props.connectHandler, "$connect", "ConnectionRoute");
this.addLambdaIntegration(props.disconnectHandler, "$disconnect", "DisconnectRoute");
if(props.defaultHandler) {
props.connectionsTable.grantWriteData(props.defaultHandler);
this.addLambdaIntegration(props.defaultHandler, "$default", "DefaultRoute");
}
}
addLambdaIntegration(handler: IFunction, routeKey: string, operationName: string, apiKeyRequired?: boolean, authorizationType?: string) {
const integration = new CfnIntegration(this, `${operationName}Integration`, {
apiId: this.api.ref,
integrationType: "AWS_PROXY",
integrationUri: `arn:aws:apigateway:${Aws.REGION}:lambda:path/2015-03-31/functions/${handler.functionArn}/invocations`
});
handler.grantInvoke(new ServicePrincipal('apigateway.amazonaws.com', {
conditions: {
"ArnLike": {
"aws:SourceArn": `arn:aws:execute-api:${Aws.REGION}:${Aws.ACCOUNT_ID}:${this.api.ref}/*/*`
}
}
}));
this.deployment.addDependency(new CfnRoute(this, `${operationName}Route`, {
apiId: this.api.ref,
routeKey,
apiKeyRequired,
authorizationType: authorizationType || "NONE",
operationName,
target: `integrations/${integration.ref}`
}));
}
}
到目前为止,我有一个完整的工作堆栈可以在这里找到:https://github.com/OrderAndCh4oS/aws-cdk-v2-apigatewayv2-websocket-demo
我发现了一些有用的文档,我现在正在通过:https://docs.aws.amazon.com/apigateway/latest/developerguide/apigateway-websocket-api-lambda-auth.html
还发现了一个使用Cognito的示例项目:https://github.com/aws-samples/websocket-api-cognito-auth-sample
已经取得了一些进展,它仍然是粗糙的边缘,但至少是工作。
我创建了一个ApiGatewayV2 CfnAuthoriser,并将其与$connect路由上的deployment.addDependency()
连接起来,并将authorizationType
设置为CUSTOM
。
处理程序只是一个常规的自定义授权器lambda,但是我必须使用querystring参数来传递令牌。
调用WebSocket (const socket = new WebSocket(`wss://xxxxxxxxxx.execute-api.eu-west-1.amazonaws.com/dev?auth=${token}
)时,在查询字符串上传递授权令牌;'
主要堆栈
import * as cdk from 'aws-cdk-lib';
import {Aws, CfnOutput} from 'aws-cdk-lib';
import {Construct} from 'constructs';
import {AttributeType, Table} from "aws-cdk-lib/aws-dynamodb";
import {WebsocketApi} from "./websocket-api";
import {RetentionDays} from "aws-cdk-lib/aws-logs";
import {NodejsFunction, NodejsFunctionProps} from "aws-cdk-lib/aws-lambda-nodejs";
import {Effect, PolicyStatement} from "aws-cdk-lib/aws-iam";
import {Environment} from "../bin/environment";
import {Runtime} from "aws-cdk-lib/aws-lambda";
export class AwsCdkV2WebsocketStack extends cdk.Stack {
constructor(scope: Construct, id: string, props: cdk.StackProps, private envs: Environment) {
super(scope, id, props);
this.addWebsocket(envs);
}
private addWebsocket(envs: Environment) {
const connectionsTable = new Table(this, 'ConnectionsTableWebsocketDemo', {
partitionKey: {name: 'connectionId', type: AttributeType.STRING},
readCapacity: 2,
writeCapacity: 1,
timeToLiveAttribute: "ttl"
});
const commonHandlerProps: NodejsFunctionProps = {
bundling: {minify: true, sourceMap: true, target: 'es2019'},
runtime: Runtime.NODEJS_18_X,
logRetention: RetentionDays.THREE_DAYS
};
const connectHandler = new NodejsFunction(this, 'ConnectHandlerWebsocketDemo', {
...commonHandlerProps,
entry: 'lambda/websocket/connect.ts',
environment: {
CONNECTIONS_TBL: connectionsTable.tableName
}
});
const authorizationHandler = new NodejsFunction(this, 'AuthorisationHandlerWebsocketDemo', {
...commonHandlerProps,
entry: 'lambda/handlers/authorisation.ts',
environment: {
// Todo: use env
ISSUER: 'https://app-auth.eu.auth0.com/',
AUDIENCE: 'https://app-demo.com',
}
});
const defaultHandler = new NodejsFunction(this, 'DefaultHandlerWebsocketDemo', {
...commonHandlerProps,
entry: 'lambda/websocket/default.ts',
environment: {
CONNECTIONS_TBL: connectionsTable.tableName
}
});
const disconnectHandler = new NodejsFunction(this, 'DisconnectHandlerWebsocketDemo', {
...commonHandlerProps,
entry: 'lambda/websocket/disconnect.ts',
environment: {
CONNECTIONS_TBL: connectionsTable.tableName
}
});
const websocketApi = new WebsocketApi(
this,
"MessageWebsocketApiWebsocketDemo",
{
apiName: "messages-api",
apiDescription: "Web Socket API for Completions",
stageName: envs.STAGE,
connectHandler,
disconnectHandler,
defaultHandler,
connectionsTable,
authorizationHandler
},
envs
);
const CONNECTION_URL = `https://${websocketApi.api.ref}.execute-api.${Aws.REGION}.amazonaws.com/${envs.STAGE}`;
const messageHandler = new NodejsFunction(this, 'CompletionHandlerWebsocketDemo', {
...commonHandlerProps,
entry: 'lambda/websocket/message.ts',
environment: {
CONNECTION_TBL: connectionsTable.tableName,
CONNECTION_URL
},
});
websocketApi.addLambdaIntegration(messageHandler, 'message', 'CompletionsRoute')
const managementApiPolicyStatement = new PolicyStatement({
effect: Effect.ALLOW,
actions: ["execute-api:ManageConnections"],
resources: [`arn:aws:execute-api:${Aws.REGION}:${Aws.ACCOUNT_ID}:${websocketApi.api.ref}/*`]
})
defaultHandler.addToRolePolicy(managementApiPolicyStatement);
messageHandler.addToRolePolicy(managementApiPolicyStatement);
new CfnOutput(this, 'WebsocketConnectionUrl', {value: CONNECTION_URL});
const websocketApiUrl = `${websocketApi.api.attrApiEndpoint}/${envs.STAGE}`
new CfnOutput(this, "WebsocketUrl", {
value: websocketApiUrl
});
}
}
WebSocket API Stack
import { ITable } from "aws-cdk-lib/aws-dynamodb";
import { IFunction } from "aws-cdk-lib/aws-lambda";
import {CfnApi, CfnIntegration, CfnRoute, CfnStage, CfnDeployment, CfnAuthorizer} from "aws-cdk-lib/aws-apigatewayv2";
import {PolicyDocument, Role, ServicePrincipal} from "aws-cdk-lib/aws-iam";
import { Aws, Stack } from 'aws-cdk-lib';
import { Construct } from 'constructs';
import {Environment} from "../bin/environment";
export interface WebsocketApiProps {
readonly apiName: string;
readonly apiDescription: string;
readonly stageName: string;
readonly connectHandler: IFunction;
readonly disconnectHandler: IFunction;
readonly connectionsTable: ITable;
readonly authorizationHandler: IFunction;
readonly defaultHandler?: IFunction;
}
export class WebsocketApi extends Construct {
readonly props: WebsocketApiProps;
readonly api: CfnApi;
readonly deployment: CfnDeployment;
constructor(parent: Stack, name: string, props: WebsocketApiProps, envs: Environment) {
super(parent, name);
this.props = props;
this.api = new CfnApi(this, 'CompletionsWebSocketApi', {
name: props.apiName,
description: props.apiDescription,
protocolType: "WEBSOCKET",
routeSelectionExpression: "$request.body.action"
});
this.deployment = new CfnDeployment(this, "WebsocketDeployment", {
apiId: this.api.ref,
});
new CfnStage(this, "WebsocketStage", {
stageName: props.stageName,
apiId: this.api.ref,
deploymentId: this.deployment.ref,
});
const role = new Role(this, 'AuthorisedRole', {
roleName: 'authorised-role',
assumedBy: new ServicePrincipal('apigateway.amazonaws.com'),
inlinePolicies: {
allowLambdaInvocation: PolicyDocument.fromJson({
Version: '2012-10-17',
Statement: [
{
Effect: 'Allow',
Action: ['lambda:InvokeFunction', 'lambda:InvokeAsync'],
Resource: `arn:aws:lambda:${envs.REGION}:${envs.ACCOUNT}:function:*`,
},
],
}),
},
});
const authorizer = new CfnAuthorizer(this, 'WorkspaceAuthoriser', {
name: 'workspace-authoriser',
apiId: this.api.ref,
authorizerType: 'REQUEST',
identitySource: ['route.request.querystring.auth'],
authorizerUri: `arn:aws:apigateway:${envs.REGION}:lambda:path/2015-03-31/functions/${this.props.authorizationHandler.functionArn}/invocations`,
authorizerCredentialsArn: role.roleArn,
});
this.addLambdaIntegration(props.connectHandler, "$connect", "ConnectionRoute", false,"CUSTOM", authorizer);
props.connectionsTable.grantWriteData(props.connectHandler);
props.connectionsTable.grantWriteData(props.disconnectHandler);
this.addLambdaIntegration(props.disconnectHandler, "$disconnect", "DisconnectRoute");
if(props.defaultHandler) {
props.connectionsTable.grantWriteData(props.defaultHandler);
this.addLambdaIntegration(props.defaultHandler, "$default", "DefaultRoute");
}
}
addLambdaIntegration(handler: IFunction, routeKey: string, operationName: string, apiKeyRequired?: boolean, authorizationType?: string, authorizer?: CfnAuthorizer) {
const integration = new CfnIntegration(this, `${operationName}Integration`, {
apiId: this.api.ref,
integrationType: "AWS_PROXY",
integrationUri: `arn:aws:apigateway:${Aws.REGION}:lambda:path/2015-03-31/functions/${handler.functionArn}/invocations`,
});
handler.grantInvoke(new ServicePrincipal('apigateway.amazonaws.com', {
conditions: {
"ArnLike": {
"aws:SourceArn": `arn:aws:execute-api:${Aws.REGION}:${Aws.ACCOUNT_ID}:${this.api.ref}/*/*`
}
}
}));
this.deployment.addDependency(new CfnRoute(this, `${operationName}RouteWebsocketDemo`, {
apiId: this.api.ref,
routeKey,
apiKeyRequired,
authorizationType: authorizationType || "NONE",
operationName,
target: `integrations/${integration.ref}`,
authorizerId: authorizer?.attrAuthorizerId,
}));
}
}
<<p>授权处理程序/strong>if (!process.env.AUDIENCE) throw new Error('Missing AUDIENCE');
if (!process.env.ISSUER) throw new Error('Missing ISSUER');
if (!process.env.AWS_REGION) throw new Error('Missing AWS_REGION');
const JWKS_URI = `${process.env.ISSUER}.well-known/jwks.json`
export const handler = async (event: any, context: any, callback: any) => {
let data;
try {
data = await authenticate(event);
} catch (err) {
console.log('UNAUTHORISED', err);
return context.fail('Unauthorized');
}
console.log('AUTHORISED', data);
return data;
};
const getPolicyDocument = (effect: any, resource: any) => {
return {
Version: '2012-10-17',
Statement: [{
Action: 'execute-api:Invoke',
Effect: effect,
Resource: resource,
}]
};
}
const getToken = (event: any) => {
if (!event.type || event.type !== 'REQUEST') {
throw new Error('Expected "event.type" parameter to have value "REQUEST"');
}
const tokenString = event.queryStringParameters?.auth;
if (!tokenString) {
throw new Error('Expected "event.queryStringParameters.auth" parameter to be set');
}
return tokenString;
}
const jwtOptions = {
audience: process.env.AUDIENCE,
issuer: process.env.ISSUER
};
const client = jwksClient({
cache: true,
rateLimit: true,
jwksRequestsPerMinute: 10,
jwksUri: JWKS_URI
});
const authenticate = (event: any) => {
console.log(event);
const token = getToken(event);
const decoded = jwt.decode(token, {complete: true});
if (!decoded || !decoded.header || !decoded.header.kid) {
throw new Error('invalid token');
}
const getSigningKey = util.promisify(client.getSigningKey);
return getSigningKey(decoded.header.kid)
.then((key: any) => {
const signingKey = key?.publicKey || key?.rsaPublicKey;
return jwt.verify(token, signingKey, jwtOptions);
})
.then((decoded: any) => ({
principalId: decoded.sub,
policyDocument: getPolicyDocument('Allow', '*'),
context: {scope: decoded.scope}
}));
}
我做了一个scratch演示项目,可能对某人有用,也可能不有用:https://github.com/OrderAndCh4oS/aws-cdk-v2-apigatewayv2-websocket-demo
我将保留这个问题,如果有人能够改进这一点,它当然还需要工作。