跳至主要內容

為你的 Go 網頁應用程式新增驗證 (Authentication)

本指南將向你展示如何將 Logto 整合到你的 Go 網頁應用程式中。

提示:
  • 以下示範基於 Gin Web Framework 構建。你也可以採取相同步驟將 Logto 整合到其他框架中。
  • Go 範例專案可在我們的 Go SDK repo 中找到。

先決條件

安裝

在專案根目錄執行:

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")
}

整合

建立會話儲存

在傳統的網頁應用程式中,使用者驗證資訊會儲存在使用者的 session 中。

Logto SDK 提供了一個 Storage 介面,你可以根據你的網頁框架實作一個 Storage 配接器,讓 Logto SDK 可以將使用者驗證資訊儲存在 session 中。

備註:

我們不建議使用基於 cookie 的 session,因為 Logto 儲存的使用者驗證資訊可能超過 cookie 的大小限制。在此範例中,我們使用基於記憶體的 session。你可以在生產環境中根據需要使用 Redis、MongoDB 和其他技術來儲存 session。

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 中介軟體作為範例來演示此過程。

將中介軟體應用於應用程式,以便我們可以在路由處理器中透過使用者請求上下文獲取使用者 session:

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()

// 在此範例中,我們使用基於記憶體的 session
store := memstore.NewStore([]byte("your session secret"))
router.Use(sessions.Sessions("logto-session", store))

router.GET("/", func(ctx *gin.Context) {
// 獲取使用者 session
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 storage:

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」:

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))
})

// ...
}

配置你的應用程式

在深入細節之前,以下是終端使用者體驗的快速概覽。登入流程可簡化如下:

  1. 你的應用程式呼叫登入方法。
  2. 使用者被重定向至 Logto 登入頁面。對於原生應用程式,系統瀏覽器會被開啟。
  3. 使用者登入後被重定向回你的應用程式(配置為重定向 URI)。

關於基於重導的登入

  1. 此驗證流程遵循 OpenID Connect (OIDC) 協議,Logto 強制執行嚴格的安全措施以保護使用者登入。
  2. 如果你有多個應用程式,可以使用相同的身分提供者 (IdP, Identity provider)(Logto)。一旦使用者登入其中一個應用程式,Logto 將在使用者訪問另一個應用程式時自動完成登入流程。

欲了解更多關於基於重導登入的原理和優勢,請參閱 Logto 登入體驗解析


備註:

在以下的程式碼片段中,我們假設你的應用程式運行在 http://localhost:3000/

配置重定向 URI

切換到 Logto Console 的應用程式詳細資訊頁面。新增一個重定向 URI http://localhost:3000/callback

Logto Console 中的重定向 URI

就像登入一樣,使用者應被重定向到 Logto 以登出共享會話。完成後,將使用者重定向回你的網站會很不錯。例如,將 http://localhost:3000/ 新增為登出後重定向 URI 區段。

然後點擊「儲存」以保存更改。

處理重定向

當使用者在 Logto 登入頁面成功登入後,Logto 會將使用者重定向到 Redirect 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, "/")
})

// ...
}

實作登入路由

在設定好 redirect 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 處理。
// 使用者登入後將被重定向到 Redirect 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 會將使用者重定向到登出後重定向的 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 會清除會話中的所有使用者驗證 (Authentication) 資訊。

檢查點:測試你的應用程式

現在,你可以測試你的應用程式:

  1. 執行你的應用程式,你會看到登入按鈕。
  2. 點擊登入按鈕,SDK 會初始化登入流程並將你重定向到 Logto 登入頁面。
  3. 登入後,你將被重定向回應用程式並看到登出按鈕。
  4. 點擊登出按鈕以清除權杖存儲並登出。

獲取使用者資訊

顯示使用者資訊

要顯示使用者的資訊,你可以使用 client.GetIdTokenClaims 方法。例如,新增一個路由:

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

router.GET("/user-id-token-claims", func(ctx *gin.Context) {
session := sessions.Default(ctx)
logtoClient := client.NewLogtoClient(logtoConfig, &SessionStorage{session: session})

idTokenClaims, err := logtoClient.GetIdTokenClaims()

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

ctx.JSON(http.StatusOK, idTokenClaims)
})
}

請求額外的宣告 (Claims)

你可能會發現從 client.GetIdTokenClaims() 返回的物件中缺少一些使用者資訊。這是因為 OAuth 2.0 和 OpenID Connect (OIDC) 的設計遵循最小權限原則 (PoLP, Principle of Least Privilege),而 Logto 是基於這些標準構建的。

預設情況下,僅返回有限的宣告 (Claims)。如果你需要更多資訊,可以請求額外的權限範圍 (Scopes) 以存取更多宣告。

資訊:

「宣告 (Claim)」是對主體所做的斷言;「權限範圍 (Scope)」是一組宣告。在目前的情況下,宣告是關於使用者的一部分資訊。

以下是權限範圍與宣告關係的非規範性範例:

提示:

「sub」宣告表示「主體 (Subject)」,即使用者的唯一識別符(例如使用者 ID)。

Logto SDK 將始終請求三個權限範圍:openidprofileoffline_access

要請求額外的權限範圍 (Scopes),你可以將權限範圍傳遞給 LogtoConfig 物件。例如:

main.go
logtoConfig := &client.LogtoConfig{
// ...其他配置
Scopes: []string{"email", "phone"},
}

然後你可以在 client.GetIdTokenClaims() 的返回值中訪問額外的宣告 (Claims):

idTokenClaims, error := client.GetIdTokenClaims()
// 現在你可以訪問額外的宣告 `claims.email`、`claims.phone` 等。

需要網路請求的宣告 (Claims)

為了防止 ID 權杖 (ID token) 膨脹,某些宣告 (Claims) 需要透過網路請求來獲取。例如,即使在權限範圍 (Scopes) 中請求了 custom_data 宣告,它也不會包含在使用者物件中。要存取這些宣告,你可以使用 client.FetchUserInfo() 方法

userInfo, error := client.FetchUserInfo()
// 現在你可以訪問宣告 `userInfo.custom_data`
此方法將透過請求 userinfo 端點來獲取使用者資訊。要了解更多可用的權限範圍 (Scopes) 和宣告 (Claims),請參閱 權限範圍 (Scopes) 和宣告 (Claims) 部分。

權限範圍 (Scopes) 和宣告 (Claims)

Logto 使用 OIDC 權限範圍 (Scopes) 和宣告 (Claims) 慣例 來定義從 ID 權杖 (ID token) 和 OIDC 使用者資訊端點 (userinfo endpoint) 獲取使用者資訊的權限範圍和宣告。無論是「權限範圍 (Scope)」還是「宣告 (Claim)」,都是 OAuth 2.0 和 OpenID Connect (OIDC) 規範中的術語。

以下是支援的權限範圍 (Scopes) 及其對應的宣告 (Claims):

openid

宣告名稱類型描述需要使用者資訊嗎?
substring使用者的唯一識別符

profile

宣告名稱類型描述需要使用者資訊嗎?
namestring使用者的全名
usernamestring使用者的用戶名
picturestring使用者個人資料圖片的 URL。此 URL 必須指向圖像文件(例如 PNG、JPEG 或 GIF 圖像文件),而不是包含圖像的網頁。請注意,此 URL 應特別參考適合在描述使用者時顯示的個人資料照片,而不是使用者拍攝的任意照片。
created_atnumber使用者創建的時間。時間以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。
updated_atnumber使用者資訊最後更新的時間。時間以自 Unix epoch(1970-01-01T00:00:00Z)以來的毫秒數表示。

其他 標準宣告 包括 family_namegiven_namemiddle_namenicknamepreferred_usernameprofilewebsitegenderbirthdatezoneinfolocale 也將包含在 profile 權限範圍中,無需請求使用者資訊端點。與上述宣告的不同之處在於,這些宣告僅在其值不為空時返回,而上述宣告在值為空時將返回 null

備註:

與標準宣告不同,created_atupdated_at 宣告使用毫秒而非秒。

email

宣告名稱類型描述需要使用者資訊嗎?
emailstring使用者的電子郵件地址
email_verifiedboolean電子郵件地址是否已驗證

phone

宣告名稱類型描述需要使用者資訊嗎?
phone_numberstring使用者的電話號碼
phone_number_verifiedboolean電話號碼是否已驗證

address

請參閱 OpenID Connect Core 1.0 以獲取地址宣告的詳細資訊。

custom_data

宣告名稱類型描述需要使用者資訊嗎?
custom_dataobject使用者的自訂資料

identities

宣告名稱類型描述需要使用者資訊嗎?
identitiesobject使用者的連結身分
sso_identitiesarray使用者的連結 SSO 身分

roles

宣告名稱類型描述需要使用者資訊嗎?
rolesstring[]使用者的角色

urn:logto:scope:organizations

宣告名稱類型描述需要使用者資訊嗎?
organizationsstring[]使用者所屬的組織 ID
organization_dataobject[]使用者所屬的組織資料

urn:logto:scope:organization_roles

宣告名稱類型描述需要使用者資訊嗎?
organization_rolesstring[]使用者所屬的組織角色,格式為 <organization_id>:<role_name>

考慮到效能和資料大小,如果「需要使用者資訊嗎?」為「是」,則表示該宣告不會顯示在 ID 權杖中,而會在 使用者資訊端點 回應中返回。

API 資源與組織

我們建議先閱讀 🔐 角色型存取控制 (RBAC, Role-Based Access Control),以瞭解 Logto RBAC 的基本概念以及如何正確設定 API 資源。

配置 Logto 客戶端

一旦你設定了 API 資源,就可以在應用程式中配置 Logto 時新增它們:

logtoConfig := &client.LogtoConfig{
// ...other configs
Resources: []string{"https://shopping.your-app.com/api", "https://store.your-app.com/api"},
}

每個 API 資源都有其自身的權限(權限範圍)。

例如,https://shopping.your-app.com/api 資源具有 shopping:readshopping:write 權限,而 https://store.your-app.com/api 資源具有 store:readstore:write 權限。

要請求這些權限,你可以在應用程式中配置 Logto 時新增它們:

logtoConfig := &client.LogtoConfig{
// ...other configs
Scopes: []string{"shopping:read", "shopping:write", "store:read", "store:write"},
Resources: []string{"https://shopping.your-app.com/api", "https://store.your-app.com/api"},
}

你可能會注意到權限範圍是獨立於 API 資源定義的。這是因為 OAuth 2.0 的資源標示符 (Resource Indicators) 指定請求的最終權限範圍將是所有目標服務中所有權限範圍的笛卡兒積。

因此,在上述情況中,權限範圍可以從 Logto 的定義中簡化,兩個 API 資源都可以擁有 read write 權限範圍而不需要前綴。然後,在 Logto 配置中:

logtoConfig := &client.LogtoConfig{
// ...other configs
Scopes: []string{"read", "write"},
Resources: []string{"https://shopping.your-app.com/api", "https://store.your-app.com/api"},
}

對於每個 API 資源,它將請求 readwrite 權限範圍。

備註:

請求未在 API 資源中定義的權限範圍是可以的。例如,即使 API 資源中沒有可用的 email 權限範圍,你也可以請求 email 權限範圍。不可用的權限範圍將被安全地忽略。

成功登入後,Logto 將根據使用者的角色向 API 資源發出適當的權限範圍。

為 API 資源提取存取權杖

要獲取特定 API 資源的存取權杖 (Access token),你可以使用 GetAccessToken 方法:

accessToken, error := logtoClient.GetAccessToken("https://shopping.your-app.com/api")

此方法將返回一個 JWT 存取權杖 (Access token),當使用者擁有相關權限時,可以用來存取 API 資源。如果當前快取的存取權杖 (Access token) 已過期,此方法將自動嘗試使用重新整理權杖 (Refresh token) 獲取新的存取權杖 (Access token)。

提取組織權杖

如果你對組織 (Organization) 不熟悉,請閱讀 🏢 組織(多租戶,Multi-tenancy) 以開始了解。

在配置 Logto client 時,你需要新增 core.UserScopeOrganizations 權限範圍 (scope):

logtoConfig := &client.LogtoConfig{
// ...other configs
Scopes: []string{core.UserScopeOrganizations},
}

使用者登入後,你可以為使用者獲取組織權杖 (organization token):

// 將參數替換為有效的組織 (Organization) ID。
// 使用者的有效組織 (Organization) ID 可以在 ID 權杖 (ID token) 宣告 (claim) `organizations` 中找到。
accessToken, error := logtoClient.GetOrganizationToken("organization-id")

// 或
accessTokenClaims, error := logtoClient.GetOrganizationTokenClaims("organization-id")

組織 API 資源 (Organization API resources)

若要為組織中的 API 資源取得存取權杖 (Access token),可以使用 getAccessToken 方法,並將 API 資源和組織 ID 作為參數:

accessToken, error := client.GetAccessToken(
'https://shopping.your-app.com/api',
organizationId
);

延伸閱讀

終端使用者流程:驗證流程、帳戶流程與組織流程 配置連接器 保護你的 API