Blog

Go Programlama Dili ile Restful Api Geliştirme

Bu yazıda PostgreSQL ve Go dili kullanarak basit CRUD işlemlerini yapabileceğimiz bir Restful API oluşturacağız. Yazı, Go hakkında temel düzeyde bilgisi olanlara hitap etmektedir. Eğer henüz Go ile tanışmadıysanız, tanışmak için bu yazıyı okuyup sonra tekrar buraya dönmenizi tavsiye ederim 🙂

Yukarıda bağlantısını verdiğim linkteki Go kurulumu, ortam değişkeni ayarlamaları gibi adımları geçtiyseniz, PostgreSQL kurulum ve yapılandırmalarını da tamamlayıp kodlamaya başlayabilirsiniz.

Gelen isteklerin alınması ve yönlendirilmesi için router ihtiyacımızı gorilla/mux paketini kullanacağız. Paketi yüklemek için ;

go get –u github.com/gorilla/mux

komutunu çalıştırıyoruz.

Veri tabanı bağlantımızı sağlamak ve veri tabanı işlemlerimizi hazırlamak için Go için geliştirilmiş bir ORM paketi olan jinxhu/gorm ‘u kullanacağız. GORM ‘u da çok rahat ve kolay bir şekilde kullanabilmemiz için çok başarılı bir dokümantasyonu var. Paketi yüklemek için;

go get -u github.com/jinzhu/gorm 

komutunu çalıştırıyoruz.

Kullanıcı kimlik doğrulama işlemlerini yönetebilmek için bir JWT ( JSON Web Token hakkında daha fazla bilgi için bu bağlantıyı kullanabilirsiniz ) paketine ihtiyacımız var. Bu ihtiyacımızı da dgrijalva/jwt-go paketi ile gidereceğiz. Paketi yüklemek için;

go get –u github.com/dgrijalva/jwt-go

komutunu çalıştırıyoruz.

Gopher

Environment dosyamızı (.env) yükleyebilmek için joho/godotenv paketini kullanacağız. Paketi yüklemek için;

go get –u github.com/joho/godotenv

komutunu çalıştırıyoruz.

Bu komutlarla birlikte, tüm paketlerimizi $GOPATH altına kurmuş oluyoruz.

Şimdi projemizin dosya yapısına göz atalım.

İşimize sık sık yarayacak, ufak fonksiyonlarımızı yazacağımız “util.go” dosyamızı yaratalım. Bu dosyaya JSON mesajlarını döndüren “Message” ve JSON yanıtlarını döndüren “Respond” fonksiyonlarını yazalım.

package utils
import (
    "encoding/json"
    "net/http"
)

func Message(status bool, message string) map[string]interface{} {
    return map[string]interface{}{"status": status, "message": message}
}

func Respond(w http.ResponseWriter, data map[string]interface{}) {
    w.Header().Add("Content-Type", "application/json")
    json.NewEncoder(w).Encode(data)
}

Veri tabanı bağlantı bilgilerimizi bulunduracağımız “.env” dosyasını yaratıyoruz ve yapılandırma ayarlarımızı yazıyoruz;

db_name = testapiproject
db_pass = 1 
db_user = postgres
db_type = postgres
db_host = localhost
db_port = 5433
token_password = ks_2tcVkYTIDmwr7895_MYKofCY56SyQK56HhL5TND1TJLXfn41nxuT0OK0

Şimdi de kimlik doğrulamalarını yapacağımız “auth.go” dosyamızı yaratacağız. Ancak model ve utils gibi dosyalara erişebilmemiz için projemize bir modül yaratmamız ve isim vermemiz gerekiyor. Bunun için;

go mod init projeAdi 

komutunu kullanıyoruz.

package app

import (
	"context"
	"fmt"
	"GoRestProject/models"
	u "GoRestProject/utils"
	"net/http"
	"os"
	"strings"

	jwt "github.com/dgrijalva/jwt-go"
)

var JwtAuthentication = func(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {

		notAuth := []string{"/api/user/new", "/api/user/login"} // Doğrulama istemeyen endpointler
		requestPath := r.URL.Path                               // mevcut istek yolu

		// Gelen isteğin doğrulama isteyip istemediği kontrol edilir
		for _, value := range notAuth {
			if value == requestPath {
				next.ServeHTTP(w, r)
				return
			}
		}

		response := make(map[string]interface{})
		tokenHeader := r.Header.Get("Authorization") // Header'dan token alınır

		if tokenHeader == "" { // Token yoksa "403 Unauthorized" hatası dönülür
			response = u.Message(false, "Token gönderilmelidir!")
			w.WriteHeader(http.StatusForbidden)
			w.Header().Add("Content-Type", "application/json")
			u.Respond(w, response)
			return
		}

		splitted := strings.Split(tokenHeader, " ") // Token'ın "Bearer {token} / Token {token}" formatında gelip gelmediği kontrol edilir
		if len(splitted) != 2 {
			response = u.Message(false, "Hatalı ya da geçersiz token!")
			w.WriteHeader(http.StatusForbidden)
			w.Header().Add("Content-Type", "application/json")
			u.Respond(w, response)
			return
		}

		tokenPart := splitted[1] // Token'ın doğrulama yapmamıza yarayan kısmı alınır
		tk := &models.Token{}

		token, err := jwt.ParseWithClaims(tokenPart, tk, func(token *jwt.Token) (interface{}, error) {
			return []byte(os.Getenv("token_password")), nil
		})

		if err != nil { // Token hatalı ise 403 hatası dönülür
			response = u.Message(false, "Token hatalı!")
			w.WriteHeader(http.StatusForbidden)
			w.Header().Add("Content-Type", "application/json")
			u.Respond(w, response)
			return
		}

		if !token.Valid { // Token geçersiz ise 403 hatası dönülür
			response = u.Message(false, "Token geçersiz!")
			w.WriteHeader(http.StatusForbidden)
			w.Header().Add("Content-Type", "application/json")
			u.Respond(w, response)
			return
		}

		// Doğrula başarılı ise işleme devam edilir
		fmt.Sprintf("Kullanıcı %", tk.Username) // Kullanıcı adı console'a basılır
		ctx := context.WithValue(r.Context(), "user", tk.UserId)
		r = r.WithContext(ctx)
		next.ServeHTTP(w, r)
	})
}

Bu dosyada temel olarak;

  • Kimlik doğrulaması isteyen ve istemeyen endpointler belirlendi ve gelen isteğin doğrulama isteğine göre yönlendirmesi yapıldı.
  • Doğrulama istenen bir yere gidilmek isteniyorsa, taleple birlikte iletilmiş olan header bilgisi içerisinden Token alındı.
  • Tokenın geçerliği ve doğruluğu kontrol edildi. Eğer her şey yolundaysa talep edilen uç noktaya erişime izin verildi.

Kimlik doğrulama kontrollerimizi de yazdıktan sonra artık kullanıcı yaratma ve login endpointlerini yazabiliriz. Bunun için öncelikle veri tabanı bağlantılarımızı yapacağımız ve veri tabanı migrasyon işlemlerinin yapılacağı temel model dosyamızı yaratıyoruz. “models/base.go”

package models

import (
	"fmt"
	"os"

	"github.com/jinzhu/gorm"
        _ "github.com/jinzhu/gorm/dialects/postgres"
	"github.com/joho/godotenv"
)

var db *gorm.DB //database

func init() {

	e := godotenv.Load() //Load .env file
	if e != nil {
		fmt.Print(e)
	}

	username := os.Getenv("db_user")
	password := os.Getenv("db_pass")
	dbName := os.Getenv("db_name")
	dbHost := os.Getenv("db_host")

        // Connection stringi yaratılır
	dbUri := fmt.Sprintf("host=%s user=%s dbname=%s sslmode=disable password=%s", dbHost, username, dbName, password)
	
	// Eğer Heroku üzerinde bir PostgreSQL'e sahipseniz, bu ayarlamaları yapmak yerine doğrudan 
        // heroku tarafından verilen database url'i kullanabilirsiniz
	// dbUri := fmt.Sprintf("postgres://xxxxx@xxx.compute.amazonaws.com:5432/ddjkb1easq2mec") // Database url
	
	fmt.Println(dbUri)

	conn, err := gorm.Open("postgres", dbUri)
	if err != nil {
		fmt.Print(err)
	}

	db = conn
	db.Debug().AutoMigrate(&Account{}) //Database migration
}

//returns a handle to the DB object
func GetDB() *gorm.DB {
	return db
}

Yukarıdaki kodu yazdığımızda 38. satıra denk gelen “db.Debug().AutoMigrate(&Account{})” kodu, Account modeli henüz varolmadığı için hata verecektir ama sorun değil, biraz sonra onu da yaratacağız.

Go projemiz çalıştırıldığında, “init” fonksiyonu otomatik olarak çalıştırılacak ve environment dosyamıza yazmış olduğumuz bilgiler ile veritabanı bağlantımız yapılmaya çalışılacak, eğer sorun yok ise bu sefer belirttiğimiz modeller kullanılarak veritabanı migrasyonu yapılacak.

Veri tabanı bağlantısı, migrasyonlar ve kimlik doğrulama işlemlerini tamamladıktan sonra artık projemizin ilk çalışacak kısmı olan “main.go” dosyamızı yazabiliriz.

package main

import (
	"GoRestProject/app"
	"fmt"
	"net/http"
	"os"

	"github.com/gorilla/mux"
)

func main() {
	router := mux.NewRouter()
	router.Use(app.JwtAuthentication) // Middleware'e JWT kimlik doğrulaması eklenir

	port := os.Getenv("PORT") // Environment dosyasından port bilgisi getirilir
	if port == "" {
		port = "8000" //localhost:8000
	}

	fmt.Println(port)

	err := http.ListenAndServe(":"+port, router) // Uygulamamız localhost:8000/api altında istekleri dinlemeye başlar
	if err != nil {
		fmt.Print(err)
	}
}

Veritabanı ve api’ımızın ana uç noktası hazır olduğuna göre artık kullanıcı modelini yaratabiliriz. “models/accounts.go” dosyasının en başına migrasyon esnasında, veri tabanına yaratılmasını istediğimiz structı ekleyeceğiz. Bu strunctın bir elemanı da “gorm.Model” olacak. Bu elemanın bulunmadığı structlar, migrasyonla veri tabanında yaratılmazlar.

Bunun ardından;

  • Gelen email adreslerinin doğruluğu, parola uzunluğu vs. gibi doğrulamaların bulunacağı “Validate” fonksiyonu,
  • Yeni kullanıcı hesabı yaratmamızı sağlayacak olan “Create” fonksiyonu,
  • Kullanıcı giriş işleminin gerçekleştirileceği “Login” fonksiyonu,
  • Kullanıcı bilgilerini getirmemizi sağlayacak olan “GetUser” fonksiyonları yazılacak.
package models

import (
	u "GoRestProject/utils"
	"os"
	"strings"

	"github.com/dgrijalva/jwt-go"
	"github.com/jinzhu/gorm"
	"golang.org/x/crypto/bcrypt"
)

// JWT struct
type Token struct {
	UserId   uint
	Username string
	jwt.StandardClaims
}

// Kullanıcı tablosu struct
type Account struct {
	gorm.Model        // Migrasyon işlemi yapılırken, veritabanı üzerinde accounts tablosu yaratılması için belirtilir
	Email      string `json:"email"`
	Password   string `json:"password"`
	Token      string `json:"token";sql:"-"`
}

// Gelen bilgileri doğrulama fonksiyonu
func (account *Account) Validate() (map[string]interface{}, bool) {

	if !strings.Contains(account.Email, "@") {
		return u.Message(false, "Email adresi hatalıdır!"), false
	}

	if len(account.Password) < 8 {
		return u.Message(false, "Şifreniz en az 8 karakter olmalıdır!"), false
	}

	temp := &Account{}

	// Email adresinin kayıtlı olup olmadığı kontrol edilir
	err := GetDB().Table("accounts").Where("email = ?", account.Email).First(temp).Error
	if err != nil && err != gorm.ErrRecordNotFound {
		return u.Message(false, "Bağlantı hatası oluştu. Lütfen tekrar deneyiniz!"), false
	}
	if temp.Email != "" {
		return u.Message(false, "Email adresi başka bir kullanıcı tarafından kullanılıyor."), false
	}

	return u.Message(false, "Her şey yolunda!"), true
}

// Kullanıcı hesabı yaratma fonksiyonu
func (account *Account) Create() map[string]interface{} {

	if resp, ok := account.Validate(); !ok {
		return resp
	}

	hashedPassword, _ := bcrypt.GenerateFromPassword([]byte(account.Password), bcrypt.DefaultCost)
	account.Password = string(hashedPassword)

	GetDB().Create(account)

	if account.ID <= 0 {
		return u.Message(false, "Bağlantı hatası oluştu. Kullanıcı yaratılamadı!")
	}

	// Yaratılan hesap için JWT oluşturulur
	tk := &Token{UserId: account.ID}
	token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), tk)
	tokenString, _ := token.SignedString([]byte(os.Getenv("token_password")))
	account.Token = tokenString

	account.Password = "" // Yanıt içerisinden parola silinir

	response := u.Message(true, "Hesap başarıyla yaratıldı!")
	response["account"] = account
	return response
}

// Giriş yapma fonksiyonu
func Login(email, password string) map[string]interface{} {

	account := &Account{}
	err := GetDB().Table("accounts").Where("email = ?", email).First(account).Error
	if err != nil {
		if err == gorm.ErrRecordNotFound {
			return u.Message(false, "Email adresi bulunamadı!")
		}
		return u.Message(false, "Bağlantı hatası oluştu. Lütfen tekrar deneyiniz!")
	}

	err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password))
	if err != nil && err == bcrypt.ErrMismatchedHashAndPassword { // Parola eşleşmedi
		return u.Message(false, "Parola hatalı! Lütfen tekrar deneyiniz!")
	}

	// Giriş başarılı
	account.Password = ""

	// JWT yaratılır
	tk := &Token{UserId: account.ID}
	token := jwt.NewWithClaims(jwt.GetSigningMethod("HS256"), tk)
	tokenString, _ := token.SignedString([]byte(os.Getenv("token_password")))
	account.Token = tokenString // JWT yanıta eklenir

	resp := u.Message(true, "Giriş başarılı!")
	resp["account"] = account
	return resp
}

// Kullanıcı bilgilerini getirme fonksiyonu
func GetUser(u uint) *Account {
	acc := &Account{}
	GetDB().Table("accounts").Where("id = ?", u).First(acc)
	if acc.Email == "" { // Kullanıcı bulunamadı
		return nil
	}

	acc.Password = ""
	return acc
}

Account modelimizde yazdığımız kullanıcı yaratma ve login fonksiyonlarını kullanmamız için bir de controllera ihtiyacımız var. Bu ihtiyacı da “controllers/authController.go” dosyasını yaratarak gidereceğiz. Bu dosya, router üzerinden kendisine iletilen isteklerin gövdelerini decode edip ilgili modele gönderecek ve modelden gelen yanıtı göndermemizi sağlayacak.

package controllers

import (
	"GoRestProject/models"
	u "GoRestProject/utils"
	"encoding/json"
	"net/http"
)

var CreateAccount = func(w http.ResponseWriter, r *http.Request) {

	account := &models.Account{}
	err := json.NewDecoder(r.Body).Decode(account) // İstek gövdesi decode edilir, hatalı ise hata döndürülür
	if err != nil {
		u.Respond(w, u.Message(false, "Geçersiz istek. Lütfen kontrol ediniz!"))
		return
	}

	resp := account.Create() // Hesap yaratılır
	u.Respond(w, resp)
}

var Authenticate = func(w http.ResponseWriter, r *http.Request) {

	account := &models.Account{}
	err := json.NewDecoder(r.Body).Decode(account) // İstek gövdesi decode edilir, hatalı ise hata döndürülür
	if err != nil {
		u.Respond(w, u.Message(false, "Geçersiz istek. Lütfen kontrol ediniz!"))
		return
	}

	resp := models.Login(account.Email, account.Password) // Giriş yapılır
	u.Respond(w, resp)
}

Artık, main.go içerisine bu controllerı çağıracak birer handler ekleyebilir ve ardından ilk isteğimizi atabiliriz!

“main.go” içerisinden “router.Use(app.JwtAuthentication)” kodunun altına aşağıdaki kodları ekliyoruz;

router.HandleFunc("/api/user/new", controllers.CreateAccount).Methods("POST")

router.HandleFunc("/api/user/login", controllers.Authenticate).Methods("POST")

Yukarıdaki işlemleri de yaptıktan sonra artık “go run main.go” komutu ile api’ımızı başlatabiliriz. Komutu çalıştırmamızın arından veri tabanı bağlantımız yapılacak ve migrasyon işlemi çalışarak tablolarımızı yaratacaktır.

pgAdmin4 üzerinden tablo görüntüsü

“go run main.go” komutu çalıştırıldığında, eğer Windows işletim sistemi olan bir cihazda çalışıyorsanız bir izin penceresi gelecektir. Bu ekranadan gerekli izni vermelisiniz.

Yeni Kullanıcı hesabı oluşturmak için aşağıdaki şekilde istek atılmalıdır.

Kullanıcı hesabı başarılı yaratıldıktan sonra, login işlemine geçebiliriz.

Hepsi bu kadar!

Go ile ufak bir restful api geliştirmek bu kadar hızlı ve kolay! Sorularınız olursa bu bağlantıya tıklayarak bize ulaşabilirsiniz.


Benzer
Bloglar

Mailiniz başarıyla gönderilmiştir en kısa sürede sizinle iletişime geçilecektir.

Mesajınız ulaştırılamadı! Lütfen daha sonra tekrar deneyin.