在现代Web开发领域,创建安全有效的用户身份验证流是至关重要的。随着微服务和分布式体系结构的兴起,实施强大的身份验证策略变得更加重要。
前端(BFF)身份验证模式的后端是一种设计方法,致力于提供专用的后端来处理和满足所有身份验证要求和前端应用程序(SPA)的挑战。
目标
技术博客的主要目标是教育读者有关前端(BFF)身份验证模式的后端及其在GO编程语言中的实现。该博客将在为前端应用程序构建身份验证系统的背景下提供全面的模式,其好处以及所解决的挑战。
历史
SPA通常使用基于令牌的身份验证(例如JSON Web令牌(JWTS))来验证和授权用户。但是,在水疗中心安全管理代币可能具有挑战性。将令牌存储在客户端存储中(例如本地存储或cookie)可能会使它们暴露于诸如跨站点请求伪造(CSRF)之类的潜在攻击中。开发人员必须采取严格的安全措施来保护令牌并防止未经授权的访问或滥用。
BFF模式通过引入中间层的前端来解决此问题。该层充当前端客户端和主要后端服务之间的代理,处理与身份验证有关的问题并提供专门的身份验证接口。
流程图
-
当前端需要对用户进行身份验证时,它调用BFF上的API端点(/登录)以启动登录握手。
-
BFF使用OAuth2授权代码流与Auth0连接以进行身份验证和授权用户并获取ID和访问令牌。
-
后端将用户令牌存储在缓存中。
-
为代表用户身份验证会话的前端发出了加密的cookie。
-
当前端需要调用外部API时,它将加密的cookie与URL一起传递到BFF并调用API。
-
BFF从缓存中检索访问令牌,并致电后端API,包括授权标题上的令牌。
-
当外部API返回对BFF的响应时,此响应将响应回到前端。
实施
先决条件
作为充分理解提议的解决方案的先决条件,我建议您了解以下主题(即使还不知道它们)。
项目结构
入门
为了启动实施过程,我转到Auth0 Guide注册我的Web应用程序。遵循指南,我成功地使用Auth0设置了Web应用程序的用户身份验证。有了最初的身份验证功能,我开始修改代码以合并前端(BFF)身份验证模式路由器。go
路由器
在路由器中。
/login
:当用户单击前端的登录按钮时,将触发此端点。
/logout
:当用户单击注销按钮时,前端路由到此端点。
/callback
:此端点负责使用授权代码的令牌交换过程。
/shorten
:这是前端将消耗的后端API之一。
type router struct{}
func (router *router) InitRouter(auth *authenticator.Authenticator, redis interfaces.IRedisLayer) *iris.Application {
app := iris.New()
loginHandler := controller.LoginHandler{Auth: auth}
callbackHandler := controller.CallbackHandler{Auth: auth, RedisClient: redis}
logoutHandler := controller.LogoutHandler{RedisClient: redis}
backendApiHandler := controller.BackendApiHandler{RedisClient: redis}
middlewareHandler := middleware.MiddlewareHandler{RedisClient: redis}
app.Get("/login", loginHandler.Login)
app.Get("/callback", callbackHandler.Callback)
app.Get("/logout", logoutHandler.Logout)
// Backend Api
app.Post("/shorten", middlewareHandler.IsAuthenticated, backendApiHandler.WriterRedirect)
return app
}
重要的是要注意 MiddlewareHandler.isauthenticated 在后端API路线中使用。该处理程序在验证用户的登录配置文件中起着至关重要的作用。您可以在“中间件”部分中找到实现详细信息
控制器
在控制器中,我已经为前面提到的所有路线封装了逻辑。这是定义和处理每个路线的实现详细信息的地方。
- login.go
在登录路线中,处理程序将用户重定向到身份提供商(IDP)通用登录页面。此页面允许用户执行单个登录并提供其同意。
在用户在IDP通用登录页面上提供同意后,该页面与授权代码一起重定向到 /回调端点。
type LoginHandler struct {
Auth *authenticator.Authenticator
}
func (l *LoginHandler) Login(ctx iris.Context) {
ctx.Redirect(l.Auth.AuthCodeURL(state, oauth2.SetAuthURLParam("audience", config.EnvVariables.Auth0Audience)), http.StatusTemporaryRedirect)
}
受众参数在提供有效载荷内的受众索赔方面起着至关重要的作用,这有助于用户授权。要获得更详细的理解,建议使用additional resources。
关键挑战是在 l.auth.auth.authcodeurl()函数中包括受众URL参数。通过检查oauth2 package的source code并了解其实现,我获得了如何使用 oauth2.setauthurlparam()。
- callback.go
用户访问 /登录路由后,调用 /回调URL,将状态和授权代码作为参数传递。
负责 /回调URL的处理程序验证了状态参数的值以确保其完整性和安全性。
一旦成功验证了状态价值,处理程序就可以将授权代码交换为访问令牌。
除了代币交换之外,处理程序还采取必要的步骤来保存REDIS中的令牌和用户配置文件信息,Redis是一个通常用于缓存和会话管理的数据存储。
type CallbackHandler struct {
Auth *authenticator.Authenticator
RedisClient interfaces.IRedisLayer
}
func (c *CallbackHandler) Callback(ctx iris.Context) {
if ctx.URLParam("state") != state {
ctx.StopWithJSON(http.StatusBadRequest, "Invalid state parameter.")
return
}
// Exchange an authorization code for a token.
token, err := c.Auth.Exchange(ctx.Request().Context(), ctx.URLParam("code"))
if err != nil {
ctx.StopWithJSON(http.StatusUnauthorized, "Failed to convert an authorization code into a token.")
return
}
idToken, err := c.Auth.VerifyIDToken(ctx.Request().Context(), token)
if err != nil {
ctx.StopWithJSON(http.StatusInternalServerError, "Failed to verify ID Token.")
return
}
var profile map[string]interface{}
if err := idToken.Claims(&profile); err != nil {
ctx.StopWithError(http.StatusInternalServerError, err)
return
}
err = c.RedisClient.SetKeyValue(profile["email"].(string)+"_token", token.AccessToken, 24*time.Hour)
if err != nil {
ctx.StopWithError(http.StatusInternalServerError, err)
return
}
err = c.RedisClient.HSetKeyValue(profile["email"].(string)+"_profile", profile, 24*time.Hour)
if err != nil {
ctx.StopWithError(http.StatusInternalServerError, err)
return
}
ctx.SetCookieKV("logged_id_email", profile["email"].(string))
// Redirect to logged in page.
ctx.Redirect(config.EnvVariables.FrontendURL, http.StatusTemporaryRedirect)
}
要存储令牌和配置文件信息,我已将Redis实现为缓存层。我通过将它们的电子邮件地址前缀以确保唯一性和轻松检索数据来为每个用户创建一个密钥。
此外, ctx.setCookiekv()功能用于设置加密的HTTP-only cookie。稍后,前端发送了此cookie,拨打了后端API
- logout.go
用户单击“注销”按钮时,注销处理程序执行两个主要操作。首先,它清除了所有缓存值,以确保删除任何存储的令牌或用户配置文件信息。其次,它通过Auth0(IDP)启动注销过程,有效地从身份提供商中记录了用户
type LogoutHandler struct {
RedisClient interfaces.IRedisLayer
}
func (l *LogoutHandler) Logout(ctx iris.Context) {
userCookie := ctx.GetCookie("logged_id_email")
if userCookie == "" {
ctx.StopWithError(iris.StatusUnauthorized, errors.New("please make sure user is logged in"))
return
}
// delete token key
err := l.RedisClient.DeleteKey(userCookie + "_token")
if err != nil {
ctx.StopWithError(http.StatusInternalServerError, err)
return
}
// delete profile key
err = l.RedisClient.DeleteKey(userCookie + "_profile")
if err != nil {
ctx.StopWithError(http.StatusInternalServerError, err)
return
}
logoutUrl, err := url.Parse("https://" + config.EnvVariables.Auth0Domain + "/v2/logout")
if err != nil {
ctx.StopWithError(http.StatusInternalServerError, err)
return
}
returnTo, err := url.Parse(config.EnvVariables.ShortifyFrontendDomain)
if err != nil {
ctx.StopWithError(http.StatusInternalServerError, err)
return
}
// remove the logged_id_email http-only cookie from context
ctx.RemoveCookie("logged_id_email")
parameters := url.Values{}
parameters.Add("returnTo", returnTo.String())
parameters.Add("client_id", config.EnvVariables.Auth0ClientID)
logoutUrl.RawQuery = parameters.Encode()
ctx.Redirect(logoutUrl.String(), http.StatusTemporaryRedirect)
}
要删除缓存的信息,包括用户配置文件和存储在redis中的令牌,我使用redisclient.deletekey()函数。这样可以确保从缓存中删除相关数据。此外,调用auth0 s /v2 /logout API允许我们从Auth0中有效地注销用户。
要删除特定的cookie,使用 ctx.removecookie()。此函数专门针对并删除了logged_id_emailâCookie,该函数最初是在 /回调处理程序中设置的。< /p>
- backendapi.go
当前端向后端API提出请求时,前端(BFF)的后端确保它可以从缓存中获取用户令牌。该令牌对于身份验证和授权目的至关重要。
然后,BFF将获取的令牌添加到请求的授权标题中,并将其转发到后端API。
后端API处理请求并生成响应后,BFF充当代理,并将响应发送回前端,从而允许前端和后端之间无缝通信。
type BackendApiHandler struct {
RedisClient interfaces.IRedisLayer
}
func (w *BackendApiHandler) WriterRedirect(ctx iris.Context) {
raw, err := ctx.User().GetRaw()
if err != nil {
ctx.StopWithError(500, err)
return
}
profile := raw.(map[string]string)
email := profile["email"]
token, err := w.RedisClient.GetKeyValue(email + "_token")
if err != nil {
ctx.StopWithError(500, err)
return
}
client := &http.Client{}
req, err := http.NewRequest(ctx.Request().Method, config.EnvVariables.BackendApi, ctx.Request().Body)
if err != nil {
ctx.StopWithError(500, err)
return
}
req.Header.Add("Authorization", "Bearer "+token)
req.Header.Add("Content-Type", "application/json")
res, err := client.Do(req)
if err != nil {
ctx.StopWithError(500, err)
return
}
defer res.Body.Close()
body, err := io.ReadAll(res.Body)
if err != nil {
ctx.StopWithError(500, err)
return
}
var respBody map[string]interface{}
err = json.Unmarshal(body, &respBody)
if err != nil {
ctx.StopWithError(500, err)
return
}
ctx.StopWithJSON(res.StatusCode, respBody)
}
注意 ctx.user()。getRaw()获取在下面定义的Middleware.isauthenticated()处理程序中设置的用户配置文件信息。在 ctx.user()的电子邮件的帮助下
中间件
中间件在验证从前端发送的加密cookie以及API请求中起着至关重要的作用,还检查用户是否已登录。
- isauhtenticated.go
type MiddlewareHandler struct {
RedisClient interfaces.IRedisLayer
}
// IsAuthenticated is a middleware that checks if
// the user has already been authenticated previously.
func (m *MiddlewareHandler) IsAuthenticated(ctx iris.Context) {
userCookie := ctx.GetCookie("logged_id_email")
if userCookie == "" {
ctx.StopWithError(iris.StatusUnauthorized, errors.New("please make sure user is logged in"))
return
}
value, err := m.RedisClient.HGetKeyValue(userCookie + "_profile")
if err != nil || value == nil {
ctx.StopWithError(iris.StatusUnauthorized, errors.New("please make sure user is logged in"))
return
}
ctx.SetUser(value)
ctx.Next()
}
通过利用 ctx.setuser(),用户配置文件信息存储在虹膜上下文中。此信息可以访问,并且可以在请求处理过程中由后端API处理程序使用。
身份验证者
身份验证器初始化了OAuth2提供商的新实例。
// Authenticator is used to authenticate our users.
type Authenticator struct {
*oidc.Provider
oauth2.Config
}
// New instantiates the *Authenticator.
func New() (*Authenticator, error) {
provider, err := oidc.NewProvider(
context.Background(),
"https://"+config.EnvVariables.Auth0Domain+"/",
)
if err != nil {
return nil, err
}
conf := oauth2.Config{
ClientID: config.EnvVariables.Auth0ClientID,
ClientSecret: config.EnvVariables.Auth0ClientSecret,
RedirectURL: config.EnvVariables.Auth0CallbackURL,
Endpoint: provider.Endpoint(),
Scopes: []string{oidc.ScopeOpenID, "profile", "email"},
}
return &Authenticator{
Provider: provider,
Config: conf,
}, nil
}
// VerifyIDToken verifies that an *oauth2.Token is a valid *oidc.IDToken.
func (a *Authenticator) VerifyIDToken(ctx context.Context, token *oauth2.Token) (*oidc.IDToken, error) {
rawIDToken, ok := token.Extra("id_token").(string)
if !ok {
return nil, errors.New("no id_token field in oauth2 token")
}
oidcConfig := &oidc.Config{
ClientID: a.ClientID,
}
return a.Verifier(oidcConfig).Verify(ctx, rawIDToken)
}
至关重要的是要确保Auth0应用程序的所有环境变量均正确配置和设置。此步骤是成功初始化OAuth2提供商的必要条件。
关于流程
这是用户对应用程序进行身份验证时发生的情况:
- 用户单击前端UI上的登录按钮。
- 前端将用户重定向到后端(BFF)的 /登录端点。< /li>
- BFF进一步将用户重定向到Auth0的通用登录页面。
- 用户提供了对身份验证过程的凭据和同意。
- 成功身份验证后,回调请求将发送回应用程序中的 /回调端点。< /li>
- 回调处理程序验证了已接收的状态参数,确保其完整性和安全性。
- 回调处理程序通过有担保渠道交换了接收到的授权代码。
- 已收到的访问令牌已验证。
- 在Redis服务器中提取并缓存了访问令牌和关联的配置文件。
- 处理程序将用户与仅HTTP的cookie一起重定向到前端UI。
- 当用户从前端发出后续的呼叫来后端API时,请求中包含cookie。
- cookie已通过中间件处理程序验证,这也验证了用户的登录状态并检查缓存中的配置文件信息。
- 后端API处理程序管理流程,从缓存中提取JWT访问令牌,然后将其添加到转发请求的授权标题中。
- 然后将请求转发到后端API。
- 一旦从后端收到响应,它就会转回前端。
结论
总之,GO中前端(BFF)身份验证模式的后端为构建前端应用程序的身份验证系统提供了一种强大而灵活的方法。通过将身份验证问题分为专门的后端服务,为每个前端客户量身定制,开发人员可以创建可靠,可扩展和安全的身份验证系统,以满足其应用程序的独特要求。我希望该技术博客为实施GO中实施BFF身份验证模式提供了宝贵的见解和实用指南。愉快的编码!
此示例的源代码可在mehulgohil/go-bffauth
上获得