无密码验证可以让你只输入一个 email 而无需输入密码即可登入系统. 这是一种比传统的电子邮件 / 密码验证方式登入更安全的方法.
下面我将为你展示, 如何在 Go https://golang.org/ 中实现一个 HTTP API 去提供这种服务.
流程
用户输入他的电子邮件地址.
服务器创建一个临时的一次性使用的代码 (就像一个临时密码一样) 关联到用户, 然后给用户邮箱中发送一个 "魔法链接".
用户点击魔法链接.
服务器提取魔法链接中的代码, 获取关联的用户, 并且使用一个新的 JWT 重定向到客户端.
在每次有新请求时, 客户端使用 JWT 去验证用户.
必需条件
数据库: 我们为这个服务使用了一个叫 CockroachDB https://www.cockroachlabs.com/ 的 SQL 数据库. 它非常像 postgres, 但它是用 Go 写的.
SMTP 服务器: 我们将使用一个第三方的邮件服务器去发送邮件. 开发的时我们使用 https://mailtrap.io/ .Mailtrap 发送所有的邮件到它的收件箱, 因此, 你在测试时不需要创建多个假邮件帐户.
从 Go 的主页 https://golang.org/dl/ 上安装它, 然后使用 go version(1.10.1 atm)命令去检查它能否正常工作.
从 CockroachDB 的主页 https://www.cockroachlabs.com/docs/stable/install-cockroachdb.html 上下载它, 展开它并添加到你的 PATH 变量中. 使用 cockroach version(2.0 atm)命令检查它能否正常工作.
数据库模式
现在, 我们在 GOPATH 目录下为这个项目创建一个目录, 然后使用 cockroach start 启动一个新的 CockroachDB 节点:
cockroach start --insecure --host 127.0.0.1
它会输出一些内容, 找到 SQL 地址行, 它将显示像
postgresql://root@127.0.0.1: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
- ('john@passwordless.local', 'john_doe');
这个脚本创建了一个名为 passwordless_demo 的数据库, 两个名为 users 和 verification_codes 的表, 以及为了稍后测试而插入的一些假用户. 每个验证代码都与用户关联并保存创建时间, 以用于去检查验证代码是否过期.
在另外的终端中使用 cockroach sql 命令去运行这个脚本:
cat schema.sql | cockroach sql --insecure
环境配置
需要配置两个环境变量: SMTP_USERNAME 和 SMTP_PASSWORD, 你可以从你的 mailtrap 帐户中获得它们. 将在我们的程序中用到它们.
Go 依赖
我们需要下列的 Go 包:
https://github.com/lib/pq : 它是 CockroachDB 使用的 postgres 驱动
https://github.com/matryer/way : 路由器
https://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://root@127.0.0.1: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
来源: http://developer.51cto.com/art/201806/576386.htm