当前位置: 首页 > 知识库问答 >
问题:

如何通过特定用户的承载令牌访问与S3 Bucket连接的AWS CloudFront(JWT自定义Auth)

严永丰
2023-03-14

我正在使用无服务器框架将无服务器堆栈部署到AWS。我的堆栈由一些lambda函数、DynamoDB表和API网关组成。

我使用所谓的lambda授权程序来保护API网关。另外,我有一个自定义的独立自托管Auth服务,可以生成令牌。

下面是我的Serverless.yml中的lambda授权程序定义的一个示例,以及我如何使用它来保护其他API网关endpoint:(您可以看到addUserInfo函数具有使用自定义授权程序保护的API)


functions:
    # =================================================================
    # API Gateway event handlers
    # ================================================================
  auth:
    handler: api/auth/mda-auth-server.handler

  addUserInfo:
     handler: api/user/create-replace-user-info.handler
     description: Create Or Replace user section
     events:
       - http:
           path: user
           method: post
           authorizer: 
             name: auth
             resultTtlInSeconds: ${self:custom.resultTtlInSeconds}
             identitySource: method.request.header.Authorization
             type: token
           cors:
             origin: '*'
             headers: ${self:custom.allowedHeaders}

现在我想扩展我的API,以便允许用户添加图像,所以我遵循了这种方法。因此,在这种方法中,用户将启动所谓的S3签名URL,我可以使用这个S3签名URL将图像放到我的桶中。

另外,S3 bucket不能公开访问,但它连接到CloudFront发行版。现在我错过了这里的东西,我不明白如何保护我的图像。这样我就可以用我的自定义身份验证服务保护CloudFront CDN中的映像,以便拥有有效令牌的用户可以访问这些资源吗?如何使用自定义身份验证服务保护CDN(CloudFront)并使用无服务器框架进行配置

共有1个答案

南宫鸿晖
2023-03-14

这有点棘手,我花了大约一天的时间来做好准备。

首先,我们在这里有选择:

  • 代替身份验证,我们可以对URL签名并返回签名的CloudFront URL或签名的S3 URL,这非常简单,但显然不是我想要的。
  • 第二个选项是使用lambda@edge来授权CloudFront的请求

首先,在我的auth-service.js中有以下代码(它只是一些帮助器,允许我验证自定义jwt):

import * as jwtDecode from 'jwt-decode';
import * as util from 'util';
import * as jwt from 'jsonwebtoken';
import * as jwksClient from 'jwks-rsa';


export function getToken(bearerToken) {
    if(bearerToken && bearerToken.startsWith("Bearer "))
    {
        return bearerToken.replace(/^Bearer\s/, '');
    }
    throw new Error("Invalid Bearer Token.");
};

export function getDecodedHeader(token) {
        return jwtDecode(token, { header: true });
};

export async function getSigningKey(decodedJwtTokenHeader, jwksclient){
    const key = await util.promisify(jwksclient.getSigningKey)(decodedJwtTokenHeader.kid);
    const signingKey = key.publicKey || key.rsaPublicKey;
    if (!signingKey) {
        throw new Error('could not get signing key');
    }
    return signingKey;
  };

export async function verifyToken(token,signingKey){
    return await jwt.verify(token, signingKey);
};

export function getJwksClient(jwksEndpoint){
    return jwksClient({
        cache: true,
        rateLimit: true,
        jwksRequestsPerMinute: 10,
        jwksUri: jwksEndpoint
      });
};

然后在serverless.yml中有我的文件:

service: mda-app-uploads

plugins:
  - serverless-offline
  - serverless-pseudo-parameters
  - serverless-iam-roles-per-function
  - serverless-bundle


custom:
  stage: ${opt:stage, self:provider.stage}
  resourcesBucketName: ${self:custom.stage}-mda-resources-bucket
  resourcesStages:
    prod: prod
    dev: dev
  resourcesStage: ${self:custom.resourcesStages.${self:custom.stage}, self:custom.resourcesStages.dev}


provider:
  name: aws
  runtime: nodejs12.x
  stage: ${opt:stage, 'dev'}
  region: us-east-1
  versionFunctions: true

functions: 
  oauthEdge:
    handler: src/mda-edge-auth.handler
    role: LambdaEdgeFunctionRole
    memorySize: 128
    timeout: 5


resources:
  - ${file(resources/s3-cloudfront.yml)}

这里的快速要点:

    null
Resources:

    AuthEdgeLambdaVersion:
        Type: Custom::LatestLambdaVersion
        Properties:
            ServiceToken: !GetAtt PublishLambdaVersion.Arn
            FunctionName: !Ref OauthEdgeLambdaFunction
            Nonce: "Test"

    PublishLambdaVersion:
        Type: AWS::Lambda::Function
        Properties:
            Handler: index.handler
            Runtime: nodejs12.x
            Role: !GetAtt PublishLambdaVersionRole.Arn
            Code:
                ZipFile: |
                    const {Lambda} = require('aws-sdk')
                    const {send, SUCCESS, FAILED} = require('cfn-response')
                    const lambda = new Lambda()
                    exports.handler = (event, context) => {
                        const {RequestType, ResourceProperties: {FunctionName}} = event
                        if (RequestType == 'Delete') return send(event, context, SUCCESS)
                        lambda.publishVersion({FunctionName}, (err, {FunctionArn}) => {
                        err
                            ? send(event, context, FAILED, err)
                            : send(event, context, SUCCESS, {FunctionArn})
                        })
                    }

    PublishLambdaVersionRole:
        Type: AWS::IAM::Role
        Properties:
            AssumeRolePolicyDocument:
                Version: '2012-10-17'
                Statement:
                - Effect: Allow
                  Principal:
                    Service: lambda.amazonaws.com
                  Action: sts:AssumeRole
            ManagedPolicyArns:
            - arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole
            Policies:
            - PolicyName: PublishVersion
              PolicyDocument:
                Version: '2012-10-17'
                Statement:
                - Effect: Allow
                  Action: lambda:PublishVersion
                  Resource: '*'

    LambdaEdgeFunctionRole:
        Type: "AWS::IAM::Role"
        Properties:
            Path: "/"
            ManagedPolicyArns:
                - "arn:aws:iam::aws:policy/service-role/AWSLambdaBasicExecutionRole"
            AssumeRolePolicyDocument:
                Version: "2012-10-17"
                Statement:
                -
                    Sid: "AllowLambdaServiceToAssumeRole"
                    Effect: "Allow"
                    Action: 
                        - "sts:AssumeRole"
                    Principal:
                        Service: 
                            - "lambda.amazonaws.com"
                            - "edgelambda.amazonaws.com"
    LambdaEdgeFunctionPolicy:
        Type: "AWS::IAM::Policy"
        Properties:
            PolicyName: MainEdgePolicy
            PolicyDocument:
                Version: "2012-10-17"
                Statement:
                    Effect: "Allow"
                    Action: 
                        - "lambda:GetFunction"
                        - "lambda:GetFunctionConfiguration"
                    Resource: !GetAtt AuthEdgeLambdaVersion.FunctionArn
            Roles:
                - !Ref LambdaEdgeFunctionRole


    ResourcesBucket:
        Type: AWS::S3::Bucket
        Properties:
            BucketName: ${self:custom.resourcesBucketName}
            AccessControl: Private
            CorsConfiguration:
                CorsRules:
                -   AllowedHeaders: ['*']
                    AllowedMethods: ['PUT']
                    AllowedOrigins: ['*']

    ResourcesBucketPolicy:
        Type: AWS::S3::BucketPolicy
        Properties:
            Bucket:
                Ref: ResourcesBucket
            PolicyDocument:
                Statement:
                # Read permission for CloudFront
                -   Action: s3:GetObject
                    Effect: "Allow"
                    Resource: 
                        Fn::Join: 
                            - ""
                            - 
                                - "arn:aws:s3:::"
                                - 
                                    Ref: "ResourcesBucket"
                                - "/*"
                    Principal:
                        CanonicalUser: !GetAtt CloudFrontOriginAccessIdentity.S3CanonicalUserId
                -   Action: s3:PutObject
                    Effect: "Allow"
                    Resource: 
                        Fn::Join: 
                            - ""
                            - 
                                - "arn:aws:s3:::"
                                - 
                                    Ref: "ResourcesBucket"
                                - "/*"
                    Principal:
                        AWS: !GetAtt LambdaEdgeFunctionRole.Arn

                -   Action: s3:GetObject
                    Effect: "Allow"
                    Resource: 
                        Fn::Join: 
                            - ""
                            - 
                                - "arn:aws:s3:::"
                                - 
                                    Ref: "ResourcesBucket"
                                - "/*"
                    Principal:
                        AWS: !GetAtt LambdaEdgeFunctionRole.Arn

    
    CloudFrontOriginAccessIdentity:
        Type: AWS::CloudFront::CloudFrontOriginAccessIdentity
        Properties:
            CloudFrontOriginAccessIdentityConfig:
                Comment:
                    Fn::Join: 
                        - ""
                        -
                            - "Identity for accessing CloudFront from S3 within stack "
                            - 
                                Ref: "AWS::StackName"
                            - ""


    # Cloudfront distro backed by ResourcesBucket
    ResourcesCdnDistribution:
        Type: AWS::CloudFront::Distribution
        Properties:
            DistributionConfig:
                Origins:
                    # S3 origin for private resources
                    -   DomainName: !Sub '${self:custom.resourcesBucketName}.s3.amazonaws.com'
                        Id: S3OriginPrivate
                        S3OriginConfig:
                            OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/#{CloudFrontOriginAccessIdentity}'
                    # S3 origin for public resources           
                    -   DomainName: !Sub '${self:custom.resourcesBucketName}.s3.amazonaws.com'
                        Id: S3OriginPublic
                        S3OriginConfig:
                            OriginAccessIdentity: !Sub 'origin-access-identity/cloudfront/#{CloudFrontOriginAccessIdentity}'
                Enabled: true
                Comment: CDN for public and provate static content.
                DefaultRootObject: index.html
                HttpVersion: http2
                DefaultCacheBehavior:
                    AllowedMethods:
                        - DELETE
                        - GET
                        - HEAD
                        - OPTIONS
                        - PATCH
                        - POST
                        - PUT
                    Compress: true
                    TargetOriginId: S3OriginPublic
                    ForwardedValues:
                        QueryString: false
                        Headers:
                        - Origin
                        Cookies:
                            Forward: none
                    ViewerProtocolPolicy: redirect-to-https
                CacheBehaviors:
                    - 
                        PathPattern: 'private/*'
                        TargetOriginId: S3OriginPrivate
                        AllowedMethods:
                        - DELETE
                        - GET
                        - HEAD
                        - OPTIONS
                        - PATCH
                        - POST
                        - PUT
                        Compress: true
                        LambdaFunctionAssociations:
                            - 
                                EventType: viewer-request
                                LambdaFunctionARN: !GetAtt AuthEdgeLambdaVersion.FunctionArn
                        ForwardedValues:
                            QueryString: false
                            Headers:
                                - Origin
                            Cookies:
                                Forward: none
                        ViewerProtocolPolicy: redirect-to-https
                    - 
                        PathPattern: 'public/*'
                        TargetOriginId: S3OriginPublic
                        AllowedMethods:
                        - DELETE
                        - GET
                        - HEAD
                        - OPTIONS
                        - PATCH
                        - POST
                        - PUT
                        Compress: true
                        ForwardedValues:
                            QueryString: false
                            Headers:
                                - Origin
                            Cookies:
                                Forward: none
                        ViewerProtocolPolicy: redirect-to-https

                PriceClass: PriceClass_200
    null

最后,这里是用于CDN Auth的lambda edge函数的实际代码:

import {getJwksClient, getToken, getDecodedHeader, getSigningKey, verifyToken} from '../../../../libs/services/auth-service';
import config from '../../../../config';

const response401 = {
    status: '401',
    statusDescription: 'Unauthorized'
};

exports.handler = async (event) => {
    try{
        const cfrequest = event.Records[0].cf.request;
        const headers = cfrequest.headers;
        if(!headers.authorization) {
            console.log("no auth header");
            return response401;
        }
        const jwtValue = getToken(headers.authorization);
        const client = getJwksClient(`https://${config.authDomain}/.well-known/openid-configuration/jwks`);
        const decodedJwtHeader = getDecodedHeader(jwtValue);
        if(decodedJwtHeader)
        {
          const signingKey = await getSigningKey(decodedJwtHeader, client);
          const verifiedToken = await verifyToken(jwtValue, signingKey);
          if(verifiedToken)
          {
            return cfrequest;
          }
      }else{
        throw Error("Unauthorized");
      }

    }catch(err){
      console.log(err);
      return response401;
    }
};

如果您感兴趣,我将使用IdentityServer4,并将其作为Azure中的docker映像托管,并将其用作自定义授权程序。

所以完整的场景现在我们有了一个完全私有的S3桶。它只能通过CloudFront起源访问。如果请求是通过公共源提供的,那么就不需要身份验证,但是如果请求是通过私有源提供的,那么我将触发所谓的lambda edge来对它进行身份验证并验证承载令牌。

在深入研究所有这些之前,我对AWS stack是完全陌生的,但AWS非常容易,所以我最终以完美的方式配置了所有内容。如果有不清楚的地方或有任何问题,请让我知道。

 类似资料:
  • 我有短过期(5分钟)的访问令牌和刷新令牌。 如何立即撤销特定用户的访问令牌? 例如,如果我想禁止某个用户?我点击了禁止按钮,现在我想立即撤销他的访问令牌。我不想等待令牌到期。 我需要以某种方式创建令牌黑名单,但如果我不知道特定用户的访问令牌,如何将其添加到黑名单? 我应该将所有 jwt 令牌存储在数据库还是 redis 中?代码示例将非常有帮助。 非常感谢。

  • 我有一个ADFS 4.0 OpenId连接设置,应用程序组为“Web浏览器访问Web应用程序”。我使用隐式流,能够从我的web应用程序成功登录,接收id_令牌并访问_令牌。 接下来,我在从wep应用程序发送到应用编程接口服务器的请求中使用访问令牌。我想做的是自定义访问令牌格式——添加额外的参数,因为默认情况下我只有: aud、iss、iat、exp、apptype、appid、Authmethod

  • 经过大量阅读,我找到了一种实现自定义JWT承载令牌验证器的方法,如下所示。 : 自定义验证器类: 如果令牌被盗,我想添加一个额外的安全层来验证请求是否来自生成令牌的同一个客户端。 问题: 是否有任何方法可以访问类中的,以便根据当前客户端/请求者添加自定义验证 我们是否有其他方法可以使用这种方法/中间件验证请求者的真实性

  • 这是自定义承载令牌授权机制的可接受实现吗? 授权属性 验证服务。APISettings类用于appSettings,但验证可以扩展为使用数据库。。。显然:) 实施 应用设置 } 请求头

  • 我正在尝试让我的授权服务器生成一个JWT访问令牌,其中包含一些自定义声明。 我正在使用以下依赖项: 授权服务器是这样配置的: 它使用类: 与调试器一起运行时,不会调用这两个增强器重写。

  • 我能够获得Graph API的有效访问令牌,因为有丰富的示例/文档/教程。 但是,我无法为我的自定义API获取有效的访问令牌。我使用的范围看起来像这样: 使用此作用域,我可以获取访问令牌。不幸的是,它是无效的。随后,当我尝试在自定义 API 上调用某些内容时,我收到未经授权的 401 错误。 甚至有可能使用MSAL acquireTokenSilent在自定义API上请求访问令牌吗?