前言

对于团队中存在多个项目需要开发的,且每个项目都需要实现用户认证和授权的团队来说,Zitadel 是一个比较好的解决方案,你无需为每个项目重新开发一天用户的认证和授权逻辑。

因为 Zitadel 支持多租户,且支持认证页面的品牌化定制。而作为后端,我们只需要接入官方的 SDK 即可!

如果不了解 Zitadel 的可以去看我的这篇文章 开源身份认证和授权解决方案

概念

Zitadel 为登录用户签发的 Token 有两种:

  • Access Token
  • ID Token

Access Token 又分为 Bearer Token 和 JSON Web Token,二者的区别就是是否携带一些基础数据。

ID Token 本质上也是 JWT 的一种,只是其中包含的用户信息更加全面,如包含用户的 Metadata,Role、Permission 等数据。

Token 的验证

无论是 Access Token 还是 ID Token,要实现用户身份的确认,就必须验证请求中携带的 Token 是否是指定的 Issuer 签发的,并且是否被被篡改。

要实现这些,需要通过 RSA Public Key 来进行验签,这部分逻辑有专门的名词叫 Introspection,这里也有两种形式进行 Token 的验签:

  • 资源服务器请求身份认证服务器的 Introspection Endpoint
  • 资源服务器缓存身份认证服务器的 JWK Set(也就是 RSA 的 Public Key),在本地进行验证,这种方式被称为:Self-Introspection

但是这两种方式存在对应的优势和弊端,需要根据具体业务需求来决定所使用的方式。

通过 Introspection Endpoint 实现 Token 验证的这种方式,可以实现高度统一。当用户退出登录时,或者撤销令牌时,资源服务器可以从身份认证服务器的响应中得知令牌已经失效。但这种方式的问题也比较突出,就是每个请求需要去 Introspection Endpoint 验证,这样导致 API 响应的时间可能会增加几十到上百毫秒不等!

而 Self-Introspection 的这种方式,就是避免性能问题,通过缓存身份认证服务的 JWK Set,然后再本地对 Token 进行验证,这样减少了不必要的请求,对于响应的时间没有什么影响。但问题也比较的突出,那就是这个 Token 在有效期内无法被撤销!

所以,如果你的业务如果优先考虑安全性的话,那么选择通过 Introspection Endpoint 来进行验证,如果优先考虑性能的话,则选择 Self-Introspection!

项目集成

  • Zitadel 中创建项目,并为项目创建 Web 和 API 两个应用:

Zitadel Project Setting

  • Web 应用设置如下:

Zitadel Application Setting Zitadel Token Setting

  • API 应用下创建 Key

Zitadel API Key Setting

创建完成后,会提示下载一个 Key 的 JSON 文件,这个文件就是 middleware.OSKeyPath() 指向的文件,可以通过 ZITADEL_KEY_PATH 环境变量来设置这个 Key 的路径。

另外我所使用的框架是 fiber,基于 fastrouter 路由与 Go 自带的 些许不同!

Introspection

go get github.com/zitadel/zitadel-go/v2
package main

import (
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/log"
    "github.com/gofiber/fiber/v2/middleware/adaptor"
    http_mw "github.com/zitadel/zitadel-go/v2/pkg/api/middleware/http"
    "github.com/zitadel/zitadel-go/v2/pkg/client/middleware"
)

func profile(ctx *fiber.Ctx) error {
    return ctx.SendString("Success")
}

func main() {
    app := fiber.New()
    
    api := app.Group("/api")

    introspection, err := http_mw.NewIntrospectionInterceptor("https://zitadel.local", middleware.OSKeyPath())
    if err != nil {
        log.Error(err)
    }

    api.Use(adaptor.HTTPMiddleware(introspection.Handler))

    api.Get("/user/profile", profile).Name("User profile")

    log.Fatal(app.Listen(":3000"))
}

代码说明:

  • 20 行:声明拦截器
  • 25 行:将中间件转为 fiber 的 Handler

目前官方的 SDK 尚未支持获取用户的信息,对与应用层来说只能知道 Token 是否有效,虽然在 /oauth/v2/introspect 的响应中,有用户的信息,但是在 SDK 中并未处理!

Self-Introspection

package main

import (
    "net/http"
    "strings"
    "github.com/gofiber/fiber/v2"
    "github.com/gofiber/fiber/v2/log"
    "github.com/zitadel/oidc/pkg/client/rp"
    "github.com/zitadel/oidc/pkg/oidc"
    "github.com/zitadel/oidc/pkg/op"
)

var (
    keySet   oidc.KeySet
    verifier op.AccessTokenVerifier
)

func init() {
    keySet = rp.NewRemoteKeySet(http.DefaultClient, "https://zitadel.local/oauth/v2/keys")
    verifier = op.NewAccessTokenVerifier("https://zitadel.local", keySet)
}

func authHandler() fiber.Handler {
    return func(ctx *fiber.Ctx) (err error) {
        token := ctx.Get("Authorization")
        token = strings.TrimPrefix(token, oidc.PrefixBearer)

        claims, err := op.VerifyAccessToken(context.Background(), token, verifier)
        if err != nil {
            log.Error(err)
            return ctx.JSON(http.UnAuthenticated("Authentication failed"))
        }

        ctx.Locals("user", claims)

        return ctx.Next()
    }
}

func main() {
    app := fiber.New()
    
    api := app.Group("/api")

    api.Use(authHandler())

    api.Get("/user/profile", profile).Name("User profile")

    log.Fatal(app.Listen(":3000"))
}

代码说明:

  • 19 行:声明 Remote Key Set,注意这个变量是全局是有状态的,用于缓存 Key Set
  • 20 行:声明 Token 验证器
  • 28 行:验证 Token 是否有效,并获取 Claims
  • 34 行:将 Claims 放入 fiber.Ctx 中,便于后续逻辑中获取用户信息

总结

到此,在 Go 项目中集成 Zitadel 就算完成了,对于小项目而言,采用这种方式,可以进一步降低开发周期,提前交付项目!

如果再进一步则可以在 API Gateway 或其他代理服务器,如 Traefik 上来实现用户认证和鉴权,后端通过约定好的请求头获取用户信息即可!

I hope this is helpful, Happy hacking…