سفارشی 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
. 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
ClientID در آن جدول نیز مهم خواهد بود. دوباره، مال من پاک شده است، اما به شما توجه داشته باشید.
در آخر، یک کاربر ایجاد کنید و آنها را به عنوان تأیید شده علامت بزنید.
رمز عبور آنها را علامت گذاری کنید زیرا ما در عرض یک دقیقه از Password Flow برای ورود به سیستم استفاده می کنیم
از دروازه API بازدید کنید
برای منبع اصلی محافظت شده ما، اینگونه ایجاد می شود
قسمت Authorization در BearerTokenAuthorizer که در ابتدای این مقاله تعریف کردیم، اشاره دارد.
و سپس آن Authorizer در دروازه API به این صورت تعریف می شود. به خاطر داشته باشید، اگر از Base Path Mapping همانطور که در این مقاله تعریف شده است استفاده میکنید و Autorizer را به اشتراک میگذارید، باید آن را برای هر یک از دروازههای API خود پیوست کنید.
اجرای درخواست
ما بالاخره آماده اجرای این کار هستیم.
اما اول، بیایید یک نشانه را گیر بیاوریم. یادتان هست که گفتم ClientID را در UserPool ضبط کنم؟ اکنون زمان آن است که آن را آشکار کنید.
خروجی این سه توکن شما خواهد بود.
- نشانه دسترسی
- شناسه شناسه
- Refresh Token
در درخواست بعدی می توانید از ID یا Access استفاده کنید.
انجام درخواست ساده است.
درخواست شکست
ابتدا، بیایید ببینیم با Bad Token چه اتفاقی میافتد
درخواست پستچی
و Logs شما در CloudWatch باید به این شکل باشد
درخواست موفقیت آمیز
حالا برای موفقیت!
درخواست پستچی
و Logs شما در CloudWatch باید شبیه این باشد
شما آن را انجام داده اید!
آزمایش این به صورت محلی با رویدادهای نمونه
اگر درج نمی کنم، شما می توانید برخی از آزمایشات محلی مجوز دهنده را نیز انجام دهید، نادیده گرفته می شوم. این می تواند به 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 من دیدن کنید.
مثل همیشه، از خواندن متشکریم و امیدواریم که این به شما کمک کند تا برنامه های بدون سرور جالب تری بسازید!