实现 PC 端前后分离微信二维码扫码登录全攻略

在现代应用开发中,便捷的登录方式是提升用户体验的关键。扫码登录凭借其便捷性和高普及率,成为众多应用的首选。本文将详细介绍如何在 RuoYi-Vue 框架中实现 PC 端前后分离的二维码扫码登录功能,涵盖技术原理、详细步骤与代码示例。

一、技术背景与原理

RuoYi-Vue 是基于 Spring Boot、Vue 的前后端分离权限管理系统。扫码登录基于 OAuth2.0 协议,流程大致如下:

  1. 生成二维码:应用向服务器请求生成带有唯一标识(state)的二维码 URL,用户扫码后,将用户重定向到应用指定的回调地址,并携带授权码(code)。

  2. 获取授权信息:应用使用授权码和自身的 appId、secret,向服务器换取 access_token 和 openid,openid 是用户在开放平台的唯一标识。

  3. 用户认证与登录:应用根据 openid 判断用户是否已注册,若已注册则直接登录;若未注册,可创建新用户并关联 openid,最终完成登录流程。

二、后端实现步骤

1. 配置文件添加相关配置

在​​src/main/resources/application.yml​​中添加配置信息,需替换为在开放平台申请的真实信息:

perl 复制代码
# 配置
wechat:
  appId: your_app_id
  secret: your_app_secret
  qrcodeUrl: https://open.weixin.qq.com/connect/qrconnect?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_login&state=%s#wechat_redirect

其中​​qrcodeUrl​​是提供的二维码生成接口,包含应用 ID、回调地址和状态标识。

2. 创建二维码服务接口与实现类

接口定义 :在​​com.ruoyi.framework.web.service​​​包下创建​​WeChatQrCodeService​​接口,定义生成二维码、检查状态和处理回调的方法:

typescript 复制代码
package com.ruoyi.framework.web.service;

import java.util.Map;

public interface WeChatQrCodeService {
    /**
     * 生成登录二维码
     * @return 包含二维码URL和唯一标识的Map
     */
    Map<String, Object> generateQrCode();

    /**
     * 检查二维码扫描状态
     * @param uuid 二维码唯一标识
     * @return 扫描状态结果
     */
    Map<String, Object> checkQrCodeStatus(String uuid);

    /**
     * 登录回调处理
     * @param code 授权码
     * @param uuid 二维码唯一标识
     * @return 登录结果
     */
    Map<String, Object> weChatLoginCallback(String code, String uuid);
}

实现类 :在​​com.ruoyi.framework.web.service.impl​​​包下创建​​WeChatQrCodeServiceImpl​​实现类,实现接口方法,核心代码如下:

java 复制代码
package com.ruoyi.framework.web.service.impl;

import com.alibaba.fastjson.JSONObject;
import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.common.core.domain.entity.SysUser;
import com.ruoyi.common.core.redis.RedisCache;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.manager.AsyncManager;
import com.ruoyi.framework.manager.factory.AsyncFactory;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.security.service.TokenService;
import com.ruoyi.framework.web.service.SysLoginService;
import com.ruoyi.framework.web.service.WeChatQrCodeService;
import com.ruoyi.system.service.ISysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import org.springframework.web.client.RestTemplate;

import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.concurrent.TimeUnit;

@Service
public class WeChatQrCodeServiceImpl implements WeChatQrCodeService {
    @Value("${wechat.appId}")
    private String appId;
    @Value("${wechat.secret}")
    private String secret;
    @Value("${wechat.qrcodeUrl}")
    private String qrcodeUrl;

    @Autowired
    private RedisCache redisCache;
    @Autowired
    private TokenService tokenService;
    @Autowired
    private SysLoginService loginService;
    @Autowired
    private ISysUserService userService;
    @Autowired
    private RestTemplate restTemplate;

    private static final String QRCODE_PREFIX = "wechat:qrcode:";
    private static final String QRCODE_SCANNED = "scanned";
    private static final String QRCODE_CONFIRMED = "confirmed";
    private static final String QRCODE_EXPIRED = "expired";

    @Override
    public Map<String, Object> generateQrCode() {
        String uuid = UUID.randomUUID().toString();
        String qrcodeKey = QRCODE_PREFIX + uuid;

        // 生成二维码URL,这里使用模拟URL,实际应调用接口
        String qrCodeUrl = String.format(qrcodeUrl, appId, uuid);

        // 将二维码信息存入Redis,有效期5分钟
        Map<String, Object> qrCodeInfo = new HashMap<>();
        qrCodeInfo.put("status", "pending");
        qrCodeInfo.put("createTime", System.currentTimeMillis());
        redisCache.setCacheMap(qrcodeKey, qrCodeInfo, 5, TimeUnit.MINUTES);

        Map<String, Object> result = new HashMap<>();
        result.put("uuid", uuid);
        result.put("qrCodeUrl", qrCodeUrl);
        return result;
    }

    @Override
    public Map<String, Object> checkQrCodeStatus(String uuid) {
        String qrcodeKey = QRCODE_PREFIX + uuid;
        Map<Object, Object> qrCodeInfo = redisCache.getCacheMap(qrcodeKey);

        if (qrCodeInfo == null) {
            Map<String, Object> result = new HashMap<>();
            result.put("status", QRCODE_EXPIRED);
            return result;
        }

        return new HashMap<>(qrCodeInfo);
    }

    @Override
    public Map<String, Object> weChatLoginCallback(String code, String uuid) {
        if (StringUtils.isEmpty(code) || StringUtils.isEmpty(uuid)) {
            throw new ServiceException("授权码或UUID不能为空");
        }

        String qrcodeKey = QRCODE_PREFIX + uuid;
        Map<Object, Object> qrCodeInfo = redisCache.getCacheMap(qrcodeKey);

        if (qrCodeInfo == null ||!"pending".equals(qrCodeInfo.get("status"))) {
            throw new ServiceException("二维码已过期或状态异常");
        }

        // 获取access_token和openid
        String accessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token" +
                "?appid=" + appId +
                "&secret=" + secret +
                "&code=" + code +
                "&grant_type=authorization_code";

        String response = restTemplate.getForObject(accessTokenUrl, String.class);
        JSONObject jsonObject = JSONObject.parseObject(response);

        if (jsonObject.containsKey("errcode")) {
            throw new ServiceException("获取授权信息失败: " + jsonObject.getString("errmsg"));
        }

        String openid = jsonObject.getString("openid");
        String accessToken = jsonObject.getString("access_token");

        // 根据openid查询用户
        SysUser user = userService.selectUserByOpenid(openid);

        if (user == null) {
            // 用户首次使用登录,创建新用户
            user = createWeChatUser(openid, accessToken);
        }

        // 更新二维码状态为已确认
        qrCodeInfo.put("status", QRCODE_CONFIRMED);
        qrCodeInfo.put("userId", user.getUserId());
        redisCache.setCacheMap(qrcodeKey, qrCodeInfo);

        // 记录登录信息
        AsyncManager.me().execute(AsyncFactory.recordLogininfor(user.getUserName(), "扫码登录", "成功", ""));

        // 生成token
        LoginUser loginUser = new LoginUser();
        loginUser.setUser(user);
        String token = tokenService.createToken(loginUser);

        Map<String, Object> result = new HashMap<>();
        result.put("token", token);
        result.put("userInfo", user);
        return result;
    }

    private SysUser createWeChatUser(String openid, String accessToken) {
        // 获取用户信息
        String userInfoUrl = "https://api.weixin.qq.com/sns/userinfo" +
                "?access_token=" + accessToken +
                "&openid=" + openid;

        String response = restTemplate.getForObject(userInfoUrl, String.class);
        JSONObject userInfo = JSONObject.parseObject(response);

        if (userInfo.containsKey("errcode")) {
            throw new ServiceException("获取用户信息失败: " + userInfo.getString("errmsg"));
        }

        // 创建新用户
        SysUser user = new SysUser();
        user.setUserName("wx_" + openid.substring(0, 8));
        user.setNickName(userInfo.getString("nickname"));
        user.setOpenid(openid);
        user.setAvatar(userInfo.getString("headimgurl"));
        user.setSex(userInfo.getString("sex"));
        user.setCreateBy("system");
        
        // 默认普通用户角色
        user.setRoleIds(new Long[]{2L});

        // 插入用户
        userService.insertUser(user);
        
        return user;
    }
}

3. 登录控制器添加登录接口

在​​com.ruoyi.web.controller.system.SysLoginController​​中添加登录相关接口:

typescript 复制代码
package com.ruoyi.web.controller.system;

import com.ruoyi.common.core.domain.AjaxResult;
import com.ruoyi.framework.web.service.WeChatQrCodeService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.Map;

@RestController
@RequestMapping("/api")
public class SysLoginController {
    @Autowired
    private WeChatQrCodeService weChatQrCodeService;

    /**
     * 生成登录二维码
     */
    @GetMapping("/wechat/qrcode")
    public AjaxResult generateWeChatQrCode() {
        Map<String, Object> result = weChatQrCodeService.generateQrCode();
        return AjaxResult.success(result);
    }

    /**
     * 检查二维码状态
     */
    @GetMapping("/wechat/qrcode/status")
    public AjaxResult checkWeChatQrCodeStatus(@RequestParam String uuid) {
        Map<String, Object> result = weChatQrCodeService.checkQrCodeStatus(uuid);
        return AjaxResult.success(result);
    }

    /**
     * 登录回调
     */
    @PostMapping("/wechat/login/callback")
    public AjaxResult weChatLoginCallback(@RequestParam String code, @RequestParam String uuid) {
        Map<String, Object> result = weChatQrCodeService.weChatLoginCallback(code, uuid);
        return AjaxResult.success(result);
    }
}

三、前端实现步骤

1. 封装登录 API

在​​src/api/login.js​​中添加登录相关接口:

php 复制代码
import request from '@/utils/request'

// 生成二维码
export function generateWeChatQrCode() {
  return request({
    url: '/api/wechat/qrcode',
    method: 'get'
  })
}

// 检查二维码状态
export function checkWeChatQrCodeStatus(uuid) {
  return request({
    url: '/api/wechat/qrcode/status',
    method: 'get',
    params: { uuid }
  })
}

// 登录回调
export function weChatLoginCallback(code, uuid) {
  return request({
    url: '/api/wechat/login/callback',
    method: 'post',
    params: { code, uuid }
  })
}

2. 登录页面扩展扫码登录功能

在​​src/views/login/index.vue​​中添加扫码登录选项卡,核心代码如下:

xml 复制代码
<template>
  <div class="login-container">
    <el-tabs v-model="activeTab">
      <el-tab-pane label="账号密码登录">
        <!-- 账号密码登录表单 -->
      </el-tab-pane>
      <el-tab-pane label="扫码登录">
        <div id="wechat-login-container"></div>
      </el-tab-pane>
    </el-tabs>
  </div>
</template>

<script>
import { generateWeChatQrCode, checkWeChatQrCodeStatus } from '@/api/login'

export default {
  data() {
    return {
      activeTab: 'account',
      qrcodeData: null,
      checkInterval: null
    }
  },
  mounted() {
    // 监听选项卡切换,切换到扫码登录时生成二维码
    this.$watch('activeTab', (newVal) => {
      if (newVal === 'wechat') {
        this.generateQrCode()
      }
    })
  },
  methods: {
    async generateQrCode() {
      try {
        const res = await generateWeChatQrCode()
        if (res.code === 200) {
          this.qrcodeData = res.data
          this.showQrCode()
        }
      } catch (error) {
        console.error('生成二维码失败', error)
      }
    },
    showQrCode() {
      const container = document.getElementById('wechat-login-container')
      if (!container) return
      container.innerHTML = `
        <div class="wechat-qrcode-container">
          <div class="qrcode-box">
            <img src="${this.qrcodeData.qrCodeUrl}" alt="登录二维码" class="wechat-qrcode-img">
          </div>
          <p class="qrcode-status">请使用扫描二维码登录</p>
          <p class="qrcode-tip">扫码后请在手机上确认登录</p>
        </div>
      `
      this.startCheckingStatus()
    },
    startCheckingStatus() {
      const that = this
      this.checkInterval = setInterval(() => {
        checkWeChatQrCodeStatus(this.qrcodeData.uuid).then((res) => {
          if (res.code === 200) {
            const status = res.data.status
            const statusElement = document.querySelector('.qrcode-status')
            if (status ==='scanned') {
              statusElement.textContent = '已扫码,请在手机上确认登录'
              statusElement.classList.add('status-scanned')
            } else if (status === 'confirmed') {
              statusElement.textContent = '登录成功,正在跳转...'
              statusElement.classList.add('status-confirmed')
              clearInterval(that.checkInterval)
              // 保存token并跳转
              this.handleLoginSuccess(res.data.token)
            } else if (status === 'expired') {
              statusElement.textContent = '二维码已过期,请刷新页面'
              statusElement.classList.add('status-expired')
              clearInterval(that.checkInterval)
            }
          }
        }).catch(() => {
          // 忽略错误,继续检查
        })
      }, 2000)
    },
    handleLoginSuccess(token) {
      // 保存token到localStorage
      localStorage.setItem('token', token)
      // 跳转到首页
      this.$router.push({ path: '/' })
    }
  }
}
</script>

3. 用户模块扩展 openid 管理

在​​src/store/modules/user.js​​中扩展用户模块,添加 openid 相关状态管理:

javascript 复制代码
// 在state中添加wechatOpenid
state: {
  token: getToken(),
  name: '',
  avatar: '',
  roles: [],
  permissions: [],
  wechatOpenid: '' // openid
},

// 在getters中添加wechatOpenid
getters: {
  token: state => state.token,
  name: state => state.name,
  avatar: state => state.avatar,
  roles: state => state.roles,
  permissions: state => state.permissions,
  wechatOpenid: state => state.wechatOpenid // openid
},

// 在actions的getInfo方法中添加wechatOpenid
getInfo({ commit, state }) {
  return new Promise((resolve, reject) => {
    getInfo(state.token).then(response => {
      const { user, roles, permissions } = response;
      
      if (!user) {
        reject('验证失败,请重新登录');
      }
      
      commit('SET_ROLES', roles);
      commit('SET_PERMISSIONS', permissions);
      commit('SET_NAME', user.userName);
      commit('SET_AVATAR', user.avatar);
      commit('SET_WECHAT_OPENID', user.wechatOpenid || ''); // 添加openid
      resolve(response);
    }).catch(error => {
      reject(error);
    });
  });
},

// 添加mutations
mutations: {
  // 原有mutations...
  
  // 设置openid
  SET_WECHAT_OPENID: (state, wechatOpenid) => {
    state.wechat
相关推荐
Victor3561 分钟前
MongoDB(2)MongoDB与传统关系型数据库的主要区别是什么?
后端
JaguarJack2 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端·php·服务端
BingoGo2 分钟前
PHP 应用遭遇 DDoS 攻击时会发生什么 从入门到进阶的防护指南
后端
Victor3563 分钟前
MongoDB(3)什么是文档(Document)?
后端
牛奔2 小时前
Go 如何避免频繁抢占?
开发语言·后端·golang
想用offer打牌7 小时前
MCP (Model Context Protocol) 技术理解 - 第二篇
后端·aigc·mcp
KYGALYX8 小时前
服务异步通信
开发语言·后端·微服务·ruby
掘了8 小时前
「2025 年终总结」在所有失去的人中,我最怀念我自己
前端·后端·年终总结
爬山算法9 小时前
Hibernate(90)如何在故障注入测试中使用Hibernate?
java·后端·hibernate
Moment9 小时前
富文本编辑器在 AI 时代为什么这么受欢迎
前端·javascript·后端