06.15 無密碼驗證:服務器

無密碼驗證:服務器

無密碼驗證可以讓你只輸入一個 email 而無需輸入密碼即可登入系統。這是一種比傳統的電子郵件/密碼驗證方式登入更安全的方法。

下面我將為你展示,如何在 Go 中實現一個 HTTP API 去提供這種服務。

流程

  • 用戶輸入他的電子郵件地址。
  • 服務器創建一個臨時的一次性使用的代碼(就像一個臨時密碼一樣)關聯到用戶,然後給用戶郵箱中發送一個“魔法鏈接”。
  • 用戶點擊魔法鏈接。
  • 服務器提取魔法鏈接中的代碼,獲取關聯的用戶,並且使用一個新的 JWT 重定向到客戶端。
  • 在每次有新請求時,客戶端使用 JWT 去驗證用戶。

必需條件

  • 數據庫:我們為這個服務使用了一個叫 CockroachDB 的 SQL 數據庫。它非常像 postgres,但它是用 Go 寫的。
  • SMTP 服務器:我們將使用一個第三方的郵件服務器去發送郵件。開發的時我們使用 mailtrap。Mailtrap 發送所有的郵件到它的收件箱,因此,你在測試時不需要創建多個假郵件帳戶。

從 Go 的主頁 上安裝它,然後使用 go version(1.10.1 atm)命令去檢查它能否正常工作。

從 CockroachDB 的主頁 上下載它,展開它並添加到你的 PATH 變量中。使用 cockroach version(2.0 atm)命令檢查它能否正常工作。

數據庫模式

現在,我們在 GOPATH 目錄下為這個項目創建一個目錄,然後使用 cockroach start 啟動一個新的 CockroachDB 節點:

cockroach start --insecure --host 127.0.0.1

它會輸出一些內容,找到 SQL 地址行,它將顯示像 postgresql://[email protected]:26257?sslmode=disable 這樣的內容。稍後我們將使用它去連接到數據庫。

使用如下的內容去創建一個 schema.sql 文件。

DROP DATABASE IF EXISTS passwordless_demo CASCADE;
CREATE DATABASE IF NOT EXISTS passwordless_demo;
SET DATABASE = passwordless_demo;

CREATE TABLE IF NOT EXISTS users (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
email STRING UNIQUE,
username STRING UNIQUE
);


CREATE TABLE IF NOT EXISTS verification_codes (
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
user_id UUID NOT NULL REFERENCES users ON DELETE CASCADE,
created_at TIMESTAMPTZ NOT NULL DEFAULT now()
);

INSERT INTO users (email, username) VALUES
('[email protected]', 'john_doe');

這個腳本創建了一個名為 passwordless_demo 的數據庫、兩個名為 users 和 verification_codes 的表,以及為了稍後測試而插入的一些假用戶。每個驗證代碼都與用戶關聯並保存創建時間,以用於去檢查驗證代碼是否過期。

在另外的終端中使用 cockroach sql 命令去運行這個腳本:

cat schema.sql | cockroach sql --insecure

環境配置

需要配置兩個環境變量:SMTP_USERNAME 和 SMTP_PASSWORD,你可以從你的 mailtrap 帳戶中獲得它們。將在我們的程序中用到它們。

Go 依賴

我們需要下列的 Go 包:

  • github.com/lib/pq:它是 CockroachDB 使用的 postgres 驅動
  • github.com/matryer/way: 路由器
  • github.com/dgrijalva/jwt-go: JWT 實現
go get -u github.com/lib/pq
go get -u github.com/matryer/way
go get -u github.com/dgrijalva/jwt-go

代碼

初始化函數

創建 main.go 並且通過 init 函數里的環境變量中取得一些配置來啟動。

var config struct {
port int
appURL *url.URL
databaseURL string
jwtKey []byte
smtpAddr string
smtpAuth smtp.Auth
}

func init() {
config.port, _ = strconv.Atoi(env("PORT", "80"))
config.appURL, _ = url.Parse(env("APP_URL", "http://localhost:"+strconv.Itoa(config.port)+"/"))
config.databaseURL = env("DATABASE_URL", "postgresql://[email protected]:26257/passwordless_demo?sslmode=disable")
config.jwtKey = []byte(env("JWT_KEY", "super-duper-secret-key"))
smtpHost := env("SMTP_HOST", "smtp.mailtrap.io")
config.smtpAddr = net.JoinHostPort(smtpHost, env("SMTP_PORT", "25"))
smtpUsername, ok := os.LookupEnv("SMTP_USERNAME")
if !ok {
log.Fatalln("could not find SMTP_USERNAME on environment variables")
}
smtpPassword, ok := os.LookupEnv("SMTP_PASSWORD")
if !ok {
log.Fatalln("could not find SMTP_PASSWORD on environment variables")
}
config.smtpAuth = smtp.PlainAuth("", smtpUsername, smtpPassword, smtpHost)
}

func env(key, fallbackValue string) string {
v, ok := os.LookupEnv(key)
if !ok {
return fallbackValue
}
return v

}

  • appURL 將去構建我們的 “魔法鏈接”。
  • port 將要啟動的 HTTP 服務器。
  • databaseURL 是 CockroachDB 地址,我添加 /passwordless_demo 前面的數據庫地址去表示數據庫名字。
  • jwtKey 用於簽名 JWT。
  • smtpAddr 是 SMTP_HOST + SMTP_PORT 的聯合;我們將使用它去發送郵件。
  • smtpUsername 和 smtpPassword 是兩個必需的變量。
  • smtpAuth 也是用於發送郵件。

env 函數允許我們去獲得環境變量,不存在時返回一個回退值。

主函數

var db *sql.DB

func main() {
var err error
if db, err = sql.Open("postgres", config.databaseURL); err != nil {
log.Fatalf("could not open database connection: %v\\n", err)
}
defer db.Close()
if err = db.Ping(); err != nil {
log.Fatalf("could not ping to database: %v\\n", err)
}

router := way.NewRouter()
router.HandleFunc("POST", "/api/users", jsonRequired(createUser))
router.HandleFunc("POST", "/api/passwordless/start", jsonRequired(passwordlessStart))

router.HandleFunc("GET", "/api/passwordless/verify_redirect", passwordlessVerifyRedirect)
router.Handle("GET", "/api/auth_user", authRequired(getAuthUser))

addr := fmt.Sprintf(":%d", config.port)
log.Printf("starting server at %s


分享到:


相關文章: