什么是jwt
jwt,全称 json web token,是一种开放标准(rfc 7519),用于安全地在双方之间传递信息。尤其适用于身份验证和授权场景。jwt 的设计允许信息在各方之间安全地、 compactly(紧凑地)传输,因为其自身包含了所有需要的认证信息,从而减少了需要查询数据库或会话存储的需求。
jwt主要由三部分组成,通过.
连接:
- header(头部):描述jwt的元数据,通常包括类型(通常是
jwt
)和使用的签名算法(如hs256
、rs256
等)。 - payload(载荷):包含声明(claims),即用户的相关信息。这些信息可以是公开的,也可以是私有的,但应避免放入敏感信息,因为该部分可以被解码查看。载荷中的声明可以验证,但不加密。
- signature(签名):用于验证jwt的完整性和来源。它是通过将header和payload分别进行base64编码后,再与一个秘钥(secret)一起通过指定的算法(如hmac sha256)计算得出的。
jwt的工作流程大致如下:
- 认证阶段:用户向服务器提供凭证(如用户名和密码)。服务器验证凭证无误后,生成一个jwt,其中包含用户标识符和其他声明,并使用秘钥对其进行签名。
- 使用阶段:客户端收到jwt后,可以在后续的每个请求中将其放在http请求头中发送给服务器,以此证明自己的身份。
- 验证阶段:服务器收到jwt后,会使用相同的秘钥验证jwt的签名,确保其未被篡改,并检查过期时间等其他声明,从而决定是否允许执行请求。
jwt的优势在于它的无状态性,服务器不需要存储会话信息,这减轻了服务器的压力,同时也方便了跨域认证。但需要注意的是,jwt的安全性依赖于秘钥的安全保管以及对jwt过期时间等的合理设置。
api设计
这里设计两个公共接口和一个受保护的接口。
api | 描述 |
---|---|
/api/login | 公开接口。用于用户登录 |
/api/register | 公开接口。用于用户注册 |
/api/admin/user | 保护接口,需要验证jwt |
开发准备
初始化项目目录并切换进入
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
创建第一个api
一开始我们可以在项目的根目录中创建文件main.go
touch main.go
添加以下内容
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") }
测试运行
go run main.go
客户端测试。正常的话会有以下输出
$ curl -x post http://127.0.0.1:8000/api/register {"data":"test. register api"}
完善register接口
现在register接口已经准备好了,但一般来说我们会把接口业务逻辑放在单独的文件中,而不是和接口定义写在一块。
创建一个控制器的包目录,并添加文件
mkdir controllers touch controllers/auth.go
auth.go
文件内容
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") }
重新运行测试
go run main.go
客户端测试
$ curl -x post http://127.0.0.1:8000/api/register {"data":"hello, this is register endpoint"}
解析register的客户端请求
客户端请求register api需要携带用户名和密码的参数,服务端对此做解析。编辑文件controllers/auth.go
package controllers import ( "net/http" "github.com/gin-gonic/gin" ) // /api/register的请求体 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, }) }
客户端请求测试
$ 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"}}
连接关系型数据库
一般会将数据保存到专门的数据库中,这里用postgresql来存储数据。postgres使用docker来安装。安装完postgres后,创建用户和数据库:
create user ginjwt encrypted password 'ginjwt'; create database ginjwt owner = ginjwt;
创建目录models
,这个目录将包含连接数据库和数据模型的代码。
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) } // 迁移数据表 db.automigrate(&user{}) }
新建并编辑环境配置文件.env
db_host=127.0.0.1 db_port=5432 db_user=ginjwt db_pass=ginjwt db_name=ginjwt
创建用户模型,编辑代码文件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在保存密码前对密码进行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的请求体 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, }) }
重新运行服务端后,客户端测试
$ 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接口
登录接口实现的也非常简单,只需要提供用户名和密码参数。服务端接收到客户端的请求后到数据库中去匹配,确认用户是否存在和密码是否正确。如果验证通过则返回一个token,否则返回异常响应。
首先在main.go
中注册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控制器函数
// api/login 的请求体 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, } // 调用 models.logincheck 对用户名和密码进行验证 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
文件中实现
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相关的函数放到了单独的模块中,新增相关目录并编辑文件
mkdir -p utils/token touch utils/token/token.go
以下代码为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 } // 从请求头中获取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转换为浮点数字符串,然后再转换为 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
文件中添加两个环境变量的配置。token_hour_lifespan
设置token的过期时长,api_secret
是jwt的密钥。
token_hour_lifespan=1 api_secret="wp3-sn6&gg4-lv8>gj9)"
测试,这里改用python代码进行测试
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)
创建jwt认证中间件
创建中间件目录和代码文件
mkdir middlewares touch 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() } }
在main.go
文件中注册路由的时候使用中间件
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
文件中实现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 } // 根据user_id从数据库查询数据 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
文件中实现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 }
至此,一个简单的gin-jwt应用就完成了。
客户端测试python脚本
服务端的三个接口这里用python脚本来测试
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)
运行脚本结果
{"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"}
完整示例代码
目录结构
├── client.py # 客户端测试脚本 ├── controllers # 控制器相关包 │ └── auth.go # 控制器方法实现 ├── gin-jwt.bin # 编译的二进制文件 ├── go.mod # go 项目文件 ├── go.sum # go 项目文件 ├── main.go # 程序入口文件 ├── middlewares # 中间件相关包 │ └── middlewares.go # 中间件代码文件 ├── models # 存储层相关包 │ ├── setup.go # 配置数据库连接 │ └── user.go # user模块相关数据交互的代码文件 ├── readme.md # git repo的描述文件 └── utils # 工具类包 └── token # token相关工具类包 └── 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的请求体 type reqregister struct { username string `json:"username" binding:"required"` password string `json:"password" binding:"required"` } // api/login 的请求体 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, } // 调用 models.logincheck 对用户名和密码进行验证 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 } // 根据user_id从数据库查询数据 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) } // 迁移数据表 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在保存密码前对密码进行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 = "" } // 对哈希加密的密码进行比对校验 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 } // 从请求头中获取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转换为浮点数字符串,然后再转换为 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() } }
参考
到此这篇关于golang 在gin框架中使用jwt鉴权的文章就介绍到这了,更多相关golang gin框架使用jwt鉴权内容请搜索代码网以前的文章或继续浏览下面的相关文章希望大家以后多多支持代码网!
发表评论