前端进阶之旅前端进阶之旅
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket

# 登录注册功能实现

30 秒速记

  • 分层:路由 → controller(收参数、写 session、决定响应)→ service(校验、业务规则)→ DAO(只管 SQL)
  • 密码用 bcrypt / scrypt / Argon2 慢哈希存,绝不明文、绝不 MD5,登录时比哈希不比明文
  • 查重靠数据库唯一索引兜底,代码里先查再插有并发窗口
  • 登录失败统一提示「用户名或密码错误」,不暴露账号是否存在,再加限流
  • 登录成功换一个新的 session id 再写用户信息,防会话固定

登录注册说白了就三件事:注册时把账号安全地存进库,登录时校验身份,校验通过后建立登录态。 这套代码按路由、controller、service、DAO 分层,好处是 SQL 只出现在 DAO,校验规则只在 service,controller 只管请求和响应。安全上我最先看三点:密码必须用 bcrypt 这类慢哈希存,SQL 必须参数化,登录失败的提示不能让人猜出账号是否存在。这三条任何一条没做,都不能上线。

理解「登录注册功能实现」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

登录注册看起来是 CRUD,其实是整个系统里安全要求最高的一块。把这节的代码串起来,一次注册的流程是:

  1. 路由 POST /user/signUp.json 交给 controller.signUp
  2. service.validatorSignUp 校验用户名、邮箱、密码格式和两次密码是否一致
  3. service.getExistOne 查用户名或邮箱是否已存在
  4. service.create 调 DAO 写入 user_info 表

登录则是查用户、比密码、写 session、跳转。

把它做成能上线的版本,有几处必须补上:

const bcrypt = require('bcrypt')

// 注册:只存哈希
const hash = await bcrypt.hash(formData.password, 10)
await userModel.create({ name: formData.userName, email: formData.email, password: hash })

// 登录:按用户名查出哈希,再比对
const user = await userModel.getOneByUserName(formData.userName)
const ok = user && await bcrypt.compare(formData.password, user.password)
if (!ok) {
  ctx.body = { success: false, message: '用户名或密码错误' } // 不区分账号不存在还是密码错
  return
}
@前端进阶之旅: 代码已经复制到剪贴板

数据库层给 name 和 email 都加唯一索引,插入时捕获 ER_DUP_ENTRY 转成「已存在」的提示,这样并发注册也不会出现重复账号。SQL 一律用 ? 占位符传参,不拼字符串。登录成功后先轮换 session id 再写用户信息,退出时整个销毁 session。这几条做全了,才算一个合格的登录注册。

面试官追问

追问 1密码存成 MD5 加个固定 salt,不是也看不出明文吗?
参考回答

MD5 太快了,一张显卡每秒能算上百亿次,加固定 salt 也只是换一张彩虹表。要用 bcrypt 这种故意算得慢、每个用户随机 salt 的算法,bcrypt.hash(pwd, 10) 存,bcrypt.compare(pwd, hash) 验。

追问 2注册提示「该邮箱已注册」,安全说这能被拿来枚举账号,怎么处理?
参考回答

登录接口必须统一成「用户名或密码错误」。注册接口很难完全不提示,常见做法是改成「已向该邮箱发送确认邮件」,邮箱已注册就发一封提醒邮件,接口返回始终一样,再配合验证码和按 IP 限流。

追问 3有人一分钟内对同一个账号试了几百次密码,接口怎么防?
参考回答

按账号和 IP 两个维度限流,比如同一账号连续失败 5 次锁 15 分钟或者要求验证码。只按 IP 限挡不住分布式撞库,只按账号限又可能被人恶意锁号,两个维度要一起用。

追问 4登录注册这几个接口,你会怎么写测试?
参考回答

用 supertest 直接打 Koa 应用,把注册成功、用户名重复、格式非法、密码错误、登录后访问受保护页这几条路径都跑一遍,断言状态码、返回结构和 Set-Cookie。比单测 service 函数更能发现中间件顺序、session 没写进去这类问题。

# 登录注册功能实现

# 用户模型dao操作

30 秒速记

  • DAO 只做一件事:把数据库操作包成方法,上层拿到的是一条记录或 null
  • 用模板字符串把用户输入拼进 SQL = SQL 注入,一律用 ? 占位符
  • 登录不要 WHERE password = 明文,按用户名查出哈希,交给 service 用 bcrypt.compare 比
  • 按用户名查资料的方法一定要带 WHERE name = ?,漏了就永远返回表里第一个人
  • 查重除了代码查,name / email 要建唯一索引;公开接口别 SELECT *

用户 DAO 就是把建用户、查重、登录查询、查资料这几个数据库操作封装成方法,上层不用关心 SQL 怎么写。 写 DAO 最要紧的是参数化,用户输入只能通过 ? 占位符传给驱动,不能拼进字符串,不然一个带引号的用户名就能改掉整条 SQL 的意思。另外按用户名查询时,WHERE 条件一定要真的带上,字段也按需列出来,别图省事 SELECT * 把密码哈希一路带到上层。

/**
   * 数据库创建用户
   * @param  {object} model 用户数据模型
   * @return {object}       mysql执行结果
   */
  async create ( model ) {
    let result = await dbUtils.insertData( 'user_info', model )
    return result
  },

  /**
   * 查找一个存在用户的数据
   * @param  {obejct} options 查找条件参数
   * @return {object|null}        查找结果
   */
  async getExistOne(options ) {
    let _sql = `
    SELECT * from user_info
      where email="${options.email}" or name="${options.name}"
      limit 1`
    let result = await dbUtils.query( _sql )
    if ( Array.isArray(result) && result.length > 0 ) {
      result = result[0]
    } else {
      result = null
    }
    return result
  },

  /**
   * 根据用户名和密码查找用户
   * @param  {object} options 用户名密码对象
   * @return {object|null}         查找结果
   */
  async getOneByUserNameAndPassword( options ) {
    let _sql = `
    SELECT * from user_info
      where password="${options.password}" and name="${options.name}"
      limit 1`
    let result = await dbUtils.query( _sql )
    if ( Array.isArray(result) && result.length > 0 ) {
      result = result[0]
    } else {
      result = null
    }
    return result
  },

  /**
   * 根据用户名查找用户信息
   * @param  {string} userName 用户账号名称
   * @return {object|null}     查找结果
   */
  async getUserInfoByUserName( userName ) {

    let result = await dbUtils.select(
      'user_info',
      ['id', 'email', 'name', 'detail_info', 'create_time', 'modified_time', 'modified_time' ])
    if ( Array.isArray(result) && result.length > 0 ) {
      result = result[0]
    } else {
      result = null
    }
    return result
  },

@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1用户名填 " or "1"="1,getExistOne 会怎样?
参考回答

拼出来是 where email="" or name="" or "1"="1",条件恒真,直接返回表里第一条记录,注册永远提示已存在;登录那条拼接更危险,能绕过密码。改成

基础篇
进阶篇
高频篇
精选篇
手写篇
面经篇
AI 篇
原理篇
每日一题
小程序题库
知识卡片
  • 场景篇按分类整理的大前端场景考点
  • 历年面经按年份追踪真实考点
  • 算法题库NEW在线编码即时判题
  • 专项自测100 题快速查漏
  • 前端基础
    • HTTP从报文一路讲到 HTTPS
    • 浏览器渲染、事件循环、进程
    • 计算机基础Linux、网络、操作系统
  • 进阶专项
    • 设计模式23 种模式怎么用
    • 前端系统进阶学习大型项目工程化
    • 前端综合文章长期沉淀的实践文
  • 工程与工具
    • Node学习指南从环境搭建到服务端
    • NPM工作流script、依赖与发布
    • Docker容器化部署上手
    • Canvas图形与动画实战
  • 路线与导图
    • 思维导图知识点全景图
    • 学习路线按图索骥不跑偏
  • 动态
    • 公众号动态公众号历史文章
    • 博客动态站长的技术博客
    • 开发者导航常用工具与文档站
  • AI 助手随时提问,即时解析
  • AI 模拟面试模拟真实面试 + 报告
  • AI 知识地图串起全站知识点
  • AI 定制路线按你的简历现排
AI 热点
旧版
  • 基础篇

    • 环境搭建
    • node部署
    • 基础应用
  • 内置模块

    • 1.0 本地路径处理 path
    • 2.0 文件系统操作 fs
    • 3.1 基础调试 console
    • 3.2 本地调试远程服务器上的Node代码
    • 4.1 网络服务 http
    • 4.2 网络服务 http res
    • 4.3 网络服务 http req
    • 4.4 网络服务 http server
    • 4.5 网络服务 http client
    • 4.6 网络服务 https
    • 4.7 网络TCP net
    • 4.8 网络UDP dgram
    • 4.9 域名解析 dns
    • 5.0 网络地址解析 url
    • 5.1 URL查询字符串 querystring
    • 6.1 流操作 stream
    • 6.2 逐行读取 readline
    • 7.1 进程相关 process
    • 7.2 子进程 child
    • 8.1 二进制数据 buffer
    • 8.2 二进制解码 string_decoder
    • 9.1 事件机制 events
    • 9.2 实用工具模块 util
    • 9.3 数据加密 crypto
    • 9.4 MD5入门介绍及crypto模块的应用
    • 9.4 资源压缩 zlib
    • 9.5 集群 cluster
    • 9.6 v8
  • 进阶篇

    • 5分钟入门非对称加密用法
    • cluster
    • cookie_parser深入
    • crypto模块之理论篇
    • express+cookie_parser签名机制深入剖析
    • express+session实现简易身份认证
    • https
    • log4js入门实例
    • node8_napi
    • 使用 async 控制并发
    • 使用 eventproxy 控制并发
    • 使用 superagent 与 cheerio 完成简单爬虫
    • 基于express+muter的文件上传
    • 将图片转成datauri嵌入到html
    • 常用中间件 body_parser 实现解析
    • 日志模块morgan
    • 服务端字符编解码&乱码处理 charset_enc_dec
    • 测试用例mocha,should,istanbul
    • 调试日志打印debug模块
  • Express

    • 1.1 Express概览篇
    • 1.2 Express
  • Koa2

    • 1.0 koa2概览篇
    • 1.1 快速开始
    • 1.2 async await使用
    • 1.3 koa2简析结构
    • 1.4 koa中间件开发与使用
    • 10.1 单元测试
    • 11.1 开发debug
    • 12.1 快速启动
    • 12.2 框架设计
    • 12.3 分层操作
    • 12.4 数据库设计
    • 12.5 路由设计
    • 12.6 webpack4环境搭建
    • 12.7 使用react
    • 12.8 登录注册功能实现
    • 12.9 session登录态判断处理
    • 13.1 import export使用
    • 2.1 原生koa2实现路由
    • 2.2 koa router中间
    • 3.1 GET请求数据获取
    • 3.2 POST请求数据获取
    • 3.3 koa bodyparser中间件
    • 4.1 原生koa2实现静态资源服务器
    • 4.2 koa static中间件
    • 5.1 koa2使用cookie
    • 5.2 koa2实现session
    • 6.1 koa2加载模板引擎
    • 6.2 ejs模板引擎
    • 7.1 busboy模块
    • 7.2 上传文件简单实现
    • 7.3 异步上传图片实现
    • 8.1 mysql模块
    • 8.2 async await封装使用mysql
    • 8.3 项目建表初始化
    • 9.1 原生koa2实现JSONP
    • 9.2 koa jsonp中间件
  • 其他

    • 操作数据库
    • Session 与 Token
    • Cookie、Session、Token、JWT
    • Socket
完整面试题地址:
作者:程序员poetry
扫码关注作者公众号:「前端进阶之旅」 每天分享技术干货
前端进阶之旅公众号二维码

登录注册功能实现|Nodejs专题系列

# 登录注册功能实现

30 秒速记

  • 分层:路由 → controller(收参数、写 session、决定响应)→ service(校验、业务规则)→ DAO(只管 SQL)
  • 密码用 bcrypt / scrypt / Argon2 慢哈希存,绝不明文、绝不 MD5,登录时比哈希不比明文
  • 查重靠数据库唯一索引兜底,代码里先查再插有并发窗口
  • 登录失败统一提示「用户名或密码错误」,不暴露账号是否存在,再加限流
  • 登录成功换一个新的 session id 再写用户信息,防会话固定

登录注册说白了就三件事:注册时把账号安全地存进库,登录时校验身份,校验通过后建立登录态。 这套代码按路由、controller、service、DAO 分层,好处是 SQL 只出现在 DAO,校验规则只在 service,controller 只管请求和响应。安全上我最先看三点:密码必须用 bcrypt 这类慢哈希存,SQL 必须参数化,登录失败的提示不能让人猜出账号是否存在。这三条任何一条没做,都不能上线。

理解「登录注册功能实现」时,要先明确输入、运行阶段和可观察输出。这里的交互实验会分别执行正常路径与失败路径,并把 Node.js 的关键状态变化和同屏代码对应起来;不能只记命令名称。

工程中还要验证退出码、资源清理和异常输入。属于 module 场景的配置应保持最小可复现,并在开发环境与 CI 中使用相同命令复跑,避免把平台差异或隐式默认值带到线上。

登录注册看起来是 CRUD,其实是整个系统里安全要求最高的一块。把这节的代码串起来,一次注册的流程是:

  1. 路由 POST /user/signUp.json 交给 controller.signUp
  2. service.validatorSignUp 校验用户名、邮箱、密码格式和两次密码是否一致
  3. service.getExistOne 查用户名或邮箱是否已存在
  4. service.create 调 DAO 写入 user_info 表

登录则是查用户、比密码、写 session、跳转。

把它做成能上线的版本,有几处必须补上:

const bcrypt = require('bcrypt')

// 注册:只存哈希
const hash = await bcrypt.hash(formData.password, 10)
await userModel.create({ name: formData.userName, email: formData.email, password: hash })

// 登录:按用户名查出哈希,再比对
const user = await userModel.getOneByUserName(formData.userName)
const ok = user && await bcrypt.compare(formData.password, user.password)
if (!ok) {
  ctx.body = { success: false, message: '用户名或密码错误' } // 不区分账号不存在还是密码错
  return
}
@前端进阶之旅: 代码已经复制到剪贴板

数据库层给 name 和 email 都加唯一索引,插入时捕获 ER_DUP_ENTRY 转成「已存在」的提示,这样并发注册也不会出现重复账号。SQL 一律用 ? 占位符传参,不拼字符串。登录成功后先轮换 session id 再写用户信息,退出时整个销毁 session。这几条做全了,才算一个合格的登录注册。

面试官追问

追问 1密码存成 MD5 加个固定 salt,不是也看不出明文吗?
参考回答

MD5 太快了,一张显卡每秒能算上百亿次,加固定 salt 也只是换一张彩虹表。要用 bcrypt 这种故意算得慢、每个用户随机 salt 的算法,bcrypt.hash(pwd, 10) 存,bcrypt.compare(pwd, hash) 验。

追问 2注册提示「该邮箱已注册」,安全说这能被拿来枚举账号,怎么处理?
参考回答

登录接口必须统一成「用户名或密码错误」。注册接口很难完全不提示,常见做法是改成「已向该邮箱发送确认邮件」,邮箱已注册就发一封提醒邮件,接口返回始终一样,再配合验证码和按 IP 限流。

追问 3有人一分钟内对同一个账号试了几百次密码,接口怎么防?
参考回答

按账号和 IP 两个维度限流,比如同一账号连续失败 5 次锁 15 分钟或者要求验证码。只按 IP 限挡不住分布式撞库,只按账号限又可能被人恶意锁号,两个维度要一起用。

追问 4登录注册这几个接口,你会怎么写测试?
参考回答

用 supertest 直接打 Koa 应用,把注册成功、用户名重复、格式非法、密码错误、登录后访问受保护页这几条路径都跑一遍,断言状态码、返回结构和 Set-Cookie。比单测 service 函数更能发现中间件顺序、session 没写进去这类问题。

# 登录注册功能实现

# 用户模型dao操作

30 秒速记

  • DAO 只做一件事:把数据库操作包成方法,上层拿到的是一条记录或 null
  • 用模板字符串把用户输入拼进 SQL = SQL 注入,一律用 ? 占位符
  • 登录不要 WHERE password = 明文,按用户名查出哈希,交给 service 用 bcrypt.compare 比
  • 按用户名查资料的方法一定要带 WHERE name = ?,漏了就永远返回表里第一个人
  • 查重除了代码查,name / email 要建唯一索引;公开接口别 SELECT *

用户 DAO 就是把建用户、查重、登录查询、查资料这几个数据库操作封装成方法,上层不用关心 SQL 怎么写。 写 DAO 最要紧的是参数化,用户输入只能通过 ? 占位符传给驱动,不能拼进字符串,不然一个带引号的用户名就能改掉整条 SQL 的意思。另外按用户名查询时,WHERE 条件一定要真的带上,字段也按需列出来,别图省事 SELECT * 把密码哈希一路带到上层。

/**
   * 数据库创建用户
   * @param  {object} model 用户数据模型
   * @return {object}       mysql执行结果
   */
  async create ( model ) {
    let result = await dbUtils.insertData( 'user_info', model )
    return result
  },

  /**
   * 查找一个存在用户的数据
   * @param  {obejct} options 查找条件参数
   * @return {object|null}        查找结果
   */
  async getExistOne(options ) {
    let _sql = `
    SELECT * from user_info
      where email="${options.email}" or name="${options.name}"
      limit 1`
    let result = await dbUtils.query( _sql )
    if ( Array.isArray(result) && result.length > 0 ) {
      result = result[0]
    } else {
      result = null
    }
    return result
  },

  /**
   * 根据用户名和密码查找用户
   * @param  {object} options 用户名密码对象
   * @return {object|null}         查找结果
   */
  async getOneByUserNameAndPassword( options ) {
    let _sql = `
    SELECT * from user_info
      where password="${options.password}" and name="${options.name}"
      limit 1`
    let result = await dbUtils.query( _sql )
    if ( Array.isArray(result) && result.length > 0 ) {
      result = result[0]
    } else {
      result = null
    }
    return result
  },

  /**
   * 根据用户名查找用户信息
   * @param  {string} userName 用户账号名称
   * @return {object|null}     查找结果
   */
  async getUserInfoByUserName( userName ) {

    let result = await dbUtils.select(
      'user_info',
      ['id', 'email', 'name', 'detail_info', 'create_time', 'modified_time', 'modified_time' ])
    if ( Array.isArray(result) && result.length > 0 ) {
      result = result[0]
    } else {
      result = null
    }
    return result
  },

@前端进阶之旅: 代码已经复制到剪贴板

面试官追问

追问 1用户名填 " or "1"="1,getExistOne 会怎样?
参考回答

拼出来是 where email="" or name="" or "1"="1",条件恒真,直接返回表里第一条记录,注册永远提示已存在;登录那条拼接更危险,能绕过密码。改成 query('SELECT * FROM user_info WHERE email = ? OR name = ? LIMIT 1', [email, name])。

追问 2资料页不管查谁,显示的都是同一个人,DAO 里先看哪?
参考回答

看 getUserInfoByUserName 有没有把 userName 传进查询条件。只传了表名和字段列表,没有 WHERE name = ?,查出来是全表,再取 result[0] 就永远是第一行。补上条件后加一个两个用户互查的测试。

追问 3代码里已经先查重了,为什么还要给 email 建唯一索引?
参考回答

先查再插不是原子操作,两个请求同时查都没有,然后都插进去了。唯一索引是数据库层面的保证,插入冲突时 MySQL 报 ER_DUP_ENTRY,捕获后转成「邮箱已注册」就行,预查只是为了给个友好提示。

追问 4公开主页只显示昵称和简介,DAO 继续 SELECT * 再让上层挑字段可以吗?
参考回答

我会拦。SELECT * 会把密码哈希这类敏感字段一路带到上层,哪天有人直接 ctx.body = user 就泄露了。公开接口单独写一个只查必要字段的方法,敏感列根本不出 DAO。

# 业务层操作

30 秒速记

  • service 夹在 controller 和 DAO 中间:做校验、组织业务规则、把表字段转成业务字段(detail_info → detailInfo)
  • 正则校验必须加首尾锚点:/^[a-z0-9_-]{6,16}$/,不加只能证明「里面有一段合法字符」
  • 登录改成按用户名查 + bcrypt.compare,明文密码不进 SQL
  • 用户不存在就返回 null,别用 || {} 拼出一个字段全是 undefined 的假对象
  • 校验结果返回错误码,文案交给展示层,方便多端和多语言

业务层负责把 controller 送进来的表单数据变成业务动作:校验、查重、登录比对,再把数据库字段整理成前端要的格式。 这样 controller 不依赖表结构,换数据库或改字段名只动 DAO 和 service。校验这块最常见的坑是正则没加 ^ 和 $,abc123!!! 这种用户名照样能过;另一个坑是查不到用户时硬拼一个空对象返回,前端看不出是真没有还是数据为空,我一般直接返回 null 让上层转成 404。

← 12.7 使用react12.9 session登录态判断处理 →

fe