Skip to content

go-zero jwt接入流程

1. 中间件

go-zero支持自定义中间件,我们在api文件中写上中间件的名称

@server (
	jwt:        Auth
	prefix:     /api/v1
	group:      user
	timeout:    3s
	middleware: AuthInterceptor
	maxBytes:   1048576
)

这里叫AuthInterceptor, 然后生成对应的后端代码goctl api go -api api/user.api -dir ., 在生成文件中会有一个文件夹叫middleware, 里面有一个中间件文件叫authinterceptormiddleware.go 实现该文件,这个server的所有接口就有了auth的能力

go
//authinterceptormiddleware.go
package middleware

import (
	"context"
	"github.com/zeromicro/go-zero/rest/httpx"
	"net/http"
	"user-server/internal/config"
	"user-server/internal/constant"
	"user-server/pkg/tokengenerator"
	"strings"
	"time"
)

type AuthInterceptorMiddleware struct {
	c config.Config
}

func NewAuthInterceptorMiddleware(c config.Config) *AuthInterceptorMiddleware {
	return &AuthInterceptorMiddleware{c}
}

func (m *AuthInterceptorMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
	return func(w http.ResponseWriter, r *http.Request) {
    // 1. 前端请求的http header会带个 Authorization: Bearer xxxxx
		token := r.Header.Get("Authorization")
    // 2. 解包出jwt token里面的内容
		claims, err := tokengenerator.VerifyAccessToken(m.c.Auth.AccessSecret, strings.TrimPrefix(token, "Bearer "))
		if err != nil {
			httpx.WriteJson(w, http.StatusUnauthorized, map[string]interface{}{
				"code": constant.ErrorCodeLoginNeeded,
				"msg":  err.Error(),
			})
			return
		}
    // sub即为我们加密到jwt里面的数据, 我们在服务端生成Jwt token时将username放到sub里面
		sub, err := claims.GetSubject()
		if err != nil {
			httpx.WriteJson(w, http.StatusUnauthorized, map[string]interface{}{
				"code": constant.ErrorCodeLoginNeeded,
				"msg":  err.Error(),
			})
			return
		}

    // 3. 检查jwt是否过期
		date, err := claims.GetExpirationTime()
		if err != nil || date.Unix() < time.Now().Unix() {
			httpx.WriteJson(w, http.StatusUnauthorized, map[string]interface{}{
				"code": constant.ErrorCodeTokenExpired,
				"msg":  "access_token expired",
			})
			return
		}

    // 4. 流程都通过之后将解包jwt出来的username丢到请求的ctx里面,方便写业务逻辑时直接拿到username
		ctx := context.WithValue(r.Context(), "username", sub)
		next(w, r.WithContext(ctx))
	}
}

我们只在需要鉴权的server里面加这个中间件,不需要鉴权的server不要加。tokengenerator包是用来生成jwt token的,就两个函数

go
//tokengenerator.go
package tokengenerator

import (
	jwtv5 "github.com/golang-jwt/jwt/v5"
	"github.com/google/uuid"
)

func SignAccessToken(secret string, claims jwtv5.MapClaims) (token string, err error) {
	signer := jwtv5.NewWithClaims(jwtv5.SigningMethodHS256, claims)
	token, err = signer.SignedString([]byte(secret))
	return
}

func VerifyAccessToken(secret string, token string) (data jwtv5.MapClaims, err error) {
	t, err := jwtv5.Parse(token, func(*jwtv5.Token) (interface{}, error) { return []byte(secret), nil })
	if err == nil {
		data = t.Claims.(jwtv5.MapClaims)
	}
	return
}

func GenerateRefreshToken() (token string) {
	u := uuid.New()
	return u.String()
}

2. 写业务逻辑时取username

go
func (l *UserInfoLogic) UserInfo(req *types.UserInfoReq) (resp *types.UserInfoResp, err error) {
	q := dao.Use(l.svcCtx.DB).User
	username := l.ctx.Value("username").(string) // 取username
  resp.Username = username
	return
}

3. 生成jwt

用户请求登录接口时生成jwt

go
func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err error) {
	if req.ST != "" {
		// 检查st是否有效,有效的话,存起来,拿st去请求用户信息,更新用户信息表
    // TODO: st接入流程
		l.Logger.Infof("login ST: %s", req.ST)
		err = errors.New(constant.ErrorCodeUnimplemented, "st登录暂未接入")
		return
	}

	if req.Username == "" || req.Password == "" {
		err = errors.New(constant.ErrorCodeRecordNotExist, "用户名或密码错误")
		return
	}

	q := dao.Use(l.svcCtx.DB).User
	user, err := q.WithContext(l.ctx).Where(q.Username.Eq(req.Username)).Where(q.Password.Eq(req.Password)).Take()
	if user == nil {
		err = errors.New(constant.ErrorCodeRecordNotExist, "用户名或密码错误")
		return
	}

	expire := time.Now().Unix() + l.svcCtx.Config.Auth.AccessExpire
	accessToken, err := tokengenerator.SignAccessToken(l.svcCtx.Config.Auth.AccessSecret, map[string]interface{}{
		"iss":   "cam",
		"exp":   expire,
		"sub":   req.Username,
		"admin": false,
	})

	refreshToken := tokengenerator.GenerateRefreshToken()
	token := model.Token{RefreshToken: refreshToken, Username: req.Username, ExpiredAt: time.Now().Unix() + 30*24*3600}
	err = dao.Use(l.svcCtx.DB).Token.WithContext(l.ctx).Create(&token)
	if err != nil {
		return
	}
	resp = &types.LoginResp{
		AccessToken:  accessToken,
		ExpiredAt:    expire,
		RefreshToken: refreshToken,
	}
	return
}

登录后返回access_token, 即jwt, 同时还会返回refresh_token. 客户端发现jwt过期时,会拿这个refresh_token去刷新新的jwt。refresh_token建议存在localStorage中,前后端交互时一般不传递refresh_token,只有在重新登录或者主动刷新jwt时才会传递。

4. refresh_token

我们还需要一个refresh_token的接口,当客户端发现jwt token过期时,请求refresh_token接口生成新的jwt token。

go
// RefreshToken 通过refreshToken刷新accessToken
func (l *RefreshTokenLogic) RefreshToken(req *types.RefreshTokenReq) (resp *types.RefreshTokenResp, err error) {
	q := dao.Use(l.svcCtx.DB).Token
	token, err := q.WithContext(l.ctx).Where(q.RefreshToken.Eq(req.RefreshToken)).Where(q.ExpiredAt.Gte(time.Now().Unix())).Take()
	if err != nil {
		return
	}
	expire := time.Now().Unix() + l.svcCtx.Config.Auth.AccessExpire
	accessToken, err := tokengenerator.SignAccessToken(l.svcCtx.Config.Auth.AccessSecret, map[string]interface{}{
		"iss":   "cam",
		"exp":   expire,
		"sub":   token.Username,
		"admin": false,
	})
	if err != nil {
		return
	}
	resp = &types.RefreshTokenResp{
		AccessToken:  accessToken,
		RefreshToken: token.RefreshToken,
		ExpiredAt:    expire,
	}
	return
}

Last updated: