|
|
@@ -1,39 +1,34 @@
|
|
|
package com.etotem.cfc.controller.auth;
|
|
|
|
|
|
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
|
|
|
import com.etotem.cfc.common.Result;
|
|
|
import com.etotem.cfc.dto.LoginResultDTO;
|
|
|
-import com.etotem.cfc.entity.User;
|
|
|
-import com.etotem.cfc.mapper.UserMapper;
|
|
|
import com.etotem.cfc.service.UserService;
|
|
|
import com.etotem.cfc.service.WechatWebLoginService;
|
|
|
import io.swagger.v3.oas.annotations.Operation;
|
|
|
import io.swagger.v3.oas.annotations.tags.Tag;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
-import org.springframework.web.bind.annotation.GetMapping;
|
|
|
import org.springframework.web.bind.annotation.PostMapping;
|
|
|
+import org.springframework.web.bind.annotation.RequestAttribute;
|
|
|
import org.springframework.web.bind.annotation.RequestBody;
|
|
|
import org.springframework.web.bind.annotation.RequestMapping;
|
|
|
-import org.springframework.web.bind.annotation.RequestParam;
|
|
|
import org.springframework.web.bind.annotation.RestController;
|
|
|
|
|
|
import javax.annotation.Resource;
|
|
|
-import javax.servlet.http.HttpServletResponse;
|
|
|
-import java.io.IOException;
|
|
|
import java.util.HashMap;
|
|
|
import java.util.Map;
|
|
|
import java.util.UUID;
|
|
|
|
|
|
/**
|
|
|
- * 微信开放平台「网站应用」扫码登录(桌面端 SSO)。
|
|
|
+ * 微信「小程序码」扫码登录(桌面端 SSO)。
|
|
|
*
|
|
|
- * <p>流程:桌面端 POST /qr 拿 state+url → 用户在浏览器扫码 → 微信回调 GET /callback(带 code+state)
|
|
|
- * → 服务端换 unionid 匹配 users.unionid,写 state=U{userId} → 桌面端轮询 POST /poll 取 JWT。
|
|
|
+ * <p>流程:桌面端 POST /qr 拿 state+小程序码图片 → 用户用微信扫码打开小程序落地页
|
|
|
+ * (从 scene 取 state)→ 小程序(已登录,带小程序 JWT)POST /scan 确认 →
|
|
|
+ * 服务端写 state=U{userId} → 桌面端轮询 POST /poll 取 JWT。
|
|
|
*
|
|
|
- * <p>注意:/callback 是微信服务器跳转的浏览器端点,必须用 GET——这是本项目
|
|
|
- * 「统一 @PostMapping,禁止 @GetMapping」规范的唯一例外。
|
|
|
+ * <p>全部为 POST:/qr、/poll 公开(桌面端尚未持有令牌),/scan 需小程序 JWT
|
|
|
+ * (扫码者即小程序已登录用户,据此完成与小程序账号的 SSO)。
|
|
|
*/
|
|
|
-@Tag(name = "微信网站应用登录", description = "桌面端微信扫码登录,与小程序账号 SSO")
|
|
|
+@Tag(name = "微信小程序扫码登录", description = "桌面端小程序码扫码登录,与小程序账号 SSO")
|
|
|
@RestController
|
|
|
@Slf4j
|
|
|
@RequestMapping("/api/auth/wechat-web")
|
|
|
@@ -42,50 +37,50 @@ public class WechatWebAuthController {
|
|
|
@Resource
|
|
|
private WechatWebLoginService wechatWebLoginService;
|
|
|
|
|
|
- @Resource
|
|
|
- private UserMapper userMapper;
|
|
|
-
|
|
|
@Resource
|
|
|
private UserService userService;
|
|
|
|
|
|
- @Operation(summary = "获取扫码登录二维码链接")
|
|
|
+ @Operation(summary = "获取扫码登录小程序码")
|
|
|
@PostMapping("/qr")
|
|
|
public Result<Map<String, Object>> qr() {
|
|
|
String state = UUID.randomUUID().toString().replace("-", "");
|
|
|
wechatWebLoginService.putState(state, "P");
|
|
|
- String url = wechatWebLoginService.buildQrConnectUrl(state);
|
|
|
Map<String, Object> data = new HashMap<>();
|
|
|
data.put("state", state);
|
|
|
- data.put("url", url);
|
|
|
+ try {
|
|
|
+ String base64 = wechatWebLoginService.generateQrImage(state);
|
|
|
+ data.put("qrImage", base64 == null || base64.isEmpty() ? "" : "data:image/png;base64," + base64);
|
|
|
+ } catch (RuntimeException e) {
|
|
|
+ log.error("生成登录小程序码失败: state={}", state, e);
|
|
|
+ wechatWebLoginService.removeState(state);
|
|
|
+ return Result.error("生成二维码失败:" + e.getMessage());
|
|
|
+ }
|
|
|
return Result.success(data);
|
|
|
}
|
|
|
|
|
|
- @Operation(summary = "微信扫码回调(微信服务器跳转,GET)")
|
|
|
- @GetMapping("/callback")
|
|
|
- public void callback(@RequestParam("code") String code,
|
|
|
- @RequestParam("state") String state,
|
|
|
- HttpServletResponse response) throws IOException {
|
|
|
+ @Operation(summary = "小程序扫码确认登录(小程序端调用,携带小程序 JWT)")
|
|
|
+ @PostMapping("/scan")
|
|
|
+ public Result<Map<String, Object>> scan(@RequestBody Map<String, String> body,
|
|
|
+ @RequestAttribute("userId") Long userId) {
|
|
|
+ String state = body != null ? body.get("state") : null;
|
|
|
if (state == null || state.trim().isEmpty()) {
|
|
|
- writeHtml(response, "登录失败", "缺少 state 参数");
|
|
|
- return;
|
|
|
+ return Result.error("缺少 state");
|
|
|
}
|
|
|
- Map<String, String> info = wechatWebLoginService.exchangeCode(code);
|
|
|
- String unionid = info != null ? info.get("unionid") : null;
|
|
|
- if (unionid == null || unionid.trim().isEmpty()) {
|
|
|
- wechatWebLoginService.putState(state, "N");
|
|
|
- writeHtml(response, "登录失败", "未获取到微信 unionid(请确认网站应用与小程序已绑定同一开放平台账号)");
|
|
|
- return;
|
|
|
+ Object stored = wechatWebLoginService.getState(state);
|
|
|
+ if (stored == null) {
|
|
|
+ return Result.error("二维码已过期,请重新获取");
|
|
|
+ }
|
|
|
+ if (!"P".equals(stored.toString())) {
|
|
|
+ return Result.error("二维码已被使用或已过期");
|
|
|
}
|
|
|
- User user = userMapper.selectOne(
|
|
|
- new LambdaQueryWrapper<User>().eq(User::getUnionid, unionid).last("LIMIT 1"));
|
|
|
- if (user == null) {
|
|
|
- wechatWebLoginService.putState(state, "N");
|
|
|
- writeHtml(response, "未找到账号", "该微信尚未绑定小程序账号,请先在小程序登录后再试");
|
|
|
- return;
|
|
|
+ if (userId == null) {
|
|
|
+ return Result.error("请先登录小程序");
|
|
|
}
|
|
|
- wechatWebLoginService.putState(state, "U" + user.getId());
|
|
|
- log.info("微信网站应用扫码成功: state={}, userId={}", state, user.getId());
|
|
|
- writeHtml(response, "扫码成功", "已登录,请返回客户端继续");
|
|
|
+ wechatWebLoginService.putState(state, "U" + userId);
|
|
|
+ log.info("小程序扫码确认登录: state={}, userId={}", state, userId);
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ data.put("status", "ok");
|
|
|
+ return Result.success(data);
|
|
|
}
|
|
|
|
|
|
@Operation(summary = "轮询扫码登录结果")
|
|
|
@@ -122,7 +117,7 @@ public class WechatWebAuthController {
|
|
|
data.put("role", login.getRole());
|
|
|
data.put("nickname", login.getNickname());
|
|
|
} catch (RuntimeException e) {
|
|
|
- log.warn("网站应用扫码换取 JWT 失败: state={}, error={}", state, e.getMessage());
|
|
|
+ log.warn("小程序扫码换取 JWT 失败: state={}, error={}", state, e.getMessage());
|
|
|
data.put("status", "error");
|
|
|
data.put("message", e.getMessage() != null ? e.getMessage() : "登录失败");
|
|
|
}
|
|
|
@@ -132,18 +127,4 @@ public class WechatWebAuthController {
|
|
|
data.put("message", "未知状态");
|
|
|
return Result.success(data);
|
|
|
}
|
|
|
-
|
|
|
- /** 输出极简结果页(微信内置浏览器/桌面浏览器展示) */
|
|
|
- private void writeHtml(HttpServletResponse response, String title, String html) throws IOException {
|
|
|
- response.setContentType("text/html;charset=UTF-8");
|
|
|
- String page = "<!DOCTYPE html><html lang=\"zh-CN\"><head><meta charset=\"UTF-8\">"
|
|
|
- + "<meta name=\"viewport\" content=\"width=device-width,initial-scale=1\">"
|
|
|
- + "<title>" + title + "</title></head>"
|
|
|
- + "<body style=\"font-family:-apple-system,'PingFang SC','Microsoft YaHei',sans-serif;"
|
|
|
- + "text-align:center;padding-top:80px;color:#333\">"
|
|
|
- + "<h2>" + title + "</h2>"
|
|
|
- + "<p style=\"color:#888\">" + html + "</p></body></html>";
|
|
|
- response.getWriter().write(page);
|
|
|
- response.getWriter().flush();
|
|
|
- }
|
|
|
}
|