Koa 邮箱验证的实现

直接说结论,这功能真的不好用!老老实实买其他验证方式吧。

概述

这个功能主要涉及到两个步骤,一是发邮件,二是验证,流程是:

用户注册 -> 发验证邮件 -> 访问验证 url -> 验证通过

简单来说就是先生成一条验证链接,发到对方的邮箱,让对方打开邮箱去确认,随后访问这条链接完成认证。

发邮件

通过 SMTP 协议去发送邮件,这部分不一定要自己实现,可以使用现成的模块,比如 nodemailer,只需要提供邮件服务器地址、端口等基本信息就行了。

url 的格式可以参考其他网站,生成 url 的关键在于要把身份信息放在 url 内,还得防止用户篡改或伪造。

验证

验证的关键在于得判断 url 是否合法(避免被篡改),其次是判断时效(超时后验证失败),最后是判断 url 是否被重复使用。

实现

发邮件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
// 创建一个 SMTP 客户端
const transport = nodemailer.createTransport(smtpTransport({
host: 'smtp.exmail.qq.com',
secure: true,
secureConnection: true,
port: 465,
auth: {
user: account,
pass: password
}
}))

// 用 Promise 包装发送邮件的操作
const sendMail = (opts) => {
return new Promise((resolve, reject) => {
transport.sendMail(opts, (err, resp) => {
if (err) {
reject(err)
} else {
resolve(resp)
}
})
})
}

module.exports = {
async sendVerify (name, email) {
// 这里需要一个简单的 Map 作为缓存,作用是这样的:
// 在发送验证邮件后,先以用户名 name 作为键,将发送时间储存
// 一是为了防止邮件被频繁发送,可以像其他网站一样设置 60 秒重发一回
// 二是在后续的验证操作中,确保链接是可用的,后续会做说明
const cached = simpleCache.get(name)

// 判断两次发送间隔,不足 60 秒则取消发送
if (cached) {
const createdTime = cached.created
const now = +new Date()
const oneMinute = 60000
if (now - createdTime < oneMinute) {
return {
success: false,
message: '邮件发送间隔过短'
}
}
}
// 生成一段 6 位的验证码
const code = Math.floor(100000 + Math.random() * 900000)

// 记录发送时间
const timestamp = +new Date()

// 对用户名 name,验证码 code 和发送时间 timestamp 通过密钥 key 进行签名,防止这三者被篡改
const md5 = crypto.createHash('md5')
const sign = md5.update(`${name}${code}${timestamp}${key}`).digest('hex')

// 完成对邮件内容的构造
const mailOptions = {
from: account,
to: email,
subject: '账号注册邮箱验证',
text: '',
html: `点击链接完成注册<br />http://localhost:4000/verify?user=${name}&timestamp=${timestamp}&code=${code}&sign=${sign}`
}
try {
await sendMail(mailOptions)

// 发送邮件后将发送时间 timestamp 放入缓存中
simpleCache.set(name, {
created: timestamp
})
return {
success: true,
message: '注册邮件已发送,请注意查收'
}
} catch (err) {
console.log(err)
return {
success: false,
message: err
}
}
}
}

验证

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
module.exports = {
verifyLink (opts) {
const { user, timestamp, code, sign } = opts

// 4 个参数缺一不可
if (!user || !timestamp || !code || !sign) {
return {
success: false,
message: '参数不合法,邮箱验证失败'
}
}

// 从缓存中捞出用户相关值,结果不存在有两种情况
// 要么是重复访问(上一次验证已经通过),要么是用户自己编了一个验证链接
// 这两种都算是验证失败,可以一并处理
const cached = simpleCache.get(user)
if (!cached) {
return {
success: false,
message: '账户验证失败'
}
}

// 根据验证链接的参数,生成一个签名 confirmSign,和传来的签名 sign进行比对
// 两者不一样表示其他参数被篡改,验证失败
const md5 = crypto.createHash('md5')
const confirmSign = md5.update(`${user}${code}${timestamp}${key}`).digest('hex')
if (sign !== confirmSign) {
return {
success: false,
message: '验证链接不合法'
}
}

// 获得当前时间 now,和缓存中的发邮件时间 timestamp 进行比对
// 判断时间间隔是否超过 threshold (这里是 20 分钟),超时则验证失败
const now = +new Date()
// 20 minutes
const threshold = 1000 * 60 * 20
if (now - timestamp > threshold) {
return {
success: false,
message: '验证链接失效,请重新获取'
}
}

// 通过了多种约束条件后表示验证成功,可以将缓存中的相关内容删除
simpleCache.delete(user)
return {
success: true,
message: '邮箱验证通过'
}
}
}

总结

发送验证邮件的功能还是挺常见的,实现起来也不难,但就是不好用,发邮件的操作挺耗时的(看情况可以使用消息队列),还不保证对方能接到,有条件还是用其他验证方式吧,比如短信验证、滑动验证码。