微信网页扫码登录全链路技术文档

微信网页扫码登录全链路技术文档

一、 核心概念与角色

  • 临时票据 (Code):用户扫码授权后,微信发给系统的临时凭证(有效期极短,且只能用一次)。
  • 授权状态 (State):由系统生成的随机字符串,用于关联“发起请求”和“接收回调”,防止 CSRF 攻击。
  • 回调地址 (Redirect URI):用户扫码后,浏览器重定向的目标。

二、 二维码生成方式对比

生成方式决定了用户的视觉体验,但不决定后端逻辑。

生成方式 实现机制 优点 缺点
JS 脚本 (内嵌式) 引入 wxLogin.js,在页面指定 div 中渲染 iframe。 用户无需离开当前页面,体验平滑。 需要处理 iframe 样式兼容性。
后端构造 URL (跳转式) 后端拼接授权链接,前端执行 window.location.href 简单直接,适用于移动端 H5 或独立登录页。 用户会跳离当前网站。

三、 方案 A 与 方案 B 的本质区别

这是架构设计的核心选择,决定了 code 的归宿。

1. 方案 A:直接回调后端 (推荐)

  • 路径:微信服务器 -> 后端接口 (Spring Security 拦截)。
  • 前端角色:仅作为“结果接收者”。
  • 实现逻辑
    1. redirect_uri 指向后端(如 /login/oauth2/code/wechat)。
    2. Spring Security 自动获取 code 并换取 token
    3. 后端通过 successHandler 执行 response.sendRedirect 将自定义 Token 带回前端。
  • 适用场景:追求高安全规范、希望利用 Spring Security 自动化能力的场景。

2. 方案 B:回调前端页面

  • 路径:微信服务器 -> 前端路由 -> 后端接口 (Axios 发送)。
  • 前端角色:作为“中转调度员”。
  • 实现逻辑
    1. redirect_uri 指向前端路由(如 /#/auth-callback)。
    2. 前端从 URL 截取 code,再通过 API 发送给后端。
    3. 后端仅作为一个普通接口接收 code 并换取用户信息。
  • 适用场景:需要前端完全控制登录动画、或已有成熟的移动端/多端登录逻辑。

四、 安全参数 state 的管理与校验

state 是防止攻击的关键。其管理逻辑随方案不同而变化。

1. 在“直接回调后端 (方案 A)”中:

  • 获取:Spring Security 自动生成并存入 Session
  • 校验:回调时,框架自动比对 URL 中的 state 与 Session 中的值。
  • 注意:在前后端分离架构下,需解决 Session/Cookie 跨域丢失 问题(配置 CORS 允许 Credentials)。

2. 在“回调前端页面 (方案 B)”中:

  • 获取:前端手动生成(随机串)或请求后端生成。
  • 存储:存入 localStorage (前端校验) 或 Redis (后端校验)。
  • 校验:后端收到前端传来的 codestate 后,去 Redis 比对。

五、 多平台登录扩展 (策略模式)

当需要支持微信、GitHub、Google 等多个三方登录时,建议在方案 A 的 successHandler 中采用策略模式:

  1. 识别平台:通过 registrationId 区分来源。

  2. 分发策略:定义 OAuth2UserHandler 接口,为不同平台编写 extractUserId 实现类。

  3. 统一处理:最终统一生成系统的 JWT 并重定向回前端。

六、 综合工程代码实现 (四种组合方案)

1. 后端:Spring Boot 核心逻辑

Java

@RestController
    @RequestMapping("/api/auth")
    public class AuthController {
        /**
         * 【功能 1】:为“跳转式”生成授权 URL (适配方案 A & B)
         *
         * @param scheme
         * @return
         */
        @GetMapping("/wechat-url")
        public Map<String, String> getWechatUrl(@RequestParam String scheme) {
            String baseUrl = "https://open.weixin.qq.com/connect/qrconnect";
            // 方案 A 指向后端,方案 B 指向前端
            String redirectUri = "A".equals(scheme) ? "http://api.demo.com/login/oauth2/code/wechat" : "http://www.demo.com/#/auth-callback";
            String state = UUID.randomUUID().toString().substring(0, 8);
            // 注意:生产环境此处需将 state 存入 Redis
            String url = String.format("%s?appid=%s&redirect_uri=%s&response_type=code&scope=snsapi_login&state=%s#wechat_redirect", baseUrl, "YOUR_APPID", URLEncoder.encode(redirectUri, StandardCharsets.UTF_8), state);
            return Collections.singletonMap("url", url);
        }

        /**
         * 【功能 2】:方案 B 的手动换码接口
         *
         * @param payload
         * @return
         */
        @PostMapping("/wechat/login")
        public ResponseEntity<?> handleSchemeB(@RequestBody Map<String, String> payload) {
            String code = payload.get("code");
            String state = payload.get("state");
            // 1. 校验 state (从 Redis 取)
            // 2. 调用微信 API 换取 OpenID
            // 3. 生成并返回 JWT
            return ResponseEntity.ok(Collections.singletonMap("token", "jwt_for_" + code));
        }
    }

2. 前端:Vue 3 核心逻辑

Login.vue (二维码展示页)

<script setup> 
import { onMounted } from 'vue'; 
import axios from 'axios';  

onMounted(() => {   
  // 组合 1:JS脚本 + 方案A (直接回后端) 
    new WxLogin({     
      id: "qr_a",     
      appid: "YOUR_APPID",     
      redirect_uri: encodeURIComponent("http://api.demo.com/login/oauth2/code/wechat"),     
      state: "security_state_a", 
      // 建议从后端动态获取     
      style: "black"   
    });  

    // 组合 2:JS脚本 + 方案B (回前端中转)  
    new WxLogin({    
      id: "qr_b",    
      appid: "YOUR_APPID",   
      redirect_uri: encodeURIComponent("http://www.demo.com/#/auth-callback"),  
      state: "security_state_b",   
    }); 
});  
               
// 方式 2:后端构造 URL 跳转方式 (方案 A 或 B) 
const jumpToWechat = async (scheme) => 
{
  const { data } = await axios.get(/api/auth/wechat-url?scheme=${scheme});   
  window.location.href = data.url; 
}; 
</script>  

<template>   
  <div class="login-box">    
    <div id="qr_a"></div> 
    <div id="qr_b"></div> 
    <button @click="jumpToWechat('A')">跳转方式A</button>  
  </div> 
</template>

AuthCallback.vue (方案 B 专属中转页)

<script setup> 
import { onMounted } from 'vue'; 
import { useRoute, useRouter } from 'vue-router'; 
import axios from 'axios'; 

const route = useRoute(); 
const router = useRouter();  
onMounted(async () => {   
  const { code, state } = route.query;   
  if (code) {     
  // 核心逻辑:手动搬运 code 到后端换取真正的登录凭证    
    const res = await axios.post('/api/auth/wechat/login', { code, state });     
    localStorage.setItem('token', res.data.token);
    router.push('/dashboard');   
  } 
}); 
</script>


七、 常见配置陷阱清单

  1. 域名授权:微信后台配置的“授权回调域”必须与 redirect_uri 的域名严格一致。
  2. URL 编码redirect_uri 作为参数传递时,必须进行 encodeURIComponent 处理。
  3. HTTPS 限制:部分微信接口要求回调地址必须为 HTTPS。
  4. SameSite 属性:现代浏览器对跨域 Cookie 限制严格,方案 A 需关注 Set-Cookie: SameSite=None; Secure
posted @ 2026-07-11 11:13  liftsail  阅读(35)  评论(0)    收藏  举报