feat(auth): 添加微信小程序扫码登录功能
- 新增扫码登录接口和相关服务 - 实现微信小程序端扫码登录逻辑 - 更新文档,添加微信小程序扫码登录指南 - 调整微信登录相关接口,使用 release 版本 - 新增 JWT 配置项
This commit is contained in:
213
docs/WECHAT_MINIPROGRAM_QR_LOGIN_GUIDE.md
Normal file
213
docs/WECHAT_MINIPROGRAM_QR_LOGIN_GUIDE.md
Normal file
@@ -0,0 +1,213 @@
|
||||
# 微信小程序扫码登录使用指南
|
||||
|
||||
## 概述
|
||||
|
||||
扫码登录接口现已全面支持微信小程序端,用户可以通过微信小程序扫码快速登录网页端或其他平台。
|
||||
|
||||
## 支持的平台
|
||||
|
||||
- ✅ **网页端** - 传统的网页扫码登录
|
||||
- ✅ **移动APP** - 原生移动应用扫码登录
|
||||
- ✅ **微信小程序** - 微信小程序扫码登录(新增)
|
||||
|
||||
## 接口说明
|
||||
|
||||
### 1. 生成扫码登录token
|
||||
```
|
||||
POST /api/qr-login/generate
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "生成成功",
|
||||
"data": {
|
||||
"token": "abc123def456",
|
||||
"qrCode": "qr-login:abc123def456",
|
||||
"expiresIn": 300
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 2. 检查扫码登录状态
|
||||
```
|
||||
GET /api/qr-login/status/{token}
|
||||
```
|
||||
|
||||
**响应示例:**
|
||||
```json
|
||||
{
|
||||
"code": 0,
|
||||
"message": "查询成功",
|
||||
"data": {
|
||||
"status": "confirmed",
|
||||
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
|
||||
"userInfo": {
|
||||
"userId": 123,
|
||||
"username": "user123",
|
||||
"nickname": "张三"
|
||||
},
|
||||
"expiresIn": 60
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 3. 微信小程序确认登录(专用接口)
|
||||
```
|
||||
POST /api/qr-login/wechat-confirm
|
||||
```
|
||||
|
||||
**请求示例:**
|
||||
```json
|
||||
{
|
||||
"token": "abc123def456",
|
||||
"userId": 123,
|
||||
"platform": "miniprogram",
|
||||
"wechatInfo": {
|
||||
"openid": "oABC123DEF456",
|
||||
"unionid": "uXYZ789ABC123",
|
||||
"nickname": "张三",
|
||||
"avatar": "https://wx.qlogo.cn/..."
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 微信小程序端实现示例
|
||||
|
||||
### 1. 扫码功能
|
||||
```javascript
|
||||
// 小程序扫码
|
||||
wx.scanCode({
|
||||
success: (res) => {
|
||||
const qrContent = res.result; // 例如: "qr-login:abc123def456"
|
||||
if (qrContent.startsWith('qr-login:')) {
|
||||
const token = qrContent.replace('qr-login:', '');
|
||||
this.confirmLogin(token);
|
||||
}
|
||||
}
|
||||
});
|
||||
```
|
||||
|
||||
### 2. 确认登录
|
||||
```javascript
|
||||
confirmLogin(token) {
|
||||
// 获取用户信息
|
||||
wx.getUserProfile({
|
||||
desc: '用于扫码登录',
|
||||
success: (userRes) => {
|
||||
// 调用确认登录接口
|
||||
wx.request({
|
||||
url: 'https://your-api.com/api/qr-login/wechat-confirm',
|
||||
method: 'POST',
|
||||
data: {
|
||||
token: token,
|
||||
userId: this.data.currentUserId, // 当前登录用户ID
|
||||
platform: 'miniprogram',
|
||||
wechatInfo: {
|
||||
openid: this.data.openid,
|
||||
unionid: this.data.unionid,
|
||||
nickname: userRes.userInfo.nickName,
|
||||
avatar: userRes.userInfo.avatarUrl
|
||||
}
|
||||
},
|
||||
success: (res) => {
|
||||
if (res.data.code === 0) {
|
||||
wx.showToast({
|
||||
title: '登录确认成功',
|
||||
icon: 'success'
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
```
|
||||
|
||||
## 网页端轮询状态示例
|
||||
|
||||
```javascript
|
||||
// 网页端轮询检查登录状态
|
||||
function checkLoginStatus(token) {
|
||||
const interval = setInterval(() => {
|
||||
fetch(`/api/qr-login/status/${token}`)
|
||||
.then(res => res.json())
|
||||
.then(data => {
|
||||
if (data.code === 0) {
|
||||
const status = data.data.status;
|
||||
|
||||
switch(status) {
|
||||
case 'pending':
|
||||
console.log('等待扫码...');
|
||||
break;
|
||||
case 'scanned':
|
||||
console.log('已扫码,等待确认...');
|
||||
break;
|
||||
case 'confirmed':
|
||||
console.log('登录成功!');
|
||||
localStorage.setItem('token', data.data.accessToken);
|
||||
clearInterval(interval);
|
||||
// 跳转到主页
|
||||
window.location.href = '/dashboard';
|
||||
break;
|
||||
case 'expired':
|
||||
console.log('二维码已过期');
|
||||
clearInterval(interval);
|
||||
// 重新生成二维码
|
||||
generateNewQrCode();
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
}, 2000); // 每2秒检查一次
|
||||
}
|
||||
```
|
||||
|
||||
## 状态流转
|
||||
|
||||
```
|
||||
pending (等待扫码)
|
||||
↓
|
||||
scanned (已扫码)
|
||||
↓
|
||||
confirmed (已确认) → 返回JWT token
|
||||
↓
|
||||
expired (已过期)
|
||||
```
|
||||
|
||||
## 特殊功能
|
||||
|
||||
### 1. 微信信息自动更新
|
||||
当微信小程序用户确认登录时,系统会自动更新用户的微信相关信息:
|
||||
- openid
|
||||
- unionid
|
||||
- 昵称(如果用户昵称为空)
|
||||
- 头像(如果用户头像为空)
|
||||
|
||||
### 2. 平台识别
|
||||
系统会记录用户通过哪个平台进行的扫码登录,便于后续分析和统计。
|
||||
|
||||
### 3. 安全特性
|
||||
- Token有效期5分钟
|
||||
- 确认后Token立即失效,防止重复使用
|
||||
- 支持过期自动清理
|
||||
- JWT token有效期24小时
|
||||
|
||||
## 注意事项
|
||||
|
||||
1. **微信小程序需要配置扫码权限**
|
||||
2. **确保用户已在小程序中登录**
|
||||
3. **处理用户拒绝授权的情况**
|
||||
4. **网页端需要定期轮询状态**
|
||||
5. **处理网络异常和超时情况**
|
||||
|
||||
## 错误处理
|
||||
|
||||
常见错误码:
|
||||
- `token不能为空` - 请求参数缺失
|
||||
- `扫码登录token不存在或已过期` - Token无效
|
||||
- `用户不存在` - 用户ID无效
|
||||
- `用户已被冻结` - 用户状态异常
|
||||
|
||||
建议在小程序端添加适当的错误提示和重试机制。
|
||||
@@ -0,0 +1,104 @@
|
||||
package com.gxwebsoft.auto.controller;
|
||||
|
||||
import com.gxwebsoft.auto.dto.QrLoginConfirmRequest;
|
||||
import com.gxwebsoft.auto.dto.QrLoginGenerateResponse;
|
||||
import com.gxwebsoft.auto.dto.QrLoginStatusResponse;
|
||||
import com.gxwebsoft.auto.service.QrLoginService;
|
||||
import com.gxwebsoft.common.core.web.BaseController;
|
||||
import com.gxwebsoft.common.core.web.ApiResult;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.Parameter;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.web.bind.annotation.*;
|
||||
|
||||
import javax.validation.Valid;
|
||||
|
||||
/**
|
||||
* 认证模块
|
||||
*
|
||||
* @author 科技小王子
|
||||
* @since 2025-03-06 22:50:25
|
||||
*/
|
||||
@Tag(name = "认证模块")
|
||||
@RestController
|
||||
@RequestMapping("/api/qr-login")
|
||||
public class QrLoginController extends BaseController {
|
||||
|
||||
@Autowired
|
||||
private QrLoginService qrLoginService;
|
||||
|
||||
/**
|
||||
* 生成扫码登录token
|
||||
*/
|
||||
@Operation(summary = "生成扫码登录token")
|
||||
@PostMapping("/generate")
|
||||
public ApiResult<?> generateQrLoginToken() {
|
||||
try {
|
||||
QrLoginGenerateResponse response = qrLoginService.generateQrLoginToken();
|
||||
return success("生成成功", response);
|
||||
} catch (Exception e) {
|
||||
return fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查扫码登录状态
|
||||
*/
|
||||
@Operation(summary = "检查扫码登录状态")
|
||||
@GetMapping("/status/{token}")
|
||||
public ApiResult<?> checkQrLoginStatus(
|
||||
@Parameter(description = "扫码登录token") @PathVariable String token) {
|
||||
try {
|
||||
QrLoginStatusResponse response = qrLoginService.checkQrLoginStatus(token);
|
||||
return success("查询成功", response);
|
||||
} catch (Exception e) {
|
||||
return fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 确认扫码登录
|
||||
*/
|
||||
@Operation(summary = "确认扫码登录")
|
||||
@PostMapping("/confirm")
|
||||
public ApiResult<?> confirmQrLogin(@Valid @RequestBody QrLoginConfirmRequest request) {
|
||||
try {
|
||||
QrLoginStatusResponse response = qrLoginService.confirmQrLogin(request);
|
||||
return success("确认成功", response);
|
||||
} catch (Exception e) {
|
||||
return fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫码操作(可选接口,用于移动端扫码后更新状态)
|
||||
*/
|
||||
@Operation(summary = "扫码操作")
|
||||
@PostMapping("/scan/{token}")
|
||||
public ApiResult<?> scanQrCode(@Parameter(description = "扫码登录token") @PathVariable String token) {
|
||||
try {
|
||||
boolean result = qrLoginService.scanQrCode(token);
|
||||
return success("操作成功", result);
|
||||
} catch (Exception e) {
|
||||
return fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 微信小程序扫码登录确认(便捷接口)
|
||||
*/
|
||||
@Operation(summary = "微信小程序扫码登录确认")
|
||||
@PostMapping("/wechat-confirm")
|
||||
public ApiResult<?> wechatMiniProgramConfirm(@Valid @RequestBody QrLoginConfirmRequest request) {
|
||||
try {
|
||||
// 设置平台为微信小程序
|
||||
request.setPlatform("miniprogram");
|
||||
QrLoginStatusResponse response = qrLoginService.confirmQrLogin(request);
|
||||
return success("微信小程序登录确认成功", response);
|
||||
} catch (Exception e) {
|
||||
return fail(e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
}
|
||||
@@ -0,0 +1,50 @@
|
||||
package com.gxwebsoft.auto.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
import javax.validation.constraints.NotBlank;
|
||||
|
||||
/**
|
||||
* 扫码登录确认请求
|
||||
*
|
||||
* @author 科技小王子
|
||||
* @since 2025-08-31
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "扫码登录确认请求")
|
||||
public class QrLoginConfirmRequest {
|
||||
|
||||
@Schema(description = "扫码登录token")
|
||||
@NotBlank(message = "token不能为空")
|
||||
private String token;
|
||||
|
||||
@Schema(description = "用户ID")
|
||||
private Integer userId;
|
||||
|
||||
@Schema(description = "登录平台: web-网页端, app-移动应用, miniprogram-微信小程序")
|
||||
private String platform;
|
||||
|
||||
@Schema(description = "微信小程序相关信息")
|
||||
private WechatMiniProgramInfo wechatInfo;
|
||||
|
||||
/**
|
||||
* 微信小程序信息
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "微信小程序信息")
|
||||
public static class WechatMiniProgramInfo {
|
||||
@Schema(description = "微信openid")
|
||||
private String openid;
|
||||
|
||||
@Schema(description = "微信unionid")
|
||||
private String unionid;
|
||||
|
||||
@Schema(description = "微信昵称")
|
||||
private String nickname;
|
||||
|
||||
@Schema(description = "微信头像")
|
||||
private String avatar;
|
||||
}
|
||||
|
||||
}
|
||||
55
src/main/java/com/gxwebsoft/auto/dto/QrLoginData.java
Normal file
55
src/main/java/com/gxwebsoft/auto/dto/QrLoginData.java
Normal file
@@ -0,0 +1,55 @@
|
||||
package com.gxwebsoft.auto.dto;
|
||||
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 扫码登录数据模型
|
||||
*
|
||||
* @author 科技小王子
|
||||
* @since 2025-08-31
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class QrLoginData {
|
||||
|
||||
/**
|
||||
* 扫码登录token
|
||||
*/
|
||||
private String token;
|
||||
|
||||
/**
|
||||
* 状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, expired-已过期
|
||||
*/
|
||||
private String status;
|
||||
|
||||
/**
|
||||
* 用户ID(扫码确认后设置)
|
||||
*/
|
||||
private Integer userId;
|
||||
|
||||
/**
|
||||
* 用户名(扫码确认后设置)
|
||||
*/
|
||||
private String username;
|
||||
|
||||
/**
|
||||
* 创建时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
/**
|
||||
* 过期时间
|
||||
*/
|
||||
private LocalDateTime expireTime;
|
||||
|
||||
/**
|
||||
* JWT访问令牌(确认后生成)
|
||||
*/
|
||||
private String accessToken;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,29 @@
|
||||
package com.gxwebsoft.auto.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 扫码登录生成响应
|
||||
*
|
||||
* @author 科技小王子
|
||||
* @since 2025-08-31
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "扫码登录生成响应")
|
||||
public class QrLoginGenerateResponse {
|
||||
|
||||
@Schema(description = "扫码登录token")
|
||||
private String token;
|
||||
|
||||
@Schema(description = "二维码内容")
|
||||
private String qrCode;
|
||||
|
||||
@Schema(description = "过期时间(秒)")
|
||||
private Long expiresIn;
|
||||
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.gxwebsoft.auto.dto;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
/**
|
||||
* 扫码登录状态响应
|
||||
*
|
||||
* @author 科技小王子
|
||||
* @since 2025-08-31
|
||||
*/
|
||||
@Data
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@Schema(description = "扫码登录状态响应")
|
||||
public class QrLoginStatusResponse {
|
||||
|
||||
@Schema(description = "状态: pending-等待扫码, scanned-已扫码, confirmed-已确认, expired-已过期")
|
||||
private String status;
|
||||
|
||||
@Schema(description = "JWT访问令牌(仅在confirmed状态时返回)")
|
||||
private String accessToken;
|
||||
|
||||
@Schema(description = "用户信息(仅在confirmed状态时返回)")
|
||||
private Object userInfo;
|
||||
|
||||
@Schema(description = "剩余过期时间(秒)")
|
||||
private Long expiresIn;
|
||||
|
||||
}
|
||||
46
src/main/java/com/gxwebsoft/auto/service/QrLoginService.java
Normal file
46
src/main/java/com/gxwebsoft/auto/service/QrLoginService.java
Normal file
@@ -0,0 +1,46 @@
|
||||
package com.gxwebsoft.auto.service;
|
||||
|
||||
import com.gxwebsoft.auto.dto.QrLoginConfirmRequest;
|
||||
import com.gxwebsoft.auto.dto.QrLoginGenerateResponse;
|
||||
import com.gxwebsoft.auto.dto.QrLoginStatusResponse;
|
||||
|
||||
/**
|
||||
* 扫码登录服务接口
|
||||
*
|
||||
* @author 科技小王子
|
||||
* @since 2025-08-31
|
||||
*/
|
||||
public interface QrLoginService {
|
||||
|
||||
/**
|
||||
* 生成扫码登录token
|
||||
*
|
||||
* @return QrLoginGenerateResponse
|
||||
*/
|
||||
QrLoginGenerateResponse generateQrLoginToken();
|
||||
|
||||
/**
|
||||
* 检查扫码登录状态
|
||||
*
|
||||
* @param token 扫码登录token
|
||||
* @return QrLoginStatusResponse
|
||||
*/
|
||||
QrLoginStatusResponse checkQrLoginStatus(String token);
|
||||
|
||||
/**
|
||||
* 确认扫码登录
|
||||
*
|
||||
* @param request 确认请求
|
||||
* @return QrLoginStatusResponse
|
||||
*/
|
||||
QrLoginStatusResponse confirmQrLogin(QrLoginConfirmRequest request);
|
||||
|
||||
/**
|
||||
* 扫码操作(更新状态为已扫码)
|
||||
*
|
||||
* @param token 扫码登录token
|
||||
* @return boolean
|
||||
*/
|
||||
boolean scanQrCode(String token);
|
||||
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package com.gxwebsoft.auto.service.impl;
|
||||
|
||||
import cn.hutool.core.lang.UUID;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import com.gxwebsoft.auto.dto.*;
|
||||
import com.gxwebsoft.auto.service.QrLoginService;
|
||||
import com.gxwebsoft.common.core.security.JwtSubject;
|
||||
import com.gxwebsoft.common.core.security.JwtUtil;
|
||||
import com.gxwebsoft.common.core.utils.JSONUtil;
|
||||
import com.gxwebsoft.common.core.utils.RedisUtil;
|
||||
import com.gxwebsoft.common.system.entity.User;
|
||||
import com.gxwebsoft.common.system.service.UserService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.beans.factory.annotation.Value;
|
||||
import org.springframework.stereotype.Service;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.temporal.ChronoUnit;
|
||||
import java.util.concurrent.TimeUnit;
|
||||
|
||||
import static com.gxwebsoft.common.core.constants.RedisConstants.*;
|
||||
|
||||
/**
|
||||
* 扫码登录服务实现
|
||||
*
|
||||
* @author 科技小王子
|
||||
* @since 2025-08-31
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class QrLoginServiceImpl implements QrLoginService {
|
||||
|
||||
@Autowired
|
||||
private RedisUtil redisUtil;
|
||||
|
||||
@Autowired
|
||||
private UserService userService;
|
||||
|
||||
@Value("${config.jwt.secret:websoft-jwt-secret-key-2025}")
|
||||
private String jwtSecret;
|
||||
|
||||
@Value("${config.jwt.expire:86400}")
|
||||
private Long jwtExpire;
|
||||
|
||||
@Override
|
||||
public QrLoginGenerateResponse generateQrLoginToken() {
|
||||
// 生成唯一的扫码登录token
|
||||
String token = UUID.randomUUID().toString(true);
|
||||
|
||||
// 创建扫码登录数据
|
||||
QrLoginData qrLoginData = new QrLoginData();
|
||||
qrLoginData.setToken(token);
|
||||
qrLoginData.setStatus(QR_LOGIN_STATUS_PENDING);
|
||||
qrLoginData.setCreateTime(LocalDateTime.now());
|
||||
qrLoginData.setExpireTime(LocalDateTime.now().plusSeconds(QR_LOGIN_TOKEN_TTL));
|
||||
|
||||
// 存储到Redis,设置过期时间
|
||||
String redisKey = QR_LOGIN_TOKEN_KEY + token;
|
||||
redisUtil.set(redisKey, qrLoginData, QR_LOGIN_TOKEN_TTL, TimeUnit.SECONDS);
|
||||
|
||||
log.info("生成扫码登录token: {}", token);
|
||||
|
||||
// 构造二维码内容(这里可以是前端登录页面的URL + token参数)
|
||||
String qrCodeContent = "qr-login:" + token;
|
||||
|
||||
return new QrLoginGenerateResponse(token, qrCodeContent, QR_LOGIN_TOKEN_TTL);
|
||||
}
|
||||
|
||||
@Override
|
||||
public QrLoginStatusResponse checkQrLoginStatus(String token) {
|
||||
if (StrUtil.isBlank(token)) {
|
||||
return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
|
||||
}
|
||||
|
||||
String redisKey = QR_LOGIN_TOKEN_KEY + token;
|
||||
QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
|
||||
|
||||
if (qrLoginData == null) {
|
||||
return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
|
||||
// 删除过期的token
|
||||
redisUtil.delete(redisKey);
|
||||
return new QrLoginStatusResponse(QR_LOGIN_STATUS_EXPIRED, null, null, 0L);
|
||||
}
|
||||
|
||||
// 计算剩余过期时间
|
||||
long expiresIn = ChronoUnit.SECONDS.between(LocalDateTime.now(), qrLoginData.getExpireTime());
|
||||
|
||||
QrLoginStatusResponse response = new QrLoginStatusResponse();
|
||||
response.setStatus(qrLoginData.getStatus());
|
||||
response.setExpiresIn(expiresIn);
|
||||
|
||||
// 如果已确认,返回token和用户信息
|
||||
if (QR_LOGIN_STATUS_CONFIRMED.equals(qrLoginData.getStatus())) {
|
||||
response.setAccessToken(qrLoginData.getAccessToken());
|
||||
|
||||
// 获取用户信息
|
||||
if (qrLoginData.getUserId() != null) {
|
||||
User user = userService.getByIdRel(qrLoginData.getUserId());
|
||||
if (user != null) {
|
||||
// 清除敏感信息
|
||||
user.setPassword(null);
|
||||
response.setUserInfo(user);
|
||||
}
|
||||
}
|
||||
|
||||
// 确认后删除token,防止重复使用
|
||||
redisUtil.delete(redisKey);
|
||||
}
|
||||
|
||||
return response;
|
||||
}
|
||||
|
||||
@Override
|
||||
public QrLoginStatusResponse confirmQrLogin(QrLoginConfirmRequest request) {
|
||||
String token = request.getToken();
|
||||
Integer userId = request.getUserId();
|
||||
String platform = request.getPlatform();
|
||||
|
||||
if (StrUtil.isBlank(token) || userId == null) {
|
||||
throw new RuntimeException("参数不能为空");
|
||||
}
|
||||
|
||||
String redisKey = QR_LOGIN_TOKEN_KEY + token;
|
||||
QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
|
||||
|
||||
if (qrLoginData == null) {
|
||||
throw new RuntimeException("扫码登录token不存在或已过期");
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
|
||||
redisUtil.delete(redisKey);
|
||||
throw new RuntimeException("扫码登录token已过期");
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
User user = userService.getByIdRel(userId);
|
||||
if (user == null) {
|
||||
throw new RuntimeException("用户不存在");
|
||||
}
|
||||
|
||||
// 检查用户状态
|
||||
if (user.getStatus() != null && user.getStatus() != 0) {
|
||||
throw new RuntimeException("用户已被冻结");
|
||||
}
|
||||
|
||||
// 如果是微信小程序登录,处理微信相关信息
|
||||
if ("miniprogram".equals(platform) && request.getWechatInfo() != null) {
|
||||
handleWechatMiniProgramLogin(user, request.getWechatInfo());
|
||||
}
|
||||
|
||||
// 生成JWT token
|
||||
JwtSubject jwtSubject = new JwtSubject(user.getUsername(), user.getTenantId());
|
||||
String accessToken = JwtUtil.buildToken(jwtSubject, jwtExpire, jwtSecret);
|
||||
|
||||
// 更新扫码登录数据
|
||||
qrLoginData.setStatus(QR_LOGIN_STATUS_CONFIRMED);
|
||||
qrLoginData.setUserId(userId);
|
||||
qrLoginData.setUsername(user.getUsername());
|
||||
qrLoginData.setAccessToken(accessToken);
|
||||
|
||||
// 更新Redis中的数据
|
||||
redisUtil.set(redisKey, qrLoginData, 60L, TimeUnit.SECONDS); // 给前端60秒时间获取token
|
||||
|
||||
log.info("用户 {} 通过 {} 平台确认扫码登录,token: {}", user.getUsername(),
|
||||
platform != null ? platform : "unknown", token);
|
||||
|
||||
// 清除敏感信息
|
||||
user.setPassword(null);
|
||||
|
||||
return new QrLoginStatusResponse(QR_LOGIN_STATUS_CONFIRMED, accessToken, user, 60L);
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理微信小程序登录相关逻辑
|
||||
*/
|
||||
private void handleWechatMiniProgramLogin(User user, QrLoginConfirmRequest.WechatMiniProgramInfo wechatInfo) {
|
||||
// 更新用户的微信信息
|
||||
if (StrUtil.isNotBlank(wechatInfo.getOpenid())) {
|
||||
user.setOpenid(wechatInfo.getOpenid());
|
||||
}
|
||||
if (StrUtil.isNotBlank(wechatInfo.getUnionid())) {
|
||||
user.setUnionid(wechatInfo.getUnionid());
|
||||
}
|
||||
if (StrUtil.isNotBlank(wechatInfo.getNickname()) && StrUtil.isBlank(user.getNickname())) {
|
||||
user.setNickname(wechatInfo.getNickname());
|
||||
}
|
||||
if (StrUtil.isNotBlank(wechatInfo.getAvatar()) && StrUtil.isBlank(user.getAvatar())) {
|
||||
user.setAvatar(wechatInfo.getAvatar());
|
||||
}
|
||||
|
||||
// 更新用户信息到数据库
|
||||
try {
|
||||
userService.updateById(user);
|
||||
log.info("更新用户 {} 的微信小程序信息成功", user.getUsername());
|
||||
} catch (Exception e) {
|
||||
log.warn("更新用户 {} 的微信小程序信息失败: {}", user.getUsername(), e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean scanQrCode(String token) {
|
||||
if (StrUtil.isBlank(token)) {
|
||||
return false;
|
||||
}
|
||||
|
||||
String redisKey = QR_LOGIN_TOKEN_KEY + token;
|
||||
QrLoginData qrLoginData = redisUtil.get(redisKey, QrLoginData.class);
|
||||
|
||||
if (qrLoginData == null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
// 检查是否过期
|
||||
if (LocalDateTime.now().isAfter(qrLoginData.getExpireTime())) {
|
||||
redisUtil.delete(redisKey);
|
||||
return false;
|
||||
}
|
||||
|
||||
// 只有pending状态才能更新为scanned
|
||||
if (QR_LOGIN_STATUS_PENDING.equals(qrLoginData.getStatus())) {
|
||||
qrLoginData.setStatus(QR_LOGIN_STATUS_SCANNED);
|
||||
|
||||
// 计算剩余过期时间
|
||||
long remainingSeconds = ChronoUnit.SECONDS.between(LocalDateTime.now(), qrLoginData.getExpireTime());
|
||||
redisUtil.set(redisKey, qrLoginData, remainingSeconds, TimeUnit.SECONDS);
|
||||
|
||||
log.info("扫码登录token {} 状态更新为已扫码", token);
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -27,6 +27,14 @@ public class RedisConstants {
|
||||
|
||||
|
||||
|
||||
// 扫码登录相关key
|
||||
public static final String QR_LOGIN_TOKEN_KEY = "qr-login:token:"; // 扫码登录token前缀
|
||||
public static final Long QR_LOGIN_TOKEN_TTL = 300L; // 扫码登录token过期时间(5分钟)
|
||||
public static final String QR_LOGIN_STATUS_PENDING = "pending"; // 等待扫码
|
||||
public static final String QR_LOGIN_STATUS_SCANNED = "scanned"; // 已扫码
|
||||
public static final String QR_LOGIN_STATUS_CONFIRMED = "confirmed"; // 已确认
|
||||
public static final String QR_LOGIN_STATUS_EXPIRED = "expired"; // 已过期
|
||||
|
||||
// 哗啦啦key
|
||||
public static final String getAllShop = "allShop";
|
||||
public static final String getBaseInfo = "baseInfo";
|
||||
|
||||
@@ -39,6 +39,7 @@ public class SecurityConfig {
|
||||
.permitAll()
|
||||
.antMatchers(
|
||||
"/api/login",
|
||||
"/api/qr-login/**",
|
||||
"/api/register",
|
||||
"/api/cms/website/createWebsite",
|
||||
"/druid/**",
|
||||
|
||||
@@ -402,7 +402,7 @@ public class WxLoginController extends BaseController {
|
||||
String apiUrl = "https://api.weixin.qq.com/wxa/getwxacode?access_token=" + getAccessToken();
|
||||
final HashMap<String, Object> map = new HashMap<>();
|
||||
map.put("path", "/package/admin/order-scan?orderNo=".concat(orderNo));
|
||||
map.put("env_version", "trial");
|
||||
map.put("env_version", "release");
|
||||
// 获取图片 Buffer
|
||||
byte[] qrCode = HttpRequest.post(apiUrl)
|
||||
.body(JSON.toJSONString(map))
|
||||
@@ -439,7 +439,7 @@ public class WxLoginController extends BaseController {
|
||||
final HashMap<String, Object> map = new HashMap<>();
|
||||
map.put("scene", scene);
|
||||
map.put("page", "pages/index/index");
|
||||
map.put("env_version", "trial");
|
||||
map.put("env_version", "release");
|
||||
|
||||
String jsonBody = JSON.toJSONString(map);
|
||||
System.out.println("请求的 JSON body = " + jsonBody);
|
||||
|
||||
@@ -48,6 +48,11 @@ config:
|
||||
server-url: https://server.websoft.top/api
|
||||
upload-path: /Users/gxwebsoft/JAVA/mp-java/src/main/resources/ # window(D:\Temp)
|
||||
|
||||
# JWT配置
|
||||
jwt:
|
||||
secret: websoft-jwt-secret-key-2025-dev-environment
|
||||
expire: 86400 # token过期时间(秒) 24小时
|
||||
|
||||
# 开发环境证书配置
|
||||
certificate:
|
||||
load-mode: CLASSPATH # 开发环境从classpath加载
|
||||
|
||||
Reference in New Issue
Block a user