使用 Firestore 处理会话

本教程介绍如何在 Cloud Run 上处理会话。

许多应用需要处理会话以进行身份验证和设定用户偏好设置。 Gorilla Web Toolkit sessions 软件包附带一个基于文件系统的实现,可实现此功能。但此实现不适用于可以通过多个实例提供的应用,因为各实例之间记录的会话可能不同。gorilla/sessions 软件包还附带一个基于 Cookie 的实现。但此实现需要加密 Cookie 并将整个会话存储在客户端上,而不仅仅是执行加密和存储会话 ID,整个会话对于某些应用而言可能过大。

目标

  • 编写应用。
  • 在本地运行应用。
  • 在 Cloud Run 上部署应用。

费用

在本文档中,您将使用 Google Cloud的以下收费组件:

如需根据您的预计使用量来估算费用,请使用价格计算器

新 Google Cloud 用户可能有资格申请免费试用

完成本文档中描述的任务后,您可以通过删除所创建的资源来避免继续计费。如需了解详情,请参阅清理

准备工作

  1. Sign in to your Google Cloud account. If you're new to Google Cloud, create an account to evaluate how our products perform in real-world scenarios. New customers also get $300 in free credits to run, test, and deploy workloads.
  2. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  3. Verify that billing is enabled for your Google Cloud project.

  4. Enable the Firestore API.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the API

  5. In the Google Cloud console, on the project selector page, select or create a Google Cloud project.

    Roles required to select or create a project

    • Select a project: Selecting a project doesn't require a specific IAM role—you can select any project that you've been granted a role on.
    • Create a project: To create a project, you need the Project Creator (roles/resourcemanager.projectCreator), which contains the resourcemanager.projects.create permission. Learn how to grant roles.

    Go to project selector

  6. Verify that billing is enabled for your Google Cloud project.

  7. Enable the Firestore API.

    Roles required to enable APIs

    To enable APIs, you need the Service Usage Admin IAM role (roles/serviceusage.serviceUsageAdmin), which contains the serviceusage.services.enable permission. Learn how to grant roles.

    Enable the API

  8. 在 Google Cloud 控制台中,通过 Cloud Shell 打开此应用。

    转到 Cloud Shell

    利用 Cloud Shell,您可以直接在浏览器中通过命令行访问云端资源。在浏览器中打开 Cloud Shell,然后点击继续下载示例代码并切换到应用目录。

  9. 在 Cloud Shell 中,配置 gcloud CLI 以使用新的 Google Cloud 项目:
    # Configure gcloud for your project
    gcloud config set project YOUR_PROJECT_ID
    

设置项目

  1. 在终端窗口中,将示例应用代码库克隆到本地机器:

    git clone https://github.com/GoogleCloudPlatform/golang-samples.git
  2. 转到包含示例代码的目录:

    cd golang-samples/getting-started/sessions

了解 Web 应用

此应用为每位用户显示不同语言的问候语。回访用户始终会收到同一种语言的问候语。

多个应用窗口显示不同语言的问候语。

您需要先存储会话中当前用户的相关信息,然后应用才能拿存储用户偏好设置。此示例应用使用 Firestore 来存储会话数据。

  1. 应用首先导入依赖项,指定用于保存 sessions.Store 和 HTML 模板的 app 类型,并指定问候语列表。

    import (
    	"context"
    	"html/template"
    	"log"
    	"math/rand"
    	"net/http"
    	"os"
    
    	"cloud.google.com/go/firestore"
    )
    
    // app stores a sessions.Store. Create a new app with newApp.
    type app struct {
    	tmpl         *template.Template
    	collectionID string
    	projectID    string
    }
    
    // session stores the client's session information.
    // This type is also used for executing the template.
    type session struct {
    	Greetings string `json:"greeting"`
    	Views     int    `json:"views"`
    }
    
    // greetings are the random greetings that will be assigned to sessions.
    var greetings = []string{
    	"Hello World",
    	"Hallo Welt",
    	"Ciao Mondo",
    	"Salut le Monde",
    	"Hola Mundo",
    }
    
  2. 接下来,该应用会定义 main 函数,以创建新的 app 实例、注册索引处理程序并启动 HTTP 服务器。newApp 函数通过设置 projectIDcollectionID 值来创建 app 实例,并解析 HTML 模板。

    func main() {
    	port := os.Getenv("PORT")
    	if port == "" {
    		port = "8080"
    	}
    
    	projectID := os.Getenv("GOOGLE_CLOUD_PROJECT")
    	if projectID == "" {
    		log.Fatal("GOOGLE_CLOUD_PROJECT must be set")
    	}
    
    	// collectionID is a non-empty identifier for this app, it is used as the Firestore
    	// collection name that stores the sessions.
    	//
    	// Set it to something more descriptive for your app.
    	collectionID := "hello-views"
    
    	a, err := newApp(projectID, collectionID)
    	if err != nil {
    		log.Fatalf("newApp: %v", err)
    	}
    
    	http.HandleFunc("/", a.index)
    
    	log.Printf("Listening on port %s", port)
    	if err := http.ListenAndServe(":"+port, nil); err != nil {
    		log.Fatal(err)
    	}
    }
    
    // newApp creates a new app.
    func newApp(projectID, collectionID string) (app, error) {
    	tmpl, err := template.New("Index").Parse(`<body>{{.Views}} {{if eq .Views 1}}view{{else}}views{{end}} for "{{.Greetings}}"</body>`)
    	if err != nil {
    		log.Fatalf("template.New: %v", err)
    	}
    
    	return app{
    		tmpl:         tmpl,
    		collectionID: collectionID,
    		projectID:    projectID,
    	}, nil
    }
    
  3. 索引处理程序获取用户的会话,如果需要的话,可创建一个会话。系统会为新会话分配一种随机语言,此时视图计数为 0。然后,视图计数会加 1,系统会保存会话,HTML 模板会写入响应。

    
    // index uses sessions to assign users a random greeting and keep track of
    // views.
    func (a *app) index(w http.ResponseWriter, r *http.Request) {
    	// Allows requests only for the root path ("/") to prevent duplicate calls.
    	if r.RequestURI != "/" {
    		http.NotFound(w, r)
    		return
    	}
    
    	var session session
    	var doc *firestore.DocumentRef
    
    	isNewSession := false
    
    	ctx := context.Background()
    
    	client, err := firestore.NewClient(ctx, a.projectID)
    	if err != nil {
    		log.Fatalf("firestore.NewClient: %v", err)
    	}
    	defer client.Close()
    
    	// cookieName is a non-empty identifier for this app, it is used as the key name
    	// that contains the session's id value.
    	//
    	// Set it to something more descriptive for your app.
    	cookieName := "session_id"
    
    	// If err is different to nil, it means the cookie has not been set, so it will be created.
    	cookie, err := r.Cookie(cookieName)
    	if err != nil {
    		// isNewSession flag is set to true
    		isNewSession = true
    	}
    
    	// If isNewSession flag is true, the session will be created
    	if isNewSession {
    		// Get unique id for new document
    		doc = client.Collection(a.collectionID).NewDoc()
    
    		session.Greetings = greetings[rand.Intn(len(greetings))]
    		session.Views = 1
    
    		// Cookie is set
    		cookie = &http.Cookie{
    			Name:  cookieName,
    			Value: doc.ID,
    		}
    		http.SetCookie(w, cookie)
    	} else {
    		// The session exists
    
    		// Retrieve document from collection by ID
    		docSnapshot, err := client.Collection(a.collectionID).Doc(cookie.Value).Get(ctx)
    		if err != nil {
    			log.Printf("doc.Get error: %v", err)
    			http.Error(w, "Error getting session", http.StatusInternalServerError)
    			return
    		}
    
    		// Unmarshal documents's content to local type
    		err = docSnapshot.DataTo(&session)
    		if err != nil {
    			log.Printf("doc.DataTo error: %v", err)
    			http.Error(w, "Error parsing session", http.StatusInternalServerError)
    			return
    		}
    
    		doc = docSnapshot.Ref
    
    		// Add 1 to current views value
    		session.Views++
    	}
    
    	// The document is created/updated
    	_, err = doc.Set(ctx, session)
    	if err != nil {
    		log.Printf("doc.Set error: %v", err)
    		http.Error(w, "Error creating session", http.StatusInternalServerError)
    		return
    	}
    
    	if err := a.tmpl.Execute(w, session); err != nil {
    		log.Printf("Execute: %v", err)
    	}
    }
    

    下图说明 Firestore 如何处理 Cloud Run 应用的会话。

    架构图:用户、Cloud Run、Firestore。

删除会话

您可以在 Google Cloud 控制台删除会话数据,或实施自动删除策略。如果您为会话使用 Memcache 或 Redis 等存储解决方案,系统会自动删除已过期的会话。

在本地运行

  1. 在终端窗口中,构建 sessions 二进制文件:

    go build
    
  2. 启动 HTTP 服务器:

    ./sessions
    
  3. 查看网络浏览器中的应用:

    Cloud Shell

    在 Cloud Shell 工具栏中,点击 Web 预览 Web 预览,然后选择在端口 8080 上预览

    本地机器

    在浏览器中,转到 http://localhost:8080

    您会看到五个问候语之一:“Hello World”、“Hallo Welt”、“Hola mundo”、“Salut le Monde”或“Ciao Mondo”。如果您使用其他浏览器或无痕模式打开网页,语言会更改。 您可以在 Google Cloud 控制台中查看和修改会话数据。

     Google Cloud 控制台中的 Firestore 会话。

  4. 要停止 HTTP 服务器,请在终端窗口中按 Control+C

在 Cloud Run 上部署和运行

您可以使用 Cloud Run 来构建和部署可在负载繁重和具有大量数据的情况下可靠运行的应用。

  1. 在 Cloud Run 上部署应用:
        gcloud run deploy firestore-tutorial-go 
    --source . --allow-unauthenticated --port=8080
    --set-env-vars=GOOGLE_CLOUD_PROJECT=YOUR_PROJECT_ID
  2. 访问此命令返回的网址,以查看会话数据在页面加载之间如何持久存在。

现在,问候语由在 Cloud Run 实例上运行的 Web 服务器提供。

调试应用

如果您无法连接到 Cloud Run 应用,请检查以下内容:

  1. 检查 gcloud 部署命令是否已成功完成,并且未输出任何错误。如果存在错误(例如 message=Build failed),请更正这些错误,然后重试部署 Cloud Run 应用
  2. 在 Google Cloud 控制台中,前往 Logs Explorer 页面。

    前往“日志浏览器”页面

    1. 最近选择的资源下拉列表中,点击 Cloud Run 应用,然后点击所有 module_id。您将看到访问您的应用时的请求列表,如果您未发现请求列表,请确认您是否已从下拉列表中选择所有 module_id。如果您发现 Google Cloud 控制台出现错误消息,请检查应用代码是否与“编写 Web 应用”相关部分中的代码匹配。

    2. 确保已启用 Firestore API。

清除数据

删除项目

  1. In the Google Cloud console, go to the Manage resources page.

    Go to Manage resources

  2. In the project list, select the project that you want to delete, and then click Delete.
  3. In the dialog, type the project ID, and then click Shut down to delete the project.

删除 Cloud Run 实例

  1. In the Google Cloud console, go to the Versions page for App Engine.

    Go to Versions

  2. Select the checkbox for the non-default app version that you want to delete.
  3. 如需删除应用版本,请点击删除

后续步骤