گردآوری همه چیز: ادغام GraphQL با Gin in Go

در این مرحله از سفر خود، به حوزه ادغام میانافزار با جین و اجرای میانافزار احراز هویت با استفاده از gocloak میپردازیم. با تکیه بر مبنایی که در بخشهای قبلی گذاشته شد، اکنون تلاشهای خود را با ادغام میانافزار بهطور یکپارچه در سرور GraphQL خود متحد میکنیم. با جین، یک چارچوب وب قدرتمند HTTP برای Go، ما قابلیتهای سرور خود را با ترکیب توابع میانافزار برای پیش پردازش درخواستها افزایش میدهیم. با استفاده از gocloak، یک ماژول Go برای رابط با Keycloak، ما سرور خود را با میان افزار احراز هویت ایمن می کنیم. این مرحله محوری نشاندهنده همگرایی همه عناصر قبلی است که در ایجاد تابع اجرای سرور، که اجرای سرور GraphQL API ما را هماهنگ میکند، به اوج خود میرسد. بیایید بررسی کنیم که چگونه این مؤلفه ها برای ارتقاء عملکرد و امنیت سرور ما هماهنگ می شوند.
پیاده سازی میان افزار احراز هویت با Keycloak و Gocloak
میان افزار نهایی که باید به سرور خود اضافه کنیم احراز هویت است. در این مثال، از Keycloak به عنوان ارائهدهنده هویت خود استفاده میکنیم. برای ارتباط با Keycloak در Go، از ماژول gocloak استفاده می کنیم. با استفاده از gocloak، میتوانیم با استفاده از میانافزار Gin احراز هویت را در برابر Keycloak انجام دهیم.
برای ایجاد میان افزار، با تعیین هدری که می خواهیم از درخواست بررسی کنیم، شروع می کنیم. Keycloak از پروتکل OpenID Connect استفاده می کند، بنابراین ما انتظار داریم Authorization
هدر برای شروع با کلمه “Bearer” و به دنبال آن یک فاصله و سپس رشته توکن کامل. در زیر، ثابت را تعریف می کنیم "Bearer "
به این منظور:
const headerPrefix = "Bearer "
در مرحله بعد، باید توکن را تأیید کنیم. برای رسیدن به این هدف، تابعی ایجاد می کنیم که نشانگر پایگاه داده Gorm را می پذیرد (*gorm.DB
) و یک نشانگر درخواست HTTP (*http.Request
). این تابع هدر Authorization را از درخواست استخراج میکند، توکن را با استفاده از Keycloak تأیید میکند و در صورت یافتن مطابقت در پایگاه داده، کاربر را برمیگرداند. اگر تماس ها با موفقیت تکمیل نشود، عملکرد یک خطا برمی گرداند.
func ValidateToken(db *gorm.DB, req *http.Request) (*model.User, error) {
authToken := req.Header.Get("Authorization")
authToken = strings.TrimPrefix(authToken, headerPrefix) // Strip the "Auth " from the bearer token
keycloak := config.Config.Auth
// Make call to keycloak authenticating the token
client := gocloak.NewClient(keycloak.Endpoint)
// Add certificate verification if a certificate path is set
if len(keycloak.CertificatePath) > 0 {
log.Infof("Reading certificate from %s...", keycloak.CertificatePath)
cert, err := os.ReadFile(keycloak.CertificatePath)
if err != nil {
log.Errorf("[identity.cert] Unable to read certificate => %v", err)
return nil, err
}
certPool := x509.NewCertPool()
if ok := certPool.AppendCertsFromPEM(cert); !ok {
log.Errorf("[identity.cert] Unable to add cert to pool => %v", err)
return nil, err
}
restyClient := client.RestyClient()
restyClient.SetTLSClientConfig(&tls.Config{RootCAs: certPool})
log.Info("Imported certificate to keycloak client")
}
res, err := client.RetrospectToken(req.Context(), authToken, keycloak.ClientID, keycloak.ClientSecret, keycloak.RealmName)
if err != nil {
log.Errorf("unable to validate access token => %v", err)
return nil, err
}
log.Debugf("[auth] Access Token => %v", *res)
if !*res.Active {
err = errors.New("session is not active")
log.Errorf("session is not active => %v", err)
return nil, err
}
// fetch userinfo and query the database for the user
info, err := client.GetUserInfo(req.Context(), authToken, keycloak.RealmName)
if err != nil {
log.Errorf("unable to fetch user info => %v", err)
return nil, err
}
// add the user to the database if there is no current entry for the user
var user model.User
if err = db.FirstOrCreate(&user, model.User{
Username: *info.PreferredUsername,
Name: fmt.Sprintf("%s %s", *info.GivenName, *info.FamilyName),
}).Error; err != nil {
log.Errorf("unable to save user to database => %v", err)
return nil, err
}
log.Debug(user)
return &user, nil
}
این AuthenticationMiddleware
تابع برای ادغام احراز هویت در یک وب سرور جین طراحی شده است. این تابع یک نشانگر پایگاه داده Gorm می گیرد (*gorm.DB
) به عنوان آرگومان و تابع Gin handler را برمی گرداند. در داخل گرداننده، میان افزار فراخوانی می کند ValidateToken
تابع، نشانگر پایگاه داده و درخواست HTTP فعلی را ارسال می کند. اگر اعتبار سنجی توکن ناموفق باشد، یک خطا ثبت می شود و درخواست با وضعیت HTTP 403 (ممنوع) لغو می شود. اگر توکن با موفقیت تأیید شود، اطلاعات کاربر به زمینه درخواست اضافه می شود و به کنترل کننده های پایین دستی امکان دسترسی به آن را می دهد. در نهایت، میان افزار تماس می گیرد c.Next()
برای انتقال کنترل به کنترل کننده بعدی در زنجیره.
func AuthenticationMiddleware(db *gorm.DB) gin.HandlerFunc {
return func(c *gin.Context) {
user, err := ValidateToken(db, c.Request)
if err != nil {
log.Errorf("unable to authenticate token => %v", err)
err = c.AbortWithError(http.StatusForbidden, err)
log.Debug(err)
return
}
c.Request = c.Request.WithContext(context.WithValue(c.Request.Context(), userKey, user))
c.Next()
}
}
در نهایت، ForUser
تابع برای بازیابی کاربر تأیید شده از زمینه درخواست در یک وب سرور Gin طراحی شده است. این تابع یک زمینه (ctx
) به عنوان یک آرگومان و تلاش برای استخراج اطلاعات کاربر ذخیره شده در زمینه با استفاده از یک کلید از پیش تعریف شده (userKey
). از آن استفاده می کند ctx.Value
روش دسترسی به مقدار مرتبط با userKey
و برای تبدیل آن به a یک نوع ادعا را انجام می دهد *model.User
. اگر اطلاعات کاربر پیدا نشد یا نوع اظهار ناموفق بود، تابع برمی گردد nil
. این تابع ابزار به سایر بخشهای برنامه اجازه میدهد تا به راحتی به کاربر احراز هویت شده از زمینه دسترسی داشته باشند و عملیات خاص کاربر و مدیریت دادهها را تسهیل کند.
func ForUser(ctx context.Context) *model.User {
user, _ := ctx.Value(userKey).(*model.User)
return user
}
نهایی کردن راه اندازی سرور: عملکرد اجرا
با وجود تمام اجزای میانافزاری که در سرتاسر سری ایجاد شدهاند، زمان آن رسیده است که همه چیز را با هم جمع کنیم و تنظیمات سرور را نهایی کنیم. این Run
تابع به عنوان چسب عمل می کند، ادغام اجزای مختلف میان افزار را هماهنگ می کند و سرور جین را راه اندازی می کند. این تابع معمولاً روتر Gin را مقداردهی اولیه میکند، لایههای میانافزار را به ترتیب دلخواه اعمال میکند و مسیرها یا نقاط پایانی را برای رسیدگی به درخواستهای دریافتی تعریف میکند. این پیکربندی سرور را کپسوله می کند و یک نقطه ورودی یکپارچه برای راه اندازی وب سرور فراهم می کند. با ادغام راهاندازی میانافزار و منطق اولیهسازی سرور در یک عملکرد واحد، از ثبات، نگهداری و سهولت مدیریت برای کل برنامه سرور اطمینان حاصل میکنیم.
این Run
تابع پیکربندی سرور را تنظیم می کند، مسیرها را تعریف می کند، میان افزار را اعمال می کند و سرور را راه اندازی می کند. سرور را با میان افزار پیش فرض Gin راه اندازی می کند، نقاط پایانی را برای محرک، زمین بازی GraphQL و خود GraphQL تنظیم می کند. پشته میانافزار شامل سرویسها، میانافزار بارگذار داده، و میانافزار احراز هویت برای رسیدگی به جنبههای مختلف پردازش درخواست و امنیت است. در نهایت، سرور را شروع می کند تا به میزبان و پورت پیکربندی شده گوش دهد و نقطه پایانی را برای مرجع ثبت کند.
func Run(db *gorm.DB) {
config := config.Config
endpoint := fmt.Sprintf("%s:%d", config.Service.Host, config.Service.Port)
r := gin.Default()
r.GET("/actuator/*endpoint", handlers.ActuatorHandler(db))
r.Use(middleware.Services(db, index.IndexConnection))
r.Use(middleware.DataloaderMiddleware())
r.GET(config.Service.PlaygroundPath, handlers.PlaygroundHandler())
r.Use(gin.Recovery())
secured := r.Group(config.Service.Path)
secured.Use(middleware.AuthenticationMiddleware(db))
secured.POST("https://dev.to/", handlers.GraphqlHandler())
log.Infof("Running @ http://%s", endpoint)
log.Fatal(r.Run(endpoint))
}
این Run
تابع به عنوان نقطه مرکزی منطق سرور ما عمل می کند و ادغام GraphQL با Gin in Go را هماهنگ می کند. کپسوله شده در داخل pkg/server
بسته، نشاندهنده اوج تلاشهای ما در ماژولهای مختلف و لایههای میانافزار است. در پایین نقطه ورودی برنامه ما، واقع در cmd/main.go
، استناد می کنیم server.Run
برای راه اندازی سرور و زنده کردن برنامه مجهز به GraphQL.
نکات پایانی: بازبینی GraphQL
این GraphqlHandler
تابع به عنوان نقطه ورود برای درخواست های GraphQL در سرور ما عمل می کند. a را مقدار دهی اولیه می کند config
ساختار با توابع حل کننده ارائه شده توسط ما graph
بسته بندی علاوه بر این، دستورالعمل هایی مانند اعتبار سنجی را برای استفاده در هنگام اجرای پرس و جو پیکربندی می کند. در نهایت، با استفاده از یک هندلر ایجاد می کند handler.NewDefaultServer
، در طرحواره اجرایی ایجاد شده توسط gqlgen بر اساس طرح و حل کننده های ما ارسال می شود. سپس این کنترلر به عنوان یک تابع میانافزار Gin برگردانده میشود و به آن اجازه میدهد تا درخواستهای GraphQL را که به سرور ما میآیند، پردازش کند.
func GraphqlHandler() gin.HandlerFunc {
config := generated.Config{Resolvers: &graph.Resolver{}}
// Add directives
config.Directives.Validate = directives.Validate
h := handler.NewDefaultServer(generated.NewExecutableSchema(config))
return func(c *gin.Context) { h.ServeHTTP(c.Writer, c.Request) }
}
همانطور که ما کارهای نهایی را روی سرور GraphQL خود انجام می دهیم، بیایید دوباره یکی از حل کننده های خود را بررسی کنیم تا نشان دهیم چگونه می توانیم میان افزار را به طور یکپارچه در عملیات GraphQL خود ادغام کنیم. میانافزار نقش مهمی در رهگیری و افزایش درخواستها قبل از رسیدن به حلکنندههای ما بازی میکند و به ما امکان میدهد کارهای اضافی مانند احراز هویت، ثبتنام یا دستکاری دادهها را انجام دهیم. با ادغام میانافزار در توابع حلکنندهمان، میتوانیم عملکرد و امنیت GraphQL API خود را بدون به هم ریختن منطق حلکننده خود افزایش دهیم. بیایید به جزئیات این که چگونه میان افزارها می توانند به طور یکپارچه در معماری سرور GraphQL ما گنجانده شوند، بپردازیم.
در این قطعه کد Go، ما از User Resolver که قبلاً در سرور GraphQL ما پیاده سازی شده بود، دوباره بازدید می کنیم. این تابع حلکننده که Pantries نام دارد، مسئول واکشی فهرستی از انبارهای مرتبط با یک کاربر خاص است. در داخل تابع، ما از طریق زمینه به لایه خدمات دسترسی پیدا می کنیم و از یک تابع میان افزار برای بازیابی سرویس لازم استفاده می کنیم. پس از به دست آوردن، ما متد FetchPantriesByAuthor را از PantryService فراخوانی می کنیم تا انبارهای مرتبط با کاربر را بازیابی کنیم. این تابع پارامترهای اختیاری مانند سفارش، جزئیات صفحه بندی (startAt و اندازه) را می پذیرد و یک PantryList را به همراه هر گونه خطای احتمالی که در طول فرآیند با آن مواجه می شود برمی گرداند. این حلکننده نشان میدهد که چگونه میانافزار میتواند به طور یکپارچه با توابع حلکننده ادغام شود تا عملکرد سرور GraphQL ما را افزایش دهد.
func (r *userResolver) Pantries(ctx context.Context, obj *model.User, order *model.SearchOrder, startAt *int, size *int) (*model.PantryList, error) {
services := middleware.ForServices(ctx)
return services.PantryService.FetchPantriesByAuthor(order, &obj.ID, startAt, size)
}
نتیجه گیری: همه را با هم جمع کنید
در خاتمه، این سری مقاله راهنمای جامعی برای ساخت یک سرور API قوی GraphQL در Go ارائه کرده است. ما با راهاندازی gqlgen برای ادغام GraphQL شروع کردیم، آن را مطابق با قراردادهای پروژه Go سفارشی کردیم و طرح GraphQL خود را با حلکنندهها تعریف کردیم. ما مدل دادههای خود را با استفاده از سرویسها انتزاع کردیم، آنها را با استفاده از میانافزار یکپارچه کردیم و اعتبارسنجی در سطح طرحواره را پیادهسازی کردیم. علاوه بر این، ما بازیابی دادهها را با دیتالودرها بهینه کردیم و از اجرای کارآمد پرس و جو اطمینان حاصل کردیم. در نهایت، ما همه چیز را با میان افزار احراز هویت و یک تابع اجرا که در بسته سرور محصور شده بود، به هم گره زدیم. با دنبال کردن این مراحل، ما یک پایه محکم برای ایجاد APIهای GraphQL قدرتمند در Go ایجاد کردهایم که آماده رسیدگی به موارد استفاده مختلف و مقیاسبندی به آسانی هستند.