在现代应用开发中,便捷的登录方式是提升用户体验的关键。扫码登录凭借其便捷性和高普及率,成为众多应用的首选。本文将详细介绍如何在 RuoYi-Vue 框架中实现 PC 端前后分离的二维码扫码登录功能,涵盖技术原理、详细步骤与代码示例。
一、技术背景与原理
RuoYi-Vue 是基于 Spring Boot、Vue 的前后端分离权限管理系统。扫码登录基于 OAuth2.0 协议,流程大致如下:
-
生成二维码:应用向服务器请求生成带有唯一标识(state)的二维码 URL,用户扫码后,将用户重定向到应用指定的回调地址,并携带授权码(code)。
-
获取授权信息:应用使用授权码和自身的 appId、secret,向服务器换取 access_token 和 openid,openid 是用户在开放平台的唯一标识。
-
用户认证与登录:应用根据 openid 判断用户是否已注册,若已注册则直接登录;若未注册,可创建新用户并关联 openid,最终完成登录流程。
二、后端实现步骤
1. 配置文件添加相关配置
在src/main/resources/application.yml
中添加配置信息,需替换为在开放平台申请的真实信息:
# 配置
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
接口,定义生成二维码、检查状态和处理回调的方法:
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
实现类,实现接口方法,核心代码如下:
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
中添加登录相关接口:
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
中添加登录相关接口:
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
中添加扫码登录选项卡,核心代码如下:
<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 相关状态管理:
// 在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