Go 项目使用 Zitadel 作为身份认证中心
前言⌗
对于团队中存在多个项目需要开发的,且每个项目都需要实现用户认证和授权的团队来说,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 两个应用:
- Web 应用设置如下:
- API 应用下创建 Key
创建完成后,会提示下载一个 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…