网站访问者的欺诈行为不仅令人厌烦,还会对网站所有者和其他访问者造成直接损害。请考虑以下三种可能表明存在欺诈行为的常见模式,例如账户接管或大量创建虚假账户:
用户从未知设备登录。
与平常相比,现在有更多不同设备可以访问帐户。
单个设备在短时间内创建多个账户。
浏览器指纹识别是一种有效的方法,可以抵御此类潜在恶意行为。浏览器指纹识别是通过应用多种技术来收集的,这些技术可以检测浏览器或运行浏览器的设备的独特方面,例如由于硬件和软件配置不同,浏览器渲染图像、字体或音频的方式会存在细微差异。
本文探讨了上述列表中的第三种模式:恶意行为者试图在同一台设备上短时间内创建多个虚假账户。如果在同一台设备上多次注册,且间隔合理,则应视为有效注册。例如,想想使用同一台计算机注册服务的家庭成员。但是,如果多次注册发生得很快,则可能表明存在欺诈行为。
您将学习如何使用 Go 设置一个 Web 应用,该应用提供注册页面并捕获浏览器指纹以及注册数据。如果注册指纹与不到一分钟前创建的现有帐户的指纹相同,则该应用将拒绝注册新用户。您将通过将 Fingerprint Pro JS 库集成到注册页面并使用Fingerprint Pro以 99.5% 的准确率识别设备来实现这一点。
您最终会得到一个最小的 Go 网络服务器,该服务器提供注册页面并在保存用户或拒绝注册之前确定用户的唯一设备 ID。
在开始之前,您需要设置一些东西。
该项目使用 Fingerprint Pro 服务,该服务比开源版本准确得多。您可以注册 14 天试用版(无需信用卡)。
要获取此项目的 API 密钥,请登录Fingerprint Pro 仪表板并导航至应用程序设置> API 密钥。生成一个秘密 API 密钥(后端代码将使用该密钥)和一个公共 API 密钥(可向前端代码公开该密钥)。
将两个密钥安全地保存在您的系统上。
此时,您有两个选择:要么阅读文章并在阅读过程中复制和粘贴代码,要么克隆项目存储库。
请注意,此处展示的代码最少,用于演示指纹识别。存储库中的代码添加了主页、导航栏和 CSS 样式。
如果您决定根据文章构建代码,您的最终文件布局将如下所示:
.
├── go.mod
├── go.sum
├── internal
│ ├── fingerprint
│ │ └── fingerprint.go
│ └── store
│ └── store.go
├── main.go
├── response.gotpl
├── signup.go
└── signup.gotpl
首先创建一个新目录,cd然后运行以下代码:
go mod init your/unique/module/path/and/name
随着教程的进展,您将添加更多代码。
让我们从设置存储开始,稍后您在实施注册过程时会需要它。
如果用户成功注册,他们的登录凭据、访客 ID 和注册时间戳将存储在数据库中 — 在本例中为 SQLite 数据库。SQLite 是一种嵌入式数据库,无需安装和设置单独的服务器。该软件包modernc.org/sqlite是 SQLite 到 Go 的完整移植,这意味着您甚至不需要 SQLite 库。
将本节代码放入文件internal/store/store.go中。
用户存储的核心是这个结构:
package store
import (
"fmt"
"time"
"database/sql"
_ "modernc.org/sqlite"
)
type Users struct {
db *sql.DB
}
您使用结构体而不是普通数据库对象来向其添加一些方法。结构体还有助于将数据库从应用程序逻辑中抽象出来。
要添加的第一个方法是NewUsers(),它打开用户数据库并创建一个用户表(如果不存在)。如果无法打开数据库或语句create table失败,则会出错。(将其添加到store.go。)
func NewUsers(path string) (*Users, error) {
db, err := sql.Open("sqlite", path)
if err != nil {
return nil, fmt.Errorf("openDB: %w", err)
}
err = createTable(db)
if err != nil {
return nil, fmt.Errorf("openDB: %w", err)
}
return &Users{db: db}, nil
}
要创建表,NewUsers()请调用函数CreateTable,您可以按如下方式定义该函数:
func createTable(db *sql.DB) error {
sqlStmt := `create table if not exists users(
email text not null unique,
signup_fingerprint text,
timestamp text
);
`
_, err := db.Exec(sqlStmt)
if err != nil {
return fmt.Errorf("createTable: %w", err)
}
return nil
}
您可能已经注意到数据库表不包含密码字段。这不是疏忽。代码将尽可能简单,并且不需要密码字段来证明这一概念。如果您愿意,可以添加密码字段,并在将密码存储在数据库之前使用代码对其进行哈希处理、加盐和加胡椒处理。
现在将第二个方法添加AddUser()到store.go中。AddUser()它的作用是将用户添加到数据库。更具体地说,它添加了一条包含电子邮件、访客 ID 以及当前日期和时间的记录。
SQLite 没有datetime数据类型,但您可以使用它来生成时间字符串。在下面的time.Format()函数中,您可以使用它将字符串重新转换为时间值。Check()time.Parse()
func (u *Users) Add(email, visitorId string) (status string, err error) {
timestamp := time.Now().Format(time.RFC3339)
sqlStmt := `insert into users(email, signup_fingerprint, timestamp) values (?, ?, ?)`
_, err = u.db.Exec(sqlStmt, email, visitorId, timestamp)
if err != nil {
if err.Error()[0:17] == "constraint failed" {
return "You already have signed up", nil
}
return "", fmt.Errorf("Users.Add: %w", err)
}
return "Thank you for signing up!", nil
}
接下来,编写一个函数Check(),返回(true, nil)是否在最后一分钟内另一个用户已经在同一设备上注册:
func (u *Users) Check(visitorId string) (recentLogin bool, err error) {
var signupTime string
err = u.db.QueryRow(`select timestamp from users
where signup_fingerprint = ?
order by timestamp desc limit 1`, visitorId).Scan(&signupTime)
if err == sql.ErrNoRows {
// no previous signup for this fingerprint
return false, nil
}
if err != nil {
return false, fmt.Errorf("Users.Check: %w", err)
}
st, err := time.Parse(time.RFC3339, signupTime)
if err != nil {
return false, fmt.Errorf("Users.Check: %w", err)
}
if time.Since(st) < time.Minute {
return true, nil
}
return
}
最后,您需要一个方法让Users客户关闭Users商店:
func (u *Users) Close() error {
return u.db.Close()
}
用户数据库将通过注册页面填充,新用户可以在该页面输入电子邮件。为了打击欺诈行为,您将使用此页面上的 Fingerprint Pro 脚本来生成唯一的设备指纹。但是,该页面不包含实际脚本。它使用JavaScript 代理脚本从以下形式的 URL 加载 Fingerprint Pro 脚本:
https://fpjscdn.net/v3/PUBLIC_API_KEY
您可以使用 Go 模板动态生成此 URL,并附加您的 API 密钥。创建一个名为signup.gotpl的文件(在项目的根目录中),并将以下 HTML 代码添加到其中:
<!DOCTYPE html>
<html>
<head>
<script>
const fpPromise = import("https://fpjscdn.net/v3/{{ . }}").then(
(FingerprintJS) => FingerprintJS.load()
);
fpPromise
.then((fp) => fp.get())
.then((result) => {
console.log(result.requestId, result.visitorId, result.visitorFound);
// Store the request and visitor IDs in the hidden form fields
document.getElementById("requestId").value = result.requestId;
document.getElementById("visitorId").value = result.visitorId;
});
</script>
</head>
</html>
它的工作原理如下:{{ . }}下面 HTML 模板中的构造是一个占位符。您稍后创建的 HTTP 服务器代码将用实际公钥替换此占位符。
一旦加载 Fingerprint Pro 脚本,它就会visitorId根据浏览器、操作系统和硬件的各种属性计算出一个唯一值。出于测试目的,您可以将请求 ID 和访问者 ID 写入浏览器控制台。
这两document.getElementById行代码将请求 ID 和访问者 ID 传递给订阅表单中的隐藏字段。这样,这两个值就可以毫不费力地发送到服务器。
页面主体是一个简单的注册表单。(此代码也包含在signup.gotpl中。)唯一不标准的部分是请求 ID 和访问者 ID 的两个隐藏字段:
<body>
<main>
<form action="/signup" method="POST">
<h1>Sign Up</h1>
<label for="email">Email:</label>
<input type="email" id="email" name="email" autocomplete="email" required>
<input name="visitorId" id="visitorId" value="" hidden>
<input name="requestId" id="requestId" value="" hidden>
<input type="submit" value="Sign Up">
</form>
</main>
</body>
</html>
注册表单将唯一的请求 ID 和访问者 ID 发送到服务器。在这里,我们可以测试访问者 ID 是否已存在于数据库中。但我们还能做更多。Fingerprint Go SDK提供有关特定请求或访问者的其他信息。
举个例子,让我们查询 Fingerprint Pro 服务以获取有关当前请求的更多详细信息。
该包的以下代码fingerprint包含两个函数。
New()创建一个新的 SDK 客户端。
Check()获取请求 ID 并将Botd和Identification属性打印到终端。
将此代码(您将从处理表单的 HTTP 处理程序中调用)添加到internal/fingerprint/fingerprint.go:
package fingerprint
import (
"context"
"encoding/json"
"fmt"
"log"
"os"
"strings"
"github.com/fingerprintjs/fingerprint-pro-server-api-go-sdk/v5/sdk"
)
type Client struct {
API *sdk.APIClient
Cfg *sdk.Configuration
APIKey string
}
func New() *Client {
cfg := sdk.NewConfiguration()
client := sdk.NewAPIClient(cfg)
// Default region is sdk.RegionUS
if strings.ToLower(os.Getenv("FINGERPRINT_REGION")) == "eu" {
cfg.ChangeRegion(sdk.RegionEU)
}
if strings.ToLower(os.Getenv("FINGERPRINT_REGION")) == "ap" {
cfg.ChangeRegion(sdk.RegionAsia)
}
return &Client{
API: client,
Cfg: cfg,
APIKey: os.Getenv("FINGERPRINT_SECRET_KEY"),
}
}
func (c *Client) Validate(requestId, visitorId string) (passed bool, err error) {
// Configure authorization, in our case with API Key
auth := context.WithValue(context.Background(), sdk.ContextAPIKey, sdk.APIKey{
Key: c.APIKey,
})
log.Printf("Checking request %s with API key %s in region %s\n", requestId, c.APIKey, c.Cfg.GetRegion())
response, httpRes, err := c.API.FingerprintApi.GetEvent(auth, requestId)
// See all the data that you can run verifications against
r, _ := json.MarshalIndent(response, "", "\t")
log.Printf("%v\n", string(r))
if err != nil || httpRes.StatusCode != 200 {
return false, fmt.Errorf("FingerprintApi.GetEvent: HTTP %d: %w\n", httpRes.StatusCode, err)
}
// Compare the fingerprints, to detect if the fingerprint received from the browser has been tampered with
if response.Products.Identification.Data.VisitorId != visitorId {
return false, fmt.Errorf("fingerprint mismatch: expected %s, got %s\n", visitorId, response.Products.Identification.Data.VisitorId)
}
return true, nil
}
以下函数创建并注册处理注册表单的 HTTP 处理程序。该表单发送一个 POST 请求,其中包含/signup所有字段数据,包括两个隐藏字段。
将此代码放入根目录中名为signup.go的新文件中:
package main
import (
"bytes"
"<your-module-path>/internal/fingerprint"
"<your-module-path>/internal/store"
"log"
"net/http"
"text/template"
)
func setSignupHandler(users *store.Users, tmplResponse *template.Template) {
// Define and register the handler for the signup form
http.HandleFunc("/signup", func(w http.ResponseWriter, r *http.Request) {
if r.Method != "POST" {
http.Error(w, "Invalid request method", http.StatusMethodNotAllowed)
return
}
email := r.FormValue("email")
visitorId := r.FormValue("visitorId")
requestId := r.FormValue("requestId")
log.Printf("Email: %s, Visitor ID: %s\n", email, visitorId)
// Check if the visitor ID already exists in the database
recentSignup, err := users.Check(visitorId)
if err != nil {
log.Printf("/signup: check visitor ID: %s\n", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
msg := ""
if recentSignup {
msg = "Someone else has signed up from this device in the last minute! To prevent fraudulent mass signups, we restricted the number of signups per device to one signup per minute. Please try again later."
} else {
// Add the user to the database
msg, err = users.Add(email, visitorId)
if err != nil {
log.Printf("/signup: add user: %s\n", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
}
// Get additional client information through the Go SDK
log.Printf("Server-side check for request ID %s\n", requestId)
fp := fingerprint.New()
success, err := fp.Validate(requestId, visitorId)
if err != nil {
log.Printf("/signup: validate fingerprint: %s\n", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
if !success {
msg = "Error verifying the signup attempt. Please try again."
}
// Send the response (either "thank you" or "you already signed up")
w.Header().Add("Location", "/response")
var response bytes.Buffer
err = tmplResponse.ExecuteTemplate(&response, "response", msg)
if err != nil {
log.Printf("/signup: execute template: %s\n", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
_, err = w.Write(response.Bytes())
if err != nil {
log.Printf("/signup: write response: %s\n", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
})
}
表单处理程序会将响应发送到新页面。
创建文件response.gotpl,其中包含以下代码来定义一个简单的响应页面:
<!DOCTYPE html>
<html>
<head></head>
<body>
<nav>
<a href="/signupform">Sign Up</a>
</nav>
<main>{{ . }}</main>
</body>
</html>
最后,您现在将使用设置和运行 HTTP 服务器的代码填充文件main.go。
main.go中的第一部分是包、导入和变量声明。请确保将<your-module-path>导入块替换为go.mod文件中列出的实际模块路径。
package main
import (
"bytes"
_ "embed"
"fmt"
"log"
"net/http"
"os"
"text/template"
"<your-module-path>/internal/fingerprint"
"<your-module-path>/internal/store"
"github.com/joho/godotenv"
)
// embed HTML and CSS files
var (
//go:embed signup.gotpl
signupTpl string
//go:embed response.gotpl
responseTpl string
)
请注意每个变量声明上方的注释。这些注释是go 指令,建议go命令从注释中指定的文件中填充这些变量。编译后,变量将保存signup.gotplsignupTpl的内容,并包含response.gotpl的内容。responseTpl
该函数run()完成初始化和运行 HTTP 服务器的所有工作。将此函数添加到main.go:
func run() error {
// (1) Load environment variables
err := godotenv.Load()
if err != nil {
return fmt.Errorf("load .env: %w", err)
}
// (2) Connect to the database
users, err := store.NewUsers(os.Getenv("FINGERPRINT_DATABASE_PATH"))
if err != nil {
return fmt.Errorf("open database: %w", err)
}
defer users.Close()
// (3) signup.gotpl is a Go template. Map the environment variable FINGERPRINT_PUBLIC_KEY to the "{{ . }}" placeholder in the template.
tmplSignup := template.Must(template.New("signup").Parse(signupTpl))
var signup bytes.Buffer
// (4) Insert the public API key into the template
err = tmplSignup.Execute(&signup, os.Getenv("FINGERPRINT_PUBLIC_KEY"))
if err != nil {
return fmt.Errorf("execute template: %w", err)
}
// (3 cont'd) parse response.gotpl
tmplResponse := template.Must(template.New("response").Parse(responseTpl))
// (5) Define and register handlers for the signup page, and signup request, and response page
http.HandleFunc("/signupform", func(w http.ResponseWriter, r *http.Request) {
_, err := w.Write(signup.Bytes())
if err != nil {
log.Printf("serve signup form: %s\n", err)
http.Error(w, "Internal server error", http.StatusInternalServerError)
return
}
})
setSignupHandler(users, tmplResponse)
// Start the server
port := os.Getenv("FINGERPRINT_LOCAL_PORT")
log.Printf("Sign up at http://localhost:%s/signupform\n", port)
err = http.ListenAndServe("127.0.0.1:"+port, nil)
if err != nil {
return fmt.Errorf("ListenAndServe: %w\n", err)
}
return nil
}
这段代码片段很长,但主要步骤已在注释中编号,并按编号进行解释:
将.env文件中的环境变量加载到进程环境中,在那里os.Getenv()可以透明地读取它们。
打开用户存储,又名数据库。
解析注册和响应模板。
执行注册模板。这会将公共 API 密钥嵌入到生成的 HTML 页面中。
定义注册页面和表单请求的 HTTP 处理程序。由于我们之前已经讨论过表单处理程序,因此这里不再赘述。
启动 HTTP 服务器。http.ListenAndServe()除非发生严重错误,否则将永远阻止。
最后,将函数添加main()到main.go中。此函数没有太多要做的事情。它调用run()并记录可能返回的任何错误run()。(旁注:log.Fatalf()调用os.Exit(1)以向调用 shell 发出错误情况信号。)
func main() {
err := run()
if err != nil {
log.Fatalf("%s\n", err)
}
}
您的项目现在可以运行了。
确保禁用系统上的所有广告拦截器。或者,您可以通过向 Go 应用程序添加代理服务器来绕过广告拦截器,该代理服务器将客户端请求转发到 Fingerprint。这样,注册页面就可以通过 Go 应用程序请求所有脚本。广告拦截器认为此流量是安全的。
演示应用程序通过环境变量进行配置,可以通过环境文件设置。在启动服务器之前,在运行服务器的目录中创建一个.env文件,并将以下代码添加到其中:
FINGERPRINT_SECRET_KEY=insert-secret-api-key-here
FINGERPRINT_PUBLIC_KEY=insert-public-api-key-here
FINGERPRINT_DATABASE_PATH=users.sqlite
FINGERPRINT_REGION=
FINGERPRINT_LOCAL_PORT=8787
请记住替换这些值:
您在仪表板中生成的指纹服务器的秘密 API 密钥
指纹服务器的公共 API 密钥
服务器地区:如果未设置,则默认为“美国”。如果您在注册时选择的地区是欧洲,则将其设置为“eu”,如果您选择的地区是亚太地区,则将其设置为“ap”。
或者,您可以选择不同的端口和不同的用户数据库路径。
确保您已运行以下命令来更新依赖项。
go mod tidy
该tidy命令会自动将所有缺失的依赖项下载到本地模块缓存中。
您现在可以在项目的根目录中调用以下命令。
go run .
您应该会看到服务器公布注册 URL。打开https://localhost:8787/signupform(或您设置的任何端口号)即可查看注册页面。
以下屏幕截图是使用存储库中的代码制作的,其中包括导航和 CSS 样式。
输入任意电子邮件地址,然后单击“注册”。
来自服务器端 API 调用的 JSON 输出包含大量可用于验证请求的信息。
在本文中,您了解了如何使用 Go 和 Fingerprint Pro 设置防欺诈注册流程。如您所见,只需添加几行脚本和 HTML 即可将 Fingerprint Pro 脚本嵌入注册表单,以生成浏览器/设备组合的唯一指纹。只要使用相同的浏览器和设备,这些指纹在各个会话中都有效。
指纹识别不仅需要很少的额外努力;它也不会给合法用户带来任何阻碍,但却能显著提高您的服务和用户的安全性。