Golang & Postgresql & JWT & GORM & mux
https://www.sohamkamani.com/blog/golang/2019-01-01-jwt-authentication/
In this tutorial, we are going to learn how to develop and deploy a secure REST api using Go Programming language.
Why Go?
Go is a very interesting programming language, it is a strongly typed language which compiles very fast, it performance is likened to that of C++, go has goroutines — a much more efficient replacement for Threads, and also go give you the freedom to static type on the web — I understand this is not new, i just love Go’s way.
What are we building?
We are going to build a contact/phonebook manager App, our API will allow users to add contacts to their profiles, they will be able to retrieve it in case their phone got lost.
Prerequisites
This lesson assumed you already installed the following packages
- Go
- Postgresql
- GoLand IDE
Building the App
We start by identifying the package dependencies we are going to need for this project, luckily for us, Go standard library is rich enough to build a complete website without using a third party framework(i hope i am right) — see net.http
package, but to make our work easier, we are going to need the following packages,
- gorilla/mux — A powerful URL router and dispatcher. We use this package to match URL paths with their handlers.
- jinzhu/gorm — The fantastic ORM library for Golang, aims to be developer friendly. We use this ORM(Object relational mapper) package to interact smoothly with our database
- dgrijalva/jwt-go — Used to sign and verify JWT tokens
- joho/godotenv — Used to load .env files into the project
To install any of this package, open terminal
and run
go get github.com/{package-name}
This command will install the packages into your GOPATH
.
utils.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 | 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) } |
utils.go
contain handy utils functions to build json
messages and return a json
response. Note the two function Message()
and Respond()
before we proceed.
More about JWT
JSON Web Tokens are an open, industry standard RFC 7519 method for representing claims securely between two parties. It is easy to identify web application users through sessions, however, when your web apps API is interacting with say an Android or IOS client, sessions becomes unusable because of the stateless nature of the http request. With JWT, we can create a unique token for each authenticated user, this token would be included in the header of the subsequent request made to the API server, this method allow us to identify every users that make calls to our API. Lets see the implementation below
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 | package app import ( "net/http" u "lens/utils" "strings" "go-contacts/models" jwt "github.com/dgrijalva/jwt-go" "os" "context" "fmt" ) 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" } //List of endpoints that doesn't require auth requestPath := r.URL.Path //current request path //check if request does not need authentication, serve the request if it doesn't need it for _, value := range notAuth { if value == requestPath { next.ServeHTTP(w, r) return } } response := make( map [string] interface {}) tokenHeader := r.Header.Get( "Authorization" ) //Grab the token from the header if tokenHeader == "" { //Token is missing, returns with error code 403 Unauthorized response = u.Message(false, "Missing auth token" ) w.WriteHeader(http.StatusForbidden) w.Header().Add( "Content-Type" , "application/json" ) u.Respond(w, response) return } splitted := strings.Split(tokenHeader, " " ) //The token normally comes in format `Bearer {token-body}`, we check if the retrieved token matched this requirement if len(splitted) != 2 { response = u.Message(false, "Invalid/Malformed auth token" ) w.WriteHeader(http.StatusForbidden) w.Header().Add( "Content-Type" , "application/json" ) u.Respond(w, response) return } tokenPart := splitted[1] //Grab the token part, what we are truly interested in 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 { //Malformed token, returns with http code 403 as usual response = u.Message(false, "Malformed authentication token" ) w.WriteHeader(http.StatusForbidden) w.Header().Add( "Content-Type" , "application/json" ) u.Respond(w, response) return } if !token.Valid { //Token is invalid, maybe not signed on this server response = u.Message(false, "Token is not valid." ) w.WriteHeader(http.StatusForbidden) w.Header().Add( "Content-Type" , "application/json" ) u.Respond(w, response) return } //Everything went well, proceed with the request and set the caller to the user retrieved from the parsed token fmt.Sprintf( "User %" , tk.Username) //Useful for monitoring ctx := context.WithValue(r.Context(), "user" , tk.UserId) r = r.WithContext(ctx) next.ServeHTTP(w, r) //proceed in the middleware chain! }); } |
The comments inside the code explain everything there is to know, but basically, the code create a Middleware
to intercept every requests, check for the presence of an authentication token (JWT token), verify if it is authentic and valid, then send error back to the client if we detect any deficiency in the token or proceed to serving the request otherwise(if the token is valid), you’ll see later, how we can access the user that is interacting with our API from the request.
Building the user registration and login system
We want our users to be able to register and login before backing up/storing their contacts on our system. The first thing we will need to do is connect to our database, we use a .env
file to store our database credentials, my .env
looks like this
db_name = gocontacts
db_pass = **** //This is default to the current user's password on windows for postgresql
db_user = postgres
db_type = postgres
db_host = localhost
db_port = 5434
token_password = thisIsTheJwtSecretPassword //Do not commit to git!
Then, we can connect to the database using the following snippets
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | package models import ( _ "github.com/jinzhu/gorm/dialects/postgres" "github.com/jinzhu/gorm" "os" "github.com/joho/godotenv" "fmt" ) 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" ) dbUri := fmt.Sprintf( "host=%s user=%s dbname=%s sslmode=disable password=%s" , dbHost, username, dbName, password) //Build connection string fmt.Println(dbUri) conn, err := gorm.Open( "postgres" , dbUri) if err != nil { fmt.Print(err) } db = conn db.Debug().AutoMigrate(&Account{}, &Contact{}) //Database migration } //returns a handle to the DB object func GetDB() *gorm.DB { return db } |
The code does a very simple thing, on the file init()
function — init() automatically get called by Go, the code retrieve connection information from .env
file then build a connection string and use it to connect to the database.
Creating the application entry point
So far, we’ve been able to create the JWT middleware and connect to our database. The next thing is creating the application’s entry point, see the code snippet below
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 | package main import ( "github.com/gorilla/mux" "go-contacts/app" "os" "fmt" "net/http" ) func main() { router := mux.NewRouter() router.Use(app.JwtAuthentication) //attach JWT auth middleware port := os.Getenv( "PORT" ) //Get port from .env file, we did not specify any port so this should return an empty string when tested locally if port == "" { port = "8000" //localhost } fmt.Println(port) err := http.ListenAndServe( ":" + port, router) //Launch the app, visit localhost:8000/api if err != nil { fmt.Print(err) } } |
We create a new Router object — line 13, we attach our JWT auth middleware using router’s Use()
function — line 14, and then we proceed to start listening for incoming requests.
Use the small media play button located left of func main()
to compile and launch the app, if all is good, you should see no error in the console, in case there was an error, take a second look at your database connection parameters to see that they correlate.
Creating and authenticating Users
create a new file models/accounts.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 | package models import ( "github.com/dgrijalva/jwt-go" u "lens/utils" "strings" "github.com/jinzhu/gorm" "os" "golang.org/x/crypto/bcrypt" ) /* JWT claims struct */ type Token struct { UserId uint jwt.StandardClaims } //a struct to rep user account type Account struct { gorm.Model Email string `json: "email" ` Password string `json: "password" ` Token string `json: "token" ;sql: "-" ` } //Validate incoming user details... func (account *Account) Validate() ( map [string] interface {}, bool) { if !strings.Contains(account.Email, "@" ) { return u.Message(false, "Email address is required" ), false } if len(account.Password) < 6 { return u.Message(false, "Password is required" ), false } //Email must be unique temp := &Account{} //check for errors and duplicate emails err := GetDB().Table( "accounts" ).Where( "email = ?" , account.Email).First(temp).Error if err != nil && err != gorm.ErrRecordNotFound { return u.Message(false, "Connection error. Please retry" ), false } if temp.Email != "" { return u.Message(false, "Email address already in use by another user." ), false } return u.Message(false, "Requirement passed" ), true } 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, "Failed to create account, connection error." ) } //Create new JWT token for the newly registered account 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 = "" //delete password response := u.Message(true, "Account has been created" ) response[ "account" ] = account return response } 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 address not found" ) } return u.Message(false, "Connection error. Please retry" ) } err = bcrypt.CompareHashAndPassword([]byte(account.Password), []byte(password)) if err != nil && err == bcrypt.ErrMismatchedHashAndPassword { //Password does not match! return u.Message(false, "Invalid login credentials. Please try again" ) } //Worked! Logged In account.Password = "" //Create JWT token tk := &Token{UserId: account.ID} token := jwt.NewWithClaims(jwt.GetSigningMethod( "HS256" ), tk) tokenString, _ := token.SignedString([]byte(os.Getenv( "token_password" ))) account.Token = tokenString //Store the token in the response resp := u.Message(true, "Logged In" ) resp[ "account" ] = account return resp } func GetUser(u uint) *Account { acc := &Account{} GetDB().Table( "accounts" ).Where( "id = ?" , u).First(acc) if acc.Email == "" { //User not found! return nil } acc.Password = "" return acc } |
There is a lot of puzzle inside accounts.go, lets break it down a little bit.
The first part create two structs Token
and Account
they represent a JWT token claim and a user account respectively. Function Validate()
validates the data sent from clients and function Create()
creates a new account and generate a JWT token that will be sent back to client that made the request. Function Login(username, password)
authenticate an existing user, then generate a JWT token if authentication was successful.
authController.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 | package controllers import ( "net/http" u "go-contacts/utils" "go-contacts/models" "encoding/json" ) var CreateAccount = func (w http.ResponseWriter, r *http.Request) { account := &models.Account{} err := json.NewDecoder(r.Body).Decode(account) //decode the request body into struct and failed if any error occur if err != nil { u.Respond(w, u.Message(false, "Invalid request" )) return } resp := account.Create() //Create account u.Respond(w, resp) } var Authenticate = func (w http.ResponseWriter, r *http.Request) { account := &models.Account{} err := json.NewDecoder(r.Body).Decode(account) //decode the request body into struct and failed if any error occur if err != nil { u.Respond(w, u.Message(false, "Invalid request" )) return } resp := models.Login(account.Email, account.Password) u.Respond(w, resp) } |
The content is very straightforward. It contains the handler
for /user/new
and /user/login
endpoints.
Add the following snippet to main.go
to register our new routes
router.HandleFunc("/api/user/new", controllers.CreateAccount).Methods("POST")router.HandleFunc("/api/user/login", controllers.Authenticate).Methods("POST")
The above code register both /user/new
and /user/login
endpoints and pass their corresponding request handlers.
Now, recompile the code and visit localhost:8000/api/user/new
using postman, set the request body to application/json
as shown below
If you try to call /user/new
twice with the same payload, you’ll receive a response that the email already exists, works according to our instructions.
Creating contacts
Part of our app’s functionality is letting our users create/store contacts. Contact will have name
and phone
, we will define these as struct properties. The following snippets belongs to models/contact.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 | package models import ( u "go-contacts/utils" "github.com/jinzhu/gorm" "fmt" ) type Contact struct { gorm.Model Name string `json: "name" ` Phone string `json: "phone" ` UserId uint `json: "user_id" ` //The user that this contact belongs to } /* This struct function validate the required parameters sent through the http request body returns message and true if the requirement is met */ func (contact *Contact) Validate() ( map [string] interface {}, bool) { if contact.Name == "" { return u.Message(false, "Contact name should be on the payload" ), false } if contact.Phone == "" { return u.Message(false, "Phone number should be on the payload" ), false } if contact.UserId <= 0 { return u.Message(false, "User is not recognized" ), false } //All the required parameters are present return u.Message(true, "success" ), true } func (contact *Contact) Create() ( map [string] interface {}) { if resp, ok := contact.Validate(); !ok { return resp } GetDB().Create(contact) resp := u.Message(true, "success" ) resp[ "contact" ] = contact return resp } func GetContact(id uint) (*Contact) { contact := &Contact{} err := GetDB().Table( "contacts" ).Where( "id = ?" , id).First(contact).Error if err != nil { return nil } return contact } func GetContacts(user uint) ([]*Contact) { contacts := make([]*Contact, 0) err := GetDB().Table( "contacts" ).Where( "user_id = ?" , user).Find(&contacts).Error if err != nil { fmt.Println(err) return nil } return contacts } |
Same as in models/accounts.go
we create a function Validate()
to validate the passed inputs, we return an error with messages if anything we don’t want occur, then we wrote function Create()
to insert this contact into the database.
The only part left is retrieving the contacts. Lets do it!
router.HandleFunc("/api/me/contacts", controllers.GetContactsFor).Methods("GET")
Add the above snippet to main.go
to tell the router to register /me/contacts
endpoint. Lets create controllers.GetContactsFor
handler to handle the API request.
contactsController.go
Bellow is the content of contactsController.go
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | package controllers import ( "net/http" "go-contacts/models" "encoding/json" u "go-contacts/utils" "strconv" "github.com/gorilla/mux" "fmt" ) var CreateContact = func (w http.ResponseWriter, r *http.Request) { user := r.Context().Value( "user" ) . (uint) //Grab the id of the user that send the request contact := &models.Contact{} err := json.NewDecoder(r.Body).Decode(contact) if err != nil { u.Respond(w, u.Message(false, "Error while decoding request body" )) return } contact.UserId = user resp := contact.Create() u.Respond(w, resp) } var GetContactsFor = func (w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id, err := strconv.Atoi(params[ "id" ]) if err != nil { //The passed path parameter is not an integer u.Respond(w, u.Message(false, "There was an error in your request" )) return } data := models.GetContacts(uint(id)) resp := u.Message(true, "success" ) resp[ "data" ] = data u.Respond(w, resp) } |
What it does is pretty similar to authController.go's
, but basically, it grabs the json body and decode it into Contact
struct, if there was an error, return a response immediately or insert the contacts into the database if everything went well.
Fetching Contacts that belongs to a user
Now, our users have been able to store their contacts successfully, what if they want to retrieve the contact they stored, in case their phone is lost? Visiting /me/contacts
should return a json
structure for the contacts of the API caller(current user). Check the code snippet to have a clearer picture.
Normally, retrieving user’s contacts endpoint should look like /user/{userId}/contacts
, specifying userId
as a path parameter is very dangerous, because every authenticated user can craft a request to this path and contacts of another users would be returned without any problem, this can lead to a brutal attack by hackers — I am trying to point out the usefulness of JWT
. We can easily obtain the id
of the API caller using r.Context().Value("user")
, remember we set this value inside auth.go
— Our authentication middleware
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 | package controllers import ( "net/http" "go-contacts/models" "encoding/json" u "go-contacts/utils" "strconv" "github.com/gorilla/mux" "fmt" ) var CreateContact = func (w http.ResponseWriter, r *http.Request) { user := r.Context().Value( "user" ) . (uint) //Grab the id of the user that send the request contact := &models.Contact{} err := json.NewDecoder(r.Body).Decode(contact) if err != nil { u.Respond(w, u.Message(false, "Error while decoding request body" )) return } contact.UserId = user resp := contact.Create() u.Respond(w, resp) } var GetContactsFor = func (w http.ResponseWriter, r *http.Request) { params := mux.Vars(r) id, err := strconv.Atoi(params[ "id" ]) if err != nil { //The passed path parameter is not an integer u.Respond(w, u.Message(false, "There was an error in your request" )) return } data := models.GetContacts(uint(id)) resp := u.Message(true, "success" ) resp[ "data" ] = data u.Respond(w, resp) } |
The code for this project is on github — https://github.com/adigunhammedolalekan/go-contacts
Deployment
We can easily deploy our app to heroku. Firstly, download godep
. godep is a dependency manager for Golang, similar to npm
for nodejs.
go get -u github.com/tools/godep
- Open
GoLand terminal
and rungodep save
This will create a folder callGodeps
andvender
. To learn more about godep, visit https://github.com/tools/godep - Create account on heroku.com and download
Heroku Cli
then login with your credentials - Once done, run
heroku create gocontacts
This will create an app for you on your heroku dashboard and also a remote git repository. - run the following command to push your code to heroku
git add .
git commit -m "First commit"
git push heroku master
If everything went well, your screen should look like my own.
Voila! Your app has been deployed. The next thing is setting up a remote Postgresql database.
run heroku addons:create heroku-postgresql:hobby-dev
to create the database. To learn more about this, visit https://devcenter.heroku.com/articles/heroku-postgresql
Great! We are almost there, next thing is to connect with our remote database.
Go to heroku.com and login with your credentials, you should find your newly created app on your dashboard, click on it. After that, click on settings, then click on Reveal Config Vars
Postgresql connection URI format postgres://username:password@host/dbName
, There is a var named DATABASE_URL
, this was automatically added to your .env
file when you created the postgresql database (Note: Heroku automatically replace your local .env
when you deploy your app), from this var, we will extract our database connection parameter.
If all this went well, your API should be live now!
Project Repo
https://github.com/adigunhammedolalekan/go-contacts
https://github.com/youhui/go_contacts
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· Linux系列:如何用heaptrack跟踪.NET程序的非托管内存泄露
· 开发者必知的日志记录最佳实践
· SQL Server 2025 AI相关能力初探
· Linux系列:如何用 C#调用 C方法造成内存泄露
· AI与.NET技术实操系列(二):开始使用ML.NET
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· C#/.NET/.NET Core优秀项目和框架2025年2月简报
· Manus爆火,是硬核还是营销?
· 一文读懂知识蒸馏
· 终于写完轮子一部分:tcp代理 了,记录一下