小程序开发实战:实现用户登录与授权功能 分类:公司动态 发布时间:2026-06-29

在微信小程序生态中,用户登录与授权是构建用户体系、实现个性化服务的基础能力。不同于传统Web应用的账号密码登录模式,小程序依托微信生态提供了一套安全、便捷的身份认证机制,开发者可以快速获取用户身份标识,实现业务数据与用户的绑定。本文将从原理剖析到代码实战,系统讲解小程序开发登录与授权功能的完整实现方案,涵盖前端交互逻辑、后端接口设计、会话管理以及合规性处理,帮助开发者构建安全可靠的用户认证体系。
 
一、小程序登录授权体系概述
 
1. 核心概念
小程序开发登录体系围绕微信官方提供的接口展开,涉及几个关键概念:
(1)code:用户登录凭证,有效期5分钟,由前端调用 wx.login() 获取,需发送至开发者服务器换取用户标识。
(2)openid:用户在当前小程序的唯一标识,同一用户在不同小程序中openid不同。
(3)unionid:用户在微信开放平台账号下的唯一标识,若开发者拥有多个小程序、公众号或移动应用,可通过unionid实现用户身份互通。
(4)session_key:会话密钥,用于解密用户敏感数据(如加密的手机号、用户信息),具有时效性,不可直接下发至前端。
(5)自定义登录态:开发者后端生成的会话标识(如token、sessionId),用于后续业务接口的身份校验。
 
2. 登录整体流程
小程序官方推荐的登录流程分为四步:
(1)前端调用 wx.login() 获取临时登录凭证code。
(2)前端将code发送至开发者后端服务器。
(3)后端携带code、AppID、AppSecret调用微信接口服务,换取openid和session_key。
(4)后端生成自定义登录态(如token),与openid、session_key关联存储,返回给前端。
(5)前端存储登录态,后续业务请求携带该标识进行身份校验。
 
该流程的核心设计原则是:敏感数据(session_key、AppSecret)全程保留在服务端,不暴露给前端,确保账号安全。
 
二、前端登录功能实现
 
1. 基础登录接口调用
小程序开发基础库提供了 wx.login() 方法用于获取登录凭证,这是整个登录流程的起点。
 
// app.js 中封装登录方法
App({
  globalData: {
    userToken: '',
    userInfo: null
  },
 
  onLaunch() {
    // 启动时检查登录状态
    this.checkLoginStatus();
  },
 
  // 检查本地登录态
  checkLoginStatus() {
    const token = wx.getStorageSync('userToken');
    if (token) {
      this.globalData.userToken = token;
      // 可选择性调用后端接口校验token有效性
      this.validateToken(token);
    } else {
      this.doLogin();
    }
  },
 
  // 执行登录
  doLogin() {
    return new Promise((resolve, reject) => {
      wx.login({
        success: (res) => {
          if (res.code) {
            // 将code发送给后端
            this.requestLogin(res.code).then(resolve).catch(reject);
          } else {
            reject(new Error('获取登录凭证失败:' + res.errMsg));
          }
        },
        fail: reject
      });
    });
  },
 
  // 请求后端登录接口
  requestLogin(code) {
    return new Promise((resolve, reject) => {
      wx.request({
        url: 'https://your-domain.com/api/wx/login',
        method: 'POST',
        data: { code },
        success: (res) => {
          if (res.data.code === 0) {
            const { token, userInfo } = res.data.data;
            // 存储登录态
            wx.setStorageSync('userToken', token);
            this.globalData.userToken = token;
            this.globalData.userInfo = userInfo;
            resolve(userInfo);
          } else {
            reject(new Error(res.data.message || '登录失败'));
          }
        },
        fail: reject
      });
    });
  }
});
 
2. 登录态维护与失效处理
实际业务中,后端生成的token可能存在有效期,前端需要处理token失效的场景。推荐封装统一的请求拦截器:
 
// utils/request.js
const BASE_URL = 'https://your-domain.com/api';
 
function request(options) {
  const token = wx.getStorageSync('userToken');
  
  return new Promise((resolve, reject) => {
    wx.request({
      url: BASE_URL + options.url,
      method: options.method || 'GET',
      data: options.data || {},
      header: {
        'Content-Type': 'application/json',
        'Authorization': token ? `Bearer ${token}` : ''
      },
      success: (res) => {
        // token失效,状态码401
        if (res.statusCode === 401) {
          // 清除本地登录态,重新登录
          wx.removeStorageSync('userToken');
          return reLogin().then(() => {
            // 重新发起原请求
            return request(options);
          }).then(resolve).catch(reject);
        }
        
        if (res.data.code === 0) {
          resolve(res.data.data);
        } else {
          wx.showToast({
            title: res.data.message || '请求失败',
            icon: 'none'
          });
          reject(res.data);
        }
      },
      fail: reject
    });
  });
}
 
// 静默重新登录
function reLogin() {
  const app = getApp();
  return app.doLogin();
}
 
module.exports = { request };
 
三、后端登录接口实现
 
1. 调用微信凭证校验接口
后端收到前端传来的code后,需要调用微信官方接口 auth.code2Session 换取openid和session_key。
 
接口地址:
 
GET https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code
 
以Node.js(Express)为例实现登录接口:
 
// routes/wxLogin.js
const express = require('express');
const router = express.Router();
const axios = require('axios');
const jwt = require('jsonwebtoken');
const User = require('../models/User');
 
const APPID = '你的小程序AppID';
const APP_SECRET = '你的小程序AppSecret';
const JWT_SECRET = '你的JWT密钥';
 
router.post('/login', async (req, res) => {
  const { code } = req.body;
  
  if (!code) {
    return res.json({ code: 1, message: '缺少code参数' });
  }
 
  try {
    // 调用微信接口换取openid和session_key
    const wxResponse = await axios.get('https://api.weixin.qq.com/sns/jscode2session', {
      params: {
        appid: APPID,
        secret: APP_SECRET,
        js_code: code,
        grant_type: 'authorization_code'
      }
    });
 
    const { openid, session_key, errcode, errmsg } = wxResponse.data;
    
    if (errcode) {
      return res.json({ code: 1, message: `微信登录失败:${errmsg}` });
    }
 
    // 查询或创建用户
    let user = await User.findOne({ where: { openid } });
    
    if (!user) {
      // 新用户,创建记录
      user = await User.create({
        openid,
        session_key,
        createTime: new Date()
      });
    } else {
      // 更新session_key
      await user.update({ session_key });
    }
 
    // 生成自定义登录态token
    const token = jwt.sign(
      { userId: user.id, openid },
      JWT_SECRET,
      { expiresIn: '7d' }
    );
 
    res.json({
      code: 0,
      message: '登录成功',
      data: {
        token,
        userInfo: {
          id: user.id,
          nickname: user.nickname,
          avatar: user.avatar
        }
      }
    });
 
  } catch (error) {
    console.error('登录接口异常:', error);
    res.json({ code: 1, message: '服务器内部错误' });
  }
});
 
module.exports = router;
 
2. 中间件校验token
后续业务接口需要校验用户身份,通过JWT中间件实现:
 
// middleware/auth.js
const jwt = require('jsonwebtoken');
const JWT_SECRET = '你的JWT密钥';
 
function authMiddleware(req, res, next) {
  const authHeader = req.headers.authorization;
  
  if (!authHeader || !authHeader.startsWith('Bearer ')) {
    return res.status(401).json({ code: 1, message: '未登录或登录已失效' });
  }
 
  const token = authHeader.split(' ')[1];
  
  try {
    const decoded = jwt.verify(token, JWT_SECRET);
    req.user = decoded; // 将用户信息挂载到请求对象
    next();
  } catch (error) {
    return res.status(401).json({ code: 1, message: 'token无效或已过期' });
  }
}
 
module.exports = authMiddleware;
 
四、用户信息授权功能
 
1. 头像昵称填写能力
自微信基础库2.21.2起,官方不再支持通过 wx.getUserInfo 直接获取用户头像昵称,转而提供了头像昵称填写组件能力,由用户主动选择头像、输入昵称。
 
前端实现示例:
 
<!-- pages/profile/edit.wxml -->
<view class="profile-edit">
  <view class="avatar-section">
    <text>头像</text>
    <button class="avatar-btn" open-type="chooseAvatar" bindchooseavatar="onChooseAvatar">
      <image class="avatar" src="{{avatarUrl}}" mode="aspectFill"></image>
    </button>
  </view>
  
  <view class="nickname-section">
    <text>昵称</text>
    <input 
      type="nickname" 
      class="nickname-input" 
      placeholder="请输入昵称"
      value="{{nickname}}"
      bindinput="onNicknameInput"
    />
  </view>
  
  <button class="save-btn" bindtap="saveProfile">保存</button>
</view>
 
// pages/profile/edit.js
Page({
  data: {
    avatarUrl: '/images/default-avatar.png',
    nickname: ''
  },
 
  onLoad() {
    // 加载已保存的用户信息
    const userInfo = wx.getStorageSync('userInfo');
    if (userInfo) {
      this.setData({
        avatarUrl: userInfo.avatar || '/images/default-avatar.png',
        nickname: userInfo.nickname || ''
      });
    }
  },
 
  // 选择头像
  onChooseAvatar(e) {
    const { avatarUrl } = e.detail;
    this.setData({ avatarUrl });
  },
 
  // 输入昵称
  onNicknameInput(e) {
    this.setData({ nickname: e.detail.value });
  },
 
  // 保存用户信息
  saveProfile() {
    const { avatarUrl, nickname } = this.data;
    
    if (!nickname.trim()) {
      wx.showToast({ title: '请输入昵称', icon: 'none' });
      return;
    }
 
    // 上传头像到服务器,再保存用户信息
    this.uploadAvatar(avatarUrl).then(cloudPath => {
      return this.saveUserInfo({
        avatar: cloudPath,
        nickname: nickname.trim()
      });
    }).then(() => {
      wx.showToast({ title: '保存成功' });
      wx.navigateBack();
    }).catch(err => {
      wx.showToast({ title: '保存失败', icon: 'none' });
    });
  },
 
  // 上传头像
  uploadAvatar(filePath) {
    return new Promise((resolve, reject) => {
      wx.uploadFile({
        url: 'https://your-domain.com/api/upload',
        filePath,
        name: 'file',
        header: {
          'Authorization': `Bearer ${wx.getStorageSync('userToken')}`
        },
        success: (res) => {
          const data = JSON.parse(res.data);
          if (data.code === 0) {
            resolve(data.data.url);
          } else {
            reject(data.message);
          }
        },
        fail: reject
      });
    });
  },
 
  // 调用后端保存接口
  saveUserInfo(data) {
    return wx.request({
      url: 'https://your-domain.com/api/user/profile',
      method: 'PUT',
      header: {
        'Authorization': `Bearer ${wx.getStorageSync('userToken')}`
      },
      data
    });
  }
});
 
2. 手机号授权
获取用户手机号是小程序开发业务中的高频需求,需通过 button 组件的 open-type="getPhoneNumber" 触发,用户授权后前端获得加密数据,由后端解密。
 
前端代码:
 
<button open-type="getPhoneNumber" bindgetphonenumber="onGetPhoneNumber">
  授权手机号
</button>
 
onGetPhoneNumber(e) {
  if (e.detail.errMsg === 'getPhoneNumber:ok') {
    const { encryptedData, iv } = e.detail;
    // 将加密数据发送至后端解密
    wx.request({
      url: 'https://your-domain.com/api/user/phone',
      method: 'POST',
      header: {
        'Authorization': `Bearer ${wx.getStorageSync('userToken')}`
      },
      data: { encryptedData, iv },
      success: (res) => {
        if (res.data.code === 0) {
          wx.showToast({ title: '手机号绑定成功' });
        }
      }
    });
  }
}
 
后端解密实现(Node.js):
 
const crypto = require('crypto');
 
function decryptPhone(encryptedData, iv, sessionKey) {
  // base64解码
  const encryptedDataBuffer = Buffer.from(encryptedData, 'base64');
  const keyBuffer = Buffer.from(sessionKey, 'base64');
  const ivBuffer = Buffer.from(iv, 'base64');
  
  // AES-128-CBC解密
  const decipher = crypto.createDecipheriv('aes-128-cbc', keyBuffer, ivBuffer);
  decipher.setAutoPadding(true);
  
  let decrypted = decipher.update(encryptedDataBuffer, 'binary', 'utf8');
  decrypted += decipher.final('utf8');
  
  return JSON.parse(decrypted);
}
 
五、授权最佳实践与合规要点
 
1. 授权原则
(1)按需授权:不要在用户进入小程序时立即索要全部权限,应在具体功能场景下触发授权,降低用户抵触心理。
(2)说明用途:授权前清晰告知用户授权目的,例如"授权手机号用于接收订单通知"。
(3)降级处理:用户拒绝授权后,提供备选方案,确保核心功能可用,不得强制授权。
(4)引导重新授权:用户拒绝后,下次触发对应功能时,友好引导用户前往设置页开启权限。
 
2. 常见合规问题
(1)禁止静默授权:获取用户敏感信息必须通过用户主动点击操作,不得自动弹窗诱导授权。
(2)数据最小化:只收集业务必需的用户信息,不得过度采集。
(3)隐私协议:收集用户信息前必须展示隐私政策并获得用户同意,建议在首次登录时增加协议勾选环节。
(4)session_key安全:session_key严禁下发至前端,也不可作为业务接口的身份标识,必须通过自定义登录态隔离。
 
3. 性能与体验优化
(1)静默登录:利用 wx.login 无需用户交互的特性,在小程序启动时静默完成登录,用户无感知。
(2)登录态缓存:合理设置token有效期,避免频繁登录影响体验,建议7-30天。
(3)并发登录控制:多个接口同时触发登录时,使用单例模式避免重复调用登录接口。
(4)异常兜底:微信接口调用失败时,提供重试机制和友好的错误提示。
 
六、常见问题排查
 
1. code无效或已使用:code只能使用一次且有效期5分钟,确保每次登录都调用 wx.login() 获取新code。
2. 解密失败:检查session_key是否过期、加密算法是否匹配,确保使用AES-128-CBC模式、PKCS7填充。
3. AppSecret泄露风险:严禁将AppSecret写入前端代码或提交至公开代码仓库,必须存放于服务端环境变量中。
4. unionid获取失败:需先将小程序绑定到微信开放平台账号,且用户关注了同主体的公众号时才能直接获取。
 
小程序登录与授权是构建用户体系的基石,其核心思想是"前端传凭证、后端换身份、自定义会话"。小程序开发者在实现过程中,既要保证功能的完整性,也要重视安全性与用户体验的平衡。
在线咨询
服务项目
获取报价
意见反馈
返回顶部