在 Go 语言中基于中间件避免 CSRF 攻击


工作原理

在 Go Web 编程中,我们可以基于第三方 gorilla/csrf 包避免 CSRF 攻击,和 Laravel 框架一样,这也是一个基于 HTTP 中间件避免 CSRF 攻击的解决方案,其中包含的中间件名称是 csrf.Protect

注:CSRF 全名是 Cross-Site Request Forgery,即跨站请求伪造,这是一种通过伪装授权用户的请求来攻击授信网站的恶意漏洞。

我们来看看 csrf.Protect 是如何工作的:当我们在路由器上应用这个中间件后,当请求到来时,会通过 csrf.Token 函数生成一个令牌(Token)以便发送给 HTTP 响应(可以是 HTML 表单也可以是 JSON 响应),对于 HTML 表单视图,我们可以向视图模板传递一个注入令牌值的辅助函数 csrf.TemplateField,这个辅助函数可以在客户端将令牌值发送给服务端,服务端通过验证客户端发送的令牌值和服务端保存的令牌值是否一致来验证请求来自授信客户端,从而达到避免 CSRF 攻击的目的。

gorilla/csrf 被设计为兼容当前流行的开源组件和框架,比如 Gorilla 工具集、net/http 包、GojiGinEcho 等。

使用示例

接下来,学院君来简单演示下如何在实际项目中使用 gorilla/csrf 提供的 csrf.Protect 中间件。

HTML 表单

首先是 HTML 表单,csrf.Protect 中间件使用起来非常简单,你只需要在启动 Web 服务器时将其应用到路由器上即可,然后在渲染表单视图时传递带有令牌信息的 csrf.TemplateField 函数:

package main

import (
    "github.com/gorilla/csrf"
    "github.com/gorilla/mux"
    "html/template"
    "net/http"
)

func main() {
    // 初始化路由器
    r := mux.NewRouter()
    // 注册表单页面路由(GET)
    r.HandleFunc("/signup", ShowSignupForm)
    // 提交注册表单路由(POST)
    // 如果请求字段不包含有效的 CSRF 令牌,则返回 403 响应
    r.HandleFunc("/signup/post", SubmitSignupForm).Methods("POST")

    // 应用 csrf.Protect 中间件到路由器 r
    // 该函数第一个参数是 32 位长的认证密钥(任意字符做 MD5 元算即可),用于加密 CSRF 令牌
    // 本地开发基于 HTTP 协议,所以第二个参数通过 csrf.Secure(false) 进行标识
    http.ListenAndServe(":8000",
        csrf.Protect([]byte("251e79cd5d1a994c51fd316f7040f13d"), csrf.Secure(false))(r))
}

// 注册表单页面处理器
func ShowSignupForm(w http.ResponseWriter, r *http.Request) {
    // 传递注入 CSRF 令牌的 csrf.TemplateField 函数到注册页面
    t := template.Must(template.ParseFiles("signup.html"))
    t.Execute(w, map[string]interface{}{
        csrf.TemplateTag: csrf.TemplateField(r),
    })
    // 我们还可以通过 csrf.Token(r) 直接获取令牌并将其设置到请求头:w.Header.Set("X-CSRF-Token", token)
    // 这在发送 JSON 响应到客户端或者前端 JavaScript 框架时很有用
}

// 提交注册表单处理器
func SubmitSignupForm(w http.ResponseWriter, r *http.Request) {
    // 暂不做任何处理
}

然后我们在在同级目录下新建 signup.html,通过 {{ .csrfField }} 渲染隐藏的令牌字段:

<!DOCTYPE html>
<html lang="en">
<head>
	<meta charset="UTF-8">
	<title>Signup</title>
</head>
<body>
    <form action="/signup/post" method="post">
    	{{ .csrfField }}
    	<input type="text" name="name">
    	<input type="password" name="password">
    	<hr/>
    	<button id="submit">Submit</button>
    </form>
</body>
</html>

启动 Web 服务器,在浏览器中访问 http://127.0.0.1:8000/signup,就可以通过源代码查看到隐藏的包含 CSRF 令牌的输入框了:

-w774

如果我们试图删除这个输入框或者变更 CSRF 令牌的值,提交表单,就会返回 403 响应了:

-w843

错误信息是 CSRF 令牌值无效。

JavaScript 应用

csrf.Protect 中间件还适用于前后端分离的应用,此时后端数据以接口方式提供给前端,不再有视图模板的渲染,设置中间件的方式不变,但是传递 CSRF 令牌给客户端的方式要调整:

package main

import (
    "encoding/json"
    "github.com/gorilla/csrf"
    "github.com/gorilla/mux"
    "net/http"
)

type User struct {
    Id string `json:"id"`
    Name string `json:"name"`
    Website string `json:"website"`
}

func main() {
    r := mux.NewRouter()

    // 设置路由前缀
    api := r.PathPrefix("/api").Subrouter()
    // 在子路由上应用 csrf.Protect 中间件
    api.Use(csrf.Protect([]byte("251e79cd5d1a994c51fd316f7040f13d")))
    // 如果客户端 JavaScript 应用部署在其他域名,需要通过 csrf.TrustedOrigins 设置服务端信任的客户端域名列表
    // api.Use(csrf.Protect([]byte("251e79cd5d1a994c51fd316f7040f13d"), csrf.TrustedOrigins([]string{"cli.domain.com"})))
    
    // 获取用户信息接口路由
    api.HandleFunc("/user/{id}", GetUser).Methods("GET")

    http.ListenAndServe(":8000", r)
}

func GetUser(w http.ResponseWriter, r *http.Request) {
    // 从路由参数中读取用户 id,再从数据库查询对应用户信息
    // 这里我们简单模拟下用户数据进行测试即可
    id := r.FormValue("id")
    user := User{Id: id, Name: "学院君", Website: "https://xueyuannjun.com"}

    // 获取令牌值并将其设置到响应头
    // 这样一来,咱们的 JSON 客户端或者 JavaScript 框架就可以读取响应头获取 CSRF 令牌值
    // 然后在后续发送 POST 请求时就可以通过 X-CSRF-Token 请求头中带上这个 CSRF 令牌
    w.Header().Set("X-CSRF-Token", csrf.Token(r))
    b, err := json.Marshal(user)
    if err != nil {
        http.Error(w, err.Error(), 500)
        return
    }
    w.Write(b)
}

我们启动 Web 服务器,请求 /api/user/1 接口,就可以获取如下响应信息:

-w1167

这样一来,我们就可以在客户端读取响应头中的 CSRF 令牌信息了,以 Axios 库为例,客户端可以这样发送包含 CSRF 令牌的 POST 请求:

// 你可以从响应头中读取 CSRF 令牌,也可以将其存储到单页面应用的某个全局标签里
// 然后从这个标签中读取 CSRF 令牌值,比如这里就是这么做的:
let csrfToken = document.getElementsByName("gorilla.csrf.Token")[0].value

// 初始化 Axios 请求头,包含域名、超时和 CSRF 令牌信息
const instance = axios.create({
  baseURL: "https://domain.com/api/",
  timeout: 1000,
  headers: { "X-CSRF-Token": csrfToken }
})

// 这样一来,后续发送的所有 HTTP 请求都会包含 CSRF 令牌
try {
  let resp = await instance.post(endpoint, formData)
  // 处理响应
} catch (err) {
  // 处理异常
}

关于 Go Web 编程中的 CSRF 攻击防护我们就简单介绍到这里,更多细节,请参考 gorilla/csrf 项目官方文档


点赞 取消点赞 收藏 取消收藏

<< 上一篇: 基于 Go 协程实现图片马赛克应用(下):并发重构

>> 下一篇: 基于 Go 语言开发在线论坛(一):整体设计与数据模型