在上一篇文章中,咱們介紹了 Go 模板庫text/template
。text/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.html
:golang
<!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 是一種常見的攻擊形式。在論壇之類的能夠接受用戶輸入的網站,攻擊者能夠內容中添加
標籤。若是網站未對輸入的內容進行處理,其餘用戶瀏覽該頁面時,:
點擊 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
文件中,咱們定義了模板layout
,layout
中使用了在本模板文件中定義的三個模板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 開發中是很是重要的部件,須要咱們緊緊掌握。
歡迎關注個人微信公衆號【GoUpUp】,共同窗習,一塊兒進步~
本文由博客一文多發平臺 OpenWrite 發佈!