回顾
项目已开源:基于 Golang 的 容器管理系统
背景
上节已经说到了统一返回的封装, 本节咱们就讲讲 JWT 鉴权
,这里有几个问题:
- JWT 是什么?
JSON Web Token (JWT)
是一个开放标准(RFC 7519
),它定义了一种紧凑的、自包含的方式,用于作为JSON
对象在各方之间安全地传输信息。该信息可以被验证和信任,因为它是数字签名的。它通常被用于对用户进行身份验证和授权。
-
JWT 的组成
Header
(头部): 包含了令牌的元数据和加密算法信息。Payload
(载荷): 存储了要传输的数据,如用户的身份信息和一些声明。Payload有三种类型:注册的声明(Reserved claims)、公共的声明(Public claims)和私有的声明(Private claims)Signature
(签名): 使用头部指定的加密算法对头部和载荷进行签名,以确保令牌在传输过程中没有被篡改。
-
JWT 的使用场景有哪些?
Authorization (授权)
:这是使用JWT的最常见场景。一旦用户登录,后续每个请求都将包含 JWT,允许用户访问该令牌允许的路由、服务和资源。单点登录是现在广泛使用的 JWT 的一个特性,因为它的开销很小,并且可以轻松地跨域使用。Information Exchange (信息交换)
:对于安全的在各方之间传输信息而言,JSON Web Token 无疑是一种很好的方式。因为 JWT 可以被签名,例如,用公钥/私钥对,你可以确定发送人就是它们所说的那个人。另外,由于签名是使用头和有效负载计算的,您还可以验证内容没有被篡改。
-
JWT 和 OAuth 的区别
- OAuth2是一种授权框架 ,JWT是一种认证协议。
- 无论使用哪种方式切记用HTTPS来保证数据的安全性。
- OAuth2用在使用第三方账号登录的情况(比如使用weibo,qq,github登录某个app),而JWT是用在前后端分离,,需要简单的对后台API进行保护时使用。
为什么要使用JWT?
- 跨平台和语言:JWT是基于JSON的标准,因此它在不同的编程语言和平台之间都可以轻松传递和解析。
- 状态无关性:传统的会话认证在服务端需要保存用户的会话状态,而JWT是无状态的,所有信息都被包含在令牌本身中,这使得服务端不需要保存任何状态信息,从而降低了服务端的负担。
- 安全性:JWT的签名保证了令牌的完整性和真实性,确保信息不会在传输过程中被篡改或伪造。同时,由于JWT是基于标准的,可以使用加密算法来保护敏感信息,确保令牌只能被可信的接收方解密。
- 扩展性:由于JWT允许在Payload中添加自定义的声明,因此可以在令牌中携带更多的用户信息和相关权限,满足不同应用的需求。
JWT 库的选择
Go语言中已实现多个可用JWT库,比较常用的有jwt-go、jwt-auth
两个:
- jwt-go :Golang implementation of JSON Web Tokens (JWT)
go get github.com/dgrijalva/jwt-go
- jwt-auth:JWT middleware for Golang http servers with many configuration options
go get github.com/adam-hanna/jwt-auth
这里咱们使用的是另外一个: golang-jwt
golang-jwt 源自于 jwt-go, 是 jwt-go 的开源升级版本,不过 golang-jwt 这个对 golang 的版本有要求,如果低于 1.18 的版本建议使用 jwt-go
go get github.com/golang-jwt/jwt/v5
封装 JWT 包
工具包名定义为:jwt
总共定义了 2 个文件:
- auth.go
- jwt.go
auth.go
定义
jwt Claims
, 将Claims
编码为Token
go
package jwt
import (
"encoding/json"
"errors"
"github.com/golang-jwt/jwt/v5"
"strconv"
"time"
)
const (
TypeJWT = "jwt"
)
type Auth struct {
Foo string `json:"foo"`
UID int64 `json:"uid"`
Type string `json:"type"`
jwt.RegisteredClaims
}
type ValidFunc func(c *Auth) error
var validFuncs = make(map[string]ValidFunc)
// 初始化注册
func init() {
RegisterValidFunc(TypeJWT, defaultJWTValidFunc)
}
// RegisterValidFunc 注册校验函数
func RegisterValidFunc(authType string, validFunc ValidFunc) {
validFuncs[authType] = validFunc
}
func defaultJWTValidFunc(a *Auth) error {
if a.UID == 0 {
return errors.New("uid is empty")
}
return nil
}
// Valid 校验Auth
func (a *Auth) Valid() error {
if a == nil {
return errors.New("auth is empty")
}
if a.ExpiresAt.Unix() < time.Now().Unix() {
return errors.New("auth is expired")
}
if valid, ok := validFuncs[a.Type]; ok {
return valid(a)
}
return errors.New("unknown auth type")
}
// Encode 将Auth编码成Token
func (a *Auth) Encode(sign string) (Token, error) {
if a.ExpiresAt == nil || a.ExpiresAt.Unix() <= 0 {
a.ExpiresAt = jwt.NewNumericDate(time.Unix(time.Now().Unix()+DefaultDuration, 0))
}
if a.IssuedAt == nil || a.IssuedAt.Unix() <= 0 {
a.IssuedAt = jwt.NewNumericDate(time.Unix(time.Now().Unix(), 0))
}
if a.NotBefore == nil || a.NotBefore.Unix() <= 0 {
a.NotBefore = jwt.NewNumericDate(time.Unix(time.Now().Unix(), 0))
}
a.ID = strconv.FormatInt(a.UID, 10)
a.Subject = a.Type
a.Issuer = a.Type
a.Audience = []string{sign}
// 验证 Auth
if err := a.Valid(); err != nil {
return "", err
}
token := jwt.NewWithClaims(jwt.SigningMethodHS256, a)
t, err := token.SignedString([]byte(sign))
return Token(t), err
}
// 将Auth转换为Json
func (a *Auth) String() string {
data, _ := json.Marshal(a)
return string(data)
}
jwt.go
解码
Token
并返回Claims
,放入Header
中,并封装获取函数
go
package jwt
import (
"fmt"
"github.com/CodeLine-95/go-cloud-native/tools/logz"
"github.com/golang-jwt/jwt/v5"
"net/http"
)
const (
DefaultDuration = int64(2 * 3600)
)
type Token string
// GetToken 从请求中获取jwt Token
func GetToken(r *http.Request, cookieName string) Token {
token := r.Header.Get("X-Auth")
if token == "" {
cookie, err := r.Cookie(cookieName)
if err == nil && cookie.Value != "" {
token = cookie.Value
}
}
return Token(token)
}
// Decode 将Token解码成Auth结构体, verify为true表示进行,校验失败则返回nil
func (t Token) Decode(sign string, verify bool) *Auth {
claims := &Auth{}
parser := &jwt.Parser{}
if verify {
parser = jwt.NewParser(jwt.WithoutClaimsValidation())
}
token, err := parser.ParseWithClaims(string(t), claims, func(token *jwt.Token) (interface{}, error) {
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
return nil, fmt.Errorf("not authorization")
}
return []byte(sign), nil
})
if err != nil {
logz.Error("jwt token decode", logz.F("error", err.Error()))
return nil
}
if token == nil || !token.Valid {
return nil
}
return claims
}
// SetCookie 将jwt Token保存到cookie中
func (t Token) SetCookie(w http.ResponseWriter, cookieName string) {
w.Header().Set("Set-Cookie", fmt.Sprintf("%s=%s", cookieName, string(t)))
}
// SetHeader 将jwt Token保存到请求返回的X-Auth头部
func (t Token) SetHeader(w http.ResponseWriter) {
w.Header().Set("X-Auth", string(t))
}
// String jwt Token转成字符串
func (t Token) String() string {
return string(t)
}
Gin 中间件验证 JWT
通过 Gin 的中间件特性,用来统一验证
Token
实现JWT
鉴权
定义 JWTLogin.go
go
package middleware
import (
"github.com/CodeLine-95/go-cloud-native/internal/app/constant"
"github.com/CodeLine-95/go-cloud-native/internal/pkg/base"
"github.com/CodeLine-95/go-cloud-native/internal/pkg/jwt"
"github.com/CodeLine-95/go-cloud-native/internal/pkg/response"
"github.com/CodeLine-95/go-cloud-native/internal/pkg/xlog"
"github.com/CodeLine-95/go-cloud-native/tools/logz"
"github.com/CodeLine-95/go-cloud-native/tools/traceId"
"github.com/gin-gonic/gin"
"net/http"
"strings"
)
func JWTLogin() gin.HandlerFunc {
return func(c *gin.Context) {
var name string
nameInfo, err := c.Request.Cookie("userName")
if err == nil && nameInfo.Value != "" {
name = nameInfo.Value
}
xlog.Info(traceId.GetLogContext(c, "JWTLogin", logz.F("name", name)))
if userName, ok := c.Get(constant.UserName); ok {
xlog.Info(traceId.GetLogContext(c, "JWTLogin", logz.F("name", name), logz.F("userName", userName)))
c.Next()
} else {
accept := c.Request.Header.Get("Accept")
if strings.Index(accept, "html") > -1 {
c.Abort()
return
} else {
// 获取 token
token := jwt.GetToken(c.Request, "")
// 验证token非空
if token == "" {
response.Error(c, http.StatusOK, err, constant.ErrorMsg[constant.ErrorNotLogin])
return
}
// token验证是否失效
auth := token.Decode(base.JwtSignKey, false)
if auth == nil {
response.Error(c, http.StatusOK, err, constant.ErrorMsg[constant.ErrorNotLogin])
return
}
// 设置到上下文
c.Set("auth", auth)
c.Next()
}
}
}
}
使用 Gin 中间件
go
var handlersFuncMap []gin.HandlerFunc
func init() {
// 注册 JWTLogin 中间件
handlersFuncMap = append(handlersFuncMap, middleware.JWTLogin())
}
func InitRouter(r *gin.Engine) *gin.Engine {
...
versionRouter := r.Group(fmt.Sprintf("/%s", viper.GetString("app.apiVersion")))
...
// 批量设置中间件: jwt登录验证
versionRouter.Use(handlersFuncMap...)
...
return r
}
结束语
本节知识点汇总:
- JWT 是什么?
- JWT 的组成
- JWT 的使用场景
- JWT 和 OAuth 的区别
- 为什么要使用JWT?
- JWT 的应用
- Gin 中间件的使用