Browse Source
- 新增扫码登录接口和相关服务 - 实现微信小程序端扫码登录逻辑 - 更新文档,添加微信小程序扫码登录指南 - 调整微信登录相关接口,使用 release 版本 - 新增 JWT 配置项pan
12 changed files with 784 additions and 2 deletions
@ -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; |
||||
|
} |
||||
|
|
||||
|
} |
@ -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; |
||||
|
|
||||
|
} |
@ -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; |
||||
|
} |
||||
|
} |
Loading…
Reference in new issue