微信小程序登录方案实践-从账号体系到用户信息存储

前言

随着微信小程序的普及,如何设计一个安全可靠的登录系统变得尤为重要。本文将从实践角度详细介绍微信小程序中账号密码登录和微信登录的完整解决方案。

一、登录方案设计

1.1 登录方式选择

在小程序开发中,我们通常会遇到两种登录场景:

  1. 账号密码登录

    • 适用于已有用户系统的迁移
    • 用户可使用原有账号直接登录
    • 支持更细粒度的权限控制
  2. 微信登录

    • 一键快捷登录,用户体验好
    • 可获取用户微信资料
    • 适合新系统快速落地

1.2 技术方案对比

graph TD A[账号密码登录] --> B[用户输入账号密码] B --> C[前端发送账号密码到后端] C --> D{后端验证} D -->|成功| E[生成Token] D -->|失败| F[返回错误信息] E --> G[返回Token和用户信息] G --> H[前端存储Token和用户信息] H --> I[登录成功跳转] J[微信登录] --> K[调用wx.login获取code] K --> L[前端发送code到后端] L --> M[后端用code+appid+secret换取openid] M --> N{是否已注册} N -->|是| O[生成Token] N -->|否| P[创建新用户并生成Token] O --> Q[返回Token和用户信息] P --> Q Q --> R[前端存储Token和用户信息] R --> S[登录成功跳转]

二、账号密码登录实现

2.1 界面实现

因为先是做了pc端的项目,把pc端的功能都开发好之后要转到微信小程序的开发上,pc端的登录功能做了好多遍了但微信小程序还没做过,学了一下,账号密码登录的还是用后端的接口就行,逻辑是一样的,重点是微信登录实现。

ini 复制代码
<!-- 登录表单 -->
    <view class="form-container" hidden="{{activeTab !== 'login'}}">
      <form bindsubmit="handleLogin">
        <view class="form-item input-wrap">
          <text class="iconfont icon-icon_user"></text>
          <input name="username" model:value="{{loginForm.username}}"
           bindinput="handleInput"
           data-form="loginForm"
           data-field="username"
           placeholder="请输入用户名" />
        </view>
        
        <view class="form-item input-wrap">
            <text class="iconfont icon-icon_lock"></text>
            <input name="password" model:value="{{loginForm.password}}" 
            type="password" 
            bindinput="handleInput"
            data-form="loginForm"
            data-field="password"
            placeholder="请输入密码" />
        </view>

        <view class="form-item verify-code">
          <view class="input-wrap">
            <text class="iconfont icon-iconkey"></text>
            <input name="captcha" model:value="{{loginForm.captcha}}" 
            maxlength="5" 
            bindinput="handleInput"
            data-form="loginForm"
            data-field="captcha"
            placeholder="请输入验证码" />
          </view>
          <image class="code-img" src="{{verifyCodeUrl}}" bindtap="refreshVerifyCode"></image>
        </view>

        <button class="submit-btn" form-type="submit" bindtap="handleLogin">立即登录</button>
      </form>
    </view>

    <view class="register-link" bindtap="switchTab" data-tab="register" hidden="{{activeTab !== 'login'}}">
    还没有账号?<text bindtap="goRegister">立即注册</text>
  </view>
// 复制代码
Page({
  data: {
     activeTab: 'login',
    // 登录表单数据
    loginForm: {
      username: '',
      password: '',
      captcha: '',
      uuid: ''
    },
  },


   // 切换登录/注册标签
  switchTab(e) {
    const tab = e.currentTarget.dataset.tab
    this.setData({
      activeTab: tab
    })
  },

  // 处理输入框获取焦点
  handleFocus(e) {
    const { field, form } = e.currentTarget.dataset
    this.clearError(form, field)
  },

  // 处理输入框内容变化
  handleInput(e) {
    const { field, form } = e.currentTarget.dataset;
    const value = e.detail.value;
    this.setData({
      [`${form}.${field}`]: value
    });
  },
 // 处理登录
  async handleLogin() {
    console.log(this.data.loginForm);
    if (!this.validateLoginForm()) return
    
    try {
      wx.showLoading({
        title: '登录中...',
        mask: true
      })
      
      const res = await request('/sys/auth/login',this.data.loginForm,'POST')
      
      wx.hideLoading()
      
      if (!res || !res.data) {
        throw new Error('服务器返回数据异常')
      }
      
      console.log('登录响应:', res.data)
      
      if (res.code === 0) {
        // 登录成功
        wx.showToast({
          title: '登录成功',
          icon: 'success'
        })
        // 保存token
        wx.setStorageSync('token', res.data.access_token)
        // 跳转到首页
        wx.switchTab({
          url: '/pages/home/home'
        })
      } else {
        // 登录失败
        wx.showToast({
          title: res.msg || '用户名或密码错误',
          icon: 'none'
        })
        this.refreshVerifyCode()
      }
    } catch (error) {
      wx.hideLoading()
      console.error('登录失败:', error)
      wx.showToast({
        title: error.message || '登录失败,请重试',
        icon: 'none'
      })
      this.refreshVerifyCode()
    }
  },
})

在微信小程序的 <input> 组件中,name 属性主要有以下两个核心作用:

1. 表单提交时作为字段标识

当使用 <form> 表单提交时(通过 bindsubmit 事件),name 会成为数据对象的 key,例如:

ini 复制代码
<form bindsubmit="handleSubmit"> 
    <input name="username" value="张三" />
    <button form-type="submit">提交</button>
</form>

提交后的事件对象中会包含:

ini 复制代码
e.detail.value = { username: "张三" // name作为字段名 }

2. 区分不同输入框(配合统一处理函数)

在 handleInput 场景下:

ini 复制代码
<input name="username" bindinput="handleInput" />

可以通过 e.currentTarget.dataset 或 e.detail 获取输入框标识:

javascript 复制代码
handleInput(e) {
const field = e.currentTarget.dataset.name || e.currentTarget.dataset.field
const value = e.detail.value
this.setData({ [`loginForm.${field}`]: value }) }

核心属性解析

  1. name="username"
  • 表单提交时作为字段标识(与<form>配合使用)

  • bindsubmit事件中会生成e.detail.value.username的字段名

  1. value="{{loginForm.username}}"
  • 实现数据双向绑定的关键

  • 显示login.jsdata.loginForm.username的初始值

  • 用户输入时会通过bindinput事件更新对应数据

  1. bindinput="handleInput"
  • 绑定输入事件处理函数

  • 每次输入内容变化时触发handleInput方法

  1. data-id="username"
  • 自定义数据属性(非官方标准属性)

  • 可通过e.currentTarget.dataset.id获取值

  • 通常用于在统一处理函数中区分不同输入框

特别注意事项:

  1. 如果同时使用namedata-id会造成冗余,建议:
  • 表单提交场景保留name

  • 非表单场景可只用data-id

  1. 微信小程序新版支持model:value实现自动双向绑定:
ini 复制代码
<input model:value="{{loginForm.username}}" />

三、微信登录实现

微信小程序的微信登录是基于微信开放能力的一种便捷登录方式,其核心逻辑是通过微信提供的接口获取用户标识,然后与开发者服务器进行交互完成登录流程。

一、前端实现流程

  1. 调用 wx.login 获取临时 code
javascript 复制代码
// 小程序端代码
wx.login({
  success: (res) => {
    if (res.code) {
      // 获取到临时登录凭证code
      console.log('获取到的code:', res.code);
      this.sendCodeToServer(res.code);
    } else {
      console.log('登录失败!' + res.errMsg);
    }
  }
});
  1. 将 code 发送到开发者服务器
javascript 复制代码
sendCodeToServer(code) {
  wx.request({
    url: 'https://your-server.com/api/wechat-login',
    method: 'POST',
    data: { code },
    success: (res) => {
      if (res.data.token) {
        // 登录成功处理
        this.handleLoginSuccess(res.data);
      }
    }
  });
}

二、后端处理流程

  1. 接受小程序传来的 code
ini 复制代码
// Node.js 示例
router.post('/api/wechat-login', async (ctx) => {
  const { code } = ctx.request.body;
  
  if (!code) {
    ctx.status = 400;
    ctx.body = { code: 400, message: '缺少code参数' };
    return;
  }
  
});
  1. 使用 code 换取 openid 和 session_key
ini 复制代码
const axios = require('axios');

async function getOpenId(code) {
  const appId = '你的小程序appid';
  const appSecret = '你的小程序appsecret';
  
  const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${appId}&secret=${appSecret}&js_code=${code}&grant_type=authorization_code`;
  
  try {
    const response = await axios.get(url);
    return response.data;
  } catch (error) {
    console.error('获取openid失败:', error);
    return null;
  }
}
  1. 处理用户信息(两种场景)

场景1:新用户首次登录

javascript 复制代码
// 检查用户是否存在
const user = await UserModel.findOne({ openid: sessionData.openid });

if (!user) {
  // 创建新用户
  const newUser = new UserModel({
    openid: sessionData.openid,
    createdAt: new Date(),
    lastLogin: new Date()
  });
  
  await newUser.save();
  return newUser;
}

场景2: 老用户登录

php 复制代码
// 更新老用户登录信息
await UserModel.updateOne(
  { openid: sessionData.openid },
  { $set: { lastLogin: new Date() } }
);
  1. 生成自定义登录态(Token)
php 复制代码
const jwt = require('jsonwebtoken');

function generateToken(user) {
  return jwt.sign(
    {
      userId: user._id,
      openid: user.openid
    },
    'your-secret-key',
    { expiresIn: '7d' }
  );
}
  1. 返回 Token 和用户消息给小程序
css 复制代码
ctx.body = {
  code: 200,
  message: '登录成功',
  token: generateToken(user),
  userInfo: {
    openid: user.openid,
    // 其他的用户信息
  }
};
sequenceDiagram 小程序->>+微信服务器: wx.login()获取code 微信服务器-->>-小程序: 返回code 小程序->>+开发者服务器: 发送code 开发者服务器->>+微信服务器: code+appid+secret换取openid 微信服务器-->>-开发者服务器: 返回openid和session_key 开发者服务器->>开发者服务器: 查询/创建用户记录 开发者服务器-->>-小程序: 返回自定义登录态(Token) 小程序->>小程序: 存储Token 小程序->>+开发者服务器: 携带Token请求用户数据 开发者服务器-->>-小程序: 返回用户数据
相关推荐
Cache技术分享3 分钟前
99. Java 继承(Inheritance)
前端·后端
SleepyZone11 分钟前
Cline 源码浅析 - 从输入到输出
前端·ai编程·cline
Struggler28115 分钟前
pinia-基于monorepo的项目结构管理
前端
Struggler28119 分钟前
SSE的使用
前端
用户58061393930025 分钟前
前端文件下载实现深度解析:Blob与ObjectURL的完美协作
前端
Lin866629 分钟前
Vue 3 + TypeScript 组件类型推断失败问题完整解决方案
前端
coding随想29 分钟前
从零开始:前端开发者的SEO优化入门与实战
前端
前端工作日常31 分钟前
我理解的JSBridge
前端
Au_ust31 分钟前
前端模块化
前端
顺丰同城前端技术团队32 分钟前
还不会用 Charles?最后一遍了啊!
前端