make docker.run
# Process:# - Generate API docs by Swagger# - Create a new Docker network for containers# - Build and run Docker containers (Fiber, PostgreSQL)# - Apply database migrations (using github.com/golang-migrate/migrate)
# ./DockerfileFROMgolang:1.16-alpineASbuilder# Move to working directory (/build).WORKDIR /build# Copy and download dependency using go mod.COPY go.mod go.sum ./RUN go mod download
# Copy the code into the container.COPY . .# Set necessary environment variables needed for our image # and build the API server.ENV CGO_ENABLED=0 GOOS=linux GOARCH=amd64RUN go build -ldflags="-s -w"-o apiserver .
FROM scratch# Copy binary and config files from /build # to root folder of scratch container.COPY --from=builder ["/build/apiserver", "/build/.env", "/"]# Export necessary port.EXPOSE 5000# Command to run when starting the container.ENTRYPOINT ["/apiserver"]
是的,我使用的是两阶段容器构建和 Golang 1.16.x。应用程序将使用 和 构建CGO_ENABLED=0,-ldflags="-s -w"以减少最终二进制文件的大小。除此之外,这是Dockerfile任何 Go 项目最常见的构建方式,您可以在任何地方使用。
// ./app/models/book_model.gopackagemodelsimport("database/sql/driver""encoding/json""errors""time""github.com/google/uuid")// Book struct to describe book object.typeBookstruct{IDuuid.UUID`db:"id" json:"id" validate:"required,uuid"`CreatedAttime.Time`db:"created_at" json:"created_at"`UpdatedAttime.Time`db:"updated_at" json:"updated_at"`UserIDuuid.UUID`db:"user_id" json:"user_id" validate:"required,uuid"`Titlestring`db:"title" json:"title" validate:"required,lte=255"`Authorstring`db:"author" json:"author" validate:"required,lte=255"`BookStatusint`db:"book_status" json:"book_status" validate:"required,len=1"`BookAttrsBookAttrs`db:"book_attrs" json:"book_attrs" validate:"required,dive"`}// BookAttrs struct to describe book attributes.typeBookAttrsstruct{Picturestring`json:"picture"`Descriptionstring`json:"description"`Ratingint`json:"rating" validate:"min=1,max=10"`}// ...
👍 我建议使用google/uuid包来创建唯一 ID,因为这是一种更通用的方法,可以保护你的应用程序免受常见的数字暴力攻击。尤其是在你的 REST API 包含未经授权且请求量受限的公共方法时。
但这还不是全部。你需要编写两个特殊方法:
Value(),用于返回结构的 JSON 编码表示;
Scan(),用于将 JSON 编码的值解码为结构字段;
它们可能看起来像这样:
// ...// Value make the BookAttrs struct implement the driver.Valuer interface.// This method simply returns the JSON-encoded representation of the struct.func(bBookAttrs)Value()(driver.Value,error){returnjson.Marshal(b)}// Scan make the BookAttrs struct implement the sql.Scanner interface.// This method simply decodes a JSON-encoded value into the struct fields.func(b*BookAttrs)Scan(valueinterface{})error{j,ok:=value.([]byte)if!ok{returnerrors.New("type assertion to []byte failed")}returnjson.Unmarshal(j,&b)}
// ./app/utils/validator.gopackageutilsimport("github.com/go-playground/validator/v10""github.com/google/uuid")// NewValidator func for create a new validator for model fields.funcNewValidator()*validator.Validate{// Create a new validator for a Book model.validate:=validator.New()// Custom validation for uuid.UUID fields._=validate.RegisterValidation("uuid",func(flvalidator.FieldLevel)bool{field:=fl.Field().String()if_,err:=uuid.Parse(field);err!=nil{returntrue}returnfalse})returnvalidate}// ValidatorErrors func for show validation errors for each invalid fields.funcValidatorErrors(errerror)map[string]string{// Define fields map.fields:=map[string]string{}// Make error message for each invalid field.for_,err:=rangeerr.(validator.ValidationErrors){fields[err.Field()]=err.Error()}returnfields}
// ./app/queries/book_query.gopackagequeriesimport("github.com/google/uuid""github.com/jmoiron/sqlx""github.com/koddr/tutorial-go-fiber-rest-api/app/models")// BookQueries struct for queries from Book model.typeBookQueriesstruct{*sqlx.DB}// GetBooks method for getting all books.func(q*BookQueries)GetBooks()([]models.Book,error){// Define books variable.books:=[]models.Book{}// Define query string.query:=`SELECT * FROM books`// Send query to database.err:=q.Get(&books,query)iferr!=nil{// Return empty object and error.returnbooks,err}// Return query result.returnbooks,nil}// GetBook method for getting one book by given ID.func(q*BookQueries)GetBook(iduuid.UUID)(models.Book,error){// Define book variable.book:=models.Book{}// Define query string.query:=`SELECT * FROM books WHERE id = $1`// Send query to database.err:=q.Get(&book,query,id)iferr!=nil{// Return empty object and error.returnbook,err}// Return query result.returnbook,nil}// CreateBook method for creating book by given Book object.func(q*BookQueries)CreateBook(b*models.Book)error{// Define query string.query:=`INSERT INTO books VALUES ($1, $2, $3, $4, $5, $6, $7, $8)`// Send query to database._,err:=q.Exec(query,b.ID,b.CreatedAt,b.UpdatedAt,b.UserID,b.Title,b.Author,b.BookStatus,b.BookAttrs)iferr!=nil{// Return only error.returnerr}// This query returns nothing.returnnil}// UpdateBook method for updating book by given Book object.func(q*BookQueries)UpdateBook(iduuid.UUID,b*models.Book)error{// Define query string.query:=`UPDATE books SET updated_at = $2, title = $3, author = $4, book_status = $5, book_attrs = $6 WHERE id = $1`// Send query to database._,err:=q.Exec(query,id,b.UpdatedAt,b.Title,b.Author,b.BookStatus,b.BookAttrs)iferr!=nil{// Return only error.returnerr}// This query returns nothing.returnnil}// DeleteBook method for delete book by given ID.func(q*BookQueries)DeleteBook(iduuid.UUID)error{// Define query string.query:=`DELETE FROM books WHERE id = $1`// Send query to database._,err:=q.Exec(query,id)iferr!=nil{// Return only error.returnerr}// This query returns nothing.returnnil}
创建模型控制器
方法原理GET:
向 API 端点发出请求;
建立与数据库的连接(否则出现错误);
进行查询以从表中获取记录books(或出现错误);
200返回已创建的书籍的状态和 JSON;
// ./app/controllers/book_controller.gopackagecontrollersimport("time""github.com/gofiber/fiber/v2""github.com/google/uuid""github.com/koddr/tutorial-go-fiber-rest-api/app/models""github.com/koddr/tutorial-go-fiber-rest-api/pkg/utils""github.com/koddr/tutorial-go-fiber-rest-api/platform/database")// GetBooks func gets all exists books.// @Description Get all exists books.// @Summary get all exists books// @Tags Books// @Accept json// @Produce json// @Success 200 {array} models.Book// @Router /v1/books [get]funcGetBooks(c*fiber.Ctx)error{// Create database connection.db,err:=database.OpenDBConnection()iferr!=nil{// Return status 500 and database connection error.returnc.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error":true,"msg":err.Error(),})}// Get all books.books,err:=db.GetBooks()iferr!=nil{// Return, if books not found.returnc.Status(fiber.StatusNotFound).JSON(fiber.Map{"error":true,"msg":"books were not found","count":0,"books":nil,})}// Return status 200 OK.returnc.JSON(fiber.Map{"error":false,"msg":nil,"count":len(books),"books":books,})}// GetBook func gets book by given ID or 404 error.// @Description Get book by given ID.// @Summary get book by given ID// @Tags Book// @Accept json// @Produce json// @Param id path string true "Book ID"// @Success 200 {object} models.Book// @Router /v1/book/{id} [get]funcGetBook(c*fiber.Ctx)error{// Catch book ID from URL.id,err:=uuid.Parse(c.Params("id"))iferr!=nil{returnc.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error":true,"msg":err.Error(),})}// Create database connection.db,err:=database.OpenDBConnection()iferr!=nil{// Return status 500 and database connection error.returnc.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error":true,"msg":err.Error(),})}// Get book by ID.book,err:=db.GetBook(id)iferr!=nil{// Return, if book not found.returnc.Status(fiber.StatusNotFound).JSON(fiber.Map{"error":true,"msg":"book with the given ID is not found","book":nil,})}// Return status 200 OK.returnc.JSON(fiber.Map{"error":false,"msg":nil,"book":book,})}// ...
方法原理POST:
向 API 端点发出请求;
检查请求是否Header具有有效的 JWT;
检查 JWT 的到期日期是否大于现在(或出现错误);
解析请求主体并将字段绑定到 Book 结构(或错误);
建立与数据库的连接(否则出现错误);
使用来自 Body 的新内容(或错误)验证结构字段;
进行查询以在表中创建新记录books(或出现错误);
返回一本新书的状态200和 JSON;
// ...// CreateBook func for creates a new book.// @Description Create a new book.// @Summary create a new book// @Tags Book// @Accept json// @Produce json// @Param title body string true "Title"// @Param author body string true "Author"// @Param book_attrs body models.BookAttrs true "Book attributes"// @Success 200 {object} models.Book// @Security ApiKeyAuth// @Router /v1/book [post]funcCreateBook(c*fiber.Ctx)error{// Get now time.now:=time.Now().Unix()// Get claims from JWT.claims,err:=utils.ExtractTokenMetadata(c)iferr!=nil{// Return status 500 and JWT parse error.returnc.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error":true,"msg":err.Error(),})}// Set expiration time from JWT data of current book.expires:=claims.Expires// Checking, if now time greather than expiration from JWT.ifnow>expires{// Return status 401 and unauthorized error message.returnc.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error":true,"msg":"unauthorized, check expiration time of your token",})}// Create new Book structbook:=&models.Book{}// Check, if received JSON data is valid.iferr:=c.BodyParser(book);err!=nil{// Return status 400 and error message.returnc.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error":true,"msg":err.Error(),})}// Create database connection.db,err:=database.OpenDBConnection()iferr!=nil{// Return status 500 and database connection error.returnc.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error":true,"msg":err.Error(),})}// Create a new validator for a Book model.validate:=utils.NewValidator()// Set initialized default data for book:book.ID=uuid.New()book.CreatedAt=time.Now()book.BookStatus=1// 0 == draft, 1 == active// Validate book fields.iferr:=validate.Struct(book);err!=nil{// Return, if some fields are not valid.returnc.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error":true,"msg":utils.ValidatorErrors(err),})}// Delete book by given ID.iferr:=db.CreateBook(book);err!=nil{// Return status 500 and error message.returnc.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error":true,"msg":err.Error(),})}// Return status 200 OK.returnc.JSON(fiber.Map{"error":false,"msg":nil,"book":book,})}// ...
方法原理PUT:
向 API 端点发出请求;
检查请求是否Header具有有效的 JWT;
检查 JWT 的到期日期是否大于现在(或出现错误);
解析请求主体并将字段绑定到 Book 结构(或错误);
建立与数据库的连接(否则出现错误);
使用来自 Body 的新内容(或错误)验证结构字段;
检查具有此 ID 的书籍是否存在(或出现错误);
进行查询以更新表中的此记录books(或出现错误);
201返回无内容的状态;
// ...// UpdateBook func for updates book by given ID.// @Description Update book.// @Summary update book// @Tags Book// @Accept json// @Produce json// @Param id body string true "Book ID"// @Param title body string true "Title"// @Param author body string true "Author"// @Param book_status body integer true "Book status"// @Param book_attrs body models.BookAttrs true "Book attributes"// @Success 201 {string} status "ok"// @Security ApiKeyAuth// @Router /v1/book [put]funcUpdateBook(c*fiber.Ctx)error{// Get now time.now:=time.Now().Unix()// Get claims from JWT.claims,err:=utils.ExtractTokenMetadata(c)iferr!=nil{// Return status 500 and JWT parse error.returnc.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error":true,"msg":err.Error(),})}// Set expiration time from JWT data of current book.expires:=claims.Expires// Checking, if now time greather than expiration from JWT.ifnow>expires{// Return status 401 and unauthorized error message.returnc.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error":true,"msg":"unauthorized, check expiration time of your token",})}// Create new Book structbook:=&models.Book{}// Check, if received JSON data is valid.iferr:=c.BodyParser(book);err!=nil{// Return status 400 and error message.returnc.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error":true,"msg":err.Error(),})}// Create database connection.db,err:=database.OpenDBConnection()iferr!=nil{// Return status 500 and database connection error.returnc.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error":true,"msg":err.Error(),})}// Checking, if book with given ID is exists.foundedBook,err:=db.GetBook(book.ID)iferr!=nil{// Return status 404 and book not found error.returnc.Status(fiber.StatusNotFound).JSON(fiber.Map{"error":true,"msg":"book with this ID not found",})}// Set initialized default data for book:book.UpdatedAt=time.Now()// Create a new validator for a Book model.validate:=utils.NewValidator()// Validate book fields.iferr:=validate.Struct(book);err!=nil{// Return, if some fields are not valid.returnc.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error":true,"msg":utils.ValidatorErrors(err),})}// Update book by given ID.iferr:=db.UpdateBook(foundedBook.ID,book);err!=nil{// Return status 500 and error message.returnc.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error":true,"msg":err.Error(),})}// Return status 201.returnc.SendStatus(fiber.StatusCreated)}// ...
方法原理DELETE:
向 API 端点发出请求;
检查请求是否Header具有有效的 JWT;
检查 JWT 的到期日期是否大于现在(或出现错误);
解析请求主体并将字段绑定到 Book 结构(或错误);
建立与数据库的连接(否则出现错误);
使用来自 Body 的新内容(或错误)验证结构字段;
检查具有此 ID 的书籍是否存在(或出现错误);
进行查询以从表中删除该记录books(或出现错误);
204返回无内容的状态;
// ...// DeleteBook func for deletes book by given ID.// @Description Delete book by given ID.// @Summary delete book by given ID// @Tags Book// @Accept json// @Produce json// @Param id body string true "Book ID"// @Success 204 {string} status "ok"// @Security ApiKeyAuth// @Router /v1/book [delete]funcDeleteBook(c*fiber.Ctx)error{// Get now time.now:=time.Now().Unix()// Get claims from JWT.claims,err:=utils.ExtractTokenMetadata(c)iferr!=nil{// Return status 500 and JWT parse error.returnc.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error":true,"msg":err.Error(),})}// Set expiration time from JWT data of current book.expires:=claims.Expires// Checking, if now time greather than expiration from JWT.ifnow>expires{// Return status 401 and unauthorized error message.returnc.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error":true,"msg":"unauthorized, check expiration time of your token",})}// Create new Book structbook:=&models.Book{}// Check, if received JSON data is valid.iferr:=c.BodyParser(book);err!=nil{// Return status 400 and error message.returnc.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error":true,"msg":err.Error(),})}// Create a new validator for a Book model.validate:=utils.NewValidator()// Validate only one book field ID.iferr:=validate.StructPartial(book,"id");err!=nil{// Return, if some fields are not valid.returnc.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error":true,"msg":utils.ValidatorErrors(err),})}// Create database connection.db,err:=database.OpenDBConnection()iferr!=nil{// Return status 500 and database connection error.returnc.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error":true,"msg":err.Error(),})}// Checking, if book with given ID is exists.foundedBook,err:=db.GetBook(book.ID)iferr!=nil{// Return status 404 and book not found error.returnc.Status(fiber.StatusNotFound).JSON(fiber.Map{"error":true,"msg":"book with this ID not found",})}// Delete book by given ID.iferr:=db.DeleteBook(foundedBook.ID);err!=nil{// Return status 500 and error message.returnc.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error":true,"msg":err.Error(),})}// Return status 204 no content.returnc.SendStatus(fiber.StatusNoContent)}
获取新访问令牌(JWT)的方法
向 API 端点发出请求;
200使用新的访问令牌返回状态和 JSON;
// ./app/controllers/token_controller.gopackagecontrollersimport("github.com/gofiber/fiber/v2""github.com/koddr/tutorial-go-fiber-rest-api/pkg/utils")// GetNewAccessToken method for create a new access token.// @Description Create a new access token.// @Summary create a new access token// @Tags Token// @Accept json// @Produce json// @Success 200 {string} status "ok"// @Router /v1/token/new [get]funcGetNewAccessToken(c*fiber.Ctx)error{// Generate a new Access token.token,err:=utils.GenerateNewAccessToken()iferr!=nil{// Return status 500 and token generation error.returnc.Status(fiber.StatusInternalServerError).JSON(fiber.Map{"error":true,"msg":err.Error(),})}returnc.JSON(fiber.Map{"error":false,"msg":nil,"access_token":token,})}
这是我们整个应用程序中最重要的功能。它从文件加载配置.env,定义 Swagger 设置,创建一个新的 Fiber 实例,连接必要的端点组并启动 API 服务器。
// ./main.gopackagemainimport("github.com/gofiber/fiber/v2""github.com/koddr/tutorial-go-fiber-rest-api/pkg/configs""github.com/koddr/tutorial-go-fiber-rest-api/pkg/middleware""github.com/koddr/tutorial-go-fiber-rest-api/pkg/routes""github.com/koddr/tutorial-go-fiber-rest-api/pkg/utils"_"github.com/joho/godotenv/autoload"// load .env file automatically_"github.com/koddr/tutorial-go-fiber-rest-api/docs"// load API Docs files (Swagger))// @title API// @version 1.0// @description This is an auto-generated API Docs.// @termsOfService http://swagger.io/terms/// @contact.name API Support// @contact.email your@mail.com// @license.name Apache 2.0// @license.url http://www.apache.org/licenses/LICENSE-2.0.html// @securityDefinitions.apikey ApiKeyAuth// @in header// @name Authorization// @BasePath /apifuncmain(){// Define Fiber config.config:=configs.FiberConfig()// Define a new Fiber app with config.app:=fiber.New(config)// Middlewares.middleware.FiberMiddleware(app)// Register Fiber's middleware for app.// Routes.routes.SwaggerRoute(app)// Register a route for API Docs (Swagger).routes.PublicRoutes(app)// Register a public routes for app.routes.PrivateRoutes(app)// Register a private routes for app.routes.NotFoundRoute(app)// Register route for 404 Error.// Start server (with graceful shutdown).utils.StartServerWithGracefulShutdown(app)}
// ./pkg/middleware/jwt_middleware.gopackagemiddlewareimport("os""github.com/gofiber/fiber/v2"jwtMiddleware"github.com/gofiber/jwt/v2")// JWTProtected func for specify routes group with JWT authentication.// See: https://github.com/gofiber/jwtfuncJWTProtected()func(*fiber.Ctx)error{// Create config for JWT authentication middleware.config:=jwtMiddleware.Config{SigningKey:[]byte(os.Getenv("JWT_SECRET_KEY")),ContextKey:"jwt",// used in private routesErrorHandler:jwtError,}returnjwtMiddleware.New(config)}funcjwtError(c*fiber.Ctx,errerror)error{// Return status 401 and failed authentication error.iferr.Error()=="Missing or malformed JWT"{returnc.Status(fiber.StatusBadRequest).JSON(fiber.Map{"error":true,"msg":err.Error(),})}// Return status 401 and failed authentication error.returnc.Status(fiber.StatusUnauthorized).JSON(fiber.Map{"error":true,"msg":err.Error(),})}
// ./pkg/routes/private_routes.gopackageroutesimport("github.com/gofiber/fiber/v2""github.com/koddr/tutorial-go-fiber-rest-api/app/controllers")// PublicRoutes func for describe group of public routes.funcPublicRoutes(a*fiber.App){// Create routes group.route:=a.Group("/api/v1")// Routes for GET method:route.Get("/books",controllers.GetBooks)// get list of all booksroute.Get("/book/:id",controllers.GetBook)// get one book by IDroute.Get("/token/new",controllers.GetNewAccessToken)// create a new access tokens}
对于私有(JWT 保护)方法:
// ./pkg/routes/private_routes.gopackageroutesimport("github.com/gofiber/fiber/v2""github.com/koddr/tutorial-go-fiber-rest-api/app/controllers""github.com/koddr/tutorial-go-fiber-rest-api/pkg/middleware")// PrivateRoutes func for describe group of private routes.funcPrivateRoutes(a*fiber.App){// Create routes group.route:=a.Group("/api/v1")// Routes for POST method:route.Post("/book",middleware.JWTProtected(),controllers.CreateBook)// create a new book// Routes for PUT method:route.Put("/book",middleware.JWTProtected(),controllers.UpdateBook)// update one book by ID// Routes for DELETE method:route.Delete("/book",middleware.JWTProtected(),controllers.DeleteBook)// delete one book by ID}
对于 Swagger:
// ./pkg/routes/swagger_route.gopackageroutesimport("github.com/gofiber/fiber/v2"swagger"github.com/arsmn/fiber-swagger/v2")// SwaggerRoute func for describe group of API Docs routes.funcSwaggerRoute(a*fiber.App){// Create routes group.route:=a.Group("/swagger")// Routes for GET method:route.Get("*",swagger.Handler)// get one user by ID}
Not found(404)路线:
// ./pkg/routes/not_found_route.gopackageroutesimport"github.com/gofiber/fiber/v2"// NotFoundRoute func for describe 404 Error route.funcNotFoundRoute(a*fiber.App){// Register new special route.a.Use(// Anonimus function.func(c*fiber.Ctx)error{// Return HTTP 404 status and JSON response.returnc.Status(fiber.StatusNotFound).JSON(fiber.Map{"error":true,"msg":"sorry, endpoint is not found",})},)}
// ./platform/database/open_db_connection.gopackagedatabaseimport"github.com/koddr/tutorial-go-fiber-rest-api/app/queries"// Queries struct for collect all app queries.typeQueriesstruct{*queries.BookQueries// load queries from Book model}// OpenDBConnection func for opening database connection.funcOpenDBConnection()(*Queries,error){// Define a new PostgreSQL connection.db,err:=PostgreSQLConnection()iferr!=nil{returnnil,err}return&Queries{// Set queries from models:BookQueries:&queries.BookQueries{DB:db},// from Book model},nil}
所选数据库的具体连接设置:
// ./platform/database/postgres.gopackagedatabaseimport("fmt""os""strconv""time""github.com/jmoiron/sqlx"_"github.com/jackc/pgx/v4/stdlib"// load pgx driver for PostgreSQL)// PostgreSQLConnection func for connection to PostgreSQL database.funcPostgreSQLConnection()(*sqlx.DB,error){// Define database connection settings.maxConn,_:=strconv.Atoi(os.Getenv("DB_MAX_CONNECTIONS"))maxIdleConn,_:=strconv.Atoi(os.Getenv("DB_MAX_IDLE_CONNECTIONS"))maxLifetimeConn,_:=strconv.Atoi(os.Getenv("DB_MAX_LIFETIME_CONNECTIONS"))// Define database connection for PostgreSQL.db,err:=sqlx.Connect("pgx",os.Getenv("DB_SERVER_URL"))iferr!=nil{returnnil,fmt.Errorf("error, not connected to database, %w",err)}// Set database connection settings.db.SetMaxOpenConns(maxConn)// the default is 0 (unlimited)db.SetMaxIdleConns(maxIdleConn)// defaultMaxIdleConns = 2db.SetConnMaxLifetime(time.Duration(maxLifetimeConn))// 0, connections are reused forever// Try to ping database.iferr:=db.Ping();err!=nil{deferdb.Close()// close database connectionreturnnil,fmt.Errorf("error, not sent ping to database, %w",err)}returndb,nil}
// ./pkg/utils/start_server.gopackageutilsimport("log""os""os/signal""github.com/gofiber/fiber/v2")// StartServerWithGracefulShutdown function for starting server with a graceful shutdown.funcStartServerWithGracefulShutdown(a*fiber.App){// Create channel for idle connections.idleConnsClosed:=make(chanstruct{})gofunc(){sigint:=make(chanos.Signal,1)signal.Notify(sigint,os.Interrupt)// Catch OS signals.<-sigint// Received an interrupt signal, shutdown.iferr:=a.Shutdown();err!=nil{// Error from closing listeners, or context timeout:log.Printf("Oops... Server is not shutting down! Reason: %v",err)}close(idleConnsClosed)}()// Run server.iferr:=a.Listen(os.Getenv("SERVER_URL"));err!=nil{log.Printf("Oops... Server is not running! Reason: %v",err)}<-idleConnsClosed}// StartServer func for starting a simple server.funcStartServer(a*fiber.App){// Run server.iferr:=a.Listen(os.Getenv("SERVER_URL"));err!=nil{log.Printf("Oops... Server is not running! Reason: %v",err)}}
生成有效的 JWT:
// ./pkg/utils/jwt_generator.gopackageutilsimport("os""strconv""time""github.com/golang-jwt/jwt")// GenerateNewAccessToken func for generate a new Access token.funcGenerateNewAccessToken()(string,error){// Set secret key from .env file.secret:=os.Getenv("JWT_SECRET_KEY")// Set expires minutes count for secret key from .env file.minutesCount,_:=strconv.Atoi(os.Getenv("JWT_SECRET_KEY_EXPIRE_MINUTES_COUNT"))// Create a new claims.claims:=jwt.MapClaims{}// Set public claims:claims["exp"]=time.Now().Add(time.Minute*time.Duration(minutesCount)).Unix()// Create a new JWT access token with claims.token:=jwt.NewWithClaims(jwt.SigningMethodHS256,claims)// Generate token.t,err:=token.SignedString([]byte(secret))iferr!=nil{// Return error, it JWT token generation failed.return"",err}returnt,nil}
解析并验证 JWT:
// ./pkg/utils/jwt_parser.gopackageutilsimport("os""strings""github.com/golang-jwt/jwt""github.com/gofiber/fiber/v2")// TokenMetadata struct to describe metadata in JWT.typeTokenMetadatastruct{Expiresint64}// ExtractTokenMetadata func to extract metadata from JWT.funcExtractTokenMetadata(c*fiber.Ctx)(*TokenMetadata,error){token,err:=verifyToken(c)iferr!=nil{returnnil,err}// Setting and checking token and credentials.claims,ok:=token.Claims.(jwt.MapClaims)ifok&&token.Valid{// Expires time.expires:=int64(claims["exp"].(float64))return&TokenMetadata{Expires:expires,},nil}returnnil,err}funcextractToken(c*fiber.Ctx)string{bearToken:=c.Get("Authorization")// Normally Authorization HTTP header.onlyToken:=strings.Split(bearToken," ")iflen(onlyToken)==2{returnonlyToken[1]}return""}funcverifyToken(c*fiber.Ctx)(*jwt.Token,error){tokenString:=extractToken(c)token,err:=jwt.Parse(tokenString,jwtKeyFunc)iferr!=nil{returnnil,err}returntoken,nil}funcjwtKeyFunc(token*jwt.Token)(interface{},error){return[]byte(os.Getenv("JWT_SECRET_KEY")),nil}
// ./pkg/routes/private_routes_test.gopackageroutesimport("io""net/http/httptest""strings""testing""github.com/gofiber/fiber/v2""github.com/joho/godotenv""github.com/koddr/tutorial-go-fiber-rest-api/pkg/utils""github.com/stretchr/testify/assert")funcTestPrivateRoutes(t*testing.T){// Load .env.test file from the root folder.iferr:=godotenv.Load("../../.env.test");err!=nil{panic(err)}// Create a sample data string.dataString:=`{"id": "00000000-0000-0000-0000-000000000000"}`// Create access token.token,err:=utils.GenerateNewAccessToken()iferr!=nil{panic(err)}// Define a structure for specifying input and output data of a single test case.tests:=[]struct{descriptionstringroutestring// input routemethodstring// input methodtokenStringstring// input tokenbodyio.ReaderexpectedErrorboolexpectedCodeint}{{description:"delete book without JWT and body",route:"/api/v1/book",method:"DELETE",tokenString:"",body:nil,expectedError:false,expectedCode:400,},{description:"delete book without right credentials",route:"/api/v1/book",method:"DELETE",tokenString:"Bearer "+token,body:strings.NewReader(dataString),expectedError:false,expectedCode:403,},{description:"delete book with credentials",route:"/api/v1/book",method:"DELETE",tokenString:"Bearer "+token,body:strings.NewReader(dataString),expectedError:false,expectedCode:404,},}// Define a new Fiber app.app:=fiber.New()// Define routes.PrivateRoutes(app)// Iterate through test single test casesfor_,test:=rangetests{// Create a new http request with the route from the test case.req:=httptest.NewRequest(test.method,test.route,test.body)req.Header.Set("Authorization",test.tokenString)req.Header.Set("Content-Type","application/json")// Perform the request plain with the app.resp,err:=app.Test(req,-1)// the -1 disables request latency// Verify, that no error occurred, that is not expectedassert.Equalf(t,test.expectedError,err!=nil,test.description)// As expected errors lead to broken responses,// the next test case needs to be processed.iftest.expectedError{continue}// Verify, if the status code is as expected.assert.Equalf(t,test.expectedCode,resp.StatusCode,test.description)}}// ...