Golang中实现JWT身份验证
基于角色的授权。用户将能够注册帐户、登录、注销并访问受身份验证中间件保护的个人资料信息。这意味着用户只有拥有有效的 JWT 才能访问他们的个人资料。
从https://github.com/wpcodevo/go-jwt--api下载或克隆Go JWT项目。
如果没有安装Docker,从官方网站docker-compose up -d下载。在根目录中打开一个终端运行Docker容器启动PostgreSQL和pgAdmin。
执行命令go run main.go。安装所需的包,将GORM迁移到PostgreSQL数据库,启动Fiber HTTP服务。
将Golang JWT Auth.json文件从根目录导入Postman集合包含预定义的 HTTP 请求、配置。
文件导入Postman后Go服务器发送请求测试身份验证,可以注册账户、登录、注销以及访问路由。
Golang 项目
go mod init github.com//go-postgres-jwt-auth-api
go get github.com/gofiber/fiber/v2
go get github.com/google/uuid
go get github.com/go-playground/validator/v10
go get -u gorm.io/gorm
go get gorm.io/driver/postgres
go get github.com/spf13/viper
go get github.com/golang-jwt/jwt
fiber Go构建API轻量级Web框架。
uuid 用于生成和处理UUID的包
validator 结构字段和其他Go数据类型提供数据验证功能。
gorm 用于Go的ORM库。
postgres 支持PostgreSQL数据库连接的GORM驱动程序。
viper 加载环境变量和配置文件的配置管理库。
jwt 在Go中生成、解析和验证JSON令牌的包。
安装依赖项后设置一个单个端点Fiber Web服务器:端点将返回一个JSON对象包含一条简单的状态消息。
创建一个main.go文件:
package main
import (
"log"
"github.com/gofiber/fiber/v2"
)
func main() {
app := fiber.New()
app.Get("/api/healthchecker", func(c *fiber.Ctx) error {
return c.Status(fiber.StatusOK).JSON(fiber.Map{
"status": "success",
"message": "JSON Web Token Authentication",
})
})
log.Fatal(app.Listen(":8888"))
}
运行Fiber Web 服务器:
go run main.go
Docker设置PostgreSQL和pgAdmin
PostgreSQL数据库来存储应用程序的用户数据,包含pgAdmin提供管理PostgreSQL数据的图形用户界面。
docker-compose.yml
services:
postgres:
image: postgres:latest
container_name: postgres
ports:
- '6500:5432'
volumes:
- progresDB:/var/lib/postgresql/data
env_file:
- ./app.env
pgAdmin:
image: dpage/pgadmin4
container_name: pgAdmin
env_file:
- ./app.env
ports:
- '5050:80'
volumes:
progresDB:
包含必要的环境变量pgAdminapp.env让Docker Compose可以访问变量,创建一个app.env文件:
应用程序环境
POSTGRES_HOST=127.0.0.1
POSTGRES_PORT=6500
POSTGRES_USER=admin
POSTGRES_PASSWORD=password123
POSTGRES_DB=golang_fiber
DATABASE_URL=postgresql://admin:password123@localhost:6500/golang_fiber?schema=public
CLIENT_ORIGIN=http://localhost:3000
PGADMIN_DEFAULT_EMAIL=admin@admin.com
PGADMIN_DEFAULT_PASSWORD=password123
JWT_SECRET=my_ultra_secure_secret
JWT_EXPIRED_IN=60m
JWT_MAXAGE=60
将环境变量加载到应用程序中
从Viper包文件中加载环境变量app.env:
初始化程序/env.go
package initializers
import (
"time"
"github.com/spf13/viper"
)
type Env struct {
DBHost string `mapstructure:"POSTGRES_HOST"`
DBUserName string `mapstructure:"POSTGRES_USER"`
DBUserPassword string `mapstructure:"POSTGRES_PASSWORD"`
DBName string `mapstructure:"POSTGRES_DB"`
DBPort string `mapstructure:"POSTGRES_PORT"`
JwtSecret string `mapstructure:"JWT_SECRET"`
JwtExpiresIn time.Duration `mapstructure:"JWT_EXPIRED_IN"`
JwtMaxAge int `mapstructure:"JWT_MAXAGE"`
ClientOrigin string `mapstructure:"CLIENT_ORIGIN"`
}
func LoadEnv(path string) (Env Env, err error) {
viper.AddConfigPath(path)
viper.SetConfigType("env")
viper.SetConfigName("app")
viper.AutomaticEnv()
err = viper.ReadInConfig()
if err != nil {
return
}
err = viper.Unmarshal(&Env)
return
}
初始化程序/db.go
package initializers
import (
"fmt"
"log"
"os"
"github.com/wpcodevo/go-postgres-jwt-auth-api/models"
"gorm.io/driver/postgres"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var DB *gorm.DB
func ConnectDB(env *Env) {
var err error
dsn := fmt.Sprintf("host=%s user=%s password=%s dbname=%s port=%s sslmode=disable TimeZone=Asia/Shanghai", env.DBHost, env.DBUserName, env.DBUserPassword, env.DBName, env.DBPort)
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatal("Failed to connect to the Database")
}
DB.Exec("CREATE EXTENSION IF NOT EXISTS \"uuid-ossp\"")
DB.Logger = logger.Default.LogMode(logger.Info)
err = DB.AutoMigrate(&models.User{})
if err != nil {
log.Fatal("Migration Failed: \n", err.Error())
os.Exit(1)
}
fmt.Println("???? Connected Successfully to the Database")
}
身份验证逻辑
在数据库中创建用户表的代码,现在可以继续身份验证路由处理程序。
创建三个处理程序:
用于注册新用户,
用于登录现有用户,
用于注销用户。
注册用户路由处理程序命名为SignUpUser。调用请求主体中提取用户凭据规则进行验证。
由于email字段被索引为唯一的,如果已经存在具有相同email的用户,则会返回409冲突错误,表示email已被使用。
否则,从数据库返回的用户记录将排除敏感字段,例如密码,数据将包含在 JSON 响应中。
实现注册路由处理程序,创建auth.handler.g:
auth.handler.go
func SignUpUser(c *fiber.Ctx) error {
var payload *models.SignUpInput
if err := c.BodyParser(&payload); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
}
errors := models.ValidateStruct(payload)
if errors != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "errors": errors})
}
if payload.Password != payload.PasswordConfirm {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": "Passwords do not match"})
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(payload.Password), bcrypt.DefaultCost)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
}
newUser := models.User{
Name: payload.Name,
Email: strings.ToLower(payload.Email),
Password: string(hashedPassword),
Photo: &payload.Photo,
}
result := initializers.DB.Create(&newUser)
if result.Error != nil && strings.Contains(result.Error.Error(), "duplicate key value violates unique") {
return c.Status(fiber.StatusConflict).JSON(fiber.Map{"status": "fail", "message": "User with that email already exists"})
} else if result.Error != nil {
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "error", "message": "Something bad happened"})
}
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"status": "success", "data": fiber.Map{"user": models.FilterUserRecord(&newUser)}})
}
身份验证路由处理程序:
auth.handler.go
package handlers
import (
"fmt"
"strings"
"time"
"github.com/gofiber/fiber/v2"
"github.com/golang-jwt/jwt"
"github.com/wpcodevo/go-postgres-jwt-auth-api/initializers"
"github.com/wpcodevo/go-postgres-jwt-auth-api/models"
"golang.org/x/crypto/bcrypt"
)
func SignUpUser(c *fiber.Ctx) error {
var payload *models.SignUpInput
if err := c.BodyParser(&payload); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
}
errors := models.ValidateStruct(payload)
if errors != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "errors": errors})
}
if payload.Password != payload.PasswordConfirm {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": "Passwords do not match"})
}
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(payload.Password), bcrypt.DefaultCost)
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
}
newUser := models.User{
Name: payload.Name,
Email: strings.ToLower(payload.Email),
Password: string(hashedPassword),
Photo: &payload.Photo,
}
result := initializers.DB.Create(&newUser)
if result.Error != nil && strings.Contains(result.Error.Error(), "duplicate key value violates unique") {
return c.Status(fiber.StatusConflict).JSON(fiber.Map{"status": "fail", "message": "User with that email already exists"})
} else if result.Error != nil {
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "error", "message": "Something bad happened"})
}
return c.Status(fiber.StatusCreated).JSON(fiber.Map{"status": "success", "data": fiber.Map{"user": models.FilterUserRecord(&newUser)}})
}
func SignInUser(c *fiber.Ctx) error {
var payload *models.SignInInput
if err := c.BodyParser(&payload); err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": err.Error()})
}
errors := models.ValidateStruct(payload)
if errors != nil {
return c.Status(fiber.StatusBadRequest).JSON(errors)
}
var user models.User
result := initializers.DB.First(&user, "email = ?", strings.ToLower(payload.Email))
if result.Error != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": "Invalid email or Password"})
}
err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(payload.Password))
if err != nil {
return c.Status(fiber.StatusBadRequest).JSON(fiber.Map{"status": "fail", "message": "Invalid email or Password"})
}
config, _ := initializers.LoadEnv(".")
tokenByte := jwt.New(jwt.SigningMethodHS256)
now := time.Now().UTC()
claims := tokenByte.Claims.(jwt.MapClaims)
claims["sub"] = user.ID
claims["exp"] = now.Add(config.JwtExpiresIn).Unix()
claims["iat"] = now.Unix()
claims["nbf"] = now.Unix()
tokenString, err := tokenByte.SignedString([]byte(config.JwtSecret))
if err != nil {
return c.Status(fiber.StatusBadGateway).JSON(fiber.Map{"status": "fail", "message": fmt.Sprintf("generating JWT Token failed: %v", err)})
}
c.Cookie(&fiber.Cookie{
Name: "token",
Value: tokenString,
Path: "/",
MaxAge: config.JwtMaxAge * 60,
Secure: false,
HTTPOnly: true,
Domain: "localhost",
})
return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success", "token": tokenString})
}
func LogoutUser(c *fiber.Ctx) error {
expired := time.Now().Add(-time.Hour * 24)
c.Cookie(&fiber.Cookie{
Name: "token",
Value: "",
Expires: expired,
})
return c.Status(fiber.StatusOK).JSON(fiber.Map{"status": "success"})
}
你的反应是什么?