《校园生活平台从 0 到 1 的搭建》第三篇:后端的微信授权登录

✅ 一、实现目标

  1. 实现小程序微信授权登录接口,前端传入 code,后端使用 appid + secret + code 实际调用微信服务端换取 openid
  2. 后端根据 openid 判断用户是否已注册:
    • 若已存在,则为登录;
    • 若不存在,则创建新用户(即注册)。
  1. 登录或注册成功后:
    • 使用 uuid 生成唯一 token
    • 将其与用户ID及过期时间(30天)存入 tokens 表。
  1. 前端登录成功后将 token 本地存储(如:Storage),后续请求统一在请求头中携带该 token
  2. 后端使用统一的 auth 中间件校验 token
    • 过期或无效则返回 401 状态;
    • 有效则在 req.user 中注入用户信息。
  1. 提供 /api/user/profile 接口,校验通过后返回当前用户信息(如头像、昵称),供前端展示。

✅ 二、实现步骤概览

1. 配置相关依赖和模块

  • 使用 axios 调用微信 code2session 接口;

  • 使用 uuid 生成 token;

  • 使用 dayjs 手动设置一个 未来的过期时间

    npm install axios uuid dayjs

2. 更新目录结构

bash 复制代码
platform_serve/          
├── app.js                     # 项目入口
├── .env                       # 环境变量配置
├── config/
│   ├── config.js              # 读取.env中的环境变量
│   └── db.js                  # 数据库连接池
├── controllers/
│   ├── authController.js      # 登录/注册控制器
│   └── userController.js      # 用户信息控制器
├── middleware/
│   ├── auth.js                # Token校验中间件
│   └── errorHandler.js        # 错误处理
├── routes/
│   ├── auth.js                # 登录注册路由
│   └── user.js                # 用户信息路由
├── utils/
│   └── response.js            # 接口返回格式封装
└── package.json

3. 更新配置

.env 功能:集中管理小程序凭证、数据库连接、服务端口等运行环境参数,避免硬编码。

ini 复制代码
PORT=8081
WX_APPID=xxxxxx(填写自己的)
WX_SECRET=xxxxxx(填写自己的)
DB_HOST=localhost
DB_USER=root
DB_PASSWORD=yourpassword
DB_NAME=platform

config/config.js 功能 :封装 .env 变量读取逻辑,统一导出供项目其他模块使用。

yaml 复制代码
require('dotenv').config();
module.exports = {
  wx: {
    appid: process.env.WX_APPID,
    secret: process.env.WX_SECRET
  },
  db: {
    host: process.env.DB_HOST,
    user: process.env.DB_USER,
    password: process.env.DB_PASSWORD,
    database: process.env.DB_NAME
  },
  PORT: process.env.PORT || 3000
};

config/db.js 功能: 负责创建并测试 MySQL 连接池

ini 复制代码
const mysql = require('mysql');
const config = require('./config');

const pool = mysql.createPool({
  host: config.db.host,
  user: config.db.user,
  password: config.db.password,
  database: config.db.database,
  port: 3306,
});

pool.getConnection((err, connection) => {
  if (err) {
    console.error('❌ 数据库连接失败:', err.message);
  } else {
    console.log('✅ 数据库连接成功');
    connection.release();
  }
});

module.exports = pool;

4. 数据库设计

创建两张表:

1)用户表 users: 用于存储小程序用户的基本信息(如 openid、昵称、头像、角色、状态等)。
sql 复制代码
CREATE TABLE IF NOT EXISTS users (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '用户ID',
  openid VARCHAR(64) UNIQUE NOT NULL COMMENT '微信openid',
  unionid VARCHAR(64) UNIQUE DEFAULT NULL COMMENT '微信unionid',
  nickname VARCHAR(50) NOT NULL DEFAULT '' COMMENT '微信昵称',
  avatar_url VARCHAR(255) NOT NULL DEFAULT '' COMMENT '微信头像链接',
  phone VARCHAR(20) UNIQUE DEFAULT NULL COMMENT '手机号',
  password VARCHAR(255) DEFAULT NULL COMMENT '管理员登录密码',
  role ENUM('user', 'admin') NOT NULL DEFAULT 'user' COMMENT '用户角色',
  status TINYINT(1) NOT NULL DEFAULT 1 COMMENT '账号状态',
  balance DECIMAL(10,2) DEFAULT 0.00 COMMENT '账户余额',
  credit_score INT DEFAULT 100 COMMENT '信用分',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
2)token 表 tokens: 用于记录每次用户登录后生成的 token,用于身份校验,支持失效机制。
sql 复制代码
CREATE TABLE IF NOT EXISTS tokens (
  id INT UNSIGNED AUTO_INCREMENT PRIMARY KEY COMMENT '主键',
  user_id INT UNSIGNED NOT NULL COMMENT '用户ID',
  token VARCHAR(255) NOT NULL COMMENT '登录Token',
  expires_at DATETIME NOT NULL COMMENT '过期时间',
  created_at DATETIME DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  updated_at DATETIME DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '更新时间',
  FOREIGN KEY (user_id) REFERENCES users(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='Token表';

5.生成 token

  • 使用 uuid.v4() 生成随机 token;
  • 设置过期时间为 30 天后;
  • 存入 tokens 表;
  • 中间件校验是否有效,否则返回 401;
  • 前端如遇 token 过期,自动跳转登录并清除本地存储。

6.编写 通用 Token 校验中间件 middlewares/auth.js

主功能:判断请求中是否带有合法、未过期的 token,若合法则放行并附加 user_id,否则返回 401。

📦 中间件逻辑流程:

markdown 复制代码
1. 从请求头 headers 中获取 token(通常使用 Authorization 字段)。

2. 如果未携带 token,直接返回 401。

3. 在数据库 tokens 表中查找:
   - token 是否存在;
   - 当前时间是否未超过 expires_at。

4. 如果查无结果,说明 token 无效或过期,返回 401。

5. 如果验证成功,将对应的 user_id 挂载到 req.user 供后续使用。

6. 放行该请求,进入对应的控制器逻辑。
ini 复制代码
const db = require('../config/db');

module.exports = async (req, res, next) => {
  const token = req.headers['authorization'];

  if (!token) {
    return res.status(401).json({ code: 1, message: '未登录或缺少 Token' });
  }

  const sql = 'SELECT * FROM tokens WHERE token = ? AND expires_at > NOW()';
  db.query(sql, [token], (err, results) => {
    if (err) return next(err);

    if (results.length === 0) {
      return res.status(401).json({ code: 1, message: '登录状态已过期,请重新登录' });
    }

    // token 有效,将用户信息挂载到 req 上供后续接口使用
    req.user = {
      id: results[0].user_id
    };

    next();
  });
};

7.总体请求流程图

sql 复制代码
【前端调用 login 接口】
   ↓
【authController 处理】
   ↓
[ 请求微信换 openid → 查用户 → 创建/读取 → 写入 token 表 → 返回 token ]
   ↓
【前端存储 token 本地 Storage】

----------------------------- 之后所有请求都带上 token -----------------------------

【请求 /api/user/profile】
   ↓
【auth.js 中间件校验 token】
   ↓(有效)→【userController 查用户信息 → 返回】

8.编写路由

路由文件 路径前缀 是否需要 token 接口路径 控制器方法 功能说明
routes/auth.js /api/auth ❌ 否 POST /login loginOrRegister 微信小程序授权登录/注册
routes/user.js /api/user ✅ 是 GET /profile getUserProfile 获取当前登录用户信息

📁 routes/auth.js

说明 :登录和注册相关接口,不需要 token 校验

ini 复制代码
const express = require('express');
const router = express.Router();
const authController = require('../controllers/authController');

// 微信授权登录(登录/注册)
router.post('/wxlogin', authController.loginOrRegister);

module.exports = router;
✅ 接口说明:
  • POST /api/auth/login
    • 参数code(微信授权 code)、nicknameavatarUrl
    • 说明:调用微信接口获取 openid,若用户不存在则注册,生成 token 并返回

📁 routes/user.js

说明 :用户信息相关接口,必须登录,使用 token 中间件校验

ini 复制代码
const express = require('express');
const router = express.Router();
const auth = require('../middleware/auth');
const userController = require('../controllers/userController');

// 获取当前登录用户信息(需登录)
router.get('/profile', auth, userController.getUserProfile);

module.exports = router;
✅ 接口说明:
  • GET /api/user/profile
    • HeaderAuthorization: token值
    • 说明:返回用户基本资料(头像、昵称、角色等),需登录态

7.编写控制器

✅ authController.js 控制逻辑:微信授权登录

主功能 :根据小程序 code 向微信服务器换取 openid,根据 openid 实现登录或注册,并生成 token

🌐 接口:POST /api/auth/login
📦 控制逻辑流程:
markdown 复制代码
1. 接收前端传来的 code、nickname、avatarUrl 参数。
   - code 用于向微信服务端获取 openid。
   - nickname / avatarUrl 是用户信息,用于注册新用户。

2. 校验 code 是否存在。
   - 如果没有 code,直接返回错误。

3. 调用微信的 code2Session 接口(使用 axios)获取 openid。
   - 接口:https://api.weixin.qq.com/sns/jscode2session
   - 参数:appid、secret、js_code、grant_type

4. 判断是否成功获取 openid。
   - 如果失败(如 code 失效),返回错误。
   - 如果成功,继续下一步。

5. 查询数据库中是否已有该 openid 对应的用户。
   - 如果有,则视为登录,获取用户ID。
   - 如果没有,则创建新用户记录,插入 nickname 和 avatar_url,并获取新用户ID。

6. 生成一个随机 token(使用 uuid)。
   - 设置过期时间为当前时间 + 30 天(使用 dayjs)。

7. 将 token 插入 tokens 表中,关联当前用户。

8. 返回登录成功的响应:
   - token
   - 用户基本信息(id、nickname、avatar_url)
controllers/authController.js
php 复制代码
const axios = require('axios');
const { v4: uuidv4 } = require('uuid');
const db = require('../config/db');
const config = require('../config/config');
const dayjs = require('dayjs');

exports.loginOrRegister = async (req, res, next) => {
  try {
    const { code, nickname = '', avatarUrl = '' } = req.body;

    if (!code) {
      return res.status(400).json({ code: 1, message: '缺少 code 参数' });
    }

    // 请求微信服务器换取 openid
    const wxUrl = `https://api.weixin.qq.com/sns/jscode2session?appid=${config.wx.appid}&secret=${config.wx.secret}&js_code=${code}&grant_type=authorization_code`;
    const result = await axios.get(wxUrl);
    const { openid } = result.data;

    if (!openid) {
      return res.status(400).json({ code: 1, message: '微信登录失败', detail: result.data });
    }

    // 查询用户是否存在
    db.query('SELECT * FROM users WHERE openid = ?', [openid], (err, users) => {
      if (err) return next(err);

      let userId;

      const handleToken = () => {
        const token = uuidv4();
        const expiresAt = dayjs().add(30, 'day').format('YYYY-MM-DD HH:mm:ss');

        const insertTokenSql = 'INSERT INTO tokens (user_id, token, expires_at) VALUES (?, ?, ?)';
        db.query(insertTokenSql, [userId, token, expiresAt], (err2) => {
          if (err2) return next(err2);

          res.json({
            code: 0,
            message: '登录成功',
            data: {
              token,
              user: {
                id: userId,
                nickname,
                avatar_url: avatarUrl
              }
            }
          });
        });
      };

      if (users.length === 0) {
        // 未注册,新建用户
        const insertUserSql = 'INSERT INTO users (openid, nickname, avatar_url) VALUES (?, ?, ?)';
        db.query(insertUserSql, [openid, nickname, avatarUrl], (err2, result2) => {
          if (err2) return next(err2);
          userId = result2.insertId;
          handleToken();
        });
      } else {
        // 已注册,直接登录
        userId = users[0].id;
        handleToken();
      }
    });
  } catch (error) {
    next(error);
  }
};

✅ userController.js 控制逻辑:获取用户信息

主功能:基于用户 token 获取用户信息,用于小程序首页/个人中心展示。
🌐 接口:GET /api/user/profile(需 token)
📦 控制逻辑流程:
markdown 复制代码
1. 中间件 auth.js 已在 req.user 上挂载了 user_id。

2. 控制器中从 req.user.id 获取当前用户的 ID。

3. 使用该 user_id 查询用户表,获取 nickname、avatar_url、role、credit_score 等字段。

4. 如果查询为空,返回用户不存在。

5. 如果查询成功,返回用户信息数据。
controllers/userController.js
ini 复制代码
const db = require('../config/db');

exports.getUserProfile = (req, res, next) => {
  const userId = req.user.id;

  const sql = 'SELECT id, nickname, avatar_url, role, credit_score FROM users WHERE id = ?';
  db.query(sql, [userId], (err, results) => {
    if (err) return next(err);
    if (results.length === 0) {
      return res.status(404).json({ code: 1, message: '用户不存在' });
    }

    res.json({
      code: 0,
      message: '获取用户信息成功',
      data: results[0]
    });
  });
};

8. 更新项目入口 app.js

php 复制代码
const express = require('express');
const cors = require('cors');
const config = require('./config/config');
const authRoutes = require('./routes/auth');
const userRoutes = require('./routes/user');
const errorHandler = require('./middleware/errorHandler');
require('./config/db');
const app = express();
app.use(cors());
app.use(express.json());

// 路由分组挂载
app.use('/api/auth', authRoutes);
app.use('/api/user', userRoutes);

// ✅ 统一 404 错误格式
app.use((req, res) => {
  res.status(404).json({
    success: false,
    message: '接口不存在',
    error: 'Not Found'
  });
});

// ✅ 错误处理中间件
app.use(errorHandler);

app.listen(config.PORT, () => {
  console.log(`🚀 服务器已启动:http://localhost:${config.PORT}`);
});

✅ 三、启动与测试

✅ 1. 启动后端项目

复制代码
node app.js

控制台应输出数据库连接成功,并监听端口。


✅ 2. 登录测试(前端小程序)

前端在调用 wx.login 成功后获取 code:

php 复制代码
wx.login({
  success(res) {
    if (res.code) {
      uni.request({
        url: 'http://localhost:8081/api/auth/wxlogin',
        method: 'POST',
        data: { code: res.code },
        success: res => {
          if (res.data.code === 0) {
            uni.setStorageSync('token', res.data.token);
            uni.showToast({ title: '登录成功' });
          }
        }
      });
    }
  }
});

✅ 3. 后续接口使用

每次请求自动附带 token(拦截器注入),如请求当前用户资料:

ini 复制代码
request.get('/api/user/profile').then(res => {
  this.user = res.data;
});

如 token 失效,后端返回 401,前端拦截器统一处理并跳转登录页。


相关推荐
何中应13 分钟前
Bean的三种注入方式
开发语言·spring boot·后端·spring
席万里22 分钟前
基于Flask框架实现的一个在线考试系统
后端·python·flask
盟接之桥39 分钟前
盟接之桥--说制造:从“找缝隙”到“一万米深”——庖丁解牛式的制造业精进之道
大数据·前端·数据库·人工智能·物联网·制造
王中阳Go41 分钟前
12 Go Eino AI应用开发实战 | 消息队列架构
人工智能·后端·go
巴拉巴拉~~41 分钟前
Flutter 通用滑块组件 CommonSliderWidget:单值 / 范围 + 刻度 + 标签 + 样式自定义
开发语言·前端·javascript
沐森1 小时前
Rust 的CPU和IO操作
后端
Lucky_Turtle1 小时前
【Springboot】解决PageHelper在实体转Vo下出现total数据问题
java·spring boot·后端
無量1 小时前
AI工程化实践指南:从入门到落地
后端·ai编程
golang学习记1 小时前
Jetbrains 这个知名软件十年了!
后端
韭菜炒大葱1 小时前
现代前端开发工程化:Vue3 + Vite 带你从 0 到 1 搭建 Vue3 项目🚀
前端·vue.js·vite