go微服务框架Kratos笔记(七)使用jwt认证中间件
引言
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