跳到主要内容
给我们的新朋友:

Logto 是一个为现代应用和 SaaS 产品设计的 Auth0 替代方案。它提供 Cloud开源 服务,帮助你快速启动身份和管理 (IAM) 系统。享受认证 (Authentication)、授权 (Authorization) 和多租户管理 一体化

我们建议从 Logto Cloud 上的免费开发租户开始。这可以让你轻松探索所有功能。

在本文中,我们将介绍使用 GoLogto 快速构建 SAML 登录体验(用户认证 (Authentication))的步骤。

先决条件

  • 一个正在运行的 Logto 实例。查看 介绍页面 以开始。
  • Go 的基本知识。
  • 一个可用的 SAML 账户。

在 Logto 中创建一个应用程序

Logto 基于 OpenID Connect (OIDC) 认证 (Authentication) 和 OAuth 2.0 授权 (Authorization)。它支持跨多个应用程序的联合身份管理,通常称为单点登录 (SSO)。

要创建你的 传统 Web 应用程序,只需按照以下步骤操作:

  1. 打开 Logto Console。在“开始使用”部分,点击“查看全部”链接以打开应用程序框架列表。或者,你可以导航到 Logto Console > Applications,然后点击“创建应用程序”按钮。 开始使用
  2. 在打开的模态窗口中,点击“传统 Web”部分,或使用左侧的快速过滤复选框过滤所有可用的“传统 Web”框架。点击 "Go" 框架卡片以开始创建你的应用程序。 框架
  3. 输入应用程序名称,例如“Bookstore”,然后点击“创建应用程序”。

🎉 太棒了!你刚刚在 Logto 中创建了你的第一个应用程序。你将看到一个祝贺页面,其中包含详细的集成指南。按照指南查看你的应用程序中的体验将会是什么样的。

集成 Go SDK

提示:
  • 以下演示基于 Gin Web Framework 构建。你也可以通过相同的步骤将 Logto 集成到其他框架中。
  • Go 示例项目可在我们的 Go SDK 仓库 中找到。

安装

在项目根目录执行:

go get github.com/logto-io/go

github.com/logto-io/go/client 包添加到你的应用代码中:

main.go
// main.go
package main

import (
"github.com/gin-gonic/gin"
// 添加依赖
"github.com/logto-io/go/client"
)

func main() {
router := gin.Default()
router.GET("/", func(c *gin.Context) {
c.String(200, "Hello Logto!")
})
router.Run(":3000")
}

创建会话存储

在传统的 Web 应用程序中,用户认证信息会存储在用户会话中。

Logto SDK 提供了一个 Storage 接口,你可以根据你的 Web 框架实现一个 Storage 适配器,以便 Logto SDK 可以将用户认证信息存储在会话中。

备注:

我们不推荐使用基于 Cookie 的会话,因为 Logto 存储的用户认证信息可能会超过 Cookie 的大小限制。在这个例子中,我们使用基于内存的会话。你可以在生产环境中使用 Redis、MongoDB 和其他技术根据需要存储会话。

Logto SDK 中的 Storage 类型如下:

github.com/logto-io/client/storage.go
package client

type Storage interface {
GetItem(key string) string
SetItem(key, value string)
}

我们使用 github.com/gin-contrib/sessions 中间件作为示例来演示这个过程。

将中间件应用到应用程序中,以便我们可以在路由处理程序中通过用户请求上下文获取用户会话:

main.go
package main

import (
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/memstore"
"github.com/gin-gonic/gin"
"github.com/logto-io/go/client"
)

func main() {
router := gin.Default()

// 在这个例子中我们使用基于内存的会话
store := memstore.NewStore([]byte("your session secret"))
router.Use(sessions.Sessions("logto-session", store))

router.GET("/", func(ctx *gin.Context) {
// 获取用户会话
session := sessions.Default(ctx)
// ...
ctx.String(200, "Hello Logto!")
})
router.Run(":3000")
}

创建一个 session_storage.go 文件,定义一个 SessionStorage 并实现 Logto SDK 的 Storage 接口:

session_storage.go
package main

import (
"github.com/gin-contrib/sessions"
)

type SessionStorage struct {
session sessions.Session
}

func (storage *SessionStorage) GetItem(key string) string {
value := storage.session.Get(key)
if value == nil {
return ""
}
return value.(string)
}

func (storage *SessionStorage) SetItem(key, value string) {
storage.session.Set(key, value)
storage.session.Save()
}

现在,在路由处理程序中,你可以为 Logto 创建一个会话存储:

session := sessions.Default(ctx)
sessionStorage := &SessionStorage{session: session}

初始化 LogtoClient

首先,创建一个 Logto 配置:

main.go
func main() {
// ...
logtoConfig := &client.LogtoConfig{
Endpoint: "<your-logto-endpoint>", // 例如 http://localhost:3001
AppId: "<your-application-id>",
AppSecret: "<your-application-secret>",
}
// ...
}
提示:

你可以在管理控制台的应用详情页面找到并复制“应用密钥”:

App Secret

然后,你可以为每个用户请求使用上述 Logto 配置创建一个 LogtoClient

main.go
func main() {
// ...

router.GET("/", func(ctx *gin.Context) {
// 创建 LogtoClient
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(
logtoConfig,
&SessionStorage{session: session},
)

// 使用 Logto 控制主页内容
authState := "你尚未登录此网站。:("

if logtoClient.IsAuthenticated() {
authState = "你已登录此网站!:)"
}

homePage := `<h1>Hello Logto</h1>` +
"<div>" + authState + "</div>"

ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage))
})

// ...
}

实现登录路由

在配置好重定向 URI 后,我们添加一个 sign-in 路由来处理登录请求,并在主页上添加一个登录链接:

main.go
func main() {
// ...

// 在主页上添加一个链接以执行登录请求
router.GET("/", func(ctx *gin.Context) {
// ...
homePage := `<h1>Hello Logto</h1>` +
"<div>" + authState + "</div>" +
// 添加链接
`<div><a href="/sign-in">Sign In</a></div>`

ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage))
})

// 添加一个路由来处理登录请求
router.GET("/sign-in", func(ctx *gin.Context) {
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(
logtoConfig,
&SessionStorage{session: session},
)

// 登录请求由 Logto 处理。
// 用户登录后将被重定向到重定向 URI。
signInUri, err := logtoClient.SignIn("http://localhost:3000/callback")
if err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

// 将用户重定向到 Logto 登录页面。
ctx.Redirect(http.StatusTemporaryRedirect, signInUri)
})

// ...
}

现在,当用户访问 http://localhost:3000/sign-in 时,用户将被重定向到 Logto 登录页面。

实现回调路由

当用户在 Logto 登录页面成功登录后,Logto 将把用户重定向到重定向 URI。

由于重定向 URI 是 http://localhost:3000/callback,我们添加 /callback 路由来处理登录后的回调。

main.go
func main() {
// ...

// 添加一个路由来处理登录回调请求
router.GET("/callback", func(ctx *gin.Context) {
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(
logtoConfig,
&SessionStorage{session: session},
)

// 登录回调请求由 Logto 处理
err := logtoClient.HandleSignInCallback(ctx.Request)
if err != nil {
ctx.String(http.StatusInternalServerError, err.Error())
return
}

// 跳转到开发者指定的页面。
// 本例将用户带回主页。
ctx.Redirect(http.StatusTemporaryRedirect, "/")
})

// ...
}

实现登出路由

与登录流程类似,当用户登出时,Logto 将会重定向用户到登出后的重定向 URI。

现在,让我们添加 sign-out 路由来处理登出请求,并在主页上添加一个登出链接:

main.go
func main() {
// ...

// 在主页上添加一个链接以执行登出请求
router.GET("/", func(ctx *gin.Context) {
// ...
homePage := `<h1>Hello Logto</h1>` +
"<div>" + authState + "</div>" +
`<div><a href="/sign-in">Sign In</a></div>` +
// 添加链接
`<div><a href="/sign-out">Sign Out</a></div>`

ctx.Data(http.StatusOK, "text/html; charset=utf-8", []byte(homePage))
})

// 添加一个路由来处理登出请求
router.GET("/sign-out", func(ctx *gin.Context) {
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(
logtoConfig,
&SessionStorage{session: session},
)

// 登出请求由 Logto 处理。
// 用户登出后将被重定向到登出后的重定向 URI。
signOutUri, signOutErr := logtoClient.SignOut("http://localhost:3000")

if signOutErr != nil {
ctx.String(http.StatusOK, signOutErr.Error())
return
}

ctx.Redirect(http.StatusTemporaryRedirect, signOutUri)
})

// ...
}

用户发起登出请求后,Logto 将清除会话中的所有用户认证信息。

检查点:测试你的应用程序

现在,你可以测试你的应用程序:

  1. 运行你的应用程序,你将看到登录按钮。
  2. 点击登录按钮,SDK 将初始化登录过程并将你重定向到 Logto 登录页面。
  3. 登录后,你将被重定向回你的应用程序,并看到登出按钮。
  4. 点击登出按钮以清除令牌存储并登出。

添加 SAML 连接器

要实现快速登录并提高用户转化率,请将 Go 作为身份提供商 (IdP) 进行连接。Logto 社交连接器可以帮助你在几分钟内通过允许输入多个参数来建立此连接。

要添加社交连接器,只需按照以下步骤操作:

  1. 导航到 Console > Connectors > Social Connectors
  2. 点击“添加社交连接器”并选择“SAML”。
  3. 按照 README 指南完成必填字段并自定义设置。

Connector tab

备注:

如果你正在按照现场连接器指南进行操作,可以跳过下一部分。

设置 标准 SAML 应用

创建社交 IdP 的账户并注册 SAML 应用程序 (IdP)

让我们来看看 SAML 连接器的配置。

在开始之前,你可以去支持 SAML 协议的社交身份提供商创建自己的账户。Okta、OneLogin、Salesforce 和其他一些平台支持基于 SAML 协议的认证 (Authentication)。

如果你的 IdP 要求对 SAML 断言进行加密并接收签名的认证请求,你应该使用 RSA 算法生成你的私钥和相应的证书。保留私钥供你的 SP 使用,并将证书上传到 IdP。

你还需要将 ACS (Assertion Consumer Service) URL 配置为 ${your_logto_origin}/api/authn/saml/${connector_id} 以处理 IdP 的 SAML 断言。你可以在 Logto 管理控制台的 SAML 连接器详细信息页面找到你的 connectorId

备注:

根据当前 Logto 的设计,我们仅支持用于发送认证请求的重定向绑定和用于接收 SAML 断言的 POST 绑定。虽然这听起来不太酷,但我们相信当前的设计可以处理大多数用例。如果你有任何问题,请随时联系我们!

配置 SAML 连接器 (SP)

在本节中,我们将详细介绍每个属性。

entityID 必需

entityID(即 issuer)是实体标识符。它用于识别你的实体(SAML SP 实体),并在每个 SAML 请求/响应中匹配等效项。

signInEndpoint 必需

你发送 SAML 认证请求的 IdP 端点。通常,你可以在 IdP 详细信息页面找到此值(即 IdP 的 SSO URLLogin URL)。

x509Certificate 必需

从 IdP 私钥生成的 x509 证书,IdP 应该提供此值。

证书的内容带有 -----BEGIN CERTIFICATE----- 头和 -----END CERTIFICATE----- 尾。

idpMetadataXml 必需

此字段用于放置来自 IdP 元数据 XML 文件的内容。

备注:

我们使用的 XML 解析器不支持自定义命名空间。 如果 IdP 元数据带有命名空间,你应该手动删除它们。 有关 XML 文件的命名空间,请参阅 参考

assertionConsumerServiceUrl 必需

断言消费者服务 (ACS) URL 是 SP 接收 IdP 的 SAML 断言 POST 请求的端点。如我们在前面部分提到的,它通常在 IdP 设置中配置,但有些 IdP 从 SAML 认证请求中获取此值,因此我们也将此值添加为必需字段。它的值应类似于 ${your_logto_origin}/api/authn/saml/${connector_id}

signAuthnRequest

控制 SAML 认证请求是否应签名的布尔值,默认值为 false

encryptAssertion

encryptAssertion 是一个布尔值,指示 IdP 是否会加密 SAML 断言,默认值为 false

备注:

signAuthnRequestencryptAssertion 属性应与 IdP 设置的相应参数一致,否则会抛出错误,显示配置不匹配。 所有 SAML 响应都需要签名。

requestSignatureAlgorithm

这应该与 IdP 的签名算法一致,以便 Logto 可以验证 SAML 断言的签名。其值应为 http://www.w3.org/2000/09/xmldsig#rsa-sha1http://www.w3.org/2001/04/xmldsig-more#rsa-sha256http://www.w3.org/2001/04/xmldsig-more#rsa-sha512,默认值为 http://www.w3.org/2001/04/xmldsig-more#rsa-sha256

messageSigningOrder

messageSigningOrder 指示 IdP 的签名和加密顺序,其值应为 sign-then-encryptencrypt-then-sign,默认值为 sign-then-encrypt

privateKey 和 privateKeyPass

privateKey 是一个可选值,当 signAuthnRequesttrue 时是必需的。

privateKeyPass 是你在创建 privateKey 时设置的密码,必要时需要。

如果 signAuthnRequesttrue,则 IdP 需要从 privateKey 生成的相应证书来检查签名。

encPrivateKey 和 encPrivateKeyPass

encPrivateKey 是一个可选值,当 encryptAssertiontrue 时是必需的。

encPrivateKeyPass 是你在创建 encPrivateKey 时设置的密码,必要时需要。

如果 encryptAssertiontrue,则 IdP 需要从 encPrivateKey 生成的相应证书来加密 SAML 断言。

备注:

对于密钥和证书的生成,openssl 是一个很好的工具。以下是可能有用的示例命令行:

openssl genrsa -passout pass:${privateKeyPassword} -out ${encryptPrivateKeyFilename}.pem 4096
openssl req -new -x509 -key ${encryptPrivateKeyFilename}.pem -out ${encryptionCertificateFilename}.cer -days 3650

privateKeyencPrivateKey 文件被强制编码为 pkcs1 方案的 pem 字符串,这意味着私钥文件应以 -----BEGIN RSA PRIVATE KEY----- 开头并以 -----END RSA PRIVATE KEY----- 结尾。

nameIDFormat

nameIDFormat 是一个可选属性,用于声明响应的名称 ID 格式。其值可以是 urn:oasis:names:tc:SAML:1.1:nameid-format:unspecifiedurn:oasis:names:tc:SAML:1.1:nameid-format:emailAddressurn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectNameurn:oasis:names:tc:SAML:2.0:nameid-format:persistenturn:oasis:names:tc:SAML:2.0:nameid-format:transient,默认值为 urn:oasis:names:tc:SAML:2.0:nameid-format:unspecified

timeout

timeout 是时间验证的时间容差,因为你的 SP 实体和 IdP 实体之间的时间可能不同,网络连接也可能带来一些延迟。单位为毫秒,默认值为 5000(即 5 秒)。

profileMap

Logto 还提供了一个 profileMap 字段,用户可以自定义从社交供应商的配置文件中映射,这些配置文件通常不是标准的。每个 profileMap 键是 Logto 的标准用户配置文件字段名称,相应的值应为社交配置文件字段名称。在当前阶段,Logto 只关注社交配置文件中的 'id'、'name'、'avatar'、'email' 和 'phone',其中只有 'id' 是必需的,其他是可选字段。

配置类型

名称类型必需默认值
signInEndpointstringtrue
x509certificatestringtrue
idpMetadataXmlstringtrue
entityIDstringtrue
assertionConsumerServiceUrlstringtrue
messageSigningOrderencrypt-then-sign | sign-then-encryptfalsesign-then-encrypt
requestSignatureAlgorithmhttp://www.w3.org/2000/09/xmldsig#rsa-sha1 | http://www.w3.org/2001/04/xmldsig-more#rsa-sha256 | http://www.w3.org/2001/04/xmldsig-more#rsa-sha512falsehttp://www.w3.org/2001/04/xmldsig-more#rsa-sha256
signAuthnRequestbooleanfalsefalse
encryptAssertionbooleanfalsefalse
privateKeystringfalse
privateKeyPassstringfalse
nameIDFormaturn:oasis:names:tc:SAML:1.1:nameid-format:unspecified | urn:oasis:names:tc:SAML:1.1:nameid-format:emailAddress | urn:oasis:names:tc:SAML:1.1:nameid-format:X509SubjectName | urn:oasis:names:tc:SAML:2.0:nameid-format:persistent | urn:oasis:names:tc:SAML:2.0:nameid-format:transientfalseurn:oasis:names:tc:SAML:1.1:nameid-format:unspecified
timeoutnumberfalse5000
profileMapProfileMapfalse
ProfileMap 字段类型必需默认值
idstringfalseid
namestringfalsename
avatarstringfalseavatar
emailstringfalseemail
phonestringfalsephone

参考

保存你的配置

仔细检查你是否在 Logto 连接器配置区域填写了必要的值。点击“保存并完成”(或“保存更改”),SAML 连接器现在应该可用了。

在登录体验中启用 SAML 连接器

一旦你成功创建了一个社交连接器,你可以在登录体验中将其启用为“继续使用 SAML”按钮。

  1. 导航到 Console > 登录体验 > 注册和登录
  2. (可选)如果只需要社交登录,选择“无”作为注册标识符。
  3. 将配置好的 SAML 连接器添加到“社交登录”部分。

登录体验选项卡

测试和验证

返回到你的 Go 应用。你现在应该可以使用 SAML 登录了。享受吧!

拓展阅读

终端用户流程:Logto 提供开箱即用的认证 (Authentication) 流程,包括多因素认证 (MFA) 和企业单点登录 (SSO),以及强大的 API,用于灵活实现账户设置、安全验证和多租户体验。

授权 (Authorization):授权 (Authorization) 定义了用户在被认证 (Authentication) 后可以执行的操作或访问的资源。探索如何保护你的 API 以用于原生和单页应用程序,并实现基于角色的访问控制 (RBAC)。

组织 (Organizations):在多租户 SaaS 和 B2B 应用中特别有效,组织功能支持租户创建、成员管理、组织级 RBAC 和即时供应。

客户 IAM 系列:我们关于客户(或消费者)身份和访问管理的系列博客文章,从 101 到高级主题及更深入的内容。