基于Go语言gqlgen、gorm和chi实现GraphQL API时的JWT鉴权优化方案咨询
嗨,我完全懂你现在的烦恼——每个 resolver 开头都要写一遍鉴权检查,不仅重复啰嗦,还容易不小心漏写,太影响开发效率了!咱们来聊聊几个更优雅的解决方案,既能摆脱重复代码,又能保住GraphQL单端点的核心优势~
方案一:用gqlgen的自定义Directive实现字段级鉴权
这是最贴合GraphQL设计思想的方案,你可以在Schema里给需要鉴权的字段/操作打上标记,让gqlgen自动帮你完成鉴权检查,不用手动在每个resolver里写逻辑。
步骤:
- 在你的GraphQL Schema里定义一个
@auth指令:
directive @auth on FIELD_DEFINITION type Query { # 不需要鉴权的操作:登录、注册 login(email: String!, password: String!): AuthToken createUser(input: CreateUserInput!): User # 需要鉴权的操作:获取当前用户、获取帖子列表 me: User! @auth posts: [Post!]! @auth } type Mutation { # 比如创建帖子需要鉴权 createPost(input: CreatePostInput!): Post! @auth }
- 在gqlgen的配置文件(
gqlgen.yml)里注册这个指令的处理逻辑:
directives: auth: handler: github.com/your-project/pkg/auth.AuthDirective
- 实现这个指令的处理函数,从Context里读取JWT解析后的用户信息,不存在就返回错误:
package auth import ( "context" "errors" "github.com/99designs/gqlgen/graphql" ) func AuthDirective(ctx context.Context, obj interface{}, next graphql.Resolver) (interface{}, error) { // 从Context里获取之前中间件解析好的用户ID userID, ok := ctx.Value("userID").(uint) if !ok || userID == 0 { return nil, errors.New("未授权,请先登录") } // 鉴权通过,继续执行后续的resolver逻辑 return next(ctx) }
这样,所有标记了@auth的字段/操作,gqlgen生成的代码会自动先执行这个鉴权逻辑,完全不用你在resolver里手动调用检查函数,既统一又省心。
方案二:封装鉴权基类,复用鉴权逻辑
如果你不想修改Schema,或者项目已经有一定规模不好改Schema,可以把鉴权逻辑抽成一个基类结构体,让需要鉴权的resolver都嵌入这个基类,复用鉴权方法。
示例代码:
// 定义一个带鉴权方法的基类 type AuthenticatedResolver struct { *Resolver // 嵌入你原来的Resolver结构体 } // 封装鉴权逻辑,返回用户ID或错误 func (r *AuthenticatedResolver) RequireAuth(ctx context.Context) (uint, error) { userID, ok := ctx.Value("userID").(uint) if !ok { return 0, errors.New("未授权访问") } return userID, nil } // 在需要鉴权的resolver里嵌入这个基类 type queryResolver struct { AuthenticatedResolver } // 使用的时候直接调用基类的方法 func (r *queryResolver) Me(ctx context.Context) (*models.User, error) { userID, err := r.RequireAuth(ctx) if err != nil { return nil, err } // 后续业务逻辑:根据userID查询用户信息 var user models.User if err := r.DB.First(&user, userID).Error; err != nil { return nil, err } return &user, nil }
这种方式虽然还是要在resolver里调用方法,但把重复的鉴权逻辑抽离了出来,不用每次都写检查代码,而且后续如果要修改鉴权逻辑,只需要改基类的RequireAuth方法就行,维护起来更方便。
方案三:中间件结合GraphQL操作名称过滤(谨慎使用)
这个方案是在chi的中间件里直接解析GraphQL请求的操作名称,对不需要鉴权的操作(比如login、createUser)跳过检查,其他操作强制验证JWT。不过要注意:操作名称是客户端传入的,存在被伪造的风险,所以只适合内部系统或者完全信任客户端的场景。
示例中间件代码:
func AuthMiddleware(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { // 先读取请求体,解析操作名称 bodyBytes, err := io.ReadAll(r.Body) if err != nil { http.Error(w, "请求解析失败", http.StatusBadRequest) return } // 重置请求体,因为后续的GraphQL handler还要读取 r.Body = io.NopCloser(bytes.NewBuffer(bodyBytes)) var req struct { OperationName string `json:"operationName"` } if err := json.Unmarshal(bodyBytes, &req); err == nil { // 允许登录和注册操作跳过鉴权 if req.OperationName == "Login" || req.OperationName == "CreateUser" { next.ServeHTTP(w, r) return } } // 其他操作执行JWT验证 authHeader := r.Header.Get("Authorization") if authHeader == "" { http.Error(w, "缺少Authorization头", http.StatusUnauthorized) return } // 这里写你的JWT解析验证逻辑,比如提取token、验证签名、解析用户信息 tokenStr := strings.TrimPrefix(authHeader, "Bearer ") claims, err := ParseToken(tokenStr) if err != nil { http.Error(w, "token无效", http.StatusUnauthorized) return } // 把用户ID放到Context里,供后续resolver使用 ctx := context.WithValue(r.Context(), "userID", claims.UserID) next.ServeHTTP(w, r.WithContext(ctx)) }) }
不推荐单独开/login端点的原因
你提到的单独开/login端点的做法确实不太建议,因为这样就破坏了GraphQL单端点的设计优势——原本所有请求都通过一个GraphQL端点处理,现在要维护两套路由,不仅增加了代码复杂度,还可能带来跨域、路由冲突等额外问题,完全没必要。
总结一下,最推荐的是方案一(自定义Directive),它最符合GraphQL的设计理念,代码侵入性最低;方案二适合不想动Schema的场景;方案三则要根据实际场景谨慎选择。
备注:内容来源于stack exchange,提问作者pgossa

