最近学习使用jwt,准备实现一个简单的使用jwt认证的前后端。
本篇为后端设计,配合gin框架,实现登录获取token,使用token访问api过程
一. 本例中JWT 认证过程原理
a. token生成过程
生成token过程只需以下几部
定义一个Claims结构
根据用户信息创建一个claims
使用jwt.NewWithClaims生成token然后返回给客户端签名的 string
// go get -u "github.com/golang-jwt/jwt/v4"
// 设置一个密钥
var jwtKey []byte = []byte("secret")
// 1. 定义一个Claims结构, 除了RegisteredClaims 其他为自定义附加在token中的信息
type customClaims struct {
Username string `json:"username"`
IsAdmin bool `json:"IsAdmin"`
jwt.RegisteredClaims
}
// 2. 根据用户信息,创建一个claims
claims := customClaims{
Username: 'leo',
IsAdmin: true,
RegisteredClaims: jwt.RegisteredClaims{
// 过期时间设置, 还有其他字段,也可以全部不设置
ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(1 * time.Hour)},
},
}
// 3.使用claims创建并签名出token string
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
if tokenString, err := token.SignedString(jwtKey); err != nil {
fmt.Println("generate access token failed: " + err.Error()})
} else {
fmt.Println("user token is: " + tokenString)
}
b. token认证过程
关键函数和过程如下:
- tokenString解析到jwt.Token结构
- 从Token中解析到自定义的customClaims结构
jwt.ParseWithClaims(tokenString, &customClaims{}, func(t *jwt.Token) (interface{}, error) { return jwtKey, nil })
if claims, ok := token.Claims.(*customClaims); ok && token.Valid { fmt.PrintLn(claims.Username)}
// 客户端请求时添加HTTP头Authorization: Bearer tokenxxxxxxxxx
// 后端从http头中提取Authorization (其他自定义头也行,如 token, 这个好像是有个啥标准)
// 本例web 框架为gin框架,
func AuthRequired() gin.HandlerFunc {
return func(ctx *gin.Context) {
// 剔除Bearer 字符串
tokenString := strings.TrimPrefix(ctx.GetHeader("Authorization"), "Bearer ")
// 1. 解析出token
token, err := jwt.ParseWithClaims(tokenString, &customClaims{}, func(t *jwt.Token) (interface{}, error) { return jwtKey, nil })
if err != nil {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": -1, "msg": fmt.Sprintf("access token parse error: %v", err)})
return
}
// 2. 从token中解析出自定义的claims结构, 并判断token和claims是否有效过期等
if claims, ok := token.Claims.(*customClaims); ok && token.Valid {
if !claims.VerifyExpiresAt(time.Now(), false) {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": -1, "msg": "access token expired"})
return
}
// 3. 可以直接操作calims结构,如: claims.Username claims.Isadmin,这里是gin中间件,存放到ctx中,供处理函数使用
ctx.Set("claims", claims)
} else {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": -1, "msg": fmt.Sprintf("Claims parse error: %v", err)})
return
}
ctx.Next()
}
}
二: 整体代码以及测试
设计了以下两个API接口
/auth/login 登录获取token
/api/test 需要token认证才能访问
a. 测试
curl -X POST -H 'Content-Type:application/json' -d '{"username": "leo", "password": "leo"}' http://localhost:8080/auth/login
# {"code":0,"data":"eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxlbyIsIklzQWRtaW4iOmZhbHNlLCJleHAiOjE2NjE2MDU0MzF9.1ai5pvT8CQ7bJdE4-v6i_nFfl2S_cNj6UwImwtDImko","msg":""}
curl -H "Authorization: Bearer eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6ImxlbyIsIklzQWRtaW4iOmZhbHNlLCJleHAiOjE2NjE2MDU0MzF9.1ai5pvT8CQ7bJdE4-v6i_nFfl2S_cNj6UwImwtDImko" http://localhost:8080/api/test
# {"code":0,"data":"current user: leo , is admin: false"}
b. 完整代码
package main
import (
"fmt"
"log"
"net/http"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/golang-jwt/jwt/v4"
)
var jwtKey []byte = []byte("secret")
type customClaims struct {
Username string `json:"username"`
IsAdmin bool `json:"IsAdmin"`
jwt.RegisteredClaims
}
//gin jwt 认证中间件
func AuthRequired() gin.HandlerFunc {
return func(ctx *gin.Context) {
tokenString := strings.TrimPrefix(ctx.GetHeader("Authorization"), "Bearer ")
token, err := jwt.ParseWithClaims(tokenString, &customClaims{}, func(t *jwt.Token) (interface{}, error) { return jwtKey, nil })
if err != nil {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": -1, "msg": fmt.Sprintf("access token parse error: %v", err)})
return
}
if claims, ok := token.Claims.(*customClaims); ok && token.Valid {
if !claims.VerifyExpiresAt(time.Now(), false) {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": -1, "msg": "access token expired"})
return
}
ctx.Set("claims", claims)
} else {
ctx.AbortWithStatusJSON(http.StatusForbidden, gin.H{"code": -1, "msg": fmt.Sprintf("Claims parse error: %v", err)})
return
}
ctx.Next()
}
}
type loginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
func main() {
r := gin.Default()
r.POST("/auth/login", func(ctx *gin.Context) {
var req loginRequest
ctx.BindJSON(&req)
if req.Username != req.Password {
ctx.JSON(http.StatusOK, gin.H{"code": -1, "msg": "incorrect username or password"})
return
}
log.Printf("login user " + req.Username)
claims := customClaims{
Username: req.Username,
IsAdmin: req.Username == "admin",
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: &jwt.NumericDate{Time: time.Now().Add(1 * time.Hour)},
},
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
if tokenString, err := token.SignedString(jwtKey); err != nil {
ctx.JSON(http.StatusOK, gin.H{"code": -1, "msg": "generate access token failed: " + err.Error()})
} else {
ctx.JSON(http.StatusOK, gin.H{"code": 0, "msg": "", "data": tokenString})
}
})
api := r.Group("/api")
api.Use(AuthRequired())
api.GET("/test", func(ctx *gin.Context) {
claims := ctx.MustGet("claims").(*customClaims)
ctx.JSON(http.StatusOK, gin.H{"code": 0, "data": fmt.Sprintf("current user: %v , is admin: %v", claims.Username, claims.IsAdmin)})
})
r.Run(":8080")
}