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}

评论列表:
暂无评论 😭