SpringBoot 整合微信开放平台网站应用扫码登录(精简版)

一、前言

本文基于 微信开放平台 网站应用扫码登录 能力,实现后端核心逻辑,仅保留获取授权链接授权回调两个核心接口,扫码授权后自动完成系统账号创建/登录、生成业务 Token,适配 SpringBoot + Redis 。

适用场景:后台系统、PC 网站微信扫码快捷登录,无需前端轮询状态,回调接口直接完成登录逻辑。

二、前置准备

1. 微信开放平台配置

  1. 登录微信开放平台,创建网站应用
  2. 拿到 AppIDAppSecret
  3. 配置授权回调域名/地址:填写域名即可

2. 项目依赖

已基于现有项目技术栈,核心依赖说明:

  • SpringBoot、SpringWeb
  • Redis(RedisUtils 工具类)
  • Jackson(JSON 序列化)
  • RestTemplate(调用微信接口)
  • 自定义 DTO、工具类、Mapper/Service

3. 自定义实体/DTO

3.1 微信Token返回DTO WxOAuth2TokenResponse
import lombok.Data;

@Data
public class WxOAuth2TokenResponse {
    private String access_token;
    private String openid;
    private String unionid;
    private Integer expires_in;
    private String errmsg;
    private Integer errcode;
}
3.2 微信用户信息DTO WxUserInfoResponse
import lombok.Data;

@Data
public class WxUserInfoResponse {
    private String openid;
    private String unionid;
    private String nickname;
    private Integer sex; // 1-男 2-女 0-未知
    private String province;
    private String city;
    private String country;
    private String headimgurl;
    private String errmsg;
    private Integer errcode;
}

三、完整后端代码(仅保留核心两个接口)

3.1 控制器代码


import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.http.converter.StringHttpMessageConverter;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * 微信开放平台网站应用扫码登录 精简版
 * 仅保留:获取授权链接 + 授权回调 两个核心接口
 */
@RestController
@RequestMapping("wx/open")
public class WeiXinController {

    // ========== 微信开放平台配置项(务必修改为自己平台信息) ==========
    /** 网站应用 AppID */
    private static final String APP_ID = "xxxxxxxxxxxxxxxxxx";
    /** 网站应用 AppSecret */
    private static final String APP_SECRET = "xxxxxxxxx";
    /** 授权回调地址(开放平台必须配置一致) */
    private static final String REDIRECT_URL = "https://www.xxx.com/api-web/wx/open/callback";
    /** 微信获取access_token接口 */
    private static final String WX_OAUTH2_ACCESS_TOKEN_URL = "https://api.weixin.qq.com/sns/oauth2/access_token";
    /** 微信获取用户信息接口 */
    private static final String WX_USER_INFO_URL = "https://api.weixin.qq.com/sns/userinfo";
    /** 二维码/临时数据过期时间 10分钟 */
    private static final long QR_EXPIRE_SECONDS = 600L;

    // ========== 业务默认机构配置(根据自身业务修改 这里我用到登录的一些信息要直接插入进去) ==========
    private static final Long DEFAULT_SEARCH_STATION_ID = 59L;
    private static final Integer DEFAULT_INSTITUTION_ID = 19737;
    private static final Long DEFAULT_CLIENT_ID = 19737L;

    // ========== 依赖注入 ==========
    @Autowired
    private RedisUtils redisUtils;
    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private ProductToken productToken;
    @Autowired
    private cn.net.xxx.dao.system.SysUserMapper sysUserMapper;
    @Autowired
    private cn.net.xxx.dao.system.SysRoleMapper sysRoleDao;
    @Autowired
    private cn.net.xxx.dao.web.LibNewSearchStationMapper libNewSearchStationMapper;

    @Value("${redisToken.outTimes}")
    private int redisTokenOutTimes;

    private final RestTemplate restTemplate;
    private final ObjectMapper objectMapper = new ObjectMapper();

    /**
     * 初始化RestTemplate,统一编码为UTF-8,避免微信中文乱码
     */
    public WeiXinController() {
        RestTemplate rt = new RestTemplate();
        rt.getMessageConverters().set(1, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        this.restTemplate = rt;
    }

    // ===================== 接口1:获取微信授权链接(生成二维码用) =====================
    /**
     * @api {GET} /wx/open/getQrCodeUrl 获取微信扫码授权链接
     * @apiDescription 前端拿到 authorizeUrl 后生成二维码,用户扫码跳转微信授权页
     * @return 授权链接 + 随机state(防CSRF)
     */
    @GetMapping("getQrCodeUrl")
    public JsonResult getQrCodeUrl() {
        JsonResult jsonResult = new JsonResult();
        try {
            // 1. 生成随机state,防止CSRF攻击
            String state = UUID.randomUUID().toString().replaceAll("-", "");
            // 2. state存入Redis,设置过期时间
            redisUtils.set("wx:login:state:" + state, "0", QR_EXPIRE_SECONDS);
            // 3. 编码回调地址(微信要求URL编码)
            String encodedRedirectUri = URLEncoder.encode(REDIRECT_URL, StandardCharsets.UTF_8.name());
            // 4. 拼接微信扫码登录授权链接 scope=snsapi_login 固定为网站应用扫码
            String authorizeUrl = "https://open.weixin.qq.com/connect/qrconnect" +
                    "?appid=" + APP_ID +
                    "&redirect_uri=" + encodedRedirectUri +
                    "&response_type=code" +
                    "&scope=snsapi_login" +
                    "&state=" + state +
                    "#wechat_redirect";

            Map<String, Object> data = new HashMap<>();
            data.put("authorizeUrl", authorizeUrl);
            data.put("state", state);

            jsonResult.setResultCode(200);
            jsonResult.setData(data);
            jsonResult.setMessage("获取授权链接成功");
        } catch (Exception e) {
            e.printStackTrace();
            jsonResult.setResultCode(-1);
            jsonResult.setMessage("获取授权链接失败:" + e.getMessage());
        }
        return jsonResult;
    }

    // ===================== 接口2:微信授权回调(核心登录逻辑) =====================
    /**
     * @api {GET} /wx/open/callback 微信授权回调接口
     * @apiDescription 用户扫码并同意授权后,微信自动跳转该地址,携带code、state
     * 逻辑:获取token -> 获取微信用户信息 -> 自动注册/查询系统用户 -> 生成登录Token
     */
    @GetMapping("callback")
    public JsonResult callback(@RequestParam String code, @RequestParam String state) {
        JsonResult jsonResult = new JsonResult();
        String redisKey = "wx:login:state:" + state;
        try {
            // 标记状态:已扫码
            redisUtils.set(redisKey, "1", QR_EXPIRE_SECONDS);

            // 1. 根据code换取微信access_token
            String tokenUrl = WX_OAUTH2_ACCESS_TOKEN_URL +
                    "?appid=" + APP_ID +
                    "&secret=" + APP_SECRET +
                    "&code=" + code +
                    "&grant_type=authorization_code";
            String tokenStr = restTemplate.getForObject(tokenUrl, String.class);
            WxOAuth2TokenResponse tokenResult = objectMapper.readValue(tokenStr, WxOAuth2TokenResponse);

            if (tokenResult == null || tokenResult.getAccess_token() == null) {
                redisUtils.set(redisKey, "-1", 300L);
                jsonResult.setResultCode(-1);
                jsonResult.setMessage("微信授权失败:" + (tokenResult != null ? tokenResult.getErrmsg() : "获取Token为空"));
                return jsonResult;
            }

            // 2. 根据access_token + openid 获取微信用户详细信息
            String userInfoUrl = WX_USER_INFO_URL +
                    "?access_token=" + tokenResult.getAccess_token() +
                    "&openid=" + tokenResult.getOpenid();
            String userStr = restTemplate.getForObject(userInfoUrl, String.class);
            WxUserInfoResponse userInfo = objectMapper.readValue(userStr, WxUserInfoResponse);

            if (userInfo == null || userInfo.getUnionid() == null) {
                redisUtils.set(redisKey, "-1", 300L);
                jsonResult.setResultCode(-1);
                jsonResult.setMessage("获取微信用户信息失败");
                return jsonResult;
            }

            // 3. 以微信unionid作为账号前缀,查询系统用户
            String wxUsername = "wx_" + userInfo.getUnionid();
            SysUser sysUser = sysUserService.getUserByUnionId(wxUsername);

            // 4. 新用户:自动注册账号
            if (sysUser == null) {
                sysUser = new SysUser();
                sysUser.setUsername(wxUsername);
                sysUser.setCreateTime(new Date());
                // Shiro 双重SHA256加密密码
                String pwdHash = new Sha256Hash(new Sha256Hash(wxUsername).toHex()).toHex();
                sysUser.setPassword(pwdHash);
                sysUser.setFullName(userInfo.getNickname());

                // 性别转换:微信 1男/2女/0未知 → 系统 1男/0女
                if (userInfo.getSex() != null) {
                    if (userInfo.getSex() == 1) {
                        sysUser.setSex(1L);
                    } else if (userInfo.getSex() == 2) {
                        sysUser.setSex(0L);
                    } else {
                        sysUser.setSex(1L);
                    }
                }
                sysUser.setEmail("");
                sysUser.setMobile("");
                sysUser.setStatus((byte) 1);
                sysUser.setSysType(2);
                sysUser.setUserType(0);
                sysUser.setInstitutionId(DEFAULT_INSTITUTION_ID);
                sysUser.setClientId(DEFAULT_CLIENT_ID);
                sysUser.setSearchStationId(DEFAULT_SEARCH_STATION_ID);

                // 执行注册
                int userId = sysUserService.registered(sysUser);
                sysUser.setUserId(Long.valueOf(userId));
                // 重新查询完整用户信息
                sysUser = sysUserMapper.selectByPrimaryKey(Long.valueOf(userId));
            }

            // 5. 生成系统登录Token
            String randomCode = UUID.randomUUID().toString().replaceAll("-", "");
            Map<String, Object> tokenMap = productToken.productToken(sysUser.getUserId().toString(), randomCode);
            String loginToken = tokenMap.get("token").toString();

            // 清空密码、更新登录时间
            sysUser.setPassword("");
            sysUser.setLoginTime(new Date());

            // 6. 查询用户角色
            List<SysRoleEntity> roleList = sysRoleDao.getRoleByUserId(sysUser.getUserId());
            if (roleList != null && !roleList.isEmpty()) {
                SysRoleEntity role = roleList.get(0);
                sysUser.setRoleId(role.getRoleId().intValue());
                sysUser.setUserType(role.getType());
            }

            // 7. 补充其他信息
            if (sysUser.getUserType() != 1 && sysUser.getSearchStationId() != null) {
                LibNewSearchStation station = sysUserMapper.getInstitutionById(sysUser.getSearchStationId().intValue());
                if (station != null) {
                    sysUser.setInstitutionName(station.getName());
                }
            }

            // 8. 数据存入Redis,设置过期时间
            redisUtils.set(loginToken, sysUser, (long) redisTokenOutTimes);
            redisUtils.set("wx:login:token:" + state, loginToken, QR_EXPIRE_SECONDS);
            redisUtils.set(redisKey, "2", 300L);

            // 9. 组装返回结果,前端直接使用token登录系统
            Map<String, Object> resultData = new HashMap<>();
            resultData.put("token", loginToken);
            resultData.put("user", sysUser);

            // 补充配置
            LibNewSearchStation station = libNewSearchStationMapper.getById(DEFAULT_SEARCH_STATION_ID);
            resultData.put("workType", (station != null && station.getWorkType() != null) ? station.getWorkType() : "1");

            jsonResult.setResultCode(200);
            jsonResult.setData(resultData);
            jsonResult.setMessage("微信扫码登录成功");
        } catch (Exception e) {
            e.printStackTrace();
            redisUtils.set(redisKey, "-1", 300L);
            jsonResult.setResultCode(-1);
            jsonResult.setMessage("微信登录异常:" + e.getMessage());
        }
        return jsonResult;
    }
}

四、接口调用流程

1. 整体流程

  1. 前端调用 getQrCodeUrl → 获取 authorizeUrlstate
  2. 前端将 authorizeUrl 生成二维码,用户微信扫码
  3. 用户在微信中同意授权 → 微信自动跳转到后端 callback 接口,携带 codestate
  4. 后端 callback 完成:微信接口调用 → 用户查询/自动注册 → 生成业务 Token
  5. 后端返回 token + 用户信息,前端拿着 token 完成系统登录

2. 接口请求示例

(1)获取授权链接
  • 请求地址:GET /wx/open/getQrCodeUrl
  • 返回示例:
{
    "resultCode": 200,
    "message": "获取授权链接成功",
    "data": {
        "authorizeUrl": "https://open.weixin.qq.com/connect/qrconnect?xxx",
        "state": "xxxxxxxxx"
    }
}
(2)授权回调(微信自动调用)
  • 请求地址:GET /wx/open/callback?code=xxx&state=xxx
  • 登录成功返回示例:
{
    "resultCode": 200,
    "message": "微信扫码登录成功",
    "data": {
        "token": "系统登录token",
        "user": {},
        "workType": "1"
    }
}

五、关键要点与踩坑总结

  1. 回调地址严格匹配
    微信开放平台配置的回调域名/地址,必须和代码中 REDIRECT_URL 的域名完全一致

  2. URL编码问题
    回调地址必须使用 URLEncoder 编码,否则微信授权链接报错。

  3. scope 固定值
    网站应用扫码登录必须使用 scope=snsapi_login,和公众号授权 snsapi_userinfo 区分开。

  4. Code 一次性有效
    微信返回的 code 有效期 5 分钟,且只能使用一次,重复调用会报错。

  5. UnionID 作为唯一标识
    使用 unionid 而非 openid,同一微信账号在同一开放平台下所有应用 unionid 唯一。

  6. Redis 过期策略
    state、临时 token 均设置过期时间,防止 Redis 数据堆积,同时避免二维码长期有效。

  7. 编码乱码
    手动初始化 RestTemplate,强制使用 UTF-8,解决微信返回昵称、中文乱码问题。

六、前端对接简单说明

  1. 调用 /wx/open/getQrCodeUrl 拿到 authorizeUrl,使用二维码组件渲染二维码。
  2. 用户扫码授权后,页面会跳转到 callback 接口,接口直接返回登录 token
  3. 前端拿到 token 后,走原有登录逻辑(如存入本地、请求登录校验接口)即可。

上面的内容为前端点击微信跳转页面打开让用户扫码 下面为 直接内嵌到当前网站里面扫码登录

微信网站应用扫码登录 前端集成教程(HTML原生 + Vue 双版本)

结合上一篇后端接口,本文讲解原生HTML页面Vue项目两种场景接入微信扫码登录,包含代码说明、配置、样式、踩坑点,配套你现有的后端接口 + 前端扫码轮询逻辑。

技术栈说明
后端接口:getQrCodeUrl(获取授权链接+state)、checkScan(轮询扫码状态)、callback(微信回调)
前端依赖:微信官方 wxLogin.js 生成扫码弹窗/二维码,fetch 请求后端接口,定时器轮询状态


一、整体业务流程回顾

  1. 前端加载微信官方 JS,调用后端 /getQrCodeUrl 获取 state
  2. 初始化微信扫码组件,渲染登录二维码
  3. 开启定时器轮询 /checkScan 接口,监听扫码、授权状态
  4. 用户微信扫码并授权 → 微信跳转到后端 callback 接口完成登录逻辑
  5. 轮询检测到 token 后,本地存储 Token,跳转主页完成登录

二、通用前置配置(必看)

2.1 微信开放平台配置(全局统一)

  1. 登录微信开放平台 → 网站应用
  2. 授权回调地址:填写 www.xxx.com 只需要填写域名即可
  3. 配置授权域名www.xxx.com
  4. 确认应用已开通 snsapi_login 网站扫码登录权限

2.2 接口地址说明

  • 获取二维码&state:/api-web/wx/open/getQrCodeUrl
  • 轮询扫码状态:/api-web/wx/open/checkScan?state=xxx
  • 微信回调地址:/api-web/wx/open/callback

2.3 跨域说明

后端需配置 CORS跨域,允许前端域名请求接口,否则 fetch 会请求失败。


三、版本一:原生 HTML 页面接入(独立页面/内嵌页面)

适合传统多页面网站、静态页面、内嵌弹窗场景,直接复制可用。

3.1 完整代码示例

<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <title>微信扫码登录</title>
    <style>
        /* 页面基础样式 */
        body {
            margin: 0;
            padding: 0;
            background-color: #f5f5f5;
        }
        .login-box {
            text-align: center;
            margin-top: 100px;
        }
        .title {
            font-size: 20px;
            color: #333;
            margin-bottom: 30px;
        }
    </style>
</head>
<body>
    <div class="login-box">
        <div class="title">请使用微信扫码登录</div>
        <!-- 微信二维码容器ID:wx_qr_console_login -->
        <div id="wx_qr_console_login"></div>
    </div>

<script>
(function(){
    // ==================== 【核心配置项 - 请根据实际环境修改】 ====================
    const APPID = "xxxxxxxxxxxxxxxxxxxx";
    // 微信回调地址
    const REDIRECT_URI = "https://www.xxx.com/api-web/wx/open/callback";
    // 接口根路径,本地/同域留空,跨域填写完整域名
    const BASE_URL = "";
    // 轮询间隔 单位:毫秒
    const POLL_INTERVAL = 1500;
    // 二维码容器ID
    const containerId = "wx_qr_console_login";
    // =========================================================================

    let state = "";
    let pollTimer = null;

    /**
     * 1. 加载微信官方扫码JS
     */
    function loadWxScript() {
        return new Promise((resolve, reject) => {
            // 已加载则直接返回
            if(window.WxLogin) return resolve();
            const script = document.createElement("script");
            script.src = "https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js";
            script.onload = resolve;
            script.onerror = () => reject("微信登录脚本加载失败,请刷新重试");
            document.head.appendChild(script);
        });
    }

    /**
     * 2. 请求后端接口,获取 state
     */
    async function getState() {
        const url = `${BASE_URL}/api-web/wx/open/getQrCodeUrl`;
        const response = await fetch(url);
        const res = await response.json();
        if(res.resultCode !== 200) {
            throw new Error(res.message || "获取二维码信息失败");
        }
        state = res.data.state;
        return state;
    }

    /**
     * 3. 轮询扫码状态
     */
    function startPoll() {
        pollTimer = setInterval(async () => {
            try {
                const url = `${BASE_URL}/api-web/wx/open/checkScan?state=${state}`;
                const response = await fetch(url);
                const res = await response.json();

                // 二维码过期
                if(res.data?.status === -1) {
                    clearInterval(pollTimer);
                    alert("二维码已过期,请刷新页面重新扫码");
                    return;
                }
                // 登录成功:后端返回 token
                if(res.data?.token) {
                    clearInterval(pollTimer);
                    // 存储登录令牌
                    localStorage.setItem("token", res.data.token);
                    localStorage.setItem("userInfo", JSON.stringify(res.data.user));
                    alert("登录成功,正在跳转首页");
                    // 跳转首页,可修改为自己业务地址
                    window.location.href = "/";
                }
            } catch (err) {
                console.error("轮询接口异常:", err);
            }
        }, POLL_INTERVAL);
    }

    /**
     * 4. 初始化微信二维码
     */
    function initWxQr() {
        new window.WxLogin({
            id: containerId,
            appid: APPID,
            scope: "snsapi_login", // 固定:网站应用扫码登录
            redirect_uri: encodeURIComponent(REDIRECT_URI),
            state: state,
            style: "black", // 二维码样式 black / white
            self_redirect: false
        });
    }

    /**
     * 主入口函数
     */
    async function run() {
        try {
            await loadWxScript();
            await getState();
            initWxQr();
            startPoll();
            console.log("微信二维码加载完成,请扫码");
        } catch (e) {
            console.error("登录初始化失败:", e.message);
            alert(e.message);
        }
    }

    // 启动登录逻辑
    run();

    // 页面销毁时清除定时器(防止内存泄漏)
    window.addEventListener("beforeunload", () => {
        if(pollTimer) clearInterval(pollTimer);
    });
})();
</script>
</body>
</html>

3.2 使用说明

  1. 修改顶部配置项APPIDREDIRECT_URI 保持和你业务一致
  2. BASE_URL
    • 前端和后端同域名:留空
    • 前后端分离跨域:填写后端域名 https://www.xxx.com
  3. 页面打开自动加载二维码,微信扫码即可登录
  4. 二维码过期会弹窗提示,刷新页面重新加载

3.3 弹窗版(浮窗二维码)

如果需要居中悬浮二维码(不占用页面布局),替换容器样式即可:

#wx_qr_console_login {
    position: fixed;
    top: 50%;
    left: 50%;
    transform: translate(-50%, -50%);
    padding: 20px;
    background: #fff;
    box-shadow: 0 0 20px #ccc;
    z-index: 99999;
}

四、版本二:Vue 项目接入(Vue2 / Vue3 通用)

分为两种场景:独立登录页弹窗登录,基于 Vue + fetch/axios 实现,推荐使用 axios 统一管理请求。

4.1 前置准备

  1. 项目已安装 axios(前后端分离必备)
    npm install axios --save
    
  2. 全局配置后端接口地址(axios 基础地址)
  3. 路由/页面提前规划好登录页

4.2 Vue 组件完整代码(单文件组件 .vue)

方式1:独立登录页面(Vue2/Vue3 通用)

<template>
  <div class="login-container">
    <div class="login-title">微信扫码登录</div>
    <!-- 微信二维码容器 -->
    <div id="wx_qr_console_login"></div>
  </div>
</template>

<script>
import axios from 'axios';

export default {
  name: "WxLogin",
  data() {
    return {
      // 核心配置
      APPID: "xxxxxxxxxxxxxxxxxxxx",
      REDIRECT_URI: "https://www.xxx.com/api-web/wx/open/callback",
      POLL_INTERVAL: 1500,
      containerId: "wx_qr_console_login",
      state: "",
      pollTimer: null
    }
  },
  mounted() {
    // 页面挂载后执行登录逻辑
    this.initLogin();
  },
  beforeDestroy() {
    // 组件销毁,清除定时器,防止内存泄漏
    if (this.pollTimer) {
      clearInterval(this.pollTimer);
      this.pollTimer = null;
    }
  },
  methods: {
    /**
     * 加载微信官方JS
     */
    loadWxScript() {
      return new Promise((resolve, reject) => {
        if (window.WxLogin) return resolve();
        const script = document.createElement("script");
        script.src = "https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js";
        script.onload = resolve;
        script.onerror = () => reject("微信脚本加载失败");
        document.head.appendChild(script);
      })
    },

    /**
     * 获取后端 state
     */
    async getState() {
      const res = await axios.get("/api-web/wx/open/getQrCodeUrl");
      if (res.data.resultCode !== 200) {
        throw new Error(res.data.message);
      }
      this.state = res.data.data.state;
    },

    /**
     * 初始化微信二维码
     */
    initWxQr() {
      new window.WxLogin({
        id: this.containerId,
        appid: this.APPID,
        scope: "snsapi_login",
        redirect_uri: encodeURIComponent(this.REDIRECT_URI),
        state: this.state,
        style: "black",
        self_redirect: false
      })
    },

    /**
     * 轮询扫码状态
     */
    startPoll() {
      this.pollTimer = setInterval(async () => {
        try {
          const res = await axios.get(`/api-web/wx/open/checkScan`, {
            params: { state: this.state }
          })
          const data = res.data.data;
          
          // 二维码过期
          if (data.status === -1) {
            clearInterval(this.pollTimer);
            this.$message.warning("二维码已过期,请刷新页面");
            return;
          }
          // 登录成功
          if (data.token) {
            clearInterval(this.pollTimer);
            // 存储token(根据你的项目改造:Vuex / localStorage / Pinia)
            localStorage.setItem("token", data.token);
            localStorage.setItem("userInfo", JSON.stringify(data.user));
            this.$message.success("登录成功");
            // 跳转首页
            this.$router.push("/");
          }
        } catch (err) {
          console.error("轮询异常:", err);
        }
      }, this.POLL_INTERVAL)
    },

    /**
     * 入口方法
     */
    async initLogin() {
      try {
        await this.loadWxScript();
        await this.getState();
        this.initWxQr();
        this.startPoll();
      } catch (e) {
        this.$message.error(e.message || "登录初始化失败");
      }
    }
  }
}
</script>

<style scoped>
.login-container {
  width: 100%;
  height: 100vh;
  display: flex;
  flex-direction: column;
  align-items: center;
  justify-content: center;
  background: #f6f6f6;
}
.login-title {
  font-size: 22px;
  color: #333;
  margin-bottom: 30px;
}
</style>

方式2:弹窗形式(点击按钮弹出扫码浮窗)

在原有页面增加按钮,点击弹出居中二维码,核心逻辑不变,仅增加显示/隐藏控制:

<template>
  <div class="page">
    <button @click="openWxLogin">微信扫码登录</button>
    <!-- 二维码浮窗 -->
    <div v-if="showQr" id="wx_qr_console_login" class="qr-box"></div>
  </div>
</template>

<script>
import axios from 'axios';
export default {
  data() {
    return {
      showQr: false,
      APPID: "xxxxxxxxxxxxxxxxxxxx",
      REDIRECT_URI: "https://www.xxxxx.com/api-web/wx/open/callback",
      POLL_INTERVAL: 1500,
      containerId: "wx_qr_console_login",
      state: "",
      pollTimer: null
    }
  },
  beforeDestroy() {
    if (this.pollTimer) clearInterval(this.pollTimer);
  },
  methods: {
    // 打开扫码弹窗
    async openWxLogin() {
      this.showQr = true;
      try {
        await this.loadWxScript();
        await this.getState();
        this.initWxQr();
        this.startPoll();
      } catch (e) {
        this.$message.error(e.message);
        this.showQr = false;
      }
    },
    // 下方方法和上面【独立登录页】完全一致,直接复制
    loadWxScript(){/* 省略,同上 */},
    getState(){/* 省略,同上 */},
    initWxQr(){/* 省略,同上 */},
    startPoll(){/* 省略,同上 */}
  }
}
</script>

<style scoped>
.qr-box {
  position: fixed;
  top: 50%;
  left: 50%;
  transform: translate(-50%, -50%);
  padding: 20px;
  background: #fff;
  box-shadow: 0 0 20px #999;
  z-index: 9999;
}
</style>

4.3 Vue 项目关键改造点

  1. Token 存储
    可根据项目架构替换:
    • 简单项目:localStorage
    • 中大型项目:Vuex / Pinia 全局状态管理
  2. UI 提示
    示例使用 element-ui$message,无UI库可替换为原生 alert

五、通用踩坑 & 问题排查

5.1 二维码无法显示

  1. 检查网络是否能加载微信JS:https://res.wx.qq.com/connect/zh_CN/htmledition/js/wxLogin.js
  2. 容器 id 必须和代码中 containerId 完全一致
  3. 页面存在 z-index 遮挡,提高二维码层级

5.2 微信扫码后跳转空白/回调失败

  1. 最高发问题REDIRECT_URI 和 微信开放平台配置不一致(协议、域名、路径必须一字不差)
  2. 回调地址必须是 HTTPS(微信网站应用强制要求)
  3. 后端接口报错:查看后端日志,检查 AppID / AppSecret 是否正确

5.3 轮询一直等待,不会登录

  1. 后端 checkScan 接口逻辑异常,打印接口返回数据排查
  2. state 在 Redis 过期,延长后端 QR_EXPIRE_SECONDS
  3. 微信 code 只能使用一次,重复扫码会失效

5.4 中文乱码

后端已对 RestTemplate 设置 UTF-8,前端无需额外处理;若仍乱码,检查页面 <meta charset="UTF-8">

5.5 跨域报错

  1. 后端添加 CORS 配置,放行前端域名
  2. 开发环境可使用代理(Vue vue.config.js 配置代理)

六、部署上线注意事项

  1. 上线前确认:微信开放平台 AppID回调地址授权域名 全部配置完成
  2. 后端密钥 AppSecret 禁止前端暴露,全部保留在后端
  3. 定时清除轮询定时器,避免页面常驻定时器造成性能问题
  4. 二维码过期时间、Token 有效期根据业务调整后端 Redis 配置

更多推荐