记录一个注册登录并有邮箱验证功能的功能,这个功能会用到很多插件,我把它拆分成几个步骤,可能需要看完全部步骤思路才会比较清晰,而且一些第三方插件的实现流程比较复杂,晦涩难懂,超人鸭弄了很多遍也只是停留在会用的阶段,但放心的是,在node中实现完整的登录注册功能,基本都是用这些插件,所以学会怎么用也是不错的。
首先看看前端的表单,一些校验直接在前端就可以完成:
接下来就是实现具体功能的步骤:
- 安装软件
1.MongoDB: 安装完设置环境变量,就可以通过git bash命令: mongod启动服务
2.redis: 安装完后可以通过git bash命令:redis-server启动
这两款软件最好装一下图形化界面,看着比较直观,redis是为了用来存储用户注册时邮箱验证码、过期时间、登录信息等,也可以直接用MongoDB来存储
- 邮箱设置
用的是qq邮箱,没有原因,方便。
打开qq邮箱点设置,点账户,开启前面两项后保存授权码:
这个授权码是唯一的,记得保密好
- 安装插件:
1.mongoose // 操作MongoDB
2.koa-generic-session
3.koa-redis // 配合上个插件操作session
4.koa-passport // 实现登录注册流程
5.passport-local // 本地策略,koa-passport就是对passport-local进行封装
6.nodemailer // 发送邮件的插件
一些基本的koa插件上面就没有列出来,像koa-router,koa-bodyparser
-
文件结构,这是我个人的文件结构
- 编写配置文件:config.js
export default{
// 数据库配置,users为数据库名称
dbs:'mongodb://127.0.0.1:27017/users',
// redis配置
redis:{
get host() {
return '127.0.0.1'
},
get port() {
return 6379
}
},
// 邮箱服务配置
smtp: {
get host() { // 服务主机地址,为腾讯邮箱
return 'smtp.qq.com'
},
get user() { // 发送者的邮箱
return '1274085986@qq.com'
},
get pass() { // 发送者的邮箱凭证,记得保密
return 'xxxxxxxxxxxxxxxxxx'
},
get code() { // 随机生成四位数的验证码
return () => {
return Math.random().toString(16).slice(2,6).toUpperCase()
}
},
get expire() { // 过期时间,为调用时间加一分钟
return () => {
return new Date().getTime() + 60 * 1000
}
}
},
}
- 编写MongoDB数据表结构,到dbs的models文件夹下面的users.js:
// 固定写法
import mongoose from 'mongoose'
const Schema = mongoose.Schema
const UserSchema = new Schema({
username:{
type:String,
unique:true,
require: true
},
password:{
type:String,
require: true
},
email:{
type:String,
require: true
}
})
export default mongoose.model('User',UserSchema)
- 编写passport权限认证,possport.js:
import passport from 'koa-passport'
import LocalStrategy from 'passport-local' // 本地策略
import UserModel from '../../dbs/models/users'
// 在使用 passport.authenticate('策略', ...) 的时候,会执行策略
passport.use(new LocalStrategy(async function(username, password, done) {
let where = {
username
}
let result = await UserModel.findOne(where)
if(result!=null) {
if(result.password === password) {
return done(null, result)
} else {
return done(null,false,'密码错误')
}
} else {
return done(null, false, '用户不存在')
}
}))
// 序列化ctx.login()触发
passport.serializeUser(function(user,done){
done(null,user)
})
// 反序列化(请求时,session中存在"passport":{"user":"1"}触发)
passport.deserializeUser(function(user,done) {
return done(null,user)
})
export default passport
整个流程我觉得最难懂的就是这块,passport-local插件是一个本地策略,koa-passport是对passport的一个封装,其中具体的流程逻辑还需要自己去研究。
- 在index.js中导入:
// 这只是上面说到的插件,一个koa项目可能要用到其他更多的插件
import mongoose from 'mongoose'
import bodyParser from 'koa-bodyparser'
import session from 'koa-generic-session'
import Redis from 'koa-redis'
import dbConfig from './dbs/config'
import passport from './interface/utils/passport'
app.keys = ['mt', 'keyskes'] // 设置cookie的签名
app.proxy = true
// key设置cookie的key,store设置外部存储
app.use(session({key:'mt',prefix: 'mt:uid', store: new Redis()}))
app.use(bodyParser({
extendTypes: ['json','from','text']
}))
// 链接数据库
mongoose.connect(dbConfig.dbs,{
useNewUrlParser: true,
useUnifiedTopology: true
})
// 引入权限认证
/**
* app.use(passport.initialize()) 会在请求周期ctx对象挂载以下方法与属性
* ctx.state.user 认证用户
* ctx.login(user) 登录用户(序列化用户)
* ctx.isAuthenticated() 判断是否认证
*/
app.use(passport.initialize())
app.use(passport.session())
- 编写发送验证码的接口,到interface下的users.js:
import Router from 'koa-router'
import Redis from 'koa-redis'
import nodeMailer from 'nodemailer'
import User from '../dbs/models/users'
import Passport from './utils/passport'
import Email from '../dbs/config' // 这里对配置的操作都是操作邮箱的
let router = new Router({ // 定义路由前缀
prefix:'/users'
})
let Store = new Redis(Email.redis).client // 初始化redis
router.post('/verify', async (ctx, next) => {
let username = ctx.request.body.username
/*
*下面一发送就会已用户名为表名,以code验证码、expire该验证码的过期时间、email邮箱为字段存储
*saveExpire 取得这个用户名发送的的验证码的过期时间
*这一步是为了防止用户点了发送验证码然后在一分钟内再次点击
*/
const saveExpire = await Store.hget(`nodemail:${username}`,'expire')
// 防止用户频繁请求
if(saveExpire && new Date().getTime() - saveExpire < 0) {
ctx.body = {
code: -1,
msg: '验证请求过于频繁,1分钟1次'
}
return false
}
// 设置邮箱配置
let transporter = nodeMailer.createTransport({
host:Email.smtp.host, // 设置邮箱服务的主机,smtp.qq.com
port:587, // 对应的端口号
secure: false,
auth: { // 用户信息
user: Email.smtp.user, // 发送者的邮箱
pass: Email.smtp.pass // 发送者邮箱的凭证,此处用qq邮箱
}
})
// 一些配置信息
let ko = {
code: Email.smtp.code(), // 从配置文件中取
expire: Email.smtp.expire(), // 从配置文件中取过期时间
email: ctx.request.body.email,
user: ctx.request.body.username
}
// 设置收件人信息
let mailOptions = {
from: `"认证邮件"<${Email.smtp.user}>`, // 设置发件人名称
to: ko.email, // 发给谁
subject:'超人鸭', // 主题
html:`您的邀请码是${ko.code}` // html模板
}
// 发送邮件
await transporter.sendMail(mailOptions,(err,info) => {
if(err) {
return console.log('error')
} else { // 发送成功
// 存储到redis
Store.hmset(`nodemail:${ko.user}`, 'code', ko.code, 'expire', ko.expire, 'email', ko.email)
}
})
ctx.body = {
code: 0,
msg: '验证码已发送,可能会有延时,有效期1分钟'
}
})
- 注册接口
router.post('/signup', async (ctx) => {
const {
username,
password,
email,
code
} = ctx.request.body;
if(code) {
// 从redis中取出该用户对应的code和code过期时间
const saveCode = await Store.hget(`nodemail:${username}`,'code')
const saveExpire = await Store.hget(`nodemail:${username}`,'expire')
if(code != saveCode) {
ctx.body = {
code: -1,
msg: '请填写正确的验证码'
}
return
} else { // 验证码相同
if(new Date().getTime() - saveExpire > 0) {
ctx.body = {
code: -1,
msg: '验证码已过期'
}
return
} else { // 不会过期
// 从数据库查询数据
let user = await User.find({
username
})
if(user.length) {
ctx.body = {
code: -1,
msg: '已被注册'
}
return
}
let nuser = await User.create({ // 往数据库添加记录
username,
password,
email
})
if(nuser) { // 注册成功
ctx.body = {
code: 0,
msg: '注册成功'
}
return
} else {
ctx.body = {
code: -1,
msg: '注册失败'
}
return
}
}
}
} else {
ctx.body = {
code: -1,
msg: '请填写验证码'
}
}
})
- 登录接口,就会用到上面说的passport来管理登录状态
router.post('/signin', async(ctx,next) => {
/**
* 使用koa-passport插件登录成功后可以设置ctx的状态,并且可以把用户信息存储在session中
* 需要安装session中间件
* 使用到本地策略,在passport.js已编写好逻辑
*/
return Passport.authenticate('local',function(err,user,info,status) {
if(err) {
ctx.body = {
code: -1,
msg: err
}
} else {
if(user) {
ctx.body = {
code: 0,
msg: '登录成功',
user
}
// passport封装的api,用来管理session
return ctx.login(user)
} else {
ctx.body = {
code: 1,
msg: info
}
}
}
})(ctx, next)
})
- 退出登录和获取用户信息接口:
router.get('/exit', async(ctx, next) => {
await ctx.logout() // passport封装的api,用来管理session
// ctx.isAuthenticated() 判断是否认证
if(!ctx.isAuthenticated()) { // 说明退出成功
ctx.body = {
code: 0
}
} else {
ctx.body = {
code: -1
}
}
})
router.get('/getUser', async(ctx) => {
if(ctx.isAuthenticated()) {
const {username,email} = ctx.session.passport.user
ctx.body = {
user:username,
email
}
} else {
ctx.body = {
user: '',
email: ''
}
}
})
- 在index.js中引入该路由
记得在users.js接口中导出路由:
export default router
在index.js中:
import users from './interface/users'
app.use(users.routes()).use(users.allowedMethods())
node的代码到这里就结束了,前端用的是vue,请求用的是axios,如果出现跨域,可以在前端代理,或者在koa中装一个koa2-cors插件,解决跨域的,用法也很简单,这里就不写了,还有前端的请求也很简单。
到这里整个流程就走完了,包括注册登录退出登录等,最复杂的passport超人鸭现在也说不太清,暂且处于能用的阶段,如果哪位朋友刚好学到这部分,请说出你的理解,欢迎指教哦。
作者微信:Promise_fulfilled