专业的编程技术博客社区

网站首页 > 博客文章 正文

Spring Boot + Vue 实现微信扫码登录的详细攻略

baijin 2025-07-28 15:11:48 博客文章 5 ℃ 0 评论

一、准备工作

1. 注册微信开放平台账号

  • 访问 微信开放平台,注册开发者账号并完成实名认证。
  • 登录后进入「管理中心」→「网站应用」→「创建网站应用」,填写应用基本信息(如应用名称、图标、简介等)。
  • 配置授权回调域(redirect_uri 的域名,例如 yourdomain.com,需与前端域名一致)。
  • 提交审核,审核通过后获取 AppIDAppSecret(后续接口调用需要)。

2. 前端项目准备

  • 使用 Vue CLI 创建项目(或已有项目),安装依赖:
npm install axios qrcodejs2 --save  # axios用于HTTP请求,qrcodejs2生成二维码

3. 后端项目准备

  • 使用 Spring Initializr 创建 Spring Boot 项目,添加依赖:
<dependencies>
    <!-- Spring Web -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <!-- Redis 缓存(存储扫码状态) -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <!-- Lombok(简化代码) -->
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <!-- JSON 工具 -->
    <dependency>
        <groupId>com.alibaba</groupId>
        <artifactId>fastjson</artifactId>
        <version>1.2.83</version>
    </dependency>
</dependencies>
  • 配置 application.yml,设置 Redis 连接和微信 AppID/AppSecret:
spring:
  redis:
    host: localhost
    port: 6379
    database: 0
    timeout: 5000

wechat:
  appid: 你的AppID
  secret: 你的AppSecret
  redirect-uri: http://yourdomain.com/api/wechat/login/callback  # 回调地址(需与微信开放平台配置一致)

二、核心流程概述

微信扫码登录的核心流程如下(前后端协作):

  1. 前端请求生成二维码:前端调用后端接口,获取微信扫码的临时 URL 和唯一标识(uuid)。
  2. 前端展示二维码:前端使用 uuid 对应的 URL 生成二维码(微信官方提供的扫码页面)。
  3. 用户扫码并确认:用户使用微信扫描二维码,选择「确认登录」。
  4. 微信回调后端:微信服务器向配置的 redirect_uri 发送回调请求(携带 code 和 state)。
  5. 后端验证并生成登录态:后端通过 code 换取 access_token 和 openid,验证用户身份后生成登录态(如 JWT)。
  6. 前端轮询状态:前端定时调用后端接口查询 uuid 对应的扫码状态,若状态为「已确认」则获取登录态,完成登录。

三、前后端具体实现

(一)后端实现

1. 定义扫码状态枚举

public enum WechatLoginStatus {
    NOT_SCAN,    // 未扫码
    SCANNED,     // 已扫码待确认
    CONFIRMED,   // 已确认登录
    EXPIRED      // 已过期
}

2. 生成二维码接口(/api/wechat/qrcode)

功能:生成唯一的 uuid,并将 uuid 与初始状态(NOT_SCAN)存入 Redis(设置过期时间,如 5 分钟);返回微信扫码的临时 URL。

@RestController
@RequestMapping("/api/wechat")
@Slf4j
public class WechatLoginController {

    @Value("${wechat.appid}")
    private String appid;
    @Value("${wechat.secret}")
    private String secret;
    @Value("${wechat.redirect-uri}")
    private String redirectUri;

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    // 生成二维码接口
    @GetMapping("/qrcode")
    public Result getQrCode() {
        // 生成唯一 uuid(作为 state 参数)
        String uuid = UUID.randomUUID().toString();
        // 微信扫码临时 URL(用户扫码后会跳转到该 URL,携带 state=uuid)
        String qrcodeUrl = "https://open.weixin.qq.com/connect/qrconnect?" +
                "appid=" + appid +
                "&redirect_uri=" + URLEncoder.encode(redirectUri, StandardCharsets.UTF_8) +
                "&response_type=code" +
                "&scope=snsapi_login" +
                "&state=" + uuid +
                "#wechat_redirect";
        // 将 uuid 存入 Redis(状态为 NOT_SCAN,过期时间 5 分钟)
        redisTemplate.opsForValue().set("wechat_login:" + uuid, WechatLoginStatus.NOT_SCAN, 5, TimeUnit.MINUTES);
        return Result.success(qrcodeUrl);
    }
}

3. 处理微信回调接口(/api/wechat/login/callback)

功能:微信服务器回调此接口,携带 code 和 state(即之前的 uuid);后端通过 code 换取 access_token 和 openid,验证用户身份后生成登录态,并更新 Redis 中 uuid 的状态为 CONFIRMED。

// 回调接口(需在微信开放平台配置此 URL)
@GetMapping("/login/callback")
public Result handleCallback(String code, String state) {
    log.info("微信回调:code={}, state={}", code, state);
    // 校验 state 是否存在(防止伪造)
    String key = "wechat_login:" + state;
    WechatLoginStatus status = (WechatLoginStatus) redisTemplate.opsForValue().get(key);
    if (status == null || status != WechatLoginStatus.SCANNED) {
        return Result.error("无效的扫码状态");
    }

    // 通过 code 换取 access_token(微信接口)
    String tokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?" +
            "appid=" + appid +
            "&secret=" + secret +
            "&code=" + code +
            "&grant_type=authorization_code";
    ResponseEntity<String> response = new RestTemplate().getForEntity(tokenUrl, String.class);
    JSONObject tokenResult = JSON.parseObject(response.getBody());
    if (tokenResult.containsKey("errcode")) {
        return Result.error("获取 access_token 失败:" + tokenResult.getString("errmsg"));
    }
    String accessToken = tokenResult.getString("access_token");
    String openid = tokenResult.getString("openid");

    // 校验 openid 是否绑定过本系统用户(根据业务需求,可跳过直接注册新用户)
    // 示例:假设 openid 对应系统用户 ID 为 123
    Long userId = userService.getUserIdByOpenid(openid);
    if (userId == null) {
        return Result.error("用户未绑定");
    }

    // 生成登录态(如 JWT)
    String token = JwtUtil.generateToken(userId);

    // 更新 Redis 状态为 CONFIRMED,并存储 token(或其他用户信息)
    redisTemplate.opsForValue().set(key, WechatLoginStatus.CONFIRMED, 30, TimeUnit.MINUTES); // 延长过期时间
    redisTemplate.opsForValue().set("user_token:" + userId, token, 30, TimeUnit.MINUTES);

    return Result.success(token);
}

4. 查询扫码状态接口(/api/wechat/check-status)

功能:前端定时调用此接口,传入 uuid,查询扫码状态(
NOT_SCAN/SCANNED/CONFIRMED/EXPIRED)。

@GetMapping("/check-status/{uuid}")
public Result checkStatus(@PathVariable String uuid) {
    String key = "wechat_login:" + uuid;
    WechatLoginStatus status = (WechatLoginStatus) redisTemplate.opsForValue().get(key);
    if (status == null) {
        return Result.error("二维码已过期");
    }
    return Result.success(status);
}

(二)前端实现

1. 生成并展示二维码(Vue 组件)

使用 qrcodejs2 生成二维码,调用后端 /api/wechat/qrcode 获取临时 URL。

<template>
  <div class="qrcode-container">
    <div id="qrcode" ref="qrcode"></div>
    <p v-if="status === 'NOT_SCAN'">请使用微信扫描二维码登录</p>
    <p v-if="status === 'SCANNED'">请在微信中确认登录</p>
    <p v-if="status === 'CONFIRMED'">登录成功!跳转中...</p>
    <p v-if="status === 'EXPIRED'">二维码已过期,请重新获取</p>
  </div>
</template>

<script>
import QRCode from 'qrcodejs2';
import axios from 'axios';

export default {
  data() {
    return {
      qrcodeUrl: '', // 微信扫码临时 URL
      uuid: '', // 唯一标识
      status: 'NOT_SCAN', // 扫码状态:NOT_SCAN/SCANNED/CONFIRMED/EXPIRED
      qrCodeTimer: null, // 二维码实例(用于销毁)
      checkStatusTimer: null // 轮询状态定时器
    };
  },
  mounted() {
    this.getQrCode();
  },
  methods: {
    // 获取二维码
    async getQrCode() {
      try {
        const res = await axios.get('/api/wechat/qrcode');
        this.qrcodeUrl = res.data.data;
        this.uuid = this.qrcodeUrl.split('state=')[1].split('#')[0]; // 从 URL 中提取 uuid
        this.renderQRCode();
        this.startPolling();
      } catch (error) {
        console.error('获取二维码失败', error);
      }
    },
    // 渲染二维码
    renderQRCode() {
      if (this.qrCodeTimer) {
        this.qrCodeTimer.clear(); // 销毁旧二维码
      }
      this.qrCodeTimer = new QRCode(this.$refs.qrcode, {
        text: this.qrcodeUrl,
        width: 200,
        height: 200
      });
    },
    // 开始轮询状态
    startPolling() {
      this.checkStatusTimer = setInterval(async () => {
        try {
          const res = await axios.get(`/api/wechat/check-status/${this.uuid}`);
          this.status = res.data.data;
          if (this.status === 'CONFIRMED') {
            clearInterval(this.checkStatusTimer);
            this.qrCodeTimer.clear();
            // 登录成功,保存 token 并跳转
            localStorage.setItem('token', res.data.data); // 假设返回的 token 在 data.data 中
            this.$router.push('/home');
          } else if (this.status === 'EXPIRED') {
            clearInterval(this.checkStatusTimer);
            this.qrCodeTimer.clear();
            alert('二维码已过期,请重新获取');
          }
        } catch (error) {
          console.error('查询状态失败', error);
        }
      }, 2000); // 每 2 秒轮询一次
    }
  },
  beforeDestroy() {
    // 组件销毁时清理定时器
    if (this.qrCodeTimer) this.qrCodeTimer.clear();
    if (this.checkStatusTimer) clearInterval(this.checkStatusTimer);
  }
};
</script>

2. 处理登录态(路由守卫)

在 Vue 路由中添加守卫,验证用户是否已登录(通过 localStorage 中的 token):

// router.js
import Vue from 'vue';
import Router from 'vue-router';
import Home from './views/Home.vue';
import Login from './views/Login.vue';

Vue.use(Router);

const router = new Router({
  routes: [
    { path: '/', redirect: '/home' },
    { path: '/home', component: Home, meta: { requiresAuth: true } },
    { path: '/login', component: Login }
  ]
});

router.beforeEach((to, from, next) => {
  const token = localStorage.getItem('token');
  if (to.meta.requiresAuth && !token) {
    next('/login'); // 未登录跳转到扫码登录页
  } else {
    next();
  }
});

export default router;

四、注意事项

  1. 微信开放平台配置
  2. 确保 redirect_uri 已在微信开放平台配置(需与后端回调地址完全一致,包括协议 http/https)。
  3. 应用的「网站应用」需配置正确的「网站地址」(前端域名)。
  4. 安全性
  5. state 参数需使用随机字符串(如 UUID),防止 CSRF 攻击。
  6. 微信回调的 code 只能使用一次,需及时换取 access_token。
  7. 敏感信息(如 AppSecret)需加密存储,避免硬编码在代码中。
  8. 缓存管理
  9. Redis 中存储的 uuid 需设置合理的过期时间(建议 5-10 分钟),避免内存泄漏。
  10. 分布式系统中需使用共享缓存(如 Redis 集群),确保多实例后端能访问同一缓存。
  11. 用户体验
  12. 前端轮询间隔建议设置为 2-3 秒,避免频繁请求。
  13. 二维码过期后需提供「刷新」按钮,重新调用 /api/wechat/qrcode 获取新二维码。
  14. 用户绑定
  15. 若系统需要用户先注册/绑定微信,可在回调接口中根据 openid 判断用户是否存在,不存在则跳转到绑定页面(需传递 state 或 uuid 关联绑定流程)。

通过以上步骤,即可实现 Spring Boot + Vue 的微信扫码登录功能。核心是理解微信扫码登录的流程,并正确处理前后端的交互和状态管理。

Tags:

本文暂时没有评论,来添加一个吧(●'◡'●)

欢迎 发表评论:

最近发表
标签列表