برنامه نویسی

سفارشی API Gateway Authorizer با Golang

یکی از چیزهای خوب در مورد ساخت با سرور بدون سرور این است که می توانید چیزها را به گونه ای طراحی کنید که قطعات قابل ترکیب باشند. این بدان معناست که می‌توانید منطق را به‌صورت منسجم با دیگر منطق‌های همفکر خود قرار دهید و سپس چیزها را به‌طور آزادانه با سایر اجزاء مرتبط نگه دارید تا تغییر کردن چیزها آسان باشد بدون اینکه خیلی شکننده باشد. هنگام ساختن یک API، شما اغلب به یک نوع Autorizer نیاز دارید تا توکن ارائه شده را تأیید کند. در این مقاله، من قصد دارم در ساخت یک API Gateway Authorizer سفارشی با Golang قدم بگذارم.

API Gateway Authorizer با Golang

برای مرجع، در اینجا نمودار معماری چیزی است که می خواهم به شما نشان دهم.

آنچه در بالا به دست می آید موارد زیر است

  • یک دروازه API برای مدیریت بارهای به منابع ما تعریف می کند
  • از لامابدا برای مدیریت مجوز استفاده می کند
  • توکن را در برابر استخر کاربران Cognito اعتبار سنجی می کند
  • برای صرفه جویی در محاسبات، از یک حافظه پنهان با یک مجموعه سفارشی TTL استفاده می کند
  • در نهایت، اگر همه چیز خوب باشد، امکان دسترسی به منبع محافظت شده را نیز فراهم می‌کند که می‌تواند موارد نادیده گرفته شده را در زمینه ادعا ارائه کند.

یک نیمه همراه برای این مقاله نیز وجود دارد که من به شما نشان خواهم داد که چگونه JWT را که با استفاده از Lambdas و DyanamoDB با آن کار خواهیم کرد گسترش دهید. اگر در مورد آن کنجکاو هستید، در اینجا مقاله به شما نشان می دهد که چگونه این کار انجام می شود

قدم زدن از طریق کد

CDK با Cognito شروع کنید

برای داشتن یک Cognito برای تأیید اعتبار، ابتدا باید یک نمونه Cognito و همچنین یک کلاینت بسازیم تا بتوانیم وارد شوید.

تعریف UserPool مانند شکل زیر است. چیز زیادی نیاز به توضیح اضافی ندارد، بنابراین اجازه دهید به سمت مشتری برویم.

this._pool = new cognito.UserPool(this, "SamplePool", {
    userPoolName: "SamplePool",
    selfSignUpEnabled: false,
    signInAliases: {
        email: true,
        username: true,
        preferredUsername: true,
    },
    autoVerify: {
        email: false,
    },
    standardAttributes: {
        email: {
            required: true,
            mutable: true,
        },
    },
    customAttributes: {
        isAdmin: new cognito.StringAttribute({ mutable: true }),
    },
    passwordPolicy: {
        minLength: 8,
        requireLowercase: true,
        requireDigits: true,
        requireUppercase: true,
        requireSymbols: true,
    },
    accountRecovery: cognito.AccountRecovery.EMAIL_ONLY,
    removalPolicy: cdk.RemovalPolicy.DESTROY,
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

افزودن کلاینت به UserPool نیز ساده است. گزینه های زیادی وجود دارد، اما گزینه من در زیر وانیلی بسیار زیبا است. با استفاده از این کلاینت، می‌توانید راهی برای ورود به سیستم با کاربر و انجام سایر توسعه برنامه‌ها در مقابل آن داشته باشید. همانطور که در ادامه مقاله خواهید دید، من فقط از Postman برای جمع آوری همه اینها استفاده می کنم.

this._pool.addClient("sample-client", {
    userPoolClientName: "sample-client",
    authFlows: {
        adminUserPassword: true,
        custom: true,
        userPassword: true,
        userSrp: false,
    },
    idTokenValidity: Duration.minutes(60),
    refreshTokenValidity: Duration.days(30),
    accessTokenValidity: Duration.minutes(60),
});
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

Autorizer را بسازید

اکنون برای “سفارشی” در ساخت یک API Gateway Authorizer سفارشی با Golang. Autorizer چیزی بیش از یک تابع Lambda نیست. بنابراین در صورت تمایل این می تواند وارداتی از پشته دیگری باشد. اما برای سادگی، من همه چیز را در این مجموعه زیرساخت گنجانده ام. اگر می‌خواهید در CDK و GoFunction عمیق‌تر شوید، در اینجا مقاله‌ای وجود دارد که به شما کمک می‌کند

تعریف تابع در CDK

export class AuthorizerFunction extends Construct {
    private readonly _func: GoFunction;

    constructor(scope: Construct, id: string, poolId: string) {
        super(scope, id);

        this._func = new GoFunction(this, "AuthorizerFunc", {
            entry: path.join(__dirname, `../../../src/authorizer`),
            functionName: "authorizer-func",
            timeout: Duration.seconds(30),
            environment: {
                USER_POOL_ID: poolId,
            },
        });
    }

    get function(): GoFunction {
        return this._func;
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

همانطور که در بالا ذکر کردم، یک پیاده سازی ساده GoFunction. تنها نکته جالب توجه، متغیر محیطی برای USER_POOL_ID است. بیایید نگاهی به این موضوع بیندازیم که چرا این مهم است.

اجرای تابع در Golang

برای این مثال از ساخت یک API Gateway Authorizer سفارشی با Golang، می‌خواهم JWT را تأیید کنم و زمینه‌های اضافی را اضافه کنم. اجرای شما می تواند بسیار متفاوت باشد، به همین دلیل است که من این رویکرد را دوست دارم. می‌توانید بر اساس نیاز چندین مجوزدهنده مختلف داشته باشید و منابع محافظت شده شما از آنچه در بالای آنها در پشته تماس اتفاق می‌افتد اطلاعی نداشته باشند.

اولین چیزی که می خواهم به شما نشان دهم این است که چگونه مجموعه کلید را برای نقطه پایانی شناخته شده Cognito ایجاد کنید. من این کار را در init() تابع چون می‌دانم زمانی که Lambda مقدار دهی اولیه می‌شود یک بار اجرا می‌شود و سپس خروجی را در متغیری ذخیره می‌کنم که خود را در سراسر فراخوانی‌های Lambda حفظ می‌کند. نه شروع های سرد، بلکه فراخوان ها.

func init() {
    log.SetFormatter(&log.JSONFormatter{
        PrettyPrint: false,
    })

    log.SetLevel(log.DebugLevel)

    region := "us-west-2"
    poolId := os.Getenv("USER_POOL_ID")
    var err error

    jwksUrl := fmt.Sprintf("https://cognito-idp.%s.amazonaws.com/%s/.well-known/jwks.json", region, poolId)
    keySet, err = jwk.Fetch(context.TODO(), jwksUrl)

    if err != nil {
        log.WithFields(log.Fields{
            "error": err,
            "url":   jwksUrl,
        }).Fatal("error getting keyset")
    }
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

را jwksUrl متغیر بالا در راهنمای توسعه دهنده AWS مستند شده است. و من از "github.com/lestrrat-go/jwx/jwt" به نمایندگی از KeySet که من برای تأیید اعتبار و انقضای توکن با آنها کار خواهم کرد. یادت باشد USER_POOL_ID متغیر در CDK بالا؟ اینجاست که مطرح می شود. ساختن آن نقطه پایانی معروف به UserPoolId نیاز دارد

بخش بعدی این فرآیند انجام اعتبار سنجی است. من قصد ندارم در این مقاله به جزئیات این مقاله بپردازم که چگونه این اتفاق می‌افتد، اما اساساً کتابخانه می‌خواهد:

  • ساختار توکن را بررسی کنید
  • بررسی کنید که کلید امضا با الگوریتم کلید مورد استفاده مطابقت دارد
  • انقضا را تأیید کنید و اینکه توکن منقضی نشده باشد

این چیز خوبی در مورد استفاده از کتابخانه است 🙂 و در اینجا نحوه فراخوانی آن آمده است.

bounds := len(event.AuthorizationToken)
token := event.AuthorizationToken[7:bounds]
parsedToken, err := jwt.Parse(
    []byte(token),
    jwt.WithKeySet(keySet),
    jwt.WithValidate(true),
)
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

خروجی از jwt.Parse یک را برمی گرداند error اگر هر یک از موارد بالا شکست بخورد. این بدان معناست که در آن صورت، شما می توانید انکار کنید. مثل این:

return events.APIGatewayCustomAuthorizerResponse{
    PrincipalID: "",
    PolicyDocument: events.APIGatewayCustomAuthorizerPolicy{
        Version: "2012-10-17",
        Statement: []events.IAMPolicyStatement{
            {
                Action:   []string{"execute-api:Invoke"},
                Effect:   "Deny", // Here is the rejection
                Resource: []string{"*"},
            },
        },
    },
    UsageIdentifierKey: "",
}, nil
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

توجه داشته باشید که من خطایی را بر نمی گردم. این به سادگی دسترسی را رد می کند. پاسخ 403 خطا نیست، پس چرا یکی را برگردانید؟

و در صورت ثابت بودن همه چیز، فقط اجازه را برگردانید.

return events.APIGatewayCustomAuthorizerResponse{
    PrincipalID: "",
    PolicyDocument: events.APIGatewayCustomAuthorizerPolicy{
        Version: "2012-10-17",
        Statement: []events.IAMPolicyStatement{
            {
                Action:   []string{"execute-api:Invoke"},
                Effect:   "Allow", // Return Allow
                Resource: []string{"*"},
            },
        },
    },
    Context:            DumpClaims(parsedToken),
    UsageIdentifierKey: "",
}, nil

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

من همچنین می خواهم آن را برجسته کنم DumpClaims تابع. چه کاری انجام می دهد؟

یکی از چیزهای جالب در مورد Lambda Authorizers این است که می توانید آنچه را که به عنوان “زمینه” ارسال می شود به طرف های پایین دست گسترش دهید. اگر بخواهید قطعاتی از توکن را به مقصد مورد نظر حمل کنید، چه؟ درخواست به همراه جزئیاتی که عمومی هستند برای JWT ارسال می‌شود، اما ادعاهای خصوصی یا مواردی که شما تمدید کرده‌اید ارسال نمی‌شود. شاید یک شناسه مشتری؟ شاید چند نقش؟

func DumpClaims(token jwt.Token) map[string]interface{} {
    m := make(map[string]interface{})

    m["customKey"] = "SomeValueHere"

    return m
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برای این مقاله، ساده است، من فقط یک را اضافه می کنم customKey به بافت. من به شما نشان خواهم داد که چگونه آن را به زودی نشان می دهد.

CDK منبع محافظت شده

نیمی از لذت ساخت یک API Gateway Authorizer سفارشی با Golang به پایان رسیده است. این فقط به این معنی است که نیمه دیگر شروع می شود! حالا که صاحب اختیار داریم چه کنیم؟ البته یک منبع محافظت شده را پشت سر بگذارید!

constructor(scope: Construct, id: string, func: IFunction) {
    super(scope, id);

    const authorizer = new TokenAuthorizer(this, "TokenAuthorizer", {
        authorizerName: "BearTokenAuthorizer",
        handler: func,
    });

    this._api = new RestApi(this, "RestApi", {
        description: "Sample API",
        restApiName: "Sample API",
        deployOptions: {
            stageName: `main`,
        },
        defaultMethodOptions: {
            authorizer: authorizer,
        },
    });
}

وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

این کد CDK دروازه API است. توجه کنید در defaultMethodOptions که من یک “مجوز” اضافه می کنم. این فقط یک است IFunction. که باز هم می تواند وارداتی باشد یا در مورد ما، Autorizerی است که ما به تازگی ساخته ایم.

اکنون با یک API، می توانیم یک منبع ایجاد کنیم.

constructor(scope: Construct, id: string, api: RestApi) {
    super(scope, id);

    this._func = new GoFunction(this, `ProtectedResource`, {
        entry: path.join(__dirname, `../../../src/protected-resource`),
        functionName: `protected-resource-func`,
        timeout: Duration.seconds(30),
    });

    api.root.addMethod(
        "GET",
        new LambdaIntegration(this._func, {
            proxy: true,
        })
    );
}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

برای مثال، من از یک ادغام پراکسی Lambda استفاده می کنم و آن را در سطح “ریشه” تعریف می کنم. بنابراین می توانیم انتظار درخواست GET را در مسیر “https://dev.to/” داشته باشیم.

کنترل کننده واقعی برای این نقطه پایانی دوباره یک نمایش ساده است.

func handler(ctx context.Context, event events.APIGatewayProxyRequest) (*events.APIGatewayProxyResponse, error) {

    success := &Response{
        Message:   "Congrats! A Payload",
        CustomKey: event.RequestContext.Authorizer["customKey"].(string),
    }

    b, _ := json.Marshal(success)
    return &events.APIGatewayProxyResponse{
        Body:       string(b),
        StatusCode: 200,
        Headers: map[string]string{
            "Content-Type": "application/json",
        },
    }, nil

}
وارد حالت تمام صفحه شوید

از حالت تمام صفحه خارج شوید

به استفاده از customKey و event.RequestContext.Authorizer["customKey"].(string). این event.RequestContext.Authorizer یک «نقشه» دارد[string]رابط{} که می توانید به نفع خود استفاده کنید.

موارد استفاده بی پایان هستند، اما من از آن برای جزئیات مشتری و نقش های کاربر و داده های نمایه ای که گسترش داده ام استفاده می کنم.

همه اش را بگذار کنار هم

اجازه دهید خروجی یک API Gateway Authorizer سفارشی را با Golang جمع آوری کنیم. برای آن، در اینجا سناریویی برای آزمایش این همه با هم وجود دارد.

اولین چیز

در یک حساب بوت استرپ:

base
cdk deploy

Create a Cognito User

Once the infrastructure is deployed, you should have

  • 2 Lambdas
    • Authorizer
    • ProtectedResource
  • API Gateway
    • One endpoint to the ProtectedResource with the Authoirzer attached
    • An Authorizer
    • A Deployed Stage
  • A Cognito UserPool

Here is what your UserPool should look like

UserPool. Notice the User Pool ID (I’ve cleared mine for reasons). You’ll want to copy that ID as it’ll matter later.

Now the Client List
ClientList

ClientID در آن جدول نیز مهم خواهد بود. دوباره، مال من پاک شده است، اما به شما توجه داشته باشید.

در آخر، یک کاربر ایجاد کنید و آنها را به عنوان تأیید شده علامت بزنید.

CreatedUser

رمز عبور آنها را علامت گذاری کنید زیرا ما در عرض یک دقیقه از Password Flow برای ورود به سیستم استفاده می کنیم

از دروازه API بازدید کنید

برای منبع اصلی محافظت شده ما، اینگونه ایجاد می شود

منبع محافظت شده

قسمت Authorization در BearerTokenAuthorizer که در ابتدای این مقاله تعریف کردیم، اشاره دارد.

و سپس آن Authorizer در دروازه API به این صورت تعریف می شود. به خاطر داشته باشید، اگر از Base Path Mapping همانطور که در این مقاله تعریف شده است استفاده می‌کنید و Autorizer را به اشتراک می‌گذارید، باید آن را برای هر یک از دروازه‌های API خود پیوست کنید.

Autorizer API Gateway

اجرای درخواست

ما بالاخره آماده اجرای این کار هستیم.

اما اول، بیایید یک نشانه را گیر بیاوریم. یادتان هست که گفتم ClientID را در UserPool ضبط کنم؟ اکنون زمان آن است که آن را آشکار کنید.

درخواست توکن را دریافت کنید

خروجی این سه توکن شما خواهد بود.

  • نشانه دسترسی
  • شناسه شناسه
  • Refresh Token

در درخواست بعدی می توانید از ID یا Access استفاده کنید.

انجام درخواست ساده است.

درخواست شکست

ابتدا، بیایید ببینیم با Bad Token چه اتفاقی می‌افتد

درخواست پستچی

درخواست بد

و Logs شما در CloudWatch باید به این شکل باشد

خرابی Cloudwatch

درخواست موفقیت آمیز

حالا برای موفقیت!

درخواست پستچی

درخواست خوب

و Logs شما در CloudWatch باید شبیه این باشد

خرابی Cloudwatch

شما آن را انجام داده اید!

آزمایش این به صورت محلی با رویدادهای نمونه

اگر درج نمی کنم، شما می توانید برخی از آزمایشات محلی مجوز دهنده را نیز انجام دهید، نادیده گرفته می شوم. این می تواند به 2 صورت اتفاق بیفتد

  1. برخی از تست های واحد
  2. استفاده از فایل رویداد آزمایشی

اجرای فایل محلی

اگر اجرا کنید سی دی کی سینت به صورت محلی در این پشته، در نهایت با a مواجه خواهید شد MainStack.template.json در cdk.out فهرست راهنما. می توانید فایل آزمایشی موجود در مخزن را به این صورت اجرا کنید

bash
sam local invoke AuthorizerFunc -t cdk.out/MainStack.template.json --event src/authorizer/test-events/e-1.json --env-vars environment.json --skip-pull-image

بسته بندی

این مقاله طولانی با جزئیات زیاد بود، اما این الگو هنگام ساختن APIهای امن و مقیاس‌پذیر با فناوری‌های بدون سرور بسیار مفید است. با افزودن یک API Gateway Authorizer سفارشی با Golang، می‌توانید این منطق مجوز را بالاتر از پشته و منابع پایین‌دستی را از پرداختن به این کد تکراری ذخیره کنید. علاوه بر این، اما با استفاده از زمینه رویداد در پایین دست Lambda خود، می توانید از PrivateClaims که ممکن است سفارشی کرده باشید استفاده کنید.

اگر می‌خواهید همه اینها را خودتان ببینید تا بتوانید آن را به صورت محلی اجرا کنید، از مخزن GitHub من دیدن کنید.

مثل همیشه، از خواندن متشکریم و امیدواریم که این به شما کمک کند تا برنامه های بدون سرور جالب تری بسازید!

نوشته های مشابه

دیدگاهتان را بنویسید

نشانی ایمیل شما منتشر نخواهد شد. بخش‌های موردنیاز علامت‌گذاری شده‌اند *

همچنین ببینید
بستن
دکمه بازگشت به بالا