基于nodejs的web服务器,实现用户登陆注册等接口功能

486 阅读3分钟

环境准备

1、安装配置node环境
2、安装MySQL数据库
3、安装redis
4、全局安装nodemon

相关文章

基于nodejs使用express 创建web服务器
基于nodejs的web服务器,连接使用redis
基于nodejs的web服务器,连接使用数据库

Github代码仓库地址

一、项目初始化及前期准备

1、初始化项目

npm init -y

2、创建目录结构

根据模块化思想,我创建的目录结构如下,仅供参考,你们可根据个人习惯进行创建 image.png
目录结构解析:所有代码放在src之下
index.js:项目入口文件
common/config.js:该文件中存放项目所有配置
common/database.js:该文件存放数据库相关代码
common/middleware.js:该文件存放项目中所有中间件函数
common/redis.js:该文件存放redis相关代码
common/utils.js:该文件存放项目所有公共方法
router/users.js:该文件存放登陆注册等接口的路由
routerHandler/user.j: 该文件存放路由处理函数
.gitignore:该文件为忽略git提交配置,其中node_moudles和package-lock.json需要忽略

3、安装所需所有第三方包

1、配置package.json文件

{
  "name": "login_register_server",
  "version": "1.0.0",
  "description": "",
  "main": "src/index.js",
  "scripts": {
    "serve": "nodemon src/index.js"
  },
  "keywords": [],
  "author": "",
  "license": "ISC",
  "dependencies": {
    "bcryptjs": "^2.4.3",
    "cors": "^2.8.5",
    "express": "^4.18.2",
    "express-jwt": "^8.4.1",
    "joi": "^17.9.2",
    "jsonwebtoken": "^9.0.0",
    "mysql": "^2.18.1",
    "redis": "^4.6.7",
    "svg-captcha": "^1.4.0",
    "uuid": "^9.0.0"
  }
}

package.json解析:
"scripts": {"serve": "nodemon src/index.js"}:配置运行代码方式,直接使用npm run serve即可执行nodemon src/index.js启动项目
bcryptjs:用于加密密码和比较明文密码和加密后密码的一致性
cors:解决跨域请求失败的问题
express:主要框架
express-jwt:用于验证token是否过期
joi:方便验证表单数据
jsonwebtoken:生成token
mysql:连接操作数据库
redis:连接操作redis
svg-captcha:生成验证码
uuid:生成唯一用户id

4、创建数据库及用户表

本文案例数据库和用户表名称均为users image.png

二、项目具体实现

1、实现公共模块

1、config.js

module.exports = {
    secretKey: 'my_users_secret_key',
    expiresIn: 5184000, // token过期时间,单位秒(token过期时间设置为60天)
    loginDuration: 86400 // token失效时间,单位秒(根据业务逻辑设置token失效时间)
}

2、database.js,连接数据库

// 导入数据库
const mysql = require('mysql')
// 创建数据库实例并连接数据库
const database = mysql.createPool({
    host: '127.0.0.1',
    user: '*****',
    password: '******',
    database: 'users'
})
// 导出数据库
module.exports = database

3、middleware.js,实现全局和局部中间件

const Joi = require('joi')
const { secretKey } = require('../common/config')
const jwt = require('jsonwebtoken')
const { redisGet } = require('../common/redis')

// 全局中间件
// 封装res.send方法
exports.resSendMiddleware = (req, res, next) => {
    res.commonResSend = (status, message, data) => {
        res.send({
            status,
            message,
            data: data ? data : null
        })
    }
    next()
}
// token解析方法
exports.analyzeToken = async (req, res, next) => {
    const headerToken = req.headers['authorization']
    if (headerToken) {
        const token = headerToken.replace('Bearer ', '')
        const userInfo = jwt.verify(token, secretKey)
        // 判断是否是有效未超时的token
        const isEfficientToken = await redisGet(userInfo.userId)
        // token有效,将用户信息存入req
        if (isEfficientToken) {
            req.userInfo = userInfo
        } else {
            return res.commonResSend(401, '身份认证失败')
        }
    }
    next()
}
// 全局错误中间件
exports.handleError = (err, req, res, next) => {
    if (err.name === 'UnauthorizedError') {
        // JWT 认证失败
        res.commonResSend(401, '身份认证失败')
    }
}

// 路由中间件
// 验证注册表单
exports.verifyRegister = (req, res, next) => {
    const userInfo = req.body
    const schema = Joi.object({
        userName: Joi.string().alphanum().min(3).max(32).required(),
        password: Joi.string().alphanum().min(6).max(32).required(),
        nickName: Joi.string().min(1).max(64),
        email: Joi.string().email(),
        phoneNumber: Joi.string().min(6).max(11).pattern(/^\d+$/),
        avater: Joi.string().dataUri()
    })
    const result = schema.validate(userInfo)
    if (result.error) {
        return res.commonResSend(1, result.error.details)
    } else {
        next()
    }
}

4、redis.js,连接redis,并封装常用方法

const redis = require('redis')
// 创建redis客户端实例
const redisClient = redis.createClient()
// 连接redis
redisClient.connect()

// Redis 客户端监听连接事件
redisClient.on('connect', () => {
    console.log('Connected to Redis server')
})

// Redis 客户端监听错误事件
redisClient.on('error', (error) => {
    console.error('Redis error:', error)
})

// 封装常用redis方法
// 设置值
const redisSet = (key, value) => {
    return new Promise((resolve, reject) => {
        if (key === undefined || !key) {
            reject("key不能为空或undefined")
            return
        } else if (typeof key !== 'string') {
            reject("key必须为字符串")
            return
        } else if (value === undefined || !value || typeof value === 'boolean') {
            reject("value不能为空、undefined或布尔值")
            return
        } else if (typeof value === "object") {
            value = JSON.stringify(value)
        }
        try {
            redisClient.set(key, value).then((replay) => {
                if (replay === 'OK') {
                    resolve(true)
                } else {
                    resolve(false)
                }
            }).catch((error) => {
                reject(error)
            })
        } catch (error) {
            reject(error)
        }
    })
}
// 设置过期时间
const redisSetTimeout = (key, timeout) => {
    return new Promise((resolve, reject) => {
        if (key === undefined || !key) {
            reject("key不能为空或undefined")
            return
        } else if (typeof key !== 'string') {
            reject("key必须为字符串")
            return
        } else if (key === undefined || !key) {
            reject("timeout不能为空、undefined或0")
            return
        } else if (typeof timeout !== 'number' || timeout <= 0) {
            reject("timeout必须为大于零的数字")
            return
        }
        try {
            redisClient.expire(key, Math.ceil(timeout)).then((replay) => {
                resolve(replay)
            }).catch((error) => {
                reject(error)
            })
        } catch (error) {
            reject(error)
        }
    })
}
// 获取值
const redisGet = (key) => {
    return new Promise((resolve, reject) => {
        if (key === undefined || !key) {
            reject("key不能为空或undefined")
            return
        } else if (typeof key !== 'string') {
            reject("key必须为字符串")
            return
        }
        try {
            redisClient.get(key).then((value) => {
                // 利用 JSON.parse(string) 报错返回正确的数据格式
                try {
                    resolve(JSON.parse(value))
                } catch (error) {
                    resolve(value)
                }
            }).catch((error) => {
                reject(error)
            })
        } catch (error) {
            reject(error)
        }
    })
}

// 获取过期时间
const redisGetTimeout = (key) => {
    return new Promise((resolve, reject) => {
        if (key === undefined || !key) {
            reject("key不能为空或undefined")
            return
        } else if (typeof key !== 'string') {
            reject("key必须为字符串")
            return
        }
        try {
            redisClient.ttl(key).then((timeout) => {
                resolve(timeout)
            }).catch((error) => {
                reject(error)
            })
        } catch (error) {
            reject(error)
        }
    })
}
// 删除值
const redisDelete = (key) => {
    return new Promise((resolve, reject) => {
        if (key === undefined || !key) {
            reject("key不能为空或undefined")
            return
        } else if (typeof key !== 'string') {
            reject("key必须为字符串")
            return
        }
        try {
            redisClient.del(key).then((replay) => {
                if (replay) {
                    resolve(true)
                } else {
                    resolve(false)
                }
            }).catch((error) => {
                reject(error)
            })
        } catch (error) {
            reject(error)
        }
    })
}

// 设置指定数据库,传入索引值,0-15
const redisSetDb = (index) => {
    return new Promise((resolve, reject) => {
        if (index < 0 || index > 15) {
            reject("请输入正确数据库索引值0-15")
            return
        }
        try {
            redisClient.select(index).then((replay) => {
                resolve(true)
            }).catch((error) => {
                reject(error)
            })
        } catch (error) {
            reject(error)
        }
    })
}

// 恢复默认数据库
const redisResetDb = () => {
    return new Promise((resolve, reject) => {
        try {
            redisClient.select(0).then((replay) => {
                resolve(true)
            }).catch((error) => {
                reject(error)
            })
        } catch (error) {
            reject(error)
        }
    })
}

module.exports = {
    redisSet,
    redisSetTimeout,
    redisGet,
    redisGetTimeout,
    redisDelete,
    redisSetDb,
    redisResetDb
}

5、utils.js

const svgCaptcha = require('svg-captcha')
// 创建验证码
exports.createCaptcha = () => {
    const svgCaptchaOption = {
        size: 4,
        noise: 3,
        color: true,
        background: '#666666'
    }
    return svgCaptcha.create(svgCaptchaOption)
}

2、创建路由处理函数模块,并导出

routerHandler/user.js

const database = require('../common/database')
const bcrypt = require('bcryptjs')
const { v4: uuid } = require('uuid')
const jwt = require('jsonwebtoken')
const { secretKey, expiresIn, loginDuration } = require('../common/config')
const {
    redisSet,
    redisSetTimeout,
    redisGet,
    redisDelete
} = require('../common/redis')
const { createCaptcha } = require('../common/utils')

/**
 * 注册接口处理函数
 * @param {
 * userName: 必传 string,
 * password: 必传 string,
 * nickName: string,
 * email: string,
 * phoneNumber: string,
 * avater: base64
 * }
 */
exports.register = (req, res) => {
    const userInfo = req.body
    const selectSql = 'select * from users where userName=?'
    database.query(selectSql, [userInfo.userName], (err, result) => {
        if (err) { return res.commonResSend(1, err.message) }
        if (result.length > 0) {
            res.commonResSend(1, '用户名已存在')
        } else {
            // 使用bcrypt加密密码
            userInfo.password = bcrypt.hashSync(userInfo.password, 10)
            // 使用uuid生成唯一id
            userInfo.userId = uuid().replace(/-/g, '')
            // 插入数据
            const insertSql = 'insert into users set ?'
            database.query(insertSql, userInfo, (err, result) => {
                if (err) { return res.commonResSend(1, err.message) }
                res.commonResSend(0, '注册成功')
            })
        }
    })
}
/**
 * 登录接口处理函数
 * @param {
 * userName: 必传 string,
 * password: 必传 string
 * }
 */
exports.login = (req, res) => {
    const { userName, password, captcha } = req.body
    if (!userName || !password) {
        res.commonResSend(1, '账号密码不能为空')
    } else {
        const selectSql = 'select * from users where userName=?'
        database.query(selectSql, [userName], async (err, result) => {
            if (err) { return res.commonResSend(1, err.message) }
            if (result.length > 0) {
                const userInfo = result[0]
                let failNumber = 0
                // 获取当前用户是否输错过密码
                const redisFailNumber = await redisGet(userName + 'failNumber')
                if (redisFailNumber) failNumber = redisFailNumber
                const redisCaptcha = await redisGet(userName + 'captcha')
                // 密码输入错误超过五次,一小时内不允许再次尝试
                if (failNumber === 5) {
                    return res.commonResSend(1, '密码输入错误超过五次,请一小时后再试')
                } else if (failNumber >= 1 && failNumber < 5 // 验证码错误时生成新的验证码并返回
                    && (!captcha || !redisCaptcha || captcha.toLowerCase() !== redisCaptcha.toLowerCase())) {
                    const { text, data } = createCaptcha()
                    await redisSet(userName + 'captcha', text)
                    await redisSetTimeout(userName + 'captcha', 60)
                    return res.commonResSend(0, '验证码错误', {
                        captcha: data,
                        failNumber
                    })
                }
                // 对比密码
                const compareResult = bcrypt.compareSync(password, userInfo.password)
                // 账号密码正确,登陆成功
                if (compareResult) {
                    const existToken = await redisGet(userInfo.userId)
                    let token = ''
                    // 存在有效token,返回该token
                    if (existToken) {
                        token = existToken
                    } else {
                        // 生成token并存入redis
                        const payload = { ...userInfo, password: '', avater: '' }
                        token = jwt.sign(payload, secretKey, { expiresIn: expiresIn + 's' })
                        await redisSet(userInfo.userId, token)
                        await redisSetTimeout(userInfo.userId, loginDuration)
                    }
                    // 登陆成功删除redis中的密码输入错误次数和验证码
                    await redisDelete(userName + 'failNumber')
                    await redisDelete(userName + 'captcha')
                    //TODO apiFox会自动加上Bearer,接口测试使用该代码,与前端对接联调加上Bearer
                    // res.commonResSend(0, '登录成功', { token: 'Bearer ' + token })
                    res.commonResSend(0, '登录成功', { token })
                } else {
                    // 密码输入错误,生成验证码返回客户端,redis储存验证码和失败次数,多次失败次数累加
                    const { text, data } = createCaptcha()
                    await redisSet(userName + 'failNumber', failNumber + 1)
                    await redisSetTimeout(userName + 'failNumber', 3600)
                    await redisSet(userName + 'captcha', text)
                    await redisSetTimeout(userName + 'captcha', 60)
                    res.commonResSend(0, '账号密码错误', {
                        captcha: data,
                        failNumber: failNumber + 1
                    })
                }
            } else {
                res.commonResSend(1, '该用户不存在')
            }
        })
    }
}
/**
 * 刷新验证码处理函数
 * @param {
 * userName: 必传 string
 * }
 */
exports.refreshCaptcha = async (req, res) => {
    const { userName } = req.body
    const { text, data } = createCaptcha()
    await redisSet(userName + 'captcha', text)
    await redisSetTimeout(userName + 'captcha', 60)
    res.commonResSend(0, '验证码刷新成功', {
        captcha: data
    })
}
/**
 * 修改密码处理函数
 * @param {
 * userName: 必传 string,
 * password: 必传 string,
 * newPassword: 必传 string,
 * confirmPassword: 必传 string
 * }
 */
exports.revisePassword = (req, res) => {
    const { userName, password, newPassword, confirmPassword } = req.body
    if (!userName || !password) {
        res.commonResSend(1, '账号密码不能为空')
    } else if (!newPassword) {
        res.commonResSend(1, '新密码不能为空')
    } else if (!confirmPassword) {
        res.commonResSend(1, '确认新密码不能为空')
    } else if (newPassword !== confirmPassword) {
        res.commonResSend(1, '新密码和确认新密码不一致')
    } else {
        const selectSql = 'select * from users where userName=?'
        database.query(selectSql, [userName], async (err, result) => {
            if (err) { return res.commonResSend(1, err.message) }
            if (result.length > 0) {
                const userInfo = result[0]
                const compareResult = bcrypt.compareSync(password, userInfo.password)
                if (compareResult) {
                    newDatabasePassword = bcrypt.hashSync(newPassword, 10)
                    const sql = `update users set password=? where userId='${userInfo.userId}'`
                    database.query(sql, [newDatabasePassword], (err, result) => {
                        if (err) { return res.commonResSend(1, err.message) }
                        res.commonResSend(0, '修改密码成功')
                    })
                } else {
                    res.commonResSend(1, '账号密码错误')
                }
            } else {
                res.commonResSend(1, '该用户不存在')
            }
        })
    }
}
// 退出登录
exports.logout = async (req, res) => {
    const userInfo = req.userInfo
    await redisDelete(userInfo.userId)
    res.commonResSend(0, '登出成功')
}

3、创建路由模块,并导入处理函数

router/user.js

const express = require('express')
const router = express.Router()
const {
    register,
    login,
    revisePassword,
    logout,
    refreshCaptcha
} = require('../routerHandler/user')
const { verifyRegister } = require('../common/middleware')


router.post('/register', verifyRegister, register)

router.post('/login', login)

router.post('/refreshCaptcha', refreshCaptcha)

router.post('/revisePassword', revisePassword)

router.post('api/logout', logout)

module.exports = router

4、导入所需模块,启动服务

服务运行在3000端口

const express = require('express')
const cors = require('cors')
const userRouter = require('./router/user')
const app = express()
const { expressjwt } = require('express-jwt')
const {
    resSendMiddleware,
    handleError,
    analyzeToken
} = require('./common/middleware')
const { secretKey } = require('./common/config')

// 处理跨域中间件
app.use(cors())
// 处理解析表单数据中间件
app.use(express.urlencoded({ extended: false }))
// 封装res.send()方法中间件
app.use(resSendMiddleware)
// 验证token中间件
app.use(expressjwt({
    secret: secretKey,
    algorithms: ["HS256"]
}).unless({
    path: ['/login', '/register', '/revisePassword', '/refreshCaptcha']
}))
// 解析token中间件
app.use(analyzeToken)
// 注册路由
app.use(userRouter)
// 注册全局错误中间件
app.use(handleError)
// 启动服务
app.listen(3000, () => {
    console.log('Server is running on port 3000')
})

三、接口测试

接口测试可使用apifox进行测试,下面是登录接口测试结果案例 image.png