|
@@ -12,6 +12,7 @@ import com.etotem.cfc.mapper.UserMapper;
|
|
|
import com.etotem.cfc.service.LoginAttemptService;
|
|
import com.etotem.cfc.service.LoginAttemptService;
|
|
|
import com.etotem.cfc.service.UserService;
|
|
import com.etotem.cfc.service.UserService;
|
|
|
import com.etotem.cfc.service.VerificationCodeService;
|
|
import com.etotem.cfc.service.VerificationCodeService;
|
|
|
|
|
+import com.etotem.cfc.service.WechatWebLoginService;
|
|
|
import lombok.extern.slf4j.Slf4j;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
|
import javax.annotation.Resource;
|
|
import javax.annotation.Resource;
|
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
import org.springframework.beans.factory.annotation.Value;
|
|
@@ -24,6 +25,7 @@ import java.util.Date;
|
|
|
import java.util.HashMap;
|
|
import java.util.HashMap;
|
|
|
import java.util.List;
|
|
import java.util.List;
|
|
|
import java.util.Map;
|
|
import java.util.Map;
|
|
|
|
|
+import java.util.UUID;
|
|
|
|
|
|
|
|
@Slf4j
|
|
@Slf4j
|
|
|
@RestController
|
|
@RestController
|
|
@@ -47,6 +49,9 @@ public class AdminAuthController {
|
|
|
@Resource
|
|
@Resource
|
|
|
private LoginAttemptService loginAttemptService;
|
|
private LoginAttemptService loginAttemptService;
|
|
|
|
|
|
|
|
|
|
+ @Resource
|
|
|
|
|
+ private WechatWebLoginService wechatWebLoginService;
|
|
|
|
|
+
|
|
|
@Value("${admin.skip-captcha:false}")
|
|
@Value("${admin.skip-captcha:false}")
|
|
|
private boolean skipCaptcha;
|
|
private boolean skipCaptcha;
|
|
|
|
|
|
|
@@ -330,6 +335,118 @@ public class AdminAuthController {
|
|
|
return Result.success(result);
|
|
return Result.success(result);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 后台管理端微信扫码登录:获取小程序码。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>复用小程序端「小程序码扫码 SSO」的 state 机制({@link WechatWebLoginService}),
|
|
|
|
|
+ * 但与 {@code /api/auth/wechat-web/*} 完全独立:此处不调用 {@code UserService.ssoLogin}
|
|
|
|
|
+ * (该方法拒绝 admin 账号),改为按后台角色白名单签发管理端 JWT。
|
|
|
|
|
+ */
|
|
|
|
|
+ @PostMapping("/wechat-scan-qr")
|
|
|
|
|
+ public Result<Map<String, Object>> wechatScanQr() {
|
|
|
|
|
+ String state = UUID.randomUUID().toString().replace("-", "");
|
|
|
|
|
+ wechatWebLoginService.putState(state, "P");
|
|
|
|
|
+ Map<String, Object> data = new HashMap<>();
|
|
|
|
|
+ data.put("state", state);
|
|
|
|
|
+ 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);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 后台管理端微信扫码登录:轮询扫码结果。
|
|
|
|
|
+ *
|
|
|
|
|
+ * <p>status: pending(待扫码)/ expired(二维码过期)/ success(已签发 JWT)/ error(无管理权限等)。
|
|
|
|
|
+ * 成功返回字段与 {@code /api/admin-auth/login} 对齐,token 的 role claim 为选中后台身份。
|
|
|
|
|
+ */
|
|
|
|
|
+ @PostMapping("/wechat-scan-poll")
|
|
|
|
|
+ public Result<Map<String, Object>> wechatScanPoll(@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 (!value.startsWith("U")) {
|
|
|
|
|
+ data.put("status", "error");
|
|
|
|
|
+ data.put("message", "未知状态");
|
|
|
|
|
+ return Result.success(data);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 已确认扫码即为终态:先清除 state,防止重复轮询重放
|
|
|
|
|
+ wechatWebLoginService.removeState(state);
|
|
|
|
|
+
|
|
|
|
|
+ Long userId;
|
|
|
|
|
+ try {
|
|
|
|
|
+ userId = Long.valueOf(value.substring(1));
|
|
|
|
|
+ } catch (NumberFormatException e) {
|
|
|
|
|
+ data.put("status", "error");
|
|
|
|
|
+ data.put("message", "二维码状态异常");
|
|
|
|
|
+ return Result.success(data);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ User user = userMapper.selectById(userId);
|
|
|
|
|
+ if (user == null) {
|
|
|
|
|
+ data.put("status", "error");
|
|
|
|
|
+ data.put("message", "用户不存在");
|
|
|
|
|
+ return Result.success(data);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // parent/child 等 C 端账号不允许登录后台
|
|
|
|
|
+ List<String> backendRoles = AdminRoles.backendRolesOf(user.getRoles(), user.getRole());
|
|
|
|
|
+ if (backendRoles.isEmpty()) {
|
|
|
|
|
+ log.warn("用户 {} 扫码登录后台管理,无后台角色,角色为: {}", userId, user.getRole());
|
|
|
|
|
+ data.put("status", "error");
|
|
|
|
|
+ data.put("message", "该账号没有管理权限");
|
|
|
|
|
+ return Result.success(data);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 身份选择:多角色账号可指定以哪个角色登录(对齐 /login 的身份选择逻辑)
|
|
|
|
|
+ String selected = backendRoles.get(0);
|
|
|
|
|
+ String loginRole = body.get("loginRole");
|
|
|
|
|
+ if (loginRole != null && !loginRole.isEmpty()) {
|
|
|
|
|
+ String normalized = AdminRoles.normalize(loginRole);
|
|
|
|
|
+ if (!backendRoles.contains(normalized)) {
|
|
|
|
|
+ data.put("status", "error");
|
|
|
|
|
+ data.put("message", "该账号没有【" + AdminRoles.roleLabel(normalized) + "】身份");
|
|
|
|
|
+ return Result.success(data);
|
|
|
|
|
+ }
|
|
|
|
|
+ selected = normalized;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ List<String> roles = userService.getUserRoles(user.getId());
|
|
|
|
|
+ if (roles.isEmpty()) {
|
|
|
|
|
+ roles = Collections.singletonList(selected);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ String token = jwtConfig.generateToken(user.getId(), buildTokenRoles(selected, roles));
|
|
|
|
|
+ log.info("后台微信扫码登录成功: userId={}, role={}", userId, selected);
|
|
|
|
|
+
|
|
|
|
|
+ data.put("status", "success");
|
|
|
|
|
+ data.put("token", token);
|
|
|
|
|
+ data.put("adminId", user.getId());
|
|
|
|
|
+ data.put("username", user.getNickname() != null ? user.getNickname() : user.getPhone());
|
|
|
|
|
+ data.put("realName", user.getRealName() != null ? user.getRealName() : user.getNickname());
|
|
|
|
|
+ data.put("role", selected);
|
|
|
|
|
+ data.put("roles", roles);
|
|
|
|
|
+ return Result.success(data);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
/**
|
|
/**
|
|
|
* 查询账号可用的后台身份列表(登录页身份选择下拉框用,公开接口)
|
|
* 查询账号可用的后台身份列表(登录页身份选择下拉框用,公开接口)
|
|
|
* 返回: [{role: 'admin', label: '超级管理员'}, ...]
|
|
* 返回: [{role: 'admin', label: '超级管理员'}, ...]
|