JSON Web Token (JWT) is an open standard (RFC 7519) that defines a compact and self-contained way for securely transmitting information between parties as a JSON object. This information can be verified and trusted because it is digitally signed. JWTs can be signed using a secret (with the HMAC algorithm) or a public/private key pair using RSA or ECDSA.
Although JWTs can be encrypted to also provide secrecy between parties, we will focus on signed tokens. Signed tokens can verify the integrity of the claims contained within it, while encrypted tokens hide those claims from other parties. When tokens are signed using public/private key pairs, the signature also certifies that only the party holding the private key is the one that signed it.
so for this project, we will need to create 2 public endpoints that will be used as authentication and 1 protected endpoint that will be protected by our JWT
this route will be used to authenticate the user by providing a username and password then generate and gives JSON Web Token in return
/api/loginof course, because we have a login route earlier we need a way to register our login information, so it can be verified.
/api/registerfor the sake of this tutorial, we will leave the registration route open. If you don't want people to be able to gain login access easily you might not want to do this.
this whil be the route for out protected endpoints
/api/admin/userto initiate our project we want to create our project dir folder and going into the directory
mkdir jwt-gin
cd jwt-ginwe can start by initiating our go.mod file to manage packages that we will be installing later on.
go mod init <your_project_name>here’s the list of packages that we will need to install for this project.
// gin framework
go get -u github.com/gin-gonic/gin
// ORM library
go get -u github.com/jinzhu/gorm
// package that we will be used to authenticate and generate our JWT
go get -u github.com/dgrijalva/jwt-go
// to help manage our environment variables
go get -u github.com/joho/godotenv
// to encrypt our user's password
go get -u golang.org/x/cryptoAlright.. now that everything is set, we can start coding our application!
we can start by creating our main.go file in our root directory
touch main.gohere is the code for our starter public endpoint
package main
import (
"net/http"
"github.com/gin-gonic/gin"
)
func main() {
r := gin.Default()
public := r.Group("/api")
public.POST("/register", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": "this is the register endpoint!"})
})
r.Run(":8080")
}and we can try to run it.
Perfect! it seems our register endpoint is working properly.
Now that our register endpoint is ready, we can start creating our controller file that will contain our logic for the registration process.
create controller package dir
mkdir controllerscreate controller file for our login that will be named auth.go
touch ./controllers/auth.gopackage controllers
import (
"net/http"
"github.com/gin-gonic/gin"
)
func Register(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{"data": "hello from controller!"})
}and we also have to update our main.go file
package main
import (
"github.com/gin-gonic/gin"
"<your_project_name>/controllers"
)
func main() {
r := gin.Default()
public := r.Group("/api")
public.POST("/register", controllers.Register)
r.Run(":8080")
}now let's test it again!
Perfect!
we need to validate inputs that are coming into our register endpoint, the only input we will need are username and password.
we will be using a validation feature that comes with gin which is called binding
if you want to know more about binding and validator
let's update our auth.go file
package controllers
import (
"net/http"
"github.com/gin-gonic/gin"
)
type RegisterInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Register(c *gin.Context){
var input RegisterInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "validated!"})
}let's test our validation!
missing password input test:
correct input test
To save our credentials in our database, we need to create a database connection to our desired database, I will be using Postgres database that is already installed in my system.
let's create models package
mkdir modelsthis folder will contain the database connection file and all our models
database connection file
touch ./models/setup.gopackage models
import (
"database/sql"
"fmt"
"log"
"os"
"github.com/jinzhu/gorm"
"github.com/joho/godotenv"
_ "github.com/lib/pq" // postgresql 驱动
)
var DB *gorm.DB
func ConnectDataBase() {
err := godotenv.Load(".env")
if err != nil {
log.Fatalf("Error loading .env file")
}
DbDriver := os.Getenv("DB_DRIVER")
DbHost := os.Getenv("DB_HOST")
DbUser := os.Getenv("DB_USER")
DbName := os.Getenv("DB_NAME")
DbPort := os.Getenv("DB_PORT")
DbPassword := os.Getenv("DB_PASSWORD")
// 创建数据库连接
db, err := sql.Open(DbDriver, fmt.Sprintf("%s://%s:%s@%s:%s/%s?sslmode=disable", DbDriver, DbUser, DbPassword, DbHost, DbPort, DbName))
if err != nil {
panic(err)
}
DB, err = gorm.Open(DbDriver, db)
if err != nil {
fmt.Println("Cannot connect to database ", DbDriver)
log.Fatal("connection error:", err)
} else {
fmt.Println("We are connected to the database ", DbDriver)
}
DB.AutoMigrate(&User{})
}
// Cleanup, db connect closed after exits
func Cleanup() {
if DB != nil {
_ = DB.Close()
}
}Alright as you can notice in the setup.go file we will need 2 things
- env file
- User Model
we can create the .env file by simply creating it in our root directory
touch .envDB_HOST=127.0.0.1
DB_DRIVER=postgres
DB_USER=postgres
DB_PASSWORD=abc1221
DB_NAME=jwt-gin-v3
DB_PORT=15432touch ./models/user.gopackage models
import (
"github.com/jinzhu/gorm"
)
type User struct {
gorm.Model
Username string `gorm:"size:255;not null;unique" json:"username"`
Password string `gorm:"size:255;not null;" json:"password"`
}Alright, now that our database connection is set let's add it to our main.go file and test it!
package main
import (
"github.com/gin-gonic/gin"
"<your_project_name>/controllers"
"<your_project_name>/models"
)
func main() {
models.ConnectDataBase()
r := gin.Default()
public := r.Group("/api")
public.POST("/register", controllers.Register)
r.Run(":8080")
}this is what you should see in your terminal
Perfect! now we can save the credential into the database
let's update out auth.go file
package controllers
import (
"net/http"
"github.com/gin-gonic/gin"
"<your_project_name>/models"
)
type RegisterInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Register(c *gin.Context){
var input RegisterInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u := models.User{}
u.Username = input.Username
u.Password = input.Password
_,err := u.SaveUser()
if err != nil{
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message":"registration success"})
}in order to save our User data we need to create the SaveUser() function in our User model but we also need to create gorm hooks BeforeSave() function that will be running the hash process to the user’s password.
let's update ourt user.go file
package models
import (
"html"
"strings"
"github.com/jinzhu/gorm"
"golang.org/x/crypto/bcrypt"
)
type User struct {
gorm.Model
Username string `gorm:"size:255;not null;unique" json:"username"`
Password string `gorm:"size:255;not null;" json:"password"`
}
func (u *User) SaveUser() (*User, error) {
var err error
err = DB.Create(&u).Error
if err != nil {
return &User{}, err
}
return u, nil
}
// 钩子事件,用户存入数据之前的操作
func (u *User) BeforeSave() error {
//turn password into hash
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password),bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashedPassword)
//remove spaces in username
u.Username = html.EscapeString(strings.TrimSpace(u.Username))
return nil
}Let's test it!
Perfect, It means the credential is saved and hashed and we can use it to validate the login process!
what our login endpoint going to do is very simple, it will receive a username and password, check whether it matches the credential in our database, if it does return a token, if it doesn’t return an error response.
let's add a new route to our main.go
package main
import (
"github.com/gin-gonic/gin"
"<your_project_name>/controllers"
"<your_project_name>/models"
)
func main() {
models.ConnectDataBase()
r := gin.Default()
public := r.Group("/api")
public.POST("/register", controllers.Register)
public.POST("/login",controllers.Login)
r.Run(":8080")
}add Login function in auth.go controller
package controllers
import (
"net/http"
"github.com/gin-gonic/gin"
"<your_project_name>/models"
)
type LoginInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Login(c *gin.Context) {
var input LoginInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u := models.User{}
u.Username = input.Username
u.Password = input.Password
token, err := models.LoginCheck(u.Username, u.Password)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "username or password is incorrect."})
return
}
c.JSON(http.StatusOK, gin.H{"token":token})
}
// 注册接口参数
type RegisterInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Register(c *gin.Context){
var input RegisterInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u := models.User{}
u.Username = input.Username
u.Password = input.Password
_,err := u.SaveUser()
if err != nil{
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message":"registration success"})
}the Login controller function will call LoginCheck function from user.go
lets add it to our user.go file
package models
import (
"html"
"strings"
"github.com/jinzhu/gorm"
"golang.org/x/crypto/bcrypt"
"<your_project_name>/utils/token"
)
type User struct {
gorm.Model
Username string `gorm:"size:255;not null;unique" json:"username"`
Password string `gorm:"size:255;not null;" json:"password"`
}
// 密码校验
func VerifyPassword(password,hashedPassword string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
// 登录检查
func LoginCheck(username string, password string) (string,error) {
var err error
u := User{}
err = DB.Model(User{}).Where("username = ?", username).Take(&u).Error
if err != nil {
return "", err
}
err = VerifyPassword(password, u.Password)
if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {
return "", err
}
token,err := token.GenerateToken(u.ID)
if err != nil {
return "",err
}
return token,nil
}
func (u *User) SaveUser() (*User, error) {
var err error
err = DB.Create(&u).Error
if err != nil {
return &User{}, err
}
return u, nil
}
func (u *User) BeforeSave() error {
//turn password into hash
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password),bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashedPassword)
//remove spaces in username
u.Username = html.EscapeString(strings.TrimSpace(u.Username))
return nil
}we also need to create token.go file that will contain all of our token processing functions, we will create a new directory which is called utils
mkdir utils
mkdir ./utils/token
touch ./utils/token/token.gohere’s token.go file along with several functions that we will be using later on as well.
type User struct {
gorm.Model
Username string `gorm:"size:255;not null;unique" json:"username"`
Password string `gorm:"size:255;not null;" json:"password"`
}
// VerifyPassword, verity is pass by password hash
func VerifyPassword(password, hashedPassword string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
// 登录检查
func LoginCheck(username string, password string) (string, error) {
var (
err error
u = User{}
)
if err := DB.Model(User{}).Where("username = ?", username).Take(&u).Error; err != nil {
return "", err
}
err = VerifyPassword(password, u.Password)
if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {
return "", err
}
if tokenStr, err := token.GenerateToken(u.ID); err != nil {
return "", err
} else {
return tokenStr, nil
}
}
func (u *User) SaveUser() (*User, error) {
var err error
err = DB.Create(&u).Error
if err != nil {
return &User{}, err
}
return u, nil
}
func (u *User) BeforeSave() error {
//turn password into hash
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashedPassword)
//remove spaces in username
u.Username = html.EscapeString(strings.TrimSpace(u.Username))
return nil
}we also need to add 2 new variables into our .env file
API_SECRET=yoursecretstring
TOKEN_HOUR_LIFESPAN=1TOKEN_HOUR_LIFESPAN will determine how long each token will last (hour
API_SECRET is your own secret string for signing the token
let's run and test our Login
Perfect!
lets create our middleware.go file
mkdir middlewares
touch ./middlewares/middlewares.gopackage middlewares
import (
"net/http"
"github.com/gin-gonic/gin"
"<your_project_name>/utils/token"
)
func JwtAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
err := token.TokenValid(c)
if err != nil {
c.String(http.StatusUnauthorized, "Unauthorized")
c.Abort()
return
}
c.Next()
}
}let's implement to our middleware to our main.go
package main
import (
"github.com/gin-gonic/gin"
"<your_project_name>/controllers"
"<your_project_name>/models"
"<your_project_name>/middlewares"
)
func main() {
models.ConnectDataBase()
r := gin.Default()
public := r.Group("/api")
public.POST("/register", controllers.Register)
public.POST("/login",controllers.Login)
protected := r.Group("/api/admin")
// 以下接口将需要使用jwt进行认证
protected.Use(middlewares.JwtAuthMiddleware())
protected.GET("/user",controllers.CurrentUser)
r.Run(":8080")
}lets add CurrentUser function in our auth.go file so we can return the current authenticated user data.
package controllers
import (
"net/http"
"github.com/gin-gonic/gin"
"<your_project_name>/models"
"<your_project_name>/utils/token"
)
func CurrentUser(c *gin.Context){
user_id, err := token.ExtractTokenID(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u,err := models.GetUserByID(user_id)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message":"success","data":u})
}
type LoginInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Login(c *gin.Context) {
var input LoginInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u := models.User{}
u.Username = input.Username
u.Password = input.Password
token, err := models.LoginCheck(u.Username, u.Password)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "username or password is incorrect."})
return
}
c.JSON(http.StatusOK, gin.H{"token":token})
}
type RegisterInput struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Register(c *gin.Context){
var input RegisterInput
if err := c.ShouldBindJSON(&input); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u := models.User{}
u.Username = input.Username
u.Password = input.Password
_,err := u.SaveUser()
if err != nil{
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message":"registration success"})
}lets add GetUserByID function into our user.go
package models
import (
"errors"
"html"
"strings"
"github.com/jinzhu/gorm"
"golang.org/x/crypto/bcrypt"
"jwt-gin-example/utils/token"
)
type User struct {
gorm.Model
Username string `gorm:"size:255;not null;unique" json:"username"`
Password string `gorm:"size:255;not null;" json:"password"`
}
// VerifyPassword, verity is pass by password hash
func VerifyPassword(password, hashedPassword string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
// GetUserByID, get user detail info but expect password by user ID
func GetUserByID(uid uint) (User, error) {
var u User
if err := DB.First(&u, uid).Error; err != nil {
return u, errors.New("user not found ")
}
u.PrepareGive()
return u, nil
}
func (u *User) PrepareGive() {
u.Password = ""
}
// 登录检查
func LoginCheck(username string, password string) (string, error) {
var (
err error
u = User{}
)
if err := DB.Model(User{}).Where("username = ?", username).Take(&u).Error; err != nil {
return "", err
}
err = VerifyPassword(password, u.Password)
if err != nil && err == bcrypt.ErrMismatchedHashAndPassword {
return "", err
}
if tokenStr, err := token.GenerateToken(u.ID); err != nil {
return "", err
} else {
return tokenStr, nil
}
}
func (u *User) SaveUser() (*User, error) {
var err error
err = DB.Create(&u).Error
if err != nil {
return &User{}, err
}
return u, nil
}
func (u *User) BeforeSave() error {
//turn password into hash
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashedPassword)
//remove spaces in username
u.Username = html.EscapeString(strings.TrimSpace(u.Username))
return nil
}you can also notice we ran PrepareGive function to remove the hashed password string before returning it for security purposes.
let's test our protected endpoint
Perfect!
By that, it will conclude the tutorial. I hope it helps you on your personal journey as a developer.







