引言
Json web token (JWT) 是一個開放標准(RFC 7519),它定義了一種緊湊的、自包含的方式,特別適用於分布式站點的單點登錄(SSO)場景。JWT的聲明一般被用來在身份提供者和服務提供者間傳遞被認證的用戶身份信息,以便於從資源服務器獲取資源,也可以增加一些額外的其它業務邏輯所必須的聲明信息,該token也可直接被用於認證,也可被加密。
jwt構成
JWT是由三段信息構成的,將這三段信息文本用.鏈接一起就構成了Jwt字符串,就像這樣
eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VybmFtZSI6IjEiLCJkaWZmZXJlbnRpYXRlIjoiMSIsImV4cCI6MTYzNjUyMTYzNywiaXNzIjoiTWljcm9zZXJ2aWNlLkJGRiJ9.5LQ_mBJLkHUxjSqoE8evh7_UahrK8WqLgvhpZ2HIIiI
第一部分我們稱它為頭部(header),第二部分我們稱其為載荷(payload),第三部分是簽證(signature)
header
jwt的頭部承載兩部分信息
typ:聲明類型
alg:聲明加密的算法
然后將頭部進行base64加密(該加密是可以對稱解密的),構成了第一部分.
playload
載荷就是存放有效信息的地方,這些有效信息包含三個部分
- 標准中注冊的聲明
- 公共聲明
- 私有聲明
標准聲明(建議但不強制使用):
- iss: jwt簽發者
- sub: jwt所面向的用戶
- aud: 接收jwt的一方
- exp: jwt的過期時間,這個過期時間必須要大於簽發時間
- nbf: 定義在什么時間之前,該jwt都是不可用的.
- iat: jwt的簽發時間
- jti: jwt的唯一身份標識,主要用來作為一次性token,從而回避重放攻擊。
公共聲明:
公共的聲明可以添加任何的信息,一般添加用戶的相關信息或其他業務需要的必要信息.但不建議添加敏感信息,因為該部分在客戶端可解密.
私有聲明:
私有聲明是提供者和消費者所共同定義的聲明,一般不建議存放敏感信息,因為base64是對稱解密的,意味着該部分信息可以歸類為明文信息。
然后將其進行base64加密,得到Jwt的第二部分。
signature
jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:
- header(base64后的)
- payload(base64后的)
- secret
此部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進行加鹽secret組合加密,然后就構成了jwt的第三部分。
將這三部分用.連接成一個完整的字符串,構成了最終的jwt
在kratos框架中的使用
實現jwt自簽邏輯
新建auth授權方法,可以在用戶登錄成功后調用Auth方法傳入參數后返回jwt
package biz
import (
"kanxun_bff/internal/conf"
"time"
"github.com/golang-jwt/jwt"
jwtv4 "github.com/golang-jwt/jwt/v4"
)
type AuthUseCase struct {
key string
}
func NewAuthUseCase() *AuthUseCase {
return &AuthUseCase{
key: "testKey",
}
}
type MyClaims struct {
UserName string `json:"username"`
jwtv4.StandardClaims
}
func (receiver AuthUseCase) Auth(username string) (string, error) {
myClaims := MyClaims{
username,
jwtv4.StandardClaims{
ExpiresAt: time.Now().Add(time.Hour * 2).Unix(), //設置JWT過期時間,此處設置為2小時
Issuer: conf.GetConfig().GetString("project.name"), //設置簽發人
},
}
claims := jwt.NewWithClaims(jwt.SigningMethodHS256, myClaims)
//加鹽
return claims.SignedString([]byte(receiver.key))
}
// CheckJWT 解析
func (receiver AuthUseCase) CheckJWT(jwtToken string) (map[string]interface{}, error) {
token, err := jwt.Parse(jwtToken, func(token *jwt.Token) (interface{}, error) {
return []byte(receiver.key), nil
})
if err != nil {
return nil, err
}
if claims, ok := token.Claims.(jwt.MapClaims); ok {
result := make(map[string]interface{}, 2)
result["username"] = claims["username"]
result["differentiate"] = claims["differentiate"]
return result, nil
} else {
return nil, errors.New("token type error")
}
}
自定義jwt中間件
本文使用自定義jwt中間件,未使用kratos官方提供的jwt
kratos jwt middleware
// NewAuthServer jwt Server中間件
func NewAuthServer(authUc *biz.AuthUseCase) func(handler middleware.Handler) middleware.Handler {
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req interface{}) (reply interface{}, err error) {
var jwtToken string
if md, ok := metadata.FromIncomingContext(ctx); ok {
jwtToken = md.Get("x-md-global-jwt")[0]
} else if header, ok := transport.FromServerContext(ctx); ok {
jwtToken = strings.SplitN(header.RequestHeader().Get("Authorization"), " ", 2)[1]
} else {
// 缺少可認證的token,返回錯誤
return nil, auth.ErrAuthFail
}
token, err := authUc.CheckJWT(jwtToken)
if err != nil {
// 缺少合法的token,返回錯誤
return nil, auth.ErrAuthFail
}
ctx = context.WithValue(ctx, "username", token["username"])
ctx = context.WithValue(ctx, "differentiate", token["differentiate"])
reply, err = handler(ctx, req)
return
}
}
}
// NewAuthClient jwt Client中間件
func NewAuthClient(authUc *biz.AuthUseCase) middleware.Middleware {
return func(handler middleware.Handler) middleware.Handler {
return func(ctx context.Context, req interface{}) (interface{}, error) {
if header, ok := transport.FromServerContext(ctx); ok {
//如果Token為空則生成token,如果
if header.RequestHeader().Get(authorizationKey) != "" {
jwtToken := strings.SplitN(header.RequestHeader().Get(authorizationKey), " ", 2)[1]
//元數據傳遞
ctx = metadata.AppendToOutgoingContext(ctx, "x-md-global-jwt", jwtToken)
} else {
token, err := authUc.Auth(
req.(*user.GetUserNameRequest).Username,
req.(*user.GetUserNameRequest).Differentiate,
)
if err != nil {
return nil, jwt.ErrGetKey
}
ctx = metadata.AppendToOutgoingContext(ctx, "x-md-global-jwt", token)
}
}
return handler(ctx, req)
}
}
}
路由白名單與jwt中間件的引入
//client
conn, err := grpc.DialInsecure(
context.Background(),
grpc.WithEndpoint("discovery:///xxx.xxx.grpc"),
grpc.WithDiscovery(r),
grpc.WithMiddleware(
recovery.Recovery(),
logging.Client(logger), //日志中間件,
tracing.Client(), //鏈路追蹤中間件
metadata.Client(), //元數據傳遞中間件
jwt.NewAuthClient(authUc), //jwt中間件
),
)
//server
var opts = []grpc.ServerOption{
grpc.Middleware(
recovery.Recovery(), //異常恢復中間件
tracing.Server(), //鏈路追蹤中間件
logging.Server(logger), //日志中間件
metrics.Server(), //監控中間件
validate.Validator(), //參數校驗
metadata.Server(), //元數據中間件
selector.Server( //jwt中間件
jwt.NewAuthServer(authUc),
).Match(func(operation string) bool {
// 白名單
r, err := regexp.Compile("/api.user.v1.User/GetUserName")
if err != nil {
// 自定義錯誤處理
return true
}
return r.FindString(operation) != operation
}).Build(),
),
}
下游服務獲取用戶信息
value := ctx.Value("username")
fmt.Println(value)
如有錯誤請留言反饋
References
https://www.jianshu.com/p/576dbf44b2ae
https://zhuanlan.zhihu.com/p/86937325
https://go-kratos.dev/docs/component/middleware/auth
https://go-kratos.dev/docs/component/metadata