|
|
@@ -0,0 +1,149 @@
|
|
|
+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.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)。
|
|
|
+ *
|
|
|
+ * <p>流程:桌面端 POST /qr 拿 state+url → 用户在浏览器扫码 → 微信回调 GET /callback(带 code+state)
|
|
|
+ * → 服务端换 unionid 匹配 users.unionid,写 state=U{userId} → 桌面端轮询 POST /poll 取 JWT。
|
|
|
+ *
|
|
|
+ * <p>注意:/callback 是微信服务器跳转的浏览器端点,必须用 GET——这是本项目
|
|
|
+ * 「统一 @PostMapping,禁止 @GetMapping」规范的唯一例外。
|
|
|
+ */
|
|
|
+@Tag(name = "微信网站应用登录", description = "桌面端微信扫码登录,与小程序账号 SSO")
|
|
|
+@RestController
|
|
|
+@Slf4j
|
|
|
+@RequestMapping("/api/auth/wechat-web")
|
|
|
+public class WechatWebAuthController {
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private WechatWebLoginService wechatWebLoginService;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private UserMapper userMapper;
|
|
|
+
|
|
|
+ @Resource
|
|
|
+ private UserService userService;
|
|
|
+
|
|
|
+ @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);
|
|
|
+ return Result.success(data);
|
|
|
+ }
|
|
|
+
|
|
|
+ @Operation(summary = "微信扫码回调(微信服务器跳转,GET)")
|
|
|
+ @GetMapping("/callback")
|
|
|
+ public void callback(@RequestParam("code") String code,
|
|
|
+ @RequestParam("state") String state,
|
|
|
+ HttpServletResponse response) throws IOException {
|
|
|
+ if (state == null || state.trim().isEmpty()) {
|
|
|
+ writeHtml(response, "登录失败", "缺少 state 参数");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ 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;
|
|
|
+ }
|
|
|
+ User user = userMapper.selectOne(
|
|
|
+ new LambdaQueryWrapper<User>().eq(User::getUnionid, unionid).last("LIMIT 1"));
|
|
|
+ if (user == null) {
|
|
|
+ wechatWebLoginService.putState(state, "N");
|
|
|
+ writeHtml(response, "未找到账号", "该微信尚未绑定小程序账号,请先在小程序登录后再试");
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ wechatWebLoginService.putState(state, "U" + user.getId());
|
|
|
+ log.info("微信网站应用扫码成功: state={}, userId={}", state, user.getId());
|
|
|
+ writeHtml(response, "扫码成功", "已登录,请返回客户端继续");
|
|
|
+ }
|
|
|
+
|
|
|
+ @Operation(summary = "轮询扫码登录结果")
|
|
|
+ @PostMapping("/poll")
|
|
|
+ public Result<Map<String, Object>> poll(@RequestBody Map<String, String> body) {
|
|
|
+ String state = body != null ? body.get("state") : null;
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
+ if (state == null || state.trim().isEmpty()) {
|
|
|
+ return Result.error("缺少 state");
|
|
|
+ }
|
|
|
+ Object stored = wechatWebLoginService.getState(state);
|
|
|
+ if (stored == null) {
|
|
|
+ data.put("status", "expired");
|
|
|
+ return Result.success(data);
|
|
|
+ }
|
|
|
+ String value = stored.toString();
|
|
|
+ if ("P".equals(value)) {
|
|
|
+ data.put("status", "pending");
|
|
|
+ return Result.success(data);
|
|
|
+ }
|
|
|
+ if ("N".equals(value)) {
|
|
|
+ wechatWebLoginService.removeState(state);
|
|
|
+ data.put("status", "notfound");
|
|
|
+ return Result.success(data);
|
|
|
+ }
|
|
|
+ if (value.startsWith("U")) {
|
|
|
+ wechatWebLoginService.removeState(state);
|
|
|
+ Long userId = Long.valueOf(value.substring(1));
|
|
|
+ try {
|
|
|
+ LoginResultDTO login = userService.ssoLogin(userId);
|
|
|
+ data.put("status", "success");
|
|
|
+ data.put("token", login.getToken());
|
|
|
+ data.put("userId", login.getUserId());
|
|
|
+ data.put("role", login.getRole());
|
|
|
+ data.put("nickname", login.getNickname());
|
|
|
+ } catch (RuntimeException e) {
|
|
|
+ log.warn("网站应用扫码换取 JWT 失败: state={}, error={}", state, e.getMessage());
|
|
|
+ data.put("status", "error");
|
|
|
+ data.put("message", e.getMessage() != null ? e.getMessage() : "登录失败");
|
|
|
+ }
|
|
|
+ return Result.success(data);
|
|
|
+ }
|
|
|
+ data.put("status", "error");
|
|
|
+ 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();
|
|
|
+ }
|
|
|
+}
|