我将为你整理一篇完整的微信扫码登录实现博客文章。
Django + Vue 微信扫码登录完整实现指南
前言
微信扫码登录是PC端常用的第三方登录方式,本文将详细介绍如何基于Django REST Framework和Vue 3实现完整的前后端分离微信扫码登录方案。需要注意的是, 微信扫码登录需要企业资质的微信开放平台账号,个人开发者无法申请。
一、准备工作
1.1 注册微信开放平台账号
访问 微信开放平台,注册企业账号并完成认证。
1.2 创建网站应用
在管理中心创建"网站应用",填写网站信息并通过审核。审核通过后获得:
- AppID
- AppSecret
1.3 配置授权回调域
在网站应用的"接口权限"中设置授权回调域,例如:your-domain.com
二、后端实现(Django)
2.1 项目配置
python
# settings.py
INSTALLED_APPS = [
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'rest_framework',
'corsheaders',
'wechat_auth',
]
# 微信开放平台配置
WECHAT_OPEN_APP_ID = 'your_app_id'
WECHAT_OPEN_APP_SECRET = 'your_app_secret'
WECHAT_REDIRECT_URI = 'http://your-domain.com/api/wechat/callback/'
# CORS配置
CORS_ALLOWED_ORIGINS = [
"http://localhost:8080",
"http://localhost:3000",
]
# 缓存配置(开发环境)
CACHES = {
'default': {
'BACKEND': 'django.core.cache.backends.locmem.LocMemCache',
}
}2.2 自定义用户模型
python
# models.py
import uuid
from django.contrib.auth.models import AbstractUser, Group, Permission
from django.db import models
class User(AbstractUser):
id = models.UUIDField(primary_key=True, default=uuid.uuid4, editable=False)
openid = models.CharField(max_length=100, unique=True, null=True, blank=True)
unionid = models.CharField(max_length=100, unique=True, null=True, blank=True)
nickname = models.CharField(max_length=100, blank=True)
avatar_url = models.URLField(blank=True)
phone = models.CharField(max_length=20, blank=True)
is_wechat_bound = models.BooleanField(default=False)
# 解决权限冲突
groups = models.ManyToManyField(
Group,
related_name='wechat_user_groups',
blank=True,
)
user_permissions = models.ManyToManyField(
Permission,
related_name='wechat_user_permissions',
blank=True,
)
class Meta:
db_table = 'users'2.3 微信API工具类
python
# wechat_utils.py
import requests
import uuid
from django.core.cache import cache
from django.conf import settings
class WeChatOpenAPI:
"""微信开放平台API封装"""
BASE_URL = "https://api.weixin.qq.com"
@classmethod
def get_access_token(cls, code):
"""通过code获取access_token"""
url = f"{cls.BASE_URL}/sns/oauth2/access_token"
params = {
'appid': settings.WECHAT_OPEN_APP_ID,
'secret': settings.WECHAT_OPEN_APP_SECRET,
'code': code,
'grant_type': 'authorization_code'
}
response = requests.get(url, params=params)
data = response.json()
if 'errcode' in data:
raise Exception(f"WeChat API Error: {data.get('errmsg')}")
return data
@classmethod
def get_user_info(cls, access_token, openid):
"""获取用户信息"""
url = f"{cls.BASE_URL}/sns/userinfo"
params = {
'access_token': access_token,
'openid': openid,
'lang': 'zh_CN'
}
response = requests.get(url, params=params)
return response.json()
class QRCodeManager:
"""二维码场景管理"""
SCENE_PREFIX = "wechat_qr_scene_"
EXPIRE_SECONDS = 300
@classmethod
def generate_scene_id(cls):
return str(uuid.uuid4()).replace('-', '')[:16]
@classmethod
def create_scene(cls, scene_id):
cache_key = f"{cls.SCENE_PREFIX}{scene_id}"
cache.set(cache_key, {
'status': 'pending',
'user_id': None,
}, cls.EXPIRE_SECONDS)
return scene_id
@classmethod
def get_scene(cls, scene_id):
cache_key = f"{cls.SCENE_PREFIX}{scene_id}"
return cache.get(cache_key)
@classmethod
def update_scene(cls, scene_id, **kwargs):
cache_key = f"{cls.SCENE_PREFIX}{scene_id}"
data = cache.get(cache_key) or {}
data.update(kwargs)
cache.set(cache_key, data, cls.EXPIRE_SECONDS)
return data2.4 JWT工具函数
python
# jwt_utils.py
import jwt
import uuid
from datetime import datetime, timedelta
from django.conf import settings
def generate_tokens(user):
"""生成JWT token"""
now = datetime.utcnow()
access_payload = {
'token_type': 'access',
'user_id': str(user.id),
'username': user.username,
'exp': now + timedelta(hours=24),
'iat': now,
'jti': str(uuid.uuid4())
}
refresh_payload = {
'token_type': 'refresh',
'user_id': str(user.id),
'exp': now + timedelta(days=7),
'iat': now,
'jti': str(uuid.uuid4())
}
access_token = jwt.encode(access_payload, settings.SECRET_KEY, algorithm='HS256')
refresh_token = jwt.encode(refresh_payload, settings.SECRET_KEY, algorithm='HS256')
return {
'access': access_token,
'refresh': refresh_token
}2.5 核心视图实现
python
# views.py
import qrcode
import io
import base64
from datetime import datetime, timedelta
from django.conf import settings
from rest_framework.views import APIView
from rest_framework.response import Response
from rest_framework.permissions import AllowAny, IsAuthenticated
from .models import User, WechatQRCode
from .wechat_utils import WeChatOpenAPI, QRCodeManager
from .jwt_utils import generate_tokens
class GenerateQRCodeView(APIView):
"""生成微信登录二维码"""
permission_classes = [AllowAny]
def get(self, request):
scene_id = QRCodeManager.generate_scene_id()
QRCodeManager.create_scene(scene_id)
expires_at = datetime.now() + timedelta(seconds=300)
WechatQRCode.objects.create(
scene_id=scene_id,
expires_at=expires_at,
status='pending'
)
# 构造微信授权URL(网站应用必须使用snsapi_login)
redirect_uri = settings.WECHAT_REDIRECT_URI
scope = "snsapi_login" # 网站应用专用
wechat_auth_url = (
f"https://open.weixin.qq.com/connect/qrconnect?"
f"appid={settings.WECHAT_OPEN_APP_ID}"
f"&redirect_uri={redirect_uri}"
f"&response_type=code"
f"&scope={scope}"
f"&state={scene_id}#wechat_redirect"
)
# 生成二维码图片
qr = qrcode.QRCode(version=1, box_size=10, border=4)
qr.add_data(wechat_auth_url)
qr.make(fit=True)
img = qr.make_image(fill_color="black", back_color="white")
buffer = io.BytesIO()
img.save(buffer, format='PNG')
qr_base64 = base64.b64encode(buffer.getvalue()).decode()
return Response({
'scene_id': scene_id,
'qr_code': f'data:image/png;base64,{qr_base64}',
'expires_in': 300,
'auth_url': wechat_auth_url
})
class CheckQRStatusView(APIView):
"""轮询检查二维码状态"""
permission_classes = [AllowAny]
def get(self, request):
scene_id = request.query_params.get('scene_id')
if not scene_id:
return Response({'error': 'Scene ID required'}, status=400)
scene_data = QRCodeManager.get_scene(scene_id)
if not scene_data:
try:
qr_record = WechatQRCode.objects.get(scene_id=scene_id)
if qr_record.status == 'confirmed' and qr_record.user:
tokens = generate_tokens(qr_record.user)
return Response({
'status': 'confirmed',
'access': tokens['access'],
'refresh': tokens['refresh'],
'user': {
'id': str(qr_record.user.id),
'username': qr_record.user.username,
'nickname': qr_record.user.nickname,
'avatar_url': qr_record.user.avatar_url,
}
})
return Response({'status': qr_record.status})
except WechatQRCode.DoesNotExist:
return Response({'status': 'expired'})
return Response({
'status': scene_data.get('status', 'pending')
})
class WechatCallbackView(APIView):
"""微信回调处理"""
permission_classes = [AllowAny]
def get(self, request):
code = request.query_params.get('code')
state = request.query_params.get('state')
if not code:
return Response({'error': 'Authorization failed'}, status=400)
try:
# 获取access_token和openid
token_data = WeChatOpenAPI.get_access_token(code)
access_token = token_data['access_token']
openid = token_data['openid']
unionid = token_data.get('unionid')
# 获取用户信息
user_info = WeChatOpenAPI.get_user_info(access_token, openid)
# 查找或创建用户
user, created = User.objects.get_or_create(
openid=openid,
defaults={
'unionid': unionid,
'username': f'wx_{openid[:8]}',
'nickname': user_info.get('nickname', ''),
'avatar_url': user_info.get('headimgurl', ''),
'is_wechat_bound': True
}
)
if not created:
user.nickname = user_info.get('nickname', user.nickname)
user.avatar_url = user_info.get('headimgurl', user.avatar_url)
user.save()
# 更新场景状态
QRCodeManager.update_scene(state, status='confirmed', user_id=str(user.id))
WechatQRCode.objects.filter(scene_id=state).update(
status='confirmed',
user=user
)
# 生成token
tokens = generate_tokens(user)
return Response({
'success': True,
'access': tokens['access'],
'refresh': tokens['refresh'],
'user': {
'id': str(user.id),
'username': user.username,
'nickname': user.nickname,
'avatar_url': user.avatar_url,
}
})
except Exception as e:
QRCodeManager.update_scene(state, status='expired')
return Response({'error': str(e)}, status=500)2.6 URL配置
python
# urls.py
from django.urls import path
from .views import *
urlpatterns = [
path('qrcode/generate/', GenerateQRCodeView.as_view(), name='generate_qr'),
path('qrcode/status/', CheckQRStatusView.as_view(), name='check_status'),
path('callback/', WechatCallbackView.as_view(), name='wechat_callback'),
]三、前端实现(Vue 3)
3.1 Axios封装
javascript
// utils/request.js
import axios from 'axios'
const request = axios.create({
baseURL: '/api',
timeout: 10000
})
// 请求拦截器
request.interceptors.request.use(config => {
const token = localStorage.getItem('access_token')
if (token) {
config.headers.Authorization = `Bearer ${token}`
}
return config
})
// 响应拦截器
request.interceptors.response.use(
response => response,
async error => {
const originalRequest = error.config
if (error.response?.status === 401 && !originalRequest._retry) {
originalRequest._retry = true
try {
const refreshToken = localStorage.getItem('refresh_token')
const response = await axios.post('/api/auth/refresh/', {
refresh: refreshToken
})
const {access} = response.data
localStorage.setItem('access_token', access)
originalRequest.headers.Authorization = `Bearer ${access}`
return request(originalRequest)
} catch (refreshError) {
localStorage.clear()
window.location.href = '/login'
return Promise.reject(refreshError)
}
}
return Promise.reject(error)
}
)
export default request3.2 登录组件
vue
<!-- WechatLogin.vue -->
<template>
<div class="wechat-login">
<div class="login-container">
<h2>微信扫码登录</h2>
<div class="qr-section" v-if="!isLoggedIn">
<div v-if="loading" class="loading">加载中...</div>
<div v-else-if="qrCodeUrl" class="qr-wrapper">
<img :src="qrCodeUrl" alt="微信登录二维码" class="qr-image"/>
<p class="qr-tip">{{ statusText }}</p>
<button
v-if="status === 'expired'"
@click="refreshQRCode"
class="refresh-btn"
>
刷新二维码
</button>
</div>
</div>
<div v-else class="success-section">
<img :src="userInfo.avatar_url" class="avatar"/>
<h3>欢迎,{{ userInfo.nickname || userInfo.username }}</h3>
<button @click="logout" class="logout-btn">退出登录</button>
</div>
</div>
</div>
</template>
<script setup>
import {ref, onMounted, onUnmounted} from 'vue'
import request from '@/utils/request'
const loading = ref(false)
const qrCodeUrl = ref('')
const sceneId = ref('')
const status = ref('pending')
const statusText = ref('请使用微信扫一扫登录')
const isLoggedIn = ref(false)
const userInfo = ref(null)
let pollTimer = null
// 生成二维码
const generateQRCode = async () => {
loading.value = true
try {
const {data} = await request.get('/wechat/qrcode/generate/')
qrCodeUrl.value = data.qr_code
sceneId.value = data.scene_id
status.value = 'pending'
statusText.value = '请使用微信扫一扫登录'
startPolling()
} catch (error) {
console.error('生成二维码失败:', error)
statusText.value = '生成二维码失败,请重试'
} finally {
loading.value = false
}
}
// 刷新二维码
const refreshQRCode = () => {
stopPolling()
generateQRCode()
}
// 轮询检查状态
const startPolling = () => {
pollTimer = setInterval(async () => {
try {
const {data} = await request.get('/wechat/qrcode/status/', {
params: {scene_id: sceneId.value}
})
status.value = data.status
switch (data.status) {
case 'pending':
statusText.value = '请使用微信扫一扫登录'
break
case 'scanned':
statusText.value = '已扫码,请在手机上确认登录'
break
case 'confirmed':
stopPolling()
handleLoginSuccess(data)
break
case 'expired':
statusText.value = '二维码已过期,请点击刷新'
stopPolling()
break
case 'cancelled':
statusText.value = '用户取消登录'
stopPolling()
break
}
} catch (error) {
console.error('检查状态失败:', error)
}
}, 2000)
}
const stopPolling = () => {
if (pollTimer) {
clearInterval(pollTimer)
pollTimer = null
}
}
const handleLoginSuccess = (data) => {
localStorage.setItem('access_token', data.access)
localStorage.setItem('refresh_token', data.refresh)
request.defaults.headers.common['Authorization'] = `Bearer ${data.access}`
userInfo.value = data.user
isLoggedIn.value = true
}
const logout = () => {
localStorage.removeItem('access_token')
localStorage.removeItem('refresh_token')
delete request.defaults.headers.common['Authorization']
isLoggedIn.value = false
userInfo.value = null
generateQRCode()
}
onMounted(() => {
const token = localStorage.getItem('access_token')
if (token) {
// 可添加获取用户信息逻辑
isLoggedIn.value = true
} else {
generateQRCode()
}
})
onUnmounted(() => {
stopPolling()
})
</script>
<style scoped>
.wechat-login {
display: flex;
justify-content: center;
align-items: center;
min-height: 100vh;
background: #f5f5f5;
}
.login-container {
background: white;
padding: 40px;
border-radius: 12px;
box-shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
text-align: center;
min-width: 300px;
}
.qr-image {
width: 200px;
height: 200px;
border: 1px solid #eee;
padding: 10px;
border-radius: 8px;
}
.qr-tip {
margin-top: 15px;
color: #666;
font-size: 14px;
}
.refresh-btn {
margin-top: 15px;
padding: 8px 20px;
background: #07c160;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
.avatar {
width: 80px;
height: 80px;
border-radius: 50%;
margin-bottom: 15px;
}
.logout-btn {
margin-top: 20px;
padding: 10px 30px;
background: #ff4d4f;
color: white;
border: none;
border-radius: 4px;
cursor: pointer;
}
</style>3.3 Vite代理配置
javascript
// vite.config.js
import {defineConfig} from 'vite'
import vue from '@vitejs/plugin-vue'
export default defineConfig({
plugins: [vue()],
server: {
port: 8080,
proxy: {
'/api': {
target: 'http://localhost:8000',
changeOrigin: true,
}
}
}
})四、常见问题
4.1 "没有scope参数错误或没有scope权限"
原因:使用了错误的scope或应用类型不对。
解决:网站应用必须使用 snsapi_login,且必须在微信开放平台创建网站应用而非公众号。
4.2 回调地址不匹配
确保GitHub OAuth App设置中的Authorization callback URL与代码中的redirect_uri完全一致,包括协议、域名、端口和路径。
4.3 二维码过期处理
前端需要实现过期检测和刷新机制,通常二维码有效期为5分钟。
五、个人开发者替代方案
由于微信扫码登录需要企业资质,个人开发者可以考虑:
- GitHub登录(完全免费)
- Google登录
- 手机号+短信验证码
- 邮箱+密码登录
具体实现可参考GitHub登录方案,架构类似,只是OAuth接口不同。
六、总结
本文介绍了完整的微信扫码登录实现,包括:
- Django后端API设计
- 二维码生成与状态管理
- 微信OAuth流程处理
- Vue前端轮询机制
- JWT认证集成
关键点在于理解微信开放平台的网站应用权限和正确的scope使用。对于企业级应用,这是一个稳定可靠的登录方案。
本文代码基于Django 4.2和Vue 3,可根据实际需求调整。