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
}