Golang 在gin框架中如何使用JWT鑒權(quán)
什么是JWT
JWT,全稱 JSON Web Token,是一種開(kāi)放標(biāo)準(zhǔn)(RFC 7519),用于安全地在雙方之間傳遞信息。尤其適用于身份驗(yàn)證和授權(quán)場(chǎng)景。JWT 的設(shè)計(jì)允許信息在各方之間安全地、 compactly(緊湊地)傳輸,因?yàn)槠渥陨戆怂行枰恼J(rèn)證信息,從而減少了需要查詢數(shù)據(jù)庫(kù)或會(huì)話存儲(chǔ)的需求。
JWT主要由三部分組成,通過(guò).連接:
- Header(頭部):描述JWT的元數(shù)據(jù),通常包括類型(通常是
JWT)和使用的簽名算法(如HS256、RS256等)。 - Payload(載荷):包含聲明(claims),即用戶的相關(guān)信息。這些信息可以是公開(kāi)的,也可以是私有的,但應(yīng)避免放入敏感信息,因?yàn)樵摬糠挚梢员唤獯a查看。載荷中的聲明可以驗(yàn)證,但不加密。
- Signature(簽名):用于驗(yàn)證JWT的完整性和來(lái)源。它是通過(guò)將Header和Payload分別進(jìn)行Base64編碼后,再與一個(gè)秘鑰(secret)一起通過(guò)指定的算法(如HMAC SHA256)計(jì)算得出的。
JWT的工作流程大致如下:
- 認(rèn)證階段:用戶向服務(wù)器提供憑證(如用戶名和密碼)。服務(wù)器驗(yàn)證憑證無(wú)誤后,生成一個(gè)JWT,其中包含用戶標(biāo)識(shí)符和其他聲明,并使用秘鑰對(duì)其進(jìn)行簽名。
- 使用階段:客戶端收到JWT后,可以在后續(xù)的每個(gè)請(qǐng)求中將其放在HTTP請(qǐng)求頭中發(fā)送給服務(wù)器,以此證明自己的身份。
- 驗(yàn)證階段:服務(wù)器收到JWT后,會(huì)使用相同的秘鑰驗(yàn)證JWT的簽名,確保其未被篡改,并檢查過(guò)期時(shí)間等其他聲明,從而決定是否允許執(zhí)行請(qǐng)求。
JWT的優(yōu)勢(shì)在于它的無(wú)狀態(tài)性,服務(wù)器不需要存儲(chǔ)會(huì)話信息,這減輕了服務(wù)器的壓力,同時(shí)也方便了跨域認(rèn)證。但需要注意的是,JWT的安全性依賴于秘鑰的安全保管以及對(duì)JWT過(guò)期時(shí)間等的合理設(shè)置。
API設(shè)計(jì)
這里設(shè)計(jì)兩個(gè)公共接口和一個(gè)受保護(hù)的接口。
| API | 描述 |
|---|---|
/api/login | 公開(kāi)接口。用于用戶登錄 |
/api/register | 公開(kāi)接口。用于用戶注冊(cè) |
/api/admin/user | 保護(hù)接口,需要驗(yàn)證JWT |
開(kāi)發(fā)準(zhǔn)備
初始化項(xiàng)目目錄并切換進(jìn)入
mkdir gin-jwt cd gin-jwt
使用go mod初始化工程
go mod init gin-jwt
安裝依賴
go get -u github.com/gin-gonic/gin go get -u gorm.io/gorm go get -u gorm.io/driver/postgres go get -u github.com/golang-jwt/jwt/v5 go get -u github.com/joho/godotenv go get -u golang.org/x/crypto
創(chuàng)建第一個(gè)API
一開(kāi)始我們可以在項(xiàng)目的根目錄中創(chuàng)建文件main.go
touch main.go
添加以下內(nèi)容
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": "test. register api",
})
})
}
r.Run("0.0.0.0:8000")
}測(cè)試運(yùn)行
go run main.go
客戶端測(cè)試。正常的話會(huì)有以下輸出
$ curl -X POST http://127.0.0.1:8000/api/register
{"data":"test. register api"}完善register接口
現(xiàn)在register接口已經(jīng)準(zhǔn)備好了,但一般來(lái)說(shuō)我們會(huì)把接口業(yè)務(wù)邏輯放在單獨(dú)的文件中,而不是和接口定義寫(xiě)在一塊。
創(chuàng)建一個(gè)控制器的包目錄,并添加文件
mkdir controllers touch controllers/auth.go
auth.go文件內(nèi)容
package controllers
import (
"net/http"
"github.com/gin-gonic/gin"
)
func Register(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"data": "hello, this is register endpoint",
})
}更新main.go文件
package main
import (
"github.com/gin-gonic/gin"
"gin-jwt/controllers"
)
func main() {
r := gin.Default()
public := r.Group("/api")
{
public.POST("/register", controllers.Register)
}
r.Run("0.0.0.0:8000")
}重新運(yùn)行測(cè)試
go run main.go
客戶端測(cè)試
$ curl -X POST http://127.0.0.1:8000/api/register
{"data":"hello, this is register endpoint"}解析register的客戶端請(qǐng)求
客戶端請(qǐng)求register api需要攜帶用戶名和密碼的參數(shù),服務(wù)端對(duì)此做解析。編輯文件controllers/auth.go
package controllers
import (
"net/http"
"github.com/gin-gonic/gin"
)
// /api/register的請(qǐng)求體
type ReqRegister struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Register(c *gin.Context) {
var req ReqRegister
if err := c.ShouldBindBodyWithJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"data": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"data": req,
})
}客戶端請(qǐng)求測(cè)試
$ curl -X POST http://127.0.0.1:8000/api/register -d '{"username": "zhangsan", "password": "123456"}' -H 'Content-Type=application/json'
{"data":{"username":"zhangsan","password":"123456"}}連接關(guān)系型數(shù)據(jù)庫(kù)
一般會(huì)將數(shù)據(jù)保存到專門(mén)的數(shù)據(jù)庫(kù)中,這里用PostgreSQL來(lái)存儲(chǔ)數(shù)據(jù)。Postgres使用docker來(lái)安裝。安裝完postgres后,創(chuàng)建用戶和數(shù)據(jù)庫(kù):
create user ginjwt encrypted password 'ginjwt'; create database ginjwt owner = ginjwt;
創(chuàng)建目錄models,這個(gè)目錄將包含連接數(shù)據(jù)庫(kù)和數(shù)據(jù)模型的代碼。
mkdir models
編輯文件models/setup.go
package models
import (
"fmt"
"log"
"os"
"github.com/joho/godotenv"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var DB *gorm.DB
func ConnectDatabase() {
err := godotenv.Load(".env")
if err != nil {
log.Fatalf("Error loading .env file. %v\n", err)
}
// DbDriver := os.Getenv("DB_DRIVER")
DbHost := os.Getenv("DB_HOST")
DbPort := os.Getenv("DB_PORT")
DbUser := os.Getenv("DB_USER")
DbPass := os.Getenv("DB_PASS")
DbName := os.Getenv("DB_NAME")
dsn := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai password=%s", DbHost, DbPort, DbUser, DbName, DbPass)
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("Connect to database failed, %v\n", err)
} else {
log.Printf("Connect to database success, host: %s, port: %s, user: %s, dbname: %s\n", DbHost, DbPort, DbUser, DbName)
}
// 遷移數(shù)據(jù)表
DB.AutoMigrate(&User{})
}新建并編輯環(huán)境配置文件.env
DB_HOST=127.0.0.1 DB_PORT=5432 DB_USER=ginjwt DB_PASS=ginjwt DB_NAME=ginjwt
創(chuàng)建用戶模型,編輯代碼文件models/user.go
package models
import (
"html"
"strings"
"golang.org/x/crypto/bcrypt"
"gorm.io/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"`
}
func (u *User) SaveUser() (*User, error) {
err := DB.Create(&u).Error
if err != nil {
return &User{}, err
}
return u, nil
}
// 使用gorm的hook在保存密碼前對(duì)密碼進(jìn)行hash
func (u *User) BeforeSave(tx *gorm.DB) error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashedPassword)
u.Username = html.EscapeString(strings.TrimSpace(u.Username))
return nil
}更新main.go
package main
import (
"github.com/gin-gonic/gin"
"gin-jwt/controllers"
"gin-jwt/models"
)
func init() {
models.ConnectDatabase()
}
func main() {
r := gin.Default()
public := r.Group("/api")
{
public.POST("/register", controllers.Register)
}
r.Run("0.0.0.0:8000")
}更新controllers/auth.go
package controllers
import (
"net/http"
"gin-jwt/models"
"github.com/gin-gonic/gin"
)
// /api/register的請(qǐng)求體
type ReqRegister struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Register(c *gin.Context) {
var req ReqRegister
if err := c.ShouldBindBodyWithJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"data": err.Error(),
})
return
}
u := models.User{
Username: req.Username,
Password: req.Password,
}
_, err := u.SaveUser()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"data": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "register success",
"data": req,
})
}重新運(yùn)行服務(wù)端后,客戶端測(cè)試
$ curl -X POST http://127.0.0.1:8000/api/register -d '{"username": "zhangsan", "password": "123456"}' -H 'Content-Type=application/json'
{"data":{"username":"zhangsan","password":"123456"},"message":"register success"}添加login接口
登錄接口實(shí)現(xiàn)的也非常簡(jiǎn)單,只需要提供用戶名和密碼參數(shù)。服務(wù)端接收到客戶端的請(qǐng)求后到數(shù)據(jù)庫(kù)中去匹配,確認(rèn)用戶是否存在和密碼是否正確。如果驗(yàn)證通過(guò)則返回一個(gè)token,否則返回異常響應(yīng)。
首先在main.go中注冊(cè)API
// xxx
func main() {
// xxx
r := gin.Default()
public := r.Group("/api")
{
public.POST("/register", controllers.Register)
public.POST("/login", controllers.Login)
}
}在auth.go中添加Login控制器函數(shù)
// api/login 的請(qǐng)求體
type ReqLogin struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Login(c *gin.Context) {
var req ReqLogin
if err := c.ShouldBindBodyWithJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u := models.User{
Username: req.Username,
Password: req.Password,
}
// 調(diào)用 models.LoginCheck 對(duì)用戶名和密碼進(jìn)行驗(yàn)證
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,
})
}LoginCheck方法在models/user.go文件中實(shí)現(xiàn)
package models
import (
"gin-jwt/utils/token"
"html"
"strings"
"golang.org/x/crypto/bcrypt"
"gorm.io/gorm"
)
func VerifyPassword(password, hashedPassword string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
func LoginCheck(username, 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
}這里將token相關(guān)的函數(shù)放到了單獨(dú)的模塊中,新增相關(guān)目錄并編輯文件
mkdir -p utils/token touch utils/token/token.go
以下代碼為token.go的內(nèi)容,包含的幾個(gè)函數(shù)在后面會(huì)用到
package token
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
func GenerateToken(user_id uint) (string, error) {
token_lifespan, err := strconv.Atoi(os.Getenv("TOKEN_HOUR_LIFESPAN"))
if err != nil {
return "", err
}
claims := jwt.MapClaims{}
claims["authorized"] = true
claims["user_id"] = user_id
claims["exp"] = time.Now().Add(time.Hour * time.Duration(token_lifespan)).Unix()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(os.Getenv("API_SECRET")))
}
func TokenValid(c *gin.Context) error {
tokenString := ExtractToken(c)
fmt.Println(tokenString)
_, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("API_SECRET")), nil
})
if err != nil {
return err
}
return nil
}
// 從請(qǐng)求頭中獲取token
func ExtractToken(c *gin.Context) string {
bearerToken := c.GetHeader("Authorization")
if len(strings.Split(bearerToken, " ")) == 2 {
return strings.Split(bearerToken, " ")[1]
}
return ""
}
// 從jwt中解析出user_id
func ExtractTokenID(c *gin.Context) (uint, error) {
tokenString := ExtractToken(c)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("API_SECRET")), nil
})
if err != nil {
return 0, err
}
claims, ok := token.Claims.(jwt.MapClaims)
// 如果jwt有效,將user_id轉(zhuǎn)換為浮點(diǎn)數(shù)字符串,然后再轉(zhuǎn)換為 uint32
if ok && token.Valid {
uid, err := strconv.ParseUint(fmt.Sprintf("%.0f", claims["user_id"]), 10, 32)
if err != nil {
return 0, err
}
return uint(uid), nil
}
return 0, nil
}在.env文件中添加兩個(gè)環(huán)境變量的配置。TOKEN_HOUR_LIFESPAN設(shè)置token的過(guò)期時(shí)長(zhǎng),API_SECRET是jwt的密鑰。
TOKEN_HOUR_LIFESPAN=1 API_SECRET="wP3-sN6&gG4-lV8>gJ9)"
測(cè)試,這里改用python代碼進(jìn)行測(cè)試
import requests
import json
headers = {
"Content-Type": "application/json",
}
resp = requests.get("http://127.0.0.1:8000/api/admin/user", headers=headers)
def register(username: str, password: str):
req_body = {
"username": username,
"password": password,
}
resp = requests.post("http://127.0.0.1:8000/api/register", data=json.dumps(req_body), headers=headers)
print(resp.text)
def login(username: str, password: str):
req_body = {
"username": username,
"password": password,
}
resp = requests.post("http://127.0.0.1:8000/api/login", data=json.dumps(req_body), headers=headers)
print(resp.text)
if resp.status_code == 200:
return resp.json()["token"]
else:
return ""
if __name__ == "__main__":
username = "lisi"
password = "123456"
register(username, password)
token = login(username, password)
print(token)創(chuàng)建JWT認(rèn)證中間件
創(chuàng)建中間件目錄和代碼文件
mkdir middlewares touch middlewares/middlewares.go
內(nèi)容如下
package middlewares
import (
"gin-jwt/utils/token"
"net/http"
"github.com/gin-gonic/gin"
)
func JwtAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
err := token.TokenValid(c)
if err != nil {
c.String(http.StatusUnauthorized, err.Error())
c.Abort()
return
}
c.Next()
}
}在main.go文件中注冊(cè)路由的時(shí)候使用中間件
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")
{
protected.Use(middlewares.JwtAuthMiddleware())
protected.GET("/user", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "success",
"message": "authorized",
})
})
}
r.Run("0.0.0.0:8000")
}在controllers/auth.go文件中實(shí)現(xiàn)CurrentUser
func CurrentUser(c *gin.Context) {
// 從token中解析出user_id
user_id, err := token.ExtractTokenID(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
// 根據(jù)user_id從數(shù)據(jù)庫(kù)查詢數(shù)據(jù)
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,
})
}在models/user.go文件中實(shí)現(xiàn)GetUserByID
// 返回前將用戶密碼置空
func (u *User) PrepareGive() {
u.Password = ""
}
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
}至此,一個(gè)簡(jiǎn)單的gin-jwt應(yīng)用就完成了。
客戶端測(cè)試python腳本
服務(wù)端的三個(gè)接口這里用python腳本來(lái)測(cè)試
import requests
import json
headers = {
# "Authorization": f"Bearer {token}",
"Content-Type": "application/json",
}
resp = requests.get("http://127.0.0.1:8000/api/admin/user", headers=headers)
def register(username: str, password: str):
req_body = {
"username": username,
"password": password,
}
resp = requests.post("http://127.0.0.1:8000/api/register", data=json.dumps(req_body), headers=headers)
print(resp.text)
def login(username: str, password: str):
req_body = {
"username": username,
"password": password,
}
resp = requests.post("http://127.0.0.1:8000/api/login", data=json.dumps(req_body), headers=headers)
print(resp.text)
if resp.status_code == 200:
return resp.json()["token"]
else:
return ""
def test_protect_api(token: str):
global headers
headers["Authorization"] = f"Bearer {token}"
resp = requests.get("http://127.0.0.1:8000/api/admin/user", headers=headers)
print(resp.text)
if __name__ == "__main__":
username = "lisi"
password = "123456"
register(username, password)
token = login(username, password)
test_protect_api(token)運(yùn)行腳本結(jié)果
{"message":"register success"}
{"token":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJhdXRob3JpemVkIjp0cnVlLCJleHAiOjE3MTk5NDA0NjAsInVzZXJfaWQiOjZ9.qkzn0Ot9hAb54l3RFbGUohHJ9oezGia5x_oXppbD2jQ"}
{"data":{"ID":6,"CreatedAt":"2024-07-03T00:14:20.187725+08:00","UpdatedAt":"2024-07-03T00:14:20.187725+08:00","DeletedAt":null,"username":"wangwu","password":""},"message":"success"}
完整示例代碼
目錄結(jié)構(gòu)
├── client.py # 客戶端測(cè)試腳本
├── controllers # 控制器相關(guān)包
│ └── auth.go # 控制器方法實(shí)現(xiàn)
├── gin-jwt.bin # 編譯的二進(jìn)制文件
├── go.mod # go 項(xiàng)目文件
├── go.sum # go 項(xiàng)目文件
├── main.go # 程序入口文件
├── middlewares # 中間件相關(guān)包
│ └── middlewares.go # 中間件代碼文件
├── models # 存儲(chǔ)層相關(guān)包
│ ├── setup.go # 配置數(shù)據(jù)庫(kù)連接
│ └── user.go # user模塊相關(guān)數(shù)據(jù)交互的代碼文件
├── README.md # git repo的描述文件
└── utils # 工具類包
└── token # token相關(guān)工具類包
└── token.go # token工具的代碼文件main.go
package main
import (
"log"
"github.com/gin-gonic/gin"
"gin-jwt/controllers"
"gin-jwt/middlewares"
"gin-jwt/models"
"github.com/joho/godotenv"
)
func init() {
err := godotenv.Load(".env")
if err != nil {
log.Fatalf("Error loading .env file. %v\n", err)
}
}
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")
{
protected.Use(middlewares.JwtAuthMiddleware()) // 在路由組中使用中間件
protected.GET("/user", controllers.CurrentUser)
}
r.Run("0.0.0.0:8000")
}controllers
- auth.go
package controllers
import (
"net/http"
"gin-jwt/models"
"gin-jwt/utils/token"
"github.com/gin-gonic/gin"
)
// /api/register的請(qǐng)求體
type ReqRegister struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// api/login 的請(qǐng)求體
type ReqLogin struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
func Login(c *gin.Context) {
var req ReqLogin
if err := c.ShouldBindBodyWithJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
u := models.User{
Username: req.Username,
Password: req.Password,
}
// 調(diào)用 models.LoginCheck 對(duì)用戶名和密碼進(jìn)行驗(yàn)證
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,
})
}
func Register(c *gin.Context) {
var req ReqRegister
if err := c.ShouldBindBodyWithJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"data": err.Error(),
})
return
}
u := models.User{
Username: req.Username,
Password: req.Password,
}
_, err := u.SaveUser()
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"data": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"message": "register success",
})
}
func CurrentUser(c *gin.Context) {
// 從token中解析出user_id
user_id, err := token.ExtractTokenID(c)
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{
"error": err.Error(),
})
return
}
// 根據(jù)user_id從數(shù)據(jù)庫(kù)查詢數(shù)據(jù)
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,
})
}models
- setup.go
package models
import (
"fmt"
"log"
"os"
"gorm.io/driver/postgres"
"gorm.io/gorm"
)
var DB *gorm.DB
func ConnectDatabase() {
var err error
DbHost := os.Getenv("DB_HOST")
DbPort := os.Getenv("DB_PORT")
DbUser := os.Getenv("DB_USER")
DbPass := os.Getenv("DB_PASS")
DbName := os.Getenv("DB_NAME")
dsn := fmt.Sprintf("host=%s port=%s user=%s dbname=%s sslmode=disable TimeZone=Asia/Shanghai password=%s", DbHost, DbPort, DbUser, DbName, DbPass)
DB, err = gorm.Open(postgres.Open(dsn), &gorm.Config{})
if err != nil {
log.Fatalf("Connect to database failed, %v\n", err)
} else {
log.Printf("Connect to database success, host: %s, port: %s, user: %s, dbname: %s\n", DbHost, DbPort, DbUser, DbName)
}
// 遷移數(shù)據(jù)表
DB.AutoMigrate(&User{})
}- user.go
package models
import (
"errors"
"gin-jwt/utils/token"
"html"
"strings"
"golang.org/x/crypto/bcrypt"
"gorm.io/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"`
}
func (u *User) SaveUser() (*User, error) {
err := DB.Create(&u).Error
if err != nil {
return &User{}, err
}
return u, nil
}
// 使用gorm的hook在保存密碼前對(duì)密碼進(jìn)行hash
func (u *User) BeforeSave(tx *gorm.DB) error {
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(u.Password), bcrypt.DefaultCost)
if err != nil {
return err
}
u.Password = string(hashedPassword)
u.Username = html.EscapeString(strings.TrimSpace(u.Username))
return nil
}
// 返回前將用戶密碼置空
func (u *User) PrepareGive() {
u.Password = ""
}
// 對(duì)哈希加密的密碼進(jìn)行比對(duì)校驗(yàn)
func VerifyPassword(password, hashedPassword string) error {
return bcrypt.CompareHashAndPassword([]byte(hashedPassword), []byte(password))
}
func LoginCheck(username, 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 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
}utils
- token/token.go
package token
import (
"fmt"
"os"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v5"
)
func GenerateToken(user_id uint) (string, error) {
token_lifespan, err := strconv.Atoi(os.Getenv("TOKEN_HOUR_LIFESPAN"))
if err != nil {
return "", err
}
claims := jwt.MapClaims{}
claims["authorized"] = true
claims["user_id"] = user_id
claims["exp"] = time.Now().Add(time.Hour * time.Duration(token_lifespan)).Unix()
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
return token.SignedString([]byte(os.Getenv("API_SECRET")))
}
func TokenValid(c *gin.Context) error {
tokenString := ExtractToken(c)
fmt.Println(tokenString)
_, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("API_SECRET")), nil
})
if err != nil {
return err
}
return nil
}
// 從請(qǐng)求頭中獲取token
func ExtractToken(c *gin.Context) string {
bearerToken := c.GetHeader("Authorization")
if len(strings.Split(bearerToken, " ")) == 2 {
return strings.Split(bearerToken, " ")[1]
}
return ""
}
// 從jwt中解析出user_id
func ExtractTokenID(c *gin.Context) (uint, error) {
tokenString := ExtractToken(c)
token, err := jwt.Parse(tokenString, func(token *jwt.Token) (any, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return []byte(os.Getenv("API_SECRET")), nil
})
if err != nil {
return 0, err
}
claims, ok := token.Claims.(jwt.MapClaims)
// 如果jwt有效,將user_id轉(zhuǎn)換為浮點(diǎn)數(shù)字符串,然后再轉(zhuǎn)換為 uint32
if ok && token.Valid {
uid, err := strconv.ParseUint(fmt.Sprintf("%.0f", claims["user_id"]), 10, 32)
if err != nil {
return 0, err
}
return uint(uid), nil
}
return 0, nil
}middlewares
- middlewares.go
package middlewares
import (
"gin-jwt/utils/token"
"net/http"
"github.com/gin-gonic/gin"
)
func JwtAuthMiddleware() gin.HandlerFunc {
return func(c *gin.Context) {
err := token.TokenValid(c)
if err != nil {
c.String(http.StatusUnauthorized, err.Error())
c.Abort()
return
}
c.Next()
}
}參考
到此這篇關(guān)于golang 在Gin框架中使用JWT鑒權(quán)的文章就介紹到這了,更多相關(guān)golang Gin框架使用JWT鑒權(quán)內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
Golang實(shí)現(xiàn)將視頻按照時(shí)間維度剪切的工具
這篇文章主要為大家詳細(xì)介紹了如何利用Golang實(shí)現(xiàn)將視頻按照時(shí)間維度進(jìn)行剪切,文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起了解一下2022-12-12
Golang協(xié)程常見(jiàn)面試題小結(jié)
本文主要介紹了Golang協(xié)程常見(jiàn)面試題小結(jié),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02
Golang通道阻塞情況與通道無(wú)阻塞實(shí)現(xiàn)小結(jié)
本文主要介紹了Golang通道阻塞情況與通道無(wú)阻塞實(shí)現(xiàn)小結(jié),詳細(xì)解析了通道的類型、操作方法以及垃圾回收機(jī)制,從基礎(chǔ)概念到高級(jí)應(yīng)用,具有一定的參考價(jià)值,感興趣的可以了解一下2024-03-03
gin項(xiàng)目部署到服務(wù)器并后臺(tái)啟動(dòng)的步驟
本文主要介紹了gin項(xiàng)目部署到服務(wù)器并后臺(tái)啟動(dòng)的步驟,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2023-02-02
golang常用庫(kù)之字段參數(shù)驗(yàn)證庫(kù)-validator使用詳解
這篇文章主要介紹了golang常用庫(kù):字段參數(shù)驗(yàn)證庫(kù)-validator使用,本文給大家介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或工作具有一定的參考借價(jià)值,需要的朋友可以參考下2020-10-10
Go無(wú)緩沖通道(同步通道)的實(shí)現(xiàn)
本文主要介紹了Go無(wú)緩沖通道(同步通道)的實(shí)現(xiàn),文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2025-02-02
Go語(yǔ)言之io.ReadAtLeast函數(shù)的基本使用和原理解析
io.ReadAtLeast函數(shù)是Go語(yǔ)言標(biāo)準(zhǔn)庫(kù)提供的一個(gè)工具函數(shù),能夠從數(shù)據(jù)源讀取至少指定數(shù)量的字節(jié)數(shù)據(jù)到緩沖區(qū)中,這篇文章主要介紹了io.ReadAtLeast函數(shù)的相關(guān)知識(shí),需要的朋友可以參考下2023-07-07
淺析Golang中的net/http路由注冊(cè)與請(qǐng)求處理
這篇文章主要為大家詳細(xì)介紹了Golang中的net/http路由注冊(cè)與請(qǐng)求處理的相關(guān)知識(shí),文中的示例代碼講解詳細(xì),感興趣的小伙伴可以跟隨小編一起學(xué)習(xí)一下2023-12-12

