Go Web 編程之 模板(二)

概述

上一篇文章中,咱們介紹了 Go 模板庫text/templatetext/template庫用於生成文本輸出。在 Web 開發中,涉及到不少安全方面的問題。有些數據是用戶輸入的,不能直接替換到模板中,不然可能致使注入攻擊。Go 提供了html/template庫處理這些問題。html/template提供了與text/template同樣的接口。咱們一般使用html/template生成 HTML 輸出。html

因爲上一篇文章已經詳細介紹了 Go 模板的基本概念,本文主要從使用的層面來介紹html/template庫。中間會有安全方面的內容。那就開始吧!git

初體驗

html/template庫的使用與text/template基本同樣:github

package main

import (
    "fmt"
    "html/template"
    "log"
    "net/http"
)

func indexHandler(w http.ResponseWriter, r *http.Request) {
    t, err := template.ParseFiles("hello.html")
    if err != nil {
        w.WriteHeader(500)
        fmt.Fprint(w, err)
        return
    }

    t.Execute(w, "Hello World")
}

func main() {
    mux := http.NewServeMux()
    mux.HandleFunc("/", indexHandler)

    server := &http.Server {
        Addr:    ":8080",
        Handler: mux,
    }
    if err := server.ListenAndServe(); err != nil {
        log.Fatal(err)
    }
}複製代碼

模板文件hello.htmlgolang

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Go Web 編程之 模板(二)</title>
</head>
<body>
    {{ . }}
</body>
</html>複製代碼

模板中的{{ . }}會被替換爲傳入的數據"Hello World",程序將模板執行後生成的文本經過ResponseWriter傳回客戶端。web

編譯,運行程序(個人環境 Win10 + Git Bash):編程

$ go build -o main.exe main.go
$ ./main.exe複製代碼

打開瀏覽器,輸入localhost:8080,便可看到"Hello World"頁面。瀏覽器

咱們在介紹text/template庫的時候,提到了空白問題。在生成 HTML 時通常不須要考慮這個問題,由於瀏覽器渲染的時候會自動去掉多餘的空白。安全

爲了編寫示例代碼的便利,在解析時不進行錯誤處理,html/template庫提供了Must方法。它接受兩個參數,一個模板對象指針,一個錯誤。若是錯誤參數不爲nil,直接 panic,不然返回模板對象指針。使用Must方法簡化上面的處理器:微信

func indexHandler(w http.ResponseWriter, r *http.Request) {
    t := template.Must(template.ParseFiles("hello.html"))
    t.Execute(w, "Hello World")
}複製代碼

動做

接下來,咱們經過示例再過一遍幾種動做。curl

條件動做

func conditionHandler(w http.ResponseWriter, r *http.Request) {
    age, err := strconv.ParseInt(r.URL.Query().Get("age"), 10, 64)
    if err != nil {
        fmt.Fprint(w, err)
        return
    }

    t := template.Must(template.ParseFiles("condition.html"))
    t.Execute(w, age)
}

mux.HandleFunc("/condition", conditionHandler)複製代碼

模板文件 condition.html 只有 body 部分不一樣:

<p>Your age is: {{ . }}</p>
{{ if gt . 60 }}
<p>Old People!</p>
{{ else if gt . 40 }}
<p>Middle Aged!</p>
{{ else }}
<p>Young!</p>
{{ end }}複製代碼

模板邏輯很簡單,使用內置函數gt判斷傳入的年齡處於哪一個區間,顯示對應的文本。

編譯、運行程序,打開瀏覽器,輸入localhost:8080/condition?age=10

迭代動做

迭代動做通常用於生成一個列表。

type Item struct {
    Name    string
    Price    int
}

func iterateHandler(w http.ResponseWriter, r *http.Request) {
    t := template.Must(template.ParseFiles("iterate.html"))

    items := []Item {
        { "iPhone", 5499 },
        { "iPad", 6331 },
        { "iWatch", 1499 },
        { "MacBook", 8250 },
    }
    t.Execute(w, items)
}

mux.HandleFunc("/iterate", iterateHandler)複製代碼

模板文件iterate.html

<h1>Apple Products</h1>
<ul>
{{ range . }}
<li>{{ .Name }}: ¥{{ .Price }}</li>
{{ end }}
</ul>複製代碼

再次提醒,在{{ range }}中,.會被替換爲當前遍歷的元素值。

設置動做

設置動做容許用戶在指定範圍內爲.設置值。

type User struct {
    Name    string
    Age        int
}

type Pet struct {
    Name    string
    Age        int
    Owner    User
}

func setHandler(w http.ResponseWriter, r *http.Request) {
    t := template.Must(template.ParseFiles("set.html"))

    pet := Pet {
        Name:    "Orange",
        Age:    2,
        Owner:    User {
            Name:    "dj",
            Age:    28,
        },
    }
    t.Execute(w, pet)
}

mux.HandleFunc("/set", setHandler)複製代碼

模板文件set.html

<h1>Pet Info</h1>
<p>Name: {{ .Name }}</p>
<p>Age: {{ .Age }}</p>
<p>Owner:</p>
{{ with .Owner }}
<p>Name: {{ .Name }}</p>
<p>Age: {{ .Age }}</p>
{{ end }}複製代碼

{{ with .Owner }}{{ end }}之間,能夠直接經過{{ .Name }}{{ .Age }}訪問寵物主人的信息。

包含動做

包含動做容許用戶在一個模板裏面包含另外一個模板,從而構造出嵌套的模板。

func includeHandler(w http.ResponseWriter, r *http.Request) {
    t := template.Must(template.ParseFiles("include1.html", "include2.html"))
    t.Execute(w, "Hello World!")
}複製代碼

模板include1.html

<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <meta http-equiv="X-UA-Compatible" content="ie=edge">
    <title>Go Web 編程之 模板(二)</title>
</head>
<body>
    <div>This is in template include1.html</div>
    <p>The value of dot is {{ . }}</p>
    <hr/>
    <p>Don't pass argument to include2.html:</p>
    {{ template "include2.html" }}
    <hr/>
    <p>Pass dot to include2.html</p>
    {{ template "include2.html" . }}
    <hr/>
</body>
</html>複製代碼

模板include2.html

<p>Get dot of value [{{ . }}]</p>複製代碼

建議本身動手運行一下程序,觀察輸出。

{{ template "include2.html" }}未傳入參數給模板include2.html{{ template "include2.html" . }}將模板include1.html的參數傳給了include2.html

其它元素

管道

管道咱們能夠理解爲數據的流向,在數據流向輸出的每一個階段進行特定的處理。

func pipelineHandler(w http.ResponseWriter, r *http.Request) {
    t := template.Must(template.ParseFiles("pipeline.html"))
    t.Execute(w, rand.Float64())
}

mux.HandleFunc("/pipeline", pipelineHandler)複製代碼

模板文件pipeline.html

<p>{{ . | printf "%.2f" }}</p>複製代碼

該程序實現的功能很是簡單,將傳入的浮點數格式化爲只保留小數點後兩位。|是管道符號,前面的輸出將做爲後面的輸入(若是是函數或方法調用,前面的輸出將做爲最後一個參數)。實際上,{{ . | printf "%.2f" }}的輸出fmt.Sprintf("%.2f", .表示的數據)的返回字符串相同。

函數

Go 模板庫內置了一些基礎的函數,若是要實現更爲複雜的功能,能夠自定義函數。

func formateDate(t time.Time) string {
    return t.Format("2006-01-02")
}

func funcsHandler(w http.ResponseWriter, r *http.Request) {
    funcMap := template.FuncMap{ "fdate": formateDate }
    t := template.Must(template.New("funcs.html").Funcs(funcMap).ParseFiles("funcs.html"))
    t.Execute(w, time.Now())
}

mux.HandleFunc("/funcs", funcsHandler)複製代碼

模板文件funcs.html

<div>Today is {{ . | fdate }}</div>複製代碼

自定義函數能夠接受任意多個參數,可是隻能返回一個值,或者返回一個值和一個錯誤。上面代碼中,咱們必須先經過template.New建立模板,而後調用Funcs設置自定義函數,最後再解析模板文件。由於模板文件中使用了fdate,未設置以前會解析失敗。

上下文感知

上下文感知是html/template庫的一個很是有趣的特性。根據須要替換的文本在文檔中所處的位置,模板在顯示這些內容的時候會對其進行相應的修改。上下文感知的一個常見用途就是對內容進行轉義。若是須要顯示的是 HTML 的內容,那麼進行 HTML 轉義。若是顯示的是 JavaScript 內容,那麼進行 JavaScript 轉義。Go 模板引擎還能識別出內容中的 URL 或 CSS,能夠對它們實施正確的轉義。

func contextAwareHandler(w http.ResponseWriter, r *http.Request) {
    t := template.Must(template.ParseFiles("context-aware.html"))
    t.Execute(w, `He saied: <i>"She's alone?"</i>`)
}

mux.HandleFunc("/contextAware", contextAwareHandler)複製代碼

模板文件context-aware.html

<div>{{ . }}</div>
<div><a href="/{{ . }}">Path</a></div>
<div><a href="/?q={{ . }}">Query</a></div>
<div><a onclick="f('{{ . }}')">JavaScript</a></div>複製代碼

編譯、運行程序,使用 curl 訪問localhost:8080/contextAware,獲得下面的內容:

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Go Web 編程之 模板(二)</title>
</head>
<body>
  <div>He saied: <i>"She's alone?"</i></div>
  <div><a href="/He%20saied:%20%3ci%3e%22She%27s%20alone?%22%3c/i%3e">Path</a></div>
  <div><a href="/?q=He%20saied%3a%20%3ci%3e%22She%27s%20alone%3f%22%3c%2fi%3e">Query</a></div>
  <div><a onclick="f('He saied: \x3ci\x3e\x22She\x27s alone?\x22\x3c\/i\x3e')">JavaScript</a></div>
</body>
</html>複製代碼

咱們依次來看,須要呈現的數據是He saied: "She's alone?"

  • 第一個div中,直接在頁面中顯示,其中 HTML 標籤和單、雙引號都被轉義了;
  • 第二個div中,數據出如今 URL 的路徑中,全部非法的路徑字符都被轉義了,包括空格、尖括號、單雙引號;
  • 第三個div中,數據出如今查詢字符串中,除了 URL 路徑中非法的字符,還有冒號(:)、問號(?)和斜槓也被轉義了;
  • 第四個div中,數據出如今 OnClick 代碼中,單雙引號和斜槓都被轉義了。

這四種轉義方式又有所不一樣,第一種轉義爲 HTML 字符實體,第2、三種轉義爲 URL 轉義字符(%後跟字符編碼的十六進制表示) ,第四種轉義爲 Go 中的十六進制字符表示。

防護 XSS 攻擊

XSS 是一種常見的攻擊形式。在論壇之類的能夠接受用戶輸入的網站,攻擊者能夠內容中添加 標籤。若是網站未對輸入的內容進行處理,其餘用戶瀏覽該頁面時,

點擊 Submit 按鈕:

alert代碼並無執行,爲何?

咱們以前說過 Go 模板有上下文感知的功能,它檢測到在 HTML 頁面中,因此輸入數據會被轉義。查看網頁源碼能夠看到轉義後的結果:

那麼如何才能不轉義呢?html/template提供了HTML類型,Go 模板不會對該類型的變量進行轉義。若是咱們把上面的處理器修改成:

func xssHandler(w http.ResponseWriter, r *http.Request) {
    if r.Method == "POST" {
        t := template.Must(template.ParseFiles("xss-display.html"))
        t.Execute(w, template.HTML(r.FormValue("comment")))
    } else {
        t := template.Must(template.ParseFiles("xss-form.html"))
        t.Execute(w, nil)
    }
}複製代碼

再次實驗,提交上面的內容,在 Chrome 瀏覽器中顯示:

這意味着攻擊者可讓網站上的其餘用戶執行任意可能的攻擊代碼。

嵌套模板

上一篇文章中說過,嵌套模板在定義網頁佈局時很是有用。

type NestInfo struct {
    Name string
    Todos []string
}

func nestHandler(w http.ResponseWriter, r *http.Request) {
    t := template.Must(template.ParseFiles("layout.html"))

    data := NestInfo {
        "dj", []string{"Homework", "Game", "Cleaning"},
    }
    t.ExecuteTemplate(w, "layout", data)
}複製代碼

模板文件layout.html

{{ define "layout" }}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Go Web 編程之 模板(二)</title>
</head>
<body>
  {{ template "header" . }}

  {{ template "content" . }}

  {{ template "footer" . }}
</body>
</html>
{{ end }}
 
{{ define "header" }}
<h1>Hi, {{ .Name }}!</h1>
{{ end }}

{{ define "content" }}
Todo:
<ul>
  {{ range .Todos }}
  <li>{{ . }}</li>
  {{ end }}
</ul>
{{ end }}

{{ define "footer" }}
Copyright © 2020 darjun.
{{ end }}複製代碼

咱們將網頁主體拆爲三個部分:header、content和footer。header 通常顯示導航欄,歡迎信息。content 展現主體內容。footer 中顯示版權,聯繫方式等信息。

解析以後,咱們須要調用ExecuteTemplate執行layout模板,不能直接使用Execute,由於主模板中只是定義了一些模板,沒有具體的內容。

使用塊動做定義默認模板

在上面的layout.html文件中,咱們定義了模板layoutlayout中使用了在本模板文件中定義的三個模板header/content/footer。其實這幾個模板的定義和使用能夠經過block動做合併在一塊兒:

{{ define "layout" }}
<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
  <meta http-equiv="X-UA-Compatible" content="ie=edge">
  <title>Go Web 編程之 模板(二)</title>
</head>
<body>
  {{ block "header" . }}
  <h1>Hi, {{ .Name }}!</h1>
  {{ end }}

  {{ block "content" . }}
  Todo:
  <ul>
    {{ range .Todos }}
    <li>{{ . }}</li>
    {{ end }}
  </ul>
  {{ end }}
    
  {{ block "footer" . }}
  Copyright © 2020 darjun.
  {{ end }}
</body>
</html>複製代碼

block至關於定義模板後當即使用。

{{ block "name" arg }}
...
{{ end }}複製代碼

等價於:

{{ define "name" }}
...
{{ end }}

{{ template "name" arg }}複製代碼

總結

本文詳細介紹了html/template庫,更多地經過案例來介紹,關注如何使用。模板在 Web 開發中是很是重要的部件,須要咱們緊緊掌握。

參考

  1. Go Web 編程
  2. html/template文檔

個人博客

歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~

本文由博客一文多發平臺 OpenWrite 發佈!

相關文章
相關標籤/搜索