avatar

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的能力

 1//authinterceptormiddleware.go
 2package middleware
 3
 4import (
 5	"context"
 6	"github.com/zeromicro/go-zero/rest/httpx"
 7	"net/http"
 8	"user-server/internal/config"
 9	"user-server/internal/constant"
10	"user-server/pkg/tokengenerator"
11	"strings"
12	"time"
13)
14
15type AuthInterceptorMiddleware struct {
16	c config.Config
17}
18
19func NewAuthInterceptorMiddleware(c config.Config) *AuthInterceptorMiddleware {
20	return &AuthInterceptorMiddleware{c}
21}
22
23func (m *AuthInterceptorMiddleware) Handle(next http.HandlerFunc) http.HandlerFunc {
24	return func(w http.ResponseWriter, r *http.Request) {
25    // 1. 前端请求的http header会带个 Authorization: Bearer xxxxx
26		token := r.Header.Get("Authorization")
27    // 2. 解包出jwt token里面的内容
28		claims, err := tokengenerator.VerifyAccessToken(m.c.Auth.AccessSecret, strings.TrimPrefix(token, "Bearer "))
29		if err != nil {
30			httpx.WriteJson(w, http.StatusUnauthorized, map[string]interface{}{
31				"code": constant.ErrorCodeLoginNeeded,
32				"msg":  err.Error(),
33			})
34			return
35		}
36    // sub即为我们加密到jwt里面的数据, 我们在服务端生成Jwt token时将username放到sub里面
37		sub, err := claims.GetSubject()
38		if err != nil {
39			httpx.WriteJson(w, http.StatusUnauthorized, map[string]interface{}{
40				"code": constant.ErrorCodeLoginNeeded,
41				"msg":  err.Error(),
42			})
43			return
44		}
45
46    // 3. 检查jwt是否过期
47		date, err := claims.GetExpirationTime()
48		if err != nil || date.Unix() < time.Now().Unix() {
49			httpx.WriteJson(w, http.StatusUnauthorized, map[string]interface{}{
50				"code": constant.ErrorCodeTokenExpired,
51				"msg":  "access_token expired",
52			})
53			return
54		}
55
56    // 4. 流程都通过之后将解包jwt出来的username丢到请求的ctx里面,方便写业务逻辑时直接拿到username
57		ctx := context.WithValue(r.Context(), "username", sub)
58		next(w, r.WithContext(ctx))
59	}
60}

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

 1//tokengenerator.go
 2package tokengenerator
 3
 4import (
 5	jwtv5 "github.com/golang-jwt/jwt/v5"
 6	"github.com/google/uuid"
 7)
 8
 9func SignAccessToken(secret string, claims jwtv5.MapClaims) (token string, err error) {
10	signer := jwtv5.NewWithClaims(jwtv5.SigningMethodHS256, claims)
11	token, err = signer.SignedString([]byte(secret))
12	return
13}
14
15func VerifyAccessToken(secret string, token string) (data jwtv5.MapClaims, err error) {
16	t, err := jwtv5.Parse(token, func(*jwtv5.Token) (interface{}, error) { return []byte(secret), nil })
17	if err == nil {
18		data = t.Claims.(jwtv5.MapClaims)
19	}
20	return
21}
22
23func GenerateRefreshToken() (token string) {
24	u := uuid.New()
25	return u.String()
26}

2. 写业务逻辑时取username

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

3. 生成jwt

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

 1func (l *LoginLogic) Login(req *types.LoginReq) (resp *types.LoginResp, err error) {
 2	if req.ST != "" {
 3		// 检查st是否有效,有效的话,存起来,拿st去请求用户信息,更新用户信息表
 4    // TODO: st接入流程
 5		l.Logger.Infof("login ST: %s", req.ST)
 6		err = errors.New(constant.ErrorCodeUnimplemented, "st登录暂未接入")
 7		return
 8	}
 9
10	if req.Username == "" || req.Password == "" {
11		err = errors.New(constant.ErrorCodeRecordNotExist, "用户名或密码错误")
12		return
13	}
14
15	q := dao.Use(l.svcCtx.DB).User
16	user, err := q.WithContext(l.ctx).Where(q.Username.Eq(req.Username)).Where(q.Password.Eq(req.Password)).Take()
17	if user == nil {
18		err = errors.New(constant.ErrorCodeRecordNotExist, "用户名或密码错误")
19		return
20	}
21
22	expire := time.Now().Unix() + l.svcCtx.Config.Auth.AccessExpire
23	accessToken, err := tokengenerator.SignAccessToken(l.svcCtx.Config.Auth.AccessSecret, map[string]interface{}{
24		"iss":   "cam",
25		"exp":   expire,
26		"sub":   req.Username,
27		"admin": false,
28	})
29
30	refreshToken := tokengenerator.GenerateRefreshToken()
31	token := model.Token{RefreshToken: refreshToken, Username: req.Username, ExpiredAt: time.Now().Unix() + 30*24*3600}
32	err = dao.Use(l.svcCtx.DB).Token.WithContext(l.ctx).Create(&token)
33	if err != nil {
34		return
35	}
36	resp = &types.LoginResp{
37		AccessToken:  accessToken,
38		ExpiredAt:    expire,
39		RefreshToken: refreshToken,
40	}
41	return
42}

登录后返回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。

 1// RefreshToken 通过refreshToken刷新accessToken
 2func (l *RefreshTokenLogic) RefreshToken(req *types.RefreshTokenReq) (resp *types.RefreshTokenResp, err error) {
 3	q := dao.Use(l.svcCtx.DB).Token
 4	token, err := q.WithContext(l.ctx).Where(q.RefreshToken.Eq(req.RefreshToken)).Where(q.ExpiredAt.Gte(time.Now().Unix())).Take()
 5	if err != nil {
 6		return
 7	}
 8	expire := time.Now().Unix() + l.svcCtx.Config.Auth.AccessExpire
 9	accessToken, err := tokengenerator.SignAccessToken(l.svcCtx.Config.Auth.AccessSecret, map[string]interface{}{
10		"iss":   "cam",
11		"exp":   expire,
12		"sub":   token.Username,
13		"admin": false,
14	})
15	if err != nil {
16		return
17	}
18	resp = &types.RefreshTokenResp{
19		AccessToken:  accessToken,
20		RefreshToken: token.RefreshToken,
21		ExpiredAt:    expire,
22	}
23	return
24}

评论列表:

暂无评论 😭