在Golang后端开发中,为API接口添加身份验证是保障服务安全的基础操作,JWT(JSON Web Token)凭借无状态、易扩展的特性,成为API认证的主流方案。下面我们将完整实现Golang后端API的JWT身份验证逻辑。

JWT核心原理简述
JWT本质是一个经过签名的JSON格式字符串,由三部分组成,分别是头部(Header)、载荷(Payload)、签名(Signature),三部分之间用点号分隔。服务端生成JWT后返回给客户端,客户端后续请求API时在请求头中携带该令牌,服务端校验令牌合法性后即可确认用户身份。
环境准备与依赖安装
我们需要使用Golang的第三方JWT库来简化开发,这里选择社区常用的github.com/golang-jwt/jwt/v5库,执行以下命令安装依赖:
go get github.com/golang-jwt/jwt/v5
JWT生成逻辑实现
首先定义JWT的相关配置,包括签名密钥、令牌过期时间等,然后实现生成JWT的函数:
package main
import (
"time"
"github.com/golang-jwt/jwt/v5"
)
// 定义JWT签名密钥,实际项目中建议从配置文件读取,不要硬编码
var jwtKey = []byte("your_secret_key_here")
// 定义载荷中存储的自定义字段结构
type Claims struct {
UserID uint64 `json:"user_id"`
Username string `json:"username"`
jwt.RegisteredClaims
}
// 生成JWT令牌的函数
func GenerateJWT(userID uint64, username string) (string, error) {
// 设置令牌过期时间为2小时
expirationTime := time.Now().Add(2 * time.Hour)
// 初始化载荷
claims := &Claims{
UserID: userID,
Username: username,
RegisteredClaims: jwt.RegisteredClaims{
ExpiresAt: jwt.NewNumericDate(expirationTime),
IssuedAt: jwt.NewNumericDate(time.Now()),
Issuer: "golang_api_demo",
},
}
// 使用HS256签名算法生成令牌
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
// 签名并返回令牌字符串
return token.SignedString(jwtKey)
}
JWT校验逻辑实现
生成令牌后,需要实现校验令牌合法性的函数,用于后续接口鉴权时调用:
// 校验JWT令牌的函数,返回解析后的载荷和错误
func ParseJWT(tokenStr string) (*Claims, error) {
claims := &Claims{}
// 解析令牌
token, err := jwt.ParseWithClaims(tokenStr, claims, func(token *jwt.Token) (interface{}, error) {
// 校验签名算法是否为预期的HS256
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
}
return jwtKey, nil
})
if err != nil {
return nil, err
}
// 校验令牌是否有效
if !token.Valid {
return nil, fmt.Errorf("invalid token")
}
return claims, nil
}
API接口鉴权中间件实现
为了避免在每个接口中重复编写令牌校验逻辑,我们可以实现一个HTTP中间件,统一处理所有需要认证的接口的鉴权逻辑:
package main
import (
"fmt"
"net/http"
"strings"
)
// JWT鉴权中间件
func JWTAuthMiddleware(next http.HandlerFunc) http.HandlerFunc {
return func(w http.ResponseWriter, r *http.Request) {
// 从请求头Authorization中获取令牌,格式为Bearer {token}
authHeader := r.Header.Get("Authorization")
if authHeader == "" {
http.Error(w, "缺少认证令牌", http.StatusUnauthorized)
return
}
// 分割Bearer和令牌字符串
parts := strings.SplitN(authHeader, " ", 2)
if len(parts) != 2 || parts[0] != "Bearer" {
http.Error(w, "认证令牌格式错误", http.StatusUnauthorized)
return
}
tokenStr := parts[1]
// 校验令牌
claims, err := ParseJWT(tokenStr)
if err != nil {
http.Error(w, fmt.Sprintf("令牌校验失败: %v", err), http.StatusUnauthorized)
return
}
// 可以将用户信息存入请求上下文,后续接口可以直接获取
ctx := r.Context()
ctx = context.WithValue(ctx, "user_id", claims.UserID)
ctx = context.WithValue(ctx, "username", claims.Username)
// 调用下一个处理函数
next(w, r.WithContext(ctx))
}
}
完整接口示例
下面我们实现两个接口,一个是登录接口用于生成JWT,另一个是用户信息接口需要JWT鉴权才能访问:
package main
import (
"context"
"encoding/json"
"net/http"
)
// 登录请求结构
type LoginRequest struct {
Username string `json:"username"`
Password string `json:"password"`
}
// 登录接口处理函数
func LoginHandler(w http.ResponseWriter, r *http.Request) {
if r.Method != http.MethodPost {
http.Error(w, "请求方法不支持", http.StatusMethodNotAllowed)
return
}
var req LoginRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
http.Error(w, "请求参数错误", http.StatusBadRequest)
return
}
// 这里简化用户校验逻辑,实际项目中需要查询数据库校验用户名密码
if req.Username == "test" && req.Password == "123456" {
// 生成JWT令牌
token, err := GenerateJWT(1, req.Username)
if err != nil {
http.Error(w, "生成令牌失败", http.StatusInternalServerError)
return
}
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]string{
"token": token,
})
return
}
http.Error(w, "用户名或密码错误", http.StatusUnauthorized)
}
// 获取用户信息接口处理函数,需要鉴权
func UserInfoHandler(w http.ResponseWriter, r *http.Request) {
ctx := r.Context()
// 从上下文获取用户信息
userID, _ := ctx.Value("user_id").(uint64)
username, _ := ctx.Value("username").(string)
w.Header().Set("Content-Type", "application/json")
json.NewEncoder(w).Encode(map[string]interface{}{
"user_id": userID,
"username": username,
})
}
func main() {
// 注册路由
http.HandleFunc("/login", LoginHandler)
// 用户信息接口使用鉴权中间件
http.HandleFunc("/user/info", JWTAuthMiddleware(UserInfoHandler))
// 启动服务
fmt.Println("服务启动在 :8080 端口")
http.ListenAndServe(":8080", nil)
}
注意事项
- 签名密钥需要妥善保管,不要泄露,生产环境建议通过配置文件或环境变量注入,不要硬编码在代码中。
- 令牌过期时间需要根据业务场景合理设置,过短会增加客户端刷新令牌的频率,过长会增加令牌泄露后的安全风险。
- 如果需要在令牌过期前刷新令牌,可以额外实现刷新令牌接口,校验旧令牌未过期且合法后生成新的令牌。
- 中间件中可以根据业务需求扩展校验逻辑,比如校验用户是否被禁用、令牌是否在黑名单中等。