golang实现JWT认证中间件插件库jwt-auth的使用

Golang实现JWT认证中间件插件库jwt-auth的使用

jwt-auth是一个用于Golang的JWT认证中间件库。

快速开始

下面是一个基本的使用示例:

package main

import (
  "net/http"
  "log"
  "time"

  "github.com/adam-hanna/jwt-auth/jwt"
)

var restrictedRoute jwt.Auth

var restrictedHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("Welcome to the secret area!"))
})

var regularHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  w.Write([]byte("Hello, World!"))
})

func main() {
  authErr := jwt.New(&restrictedRoute, jwt.Options{
    SigningMethodString:   "RS256",
    PrivateKeyLocation:    "keys/app.rsa",     // `$ openssl genrsa -out app.rsa 2048`
    PublicKeyLocation:     "keys/app.rsa.pub", // `$ openssl rsa -in app.rsa -pubout > app.rsa.pub`
    RefreshTokenValidTime: 72 * time.Hour,
    AuthTokenValidTime:    15 * time.Minute,
    Debug:                 false,
    IsDevEnv:              true,
  })
  if authErr != nil {
    log.Println("Error initializing the JWT's!")
    log.Fatal(authErr)
  }

  http.HandleFunc("/", regularHandler)
  // 这个路由将不可用,因为我们从未发放过token
  // 查看login_logout示例了解如何提供token
  http.Handle("/restricted", restrictedRoute.Handler(restrictedHandler))

  log.Println("Listening on localhost:3000")
  http.ListenAndServe("127.0.0.1:3000", nil)
}

设计目标

理解这个认证架构的目标非常重要。它并不适用于所有用例。请阅读并理解以下目标,并根据您的具体需求调整工作流程:

  1. 保护非关键API(例如不用于金融、医疗保健、政府等服务)
  2. 无状态
  3. 用户会话
  4. CSRF保护
  5. Web和/或移动端

设计架构

这个认证系统基于以下三个主要组件:

  1. 短生命期(分钟级)的JWT认证令牌
  2. 较长生命期(小时/天级)的JWT刷新令牌
  3. CSRF密钥字符串

1. 短生命期JWT认证令牌

短生命期的jwt认证令牌允许用户向受保护的API端点发出无状态请求。默认过期时间为15分钟,将由较长生命期的刷新令牌刷新。

2. 较长生命期JWT刷新令牌

这个较长生命期的令牌将用于更新认证令牌。这些令牌默认有72小时的过期时间,每次刷新认证令牌时都会更新。

这些刷新令牌包含一个可以被授权客户端撤销的ID。

3. CSRF密钥字符串

每个客户端将获得一个CSRF密钥字符串,该字符串将与认证和刷新令牌中的CSRF密钥相同,并在每次刷新认证令牌时更改。这些密钥默认存储在"X-CSRF-Token"响应头中,但可以将头键设置为选项。

完整示例

下面是一个完整的登录/注销示例:

package main

import (
	"log"
	"net/http"
	"time"

	"github.com/adam-hanna/jwt-auth/jwt"
)

var restrictedRoute jwt.Auth

var restrictedHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	csrfSecret := w.Header().Get("X-CSRF-Token")
	claims, err := restrictedRoute.GrabTokenClaims(r)
	log.Println(claims)

	if err != nil {
		http.Error(w, "Internal Server Error", 500)
	} else {
		w.Write([]byte("Welcome to the restricted area! CSRF: " + csrfSecret))
	}
})

var loginHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	// 验证用户凭据...
	
	// 创建claims
	claims := jwt.ClaimsType{}
	claims.StandardClaims.Id = "uniqueTokenId123" // 重要:如果你想稍后能够撤销此令牌,必须提供token ID
	claims.CustomClaims = make(map[string]interface{})
	claims.CustomClaims["Role"] = "user"

	// 发放新令牌
	err := restrictedRoute.IssueNewTokens(w, &claims)
	if err != nil {
		http.Error(w, "Internal Server Error", 500)
		return
	}

	w.Write([]byte("Login successful!"))
})

var logoutHandler = http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
	err := restrictedRoute.NullifyTokens(w, r)
	if err != nil {
		http.Error(w, "Internal Server Error", 500)
		return
	}

	w.Write([]byte("Logout successful!"))
})

func main() {
	authErr := jwt.New(&restrictedRoute, jwt.Options{
		SigningMethodString:   "RS256",
		PrivateKeyLocation:    "keys/app.rsa",
		PublicKeyLocation:     "keys/app.rsa.pub",
		RefreshTokenValidTime: 72 * time.Hour,
		AuthTokenValidTime:    15 * time.Minute,
		Debug:                 false,
		IsDevEnv:              true,
	})
	if authErr != nil {
		log.Println("Error initializing the JWT's!")
		log.Fatal(authErr)
	}

	http.Handle("/login", loginHandler)
	http.Handle("/logout", restrictedRoute.Handler(logoutHandler))
	http.Handle("/restricted", restrictedRoute.Handler(restrictedHandler))

	log.Println("Listening on localhost:3000")
	http.ListenAndServe("127.0.0.1:3000", nil)
}

选项配置

type Options struct {
  SigningMethodString   string // 可选:"HS256", "HS384", "HS512", "RS256", "RS384", "RS512", "ES256", "ES384", "ES512"
  PrivateKeyLocation    string // 仅用于RSA和ECDSA签名方法;仅在VerifyOnlyServer为false时需要
  PublicKeyLocation     string // 仅用于RSA和ECDSA签名方法
  HMACKey               []byte // 仅用于HMAC-SHA签名方法
  VerifyOnlyServer      bool   // false = 服务器可以验证和发放令牌(默认);true = 服务器只能验证令牌
  BearerTokens          bool   // false = 服务器使用cookie传输jwt(默认);true = 服务器使用请求头
  RefreshTokenValidTime time.Duration
  AuthTokenValidTime    time.Duration
  AuthTokenName         string // 默认为"AuthToken"(cookie)或"X-Auth-Token"(bearer令牌)
  RefreshTokenName      string // 默认为"RefreshToken"(cookie)或"X-Refresh-Token"(bearer令牌)
  CSRFTokenName         string // 默认为"X-CSRF-Token"
  Debug                 bool   // true = 显示更多日志
  IsDevEnv              bool   // true = 开发模式;这将设置http cookie(如果使用)为不安全;false = 生产模式;这将设置http cookie(如果使用)为安全
}

性能

基准测试结果:

$ cd jwt && go test -bench=.

BenchmarkBaseServer-2                        10000      137517 ns/op
BenchmarkValidAuthTokenWithCookies-2          5000      303160 ns/op
BenchmarkExpiredAuthTokenWithCookies-2        5000      323933 ns/op
PASS
ok    github.com/adam-hanna/jwt-auth/jwt  15.463s

许可证

MIT许可证


更多关于golang实现JWT认证中间件插件库jwt-auth的使用的实战教程也可以访问 https://www.itying.com/category-94-b0.html

1 回复

更多关于golang实现JWT认证中间件插件库jwt-auth的使用的实战系列教程也可以访问 https://www.itying.com/category-94-b0.html


Golang JWT认证中间件实现

下面我将介绍如何使用Golang实现一个JWT认证中间件库,并提供完整的示例代码。

1. 基本实现

首先我们需要安装必要的依赖:

go get github.com/golang-jwt/jwt/v5

1.1 核心代码实现

package jwt_auth

import (
	"errors"
	"net/http"
	"strings"
	"time"

	"github.com/golang-jwt/jwt/v5"
)

// 自定义Claims结构
type CustomClaims struct {
	UserID   int    `json:"user_id"`
	Username string `json:"username"`
	jwt.RegisteredClaims
}

// JWT配置
type Config struct {
	SecretKey       string        // 密钥
	ExpirationTime  time.Duration // 过期时间
	SigningMethod   string        // 签名方法 HS256/HS384/HS512
	TokenLookup     string        // token查找位置 header:<name>
	AuthScheme      string        // 认证方案 Bearer
}

// 默认配置
var defaultConfig = Config{
	SecretKey:      "your-secret-key",
	ExpirationTime: 24 * time.Hour,
	SigningMethod:  "HS256",
	TokenLookup:    "header:Authorization",
	AuthScheme:     "Bearer",
}

// JWT认证中间件
type JWTAuth struct {
	config Config
}

// 创建新的JWT认证实例
func New(config ...Config) *JWTAuth {
	cfg := defaultConfig
	if len(config) > 0 {
		cfg = config[0]
	}
	return &JWTAuth{config: cfg}
}

// 生成Token
func (j *JWTAuth) GenerateToken(userID int, username string) (string, error) {
	claims := CustomClaims{
		UserID:   userID,
		Username: username,
		RegisteredClaims: jwt.RegisteredClaims{
			ExpiresAt: jwt.NewNumericDate(time.Now().Add(j.config.ExpirationTime)),
			IssuedAt:  jwt.NewNumericDate(time.Now()),
			Issuer:    "jwt-auth",
		},
	}

	method := jwt.GetSigningMethod(j.config.SigningMethod)
	token := jwt.NewWithClaims(method, claims)
	return token.SignedString([]byte(j.config.SecretKey))
}

// 解析Token
func (j *JWTAuth) ParseToken(tokenString string) (*CustomClaims, error) {
	token, err := jwt.ParseWithClaims(tokenString, &CustomClaims{}, func(token *jwt.Token) (interface{}, error) {
		if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
			return nil, jwt.ErrSignatureInvalid
		}
		return []byte(j.config.SecretKey), nil
	})

	if err != nil {
		return nil, err
	}

	if claims, ok := token.Claims.(*CustomClaims); ok && token.Valid {
		return claims, nil
	}

	return nil, errors.New("invalid token")
}

// 中间件函数
func (j *JWTAuth) Middleware(next http.Handler) http.Handler {
	return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		// 从请求中提取token
		token, err := j.extractToken(r)
		if err != nil {
			http.Error(w, err.Error(), http.StatusUnauthorized)
			return
		}

		// 解析token
		claims, err := j.ParseToken(token)
		if err != nil {
			http.Error(w, "invalid token", http.StatusUnauthorized)
			return
		}

		// 将claims信息存入上下文
		ctx := r.Context()
		ctx = contextWithUserID(ctx, claims.UserID)
		ctx = contextWithUsername(ctx, claims.Username)
		r = r.WithContext(ctx)

		next.ServeHTTP(w, r)
	})
}

// 从请求中提取token
func (j *JWTAuth) extractToken(r *http.Request) (string, error) {
	parts := strings.Split(j.config.TokenLookup, ":")
	if len(parts) != 2 {
		return "", errors.New("invalid token lookup format")
	}

	switch parts[0] {
	case "header":
		return j.extractTokenFromHeader(r, parts[1])
	case "query":
		return j.extractTokenFromQuery(r, parts[1])
	case "cookie":
		return j.extractTokenFromCookie(r, parts[1])
	default:
		return "", errors.New("unsupported token lookup")
	}
}

// 从header中提取token
func (j *JWTAuth) extractTokenFromHeader(r *http.Request, key string) (string, error) {
	authHeader := r.Header.Get(key)
	if authHeader == "" {
		return "", errors.New("authorization header is missing")
	}

	parts := strings.SplitN(authHeader, " ", 2)
	if !(len(parts) == 2 && parts[0] == j.config.AuthScheme) {
		return "", errors.New("invalid authorization header format")
	}

	return parts[1], nil
}

// 从query参数中提取token
func (j *JWTAuth) extractTokenFromQuery(r *http.Request, key string) (string, error) {
	token := r.URL.Query().Get(key)
	if token == "" {
		return "", errors.New("token query parameter is missing")
	}
	return token, nil
}

// 从cookie中提取token
func (j *JWTAuth) extractTokenFromCookie(r *http.Request, key string) (string, error) {
	cookie, err := r.Cookie(key)
	if err != nil {
		return "", errors.New("token cookie is missing")
	}
	return cookie.Value, nil
}

1.2 上下文处理

// 上下文键类型
type contextKey string

const (
	userIDKey   contextKey = "user_id"
	usernameKey contextKey = "username"
)

// 将userID存入上下文
func contextWithUserID(ctx context.Context, userID int) context.Context {
	return context.WithValue(ctx, userIDKey, userID)
}

// 从上下文中获取userID
func UserIDFromContext(ctx context.Context) (int, bool) {
	userID, ok := ctx.Value(userIDKey).(int)
	return userID, ok
}

// 将username存入上下文
func contextWithUsername(ctx context.Context, username string) context.Context {
	return context.WithValue(ctx, usernameKey, username)
}

// 从上下文中获取username
func UsernameFromContext(ctx context.Context) (string, bool) {
	username, ok := ctx.Value(usernameKey).(string)
	return username, ok
}

2. 使用示例

2.1 初始化并使用中间件

package main

import (
	"fmt"
	"net/http"
	"time"
	
	"yourmodule/jwt_auth"
)

func main() {
	// 初始化JWT认证
	jwtAuth := jwt_auth.New(jwt_auth.Config{
		SecretKey:      "your-very-secret-key",
		ExpirationTime: 24 * time.Hour,
	})

	// 受保护的路由
	protectedHandler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
		userID, ok := jwt_auth.UserIDFromContext(r.Context())
		if !ok {
			http.Error(w, "user ID not found", http.StatusUnauthorized)
			return
		}
		
		username, _ := jwt_auth.UsernameFromContext(r.Context())
		fmt.Fprintf(w, "Welcome, %s (ID: %d)! This is a protected route.", username, userID)
	})

	// 登录路由
	http.HandleFunc("/login", func(w http.ResponseWriter, r *http.Request) {
		// 这里应该是你的用户认证逻辑
		// 假设认证成功,我们生成token
		userID := 123
		username := "john_doe"
		
		token, err := jwtAuth.GenerateToken(userID, username)
		if err != nil {
			http.Error(w, "failed to generate token", http.StatusInternalServerError)
			return
		}
		
		w.Header().Set("Content-Type", "application/json")
		fmt.Fprintf(w, `{"token": "%s"}`, token)
	})

	// 应用JWT中间件到受保护的路由
	http.Handle("/protected", jwtAuth.Middleware(protectedHandler))

	fmt.Println("Server started on :8080")
	http.ListenAndServe(":8080", nil)
}

2.2 测试

  1. 首先访问 /login 获取token
  2. 然后访问 /protected 并在Header中添加:
    Authorization: Bearer <your-token>
    

3. 功能扩展

这个基础实现可以进一步扩展:

  1. 刷新Token:添加刷新token的机制
  2. 黑名单:实现token黑名单用于登出功能
  3. 多角色支持:在claims中添加角色信息
  4. 性能优化:添加token缓存

这个JWT中间件实现提供了基本的认证功能,可以根据项目需求进行进一步定制和扩展。

回到顶部