Vue + Java 实现微信扫码登录完整指南

前言

微信扫码登录是目前非常流行的第三方登录方式,本文将详细介绍如何在Vue前端和Java后端中实现微信扫码登录功能。

准备工作

在开始之前,你需要:

  1. 注册微信开放平台账号
  2. 创建网站应用并获取以下信息:
    • AppID
    • AppSecret
  3. 设置授权回调域名

技术栈

  • 前端:Vue 3 + Vite + Axios
  • 后端:Spring Boot 2.7.x + Java 8+
  • 数据库:MySQL

项目结构

├── frontend/                # Vue前端项目
│   ├── src/
│   │   ├── components/     
│   │   │   └── WxLogin.vue # 微信登录组件
│   │   ├── api/
│   │   │   └── auth.js     # API请求
│   │   └── store/
│   │       └── user.js     # 用户状态管理
│   └── package.json
│
└── backend/                 # Spring Boot后端项目
    ├── src/main/java/
    │   └── com/example/demo/
    │       ├── controller/
    │       │   └── WxAuthController.java
    │       ├── service/
    │       │   └── WxAuthService.java
    │       ├── model/
    │       │   └── WxUserInfo.java
    │       └── config/
    │           └── WxConfig.java
    └── pom.xml

前端实现

1. 创建微信登录组件 (WxLogin.vue)

<template>
  <div class="wx-login-container">
    <div class="qrcode-wrapper">
      <div id="wx-qrcode"></div>
      <div v-if="scanStatus === 'waiting'" class="status-text">
        请使用微信扫描二维码登录
      </div>
      <div v-else-if="scanStatus === 'scanned'" class="status-text">
        扫描成功,请在手机上确认
      </div>
      <div v-else-if="scanStatus === 'expired'" class="status-text">
        二维码已过期,点击刷新
        <button @click="refreshQrCode">刷新</button>
      </div>
    </div>
  </div>
</template>

<script setup>
import { ref, onMounted } from 'vue'
import { getQrCode, checkLoginStatus } from '@/api/auth'

const scanStatus = ref('waiting')
let checkTimer = null

const initQrCode = async () => {
  try {
    const { data } = await getQrCode()
    // 渲染二维码
    new QRCode(document.getElementById('wx-qrcode'), {
      text: data.qrCodeUrl,
      width: 200,
      height: 200
    })
    
    // 开始轮询检查扫码状态
    startCheckingStatus(data.state)
  } catch (error) {
    console.error('获取二维码失败:', error)
  }
}

const startCheckingStatus = (state) => {
  checkTimer = setInterval(async () => {
    try {
      const { data } = await checkLoginStatus(state)
      scanStatus.value = data.status
      
      if (data.status === 'success') {
        clearInterval(checkTimer)
        // 登录成功,存储token并跳转
        localStorage.setItem('token', data.token)
        router.push('/dashboard')
      } else if (data.status === 'expired') {
        clearInterval(checkTimer)
      }
    } catch (error) {
      console.error('检查状态失败:', error)
    }
  }, 2000)
}

const refreshQrCode = () => {
  document.getElementById('wx-qrcode').innerHTML = ''
  scanStatus.value = 'waiting'
  initQrCode()
}

onMounted(() => {
  initQrCode()
})

onUnmounted(() => {
  if (checkTimer) {
    clearInterval(checkTimer)
  }
})
</script>

<style scoped>
.wx-login-container {
  display: flex;
  justify-content: center;
  align-items: center;
  padding: 40px;
}

.qrcode-wrapper {
  text-align: center;
}

.status-text {
  margin-top: 20px;
  color: #666;
}

button {
  margin-left: 10px;
  padding: 4px 12px;
  border: 1px solid #ddd;
  border-radius: 4px;
  cursor: pointer;
}
</style>

2. API请求封装 (auth.js)

import axios from 'axios'

const api = axios.create({
  baseURL: import.meta.env.VITE_API_URL
})

export const getQrCode = () => {
  return api.get('/api/wx/qrcode')
}

export const checkLoginStatus = (state) => {
  return api.get(`/api/wx/check-status?state=${state}`)
}

后端实现

1. 配置文件 (application.yml)

wx:
  app-id: your_app_id
  app-secret: your_app_secret
  redirect-uri: http://your-domain.com/api/wx/callback

2. 微信配置类 (WxConfig.java)

@Configuration
@ConfigurationProperties(prefix = "wx")
@Data
public class WxConfig {
    private String appId;
    private String appSecret;
    private String redirectUri;
}

3. 控制器 (WxAuthController.java)

@RestController
@RequestMapping("/api/wx")
@RequiredArgsConstructor
public class WxAuthController {

    private final WxAuthService wxAuthService;

    @GetMapping("/qrcode")
    public ResponseEntity<?> getQrCode() {
        try {
            Map<String, String> qrCodeInfo = wxAuthService.generateQrCode();
            return ResponseEntity.ok(qrCodeInfo);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("获取二维码失败");
        }
    }

    @GetMapping("/check-status")
    public ResponseEntity<?> checkStatus(@RequestParam String state) {
        try {
            Map<String, Object> status = wxAuthService.checkLoginStatus(state);
            return ResponseEntity.ok(status);
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("检查状态失败");
        }
    }

    @GetMapping("/callback")
    public void callback(@RequestParam String code, @RequestParam String state) {
        wxAuthService.handleCallback(code, state);
    }
}

4. 服务层 (WxAuthService.java)

@Service
@RequiredArgsConstructor
public class WxAuthService {

    private final WxConfig wxConfig;
    private final RedisTemplate<String, String> redisTemplate;

    public Map<String, String> generateQrCode() {
        String state = UUID.randomUUID().toString();
        String qrCodeUrl = String.format(
            "https://open.weixin.qq.com/connect/qrconnect?" +
            "appid=%s&redirect_uri=%s&response_type=code&" +
            "scope=snsapi_login&state=%s#wechat_redirect",
            wxConfig.getAppId(),
            URLEncoder.encode(wxConfig.getRedirectUri(), StandardCharsets.UTF_8),
            state
        );

        // 存储state到Redis,设置过期时间
        redisTemplate.opsForValue().set(
            "wx:qrcode:" + state,
            "WAITING",
            5,
            TimeUnit.MINUTES
        );

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

    public Map<String, Object> checkLoginStatus(String state) {
        String key = "wx:qrcode:" + state;
        String status = redisTemplate.opsForValue().get(key);
        
        Map<String, Object> result = new HashMap<>();
        if (status == null) {
            result.put("status", "expired");
        } else if (status.equals("WAITING")) {
            result.put("status", "waiting");
        } else if (status.equals("SCANNED")) {
            result.put("status", "scanned");
        } else if (status.startsWith("SUCCESS:")) {
            String token = status.substring(8);
            result.put("status", "success");
            result.put("token", token);
            // 清除Redis中的状态
            redisTemplate.delete(key);
        }
        return result;
    }

    public void handleCallback(String code, String state) {
        // 1. 通过code获取access_token
        String accessTokenUrl = String.format(
            "https://api.weixin.qq.com/sns/oauth2/access_token?" +
            "appid=%s&secret=%s&code=%s&grant_type=authorization_code",
            wxConfig.getAppId(),
            wxConfig.getAppSecret(),
            code
        );
        
        // 发起HTTP请求获取access_token
        // 使用access_token获取用户信息
        // 生成JWT token
        String token = generateToken(userInfo);
        
        // 更新Redis中的状态
        redisTemplate.opsForValue().set(
            "wx:qrcode:" + state,
            "SUCCESS:" + token,
            5,
            TimeUnit.MINUTES
        );
    }

    private String generateToken(WxUserInfo userInfo) {
        // 实现JWT token生成逻辑
        return "jwt_token";
    }
}

数据库设计

CREATE TABLE `wx_user` (
  `id` bigint(20) NOT NULL AUTO_INCREMENT,
  `openid` varchar(64) NOT NULL COMMENT '微信openid',
  `unionid` varchar(64) DEFAULT NULL COMMENT '微信unionid',
  `nickname` varchar(64) DEFAULT NULL COMMENT '微信昵称',
  `avatar` varchar(255) DEFAULT NULL COMMENT '头像URL',
  `created_at` datetime DEFAULT CURRENT_TIMESTAMP,
  `updated_at` datetime DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_openid` (`openid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;

实现流程

  1. 用户访问登录页面,前端向后端请求获取二维码
  2. 后端生成state并返回微信授权URL
  3. 前端使用URL生成二维码,并开始轮询检查登录状态
  4. 用户扫描二维码并在手机上确认登录
  5. 微信服务器回调后端接口,带上code和state
  6. 后端通过code获取用户信息,生成token
  7. 前端轮询检测到登录成功,获取token并完成登录

注意事项

  1. 安全性考虑

    • 使用HTTPS
    • state参数防止CSRF攻击
    • 及时清理过期的二维码状态
    • 敏感信息加密存储
  2. 性能优化

    • Redis缓存减少数据库压力
    • 合理设置轮询间隔
    • 二维码状态及时过期
  3. 用户体验

    • 清晰的状态提示
    • 二维码过期自动刷新
    • 登录成功后的平滑跳转

依赖配置

前端 (package.json)

{
  "dependencies": {
    "vue": "^3.3.0",
    "axios": "^1.6.0",
    "qrcode.vue": "^3.4.0",
    "vue-router": "^4.2.0",
    "pinia": "^2.1.0"
  }
}

后端 (pom.xml)

<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-web</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>org.projectlombok</groupId>
        <artifactId>lombok</artifactId>
        <optional>true</optional>
    </dependency>
    <dependency>
        <groupId>io.jsonwebtoken</groupId>
        <artifactId>jjwt</artifactId>
        <version>0.9.1</version>
    </dependency>
    <dependency>
        <groupId>mysql</groupId>
        <artifactId>mysql-connector-java</artifactId>
    </dependency>
</dependencies>

总结

本文详细介绍了如何使用Vue和Java实现微信扫码登录功能。主要包括:

  1. 前端二维码展示和状态管理
  2. 后端接口实现和数据处理
  3. 完整的登录流程和状态处理
  4. 安全性和性能考虑

通过这个实现,用户可以方便地使用微信扫码完成网站登录,提供了良好的用户体验。在实际项目中,还需要根据具体需求进行适当的调整和优化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值