Golang JWT 认证 (一)- 后端

最近学习使用jwt,准备实现一个简单的使用jwt认证的前后端。
本篇为后端设计,配合gin框架,实现登录获取token,使用token访问api过程

一. 本例中JWT 认证过程原理

Client Server 1. POST /auth/login {"username": "leo", "password": "leo"} a. 后端生成并返回 token 2. HTTP Header 带Authorization: token请求API b. 后端提取token中身份信息,并决定处理结果 Client Server

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认证过程

关键函数和过程如下:

  1. tokenString解析到jwt.Token结构
  2. 从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")
}
  • 0
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值