M5: 方法更新移植 —— 移除 accountOnly/定时同步,本地登录与 Casdoor SSO 并存,登录时自动建号
CodeQL Advanced / Analyze (actions) (push) Canceled after 0s
CodeQL Advanced / Analyze (java-kotlin) (push) Canceled after 0s
CodeQL Advanced / Analyze (javascript-typescript) (push) Canceled after 0s
CodeQL Advanced / Analyze (python) (push) Canceled after 0s
Docker Image Build & Push / manifest (push) Canceled after 0s
Docker Image Build & Push / build-and-push-arm (push) Canceled after 0s
Docker Image Build & Push / build-and-push-amd (push) Canceled after 0s
Docker Image Build & Push / manifest (push) Canceled after 0s
Docker Image Build & Push / build-and-push-arm (push) Canceled after 0s
Docker Image Build & Push / build-and-push-amd (push) Canceled after 0s
Frontend Docker Image CI / Build and Push Frontend Docker Image (push) Canceled after 0s
Gateway Docker Image CI / Build and Push Gateway Docker Image (push) Canceled after 0s

- 网关: CasdoorProperties 移除 accountOnly/sync* 配置块; SsoConfigController 移除 /sync 管理端点; UserController 移除本地登录/注册 403
- 删除 CasdoorAccountSyncService/CasdoorAdminClient/CasdoorUserDto (定时全量同步方案废弃, 由 CasdoorUserService.upsertFromCasdoor 登录时自动建号替代)
- 保留既有修复: JJWT 0.12 适配(IdTokenVerifier/validateToken), updateById 修主键, RoleResolver owner/name 组名处理, WebFlux 兼容
- 前端: LoginDialog 移除 accountOnly 条件(本地登录+SSO 按钮恒显), Header 恢复注册入口恒显
This commit is contained in:
figmar
2026-09-01 18:04:28 +08:00
parent 0982dab458
commit 4e2ec55167
8 changed files with 349 additions and 901 deletions
@@ -1,209 +0,0 @@
package com.deepdata.gateway.infrastructure.casdoor;
import com.deepdata.gateway.common.config.RoleResolver;
import com.deepdata.gateway.domain.entity.User;
import com.deepdata.gateway.domain.repository.UserRepository;
import com.deepdata.gateway.infrastructure.casdoor.dto.CasdoorUserDto;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.SchedulingConfigurer;
import org.springframework.scheduling.config.ScheduledTaskRegistrar;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.stereotype.Service;
import reactor.core.publisher.Mono;
import reactor.core.scheduler.Schedulers;
import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.HashSet;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.atomic.AtomicBoolean;
/**
* Casdoor 账号全量同步(M3:Casdoor 为账号唯一数据源)。
*
* <p>语义(每次运行):
* <ul>
* <li>Casdoor 有、本地无 → 建号(占位密码,登录只能走 SSO);</li>
* <li>两边都有 → 刷新邮箱 / user_groups / role(口径与登录 upsert 一致);
* Casdoor 禁用 → 本地禁用(存量本地 JWT 立即被 {@code validateToken} 拒绝);
* Casdoor 恢复启用 → 本地重新启用;</li>
* <li>本地有、Casdoor 无 → 本地禁用(system 账号除外)。</li>
* </ul>
*
* <p>触发:定时({@code sync-interval-minutes},默认 5 分钟)+ 管理端手动
* (POST /sso/casdoor/sync)。仅 {@code enabled && accountOnly} 时启用。
*
* @since 2026/8/31
*/
@Slf4j
@Service
@RequiredArgsConstructor
public class CasdoorAccountSyncService {
private static final String SYSTEM_USER = "system";
private final CasdoorAdminClient adminClient;
private final CasdoorProperties props;
private final UserRepository userRepository;
private final RoleResolver roleResolver;
/** 防止定时/手动同步并发重叠 */
private final AtomicBoolean running = new AtomicBoolean(false);
/**
* 执行一次全量同步。
*
* @return 摘要 {total, created, updated, disabled, reenabled, skipped}
*/
public Mono<Map<String, Object>> syncFull() {
return adminClient.listUsers()
.flatMap(remotes -> Mono.fromCallable(() -> applySync(remotes))
.subscribeOn(Schedulers.boundedElastic()));
}
private Map<String, Object> applySync(List<CasdoorUserDto> remotes) {
if (!running.compareAndSet(false, true)) {
Map<String, Object> skip = new LinkedHashMap<>();
skip.put("skipped", "already running");
return skip;
}
try {
Map<String, User> locals = new HashMap<>();
for (User u : userRepository.list()) {
if (u.getUsername() != null) {
locals.put(u.getUsername(), u);
}
}
LocalDateTime now = LocalDateTime.now();
Set<String> remoteNames = new HashSet<>();
int created = 0;
int updated = 0;
int disabled = 0;
int reenabled = 0;
for (CasdoorUserDto r : remotes) {
String username = CasdoorUserService.normalizeUsername(r.getName());
if (username == null || SYSTEM_USER.equals(username)) {
continue;
}
remoteNames.add(username);
User user = locals.get(username);
if (user == null) {
user = createUser(r, username);
userRepository.save(user);
created++;
log.info("casdoor sync: created local user [{}] role={} groups={}",
username, user.getRole(), user.getUserGroups());
continue;
}
boolean touched = false;
if (r.getEmail() != null && !r.getEmail().isBlank() && !r.getEmail().equals(user.getEmail())) {
user.setEmail(r.getEmail());
touched = true;
}
if (r.getGroups() != null && !r.getGroups().equals(user.getUserGroups())) {
user.setUserGroups(r.getGroups());
touched = true;
}
String role = roleResolver.resolveRole(username, r.getGroups());
if (role != null && !role.equals(user.getRole())) {
user.setRole(role);
touched = true;
}
boolean casdoorDisabled = r.isDisabledOrForbidden();
if (casdoorDisabled && user.isEnabled()) {
user.setEnabled(false);
disabled++;
touched = true;
log.warn("casdoor sync: disabled local user [{}] (forbidden/disabled in casdoor)", username);
} else if (!casdoorDisabled && !user.isEnabled()) {
user.setEnabled(true);
reenabled++;
touched = true;
log.info("casdoor sync: re-enabled local user [{}]", username);
}
if (touched) {
userRepository.updateById(user);
updated++;
}
}
// 本地有、Casdoor 无 → 禁用
for (Map.Entry<String, User> e : locals.entrySet()) {
if (SYSTEM_USER.equals(e.getKey())) {
continue;
}
if (!remoteNames.contains(e.getKey()) && e.getValue().isEnabled()) {
e.getValue().setEnabled(false);
userRepository.updateById(e.getValue());
disabled++;
log.warn("casdoor sync: disabled local user [{}] (absent in casdoor)", e.getKey());
}
}
Map<String, Object> summary = new LinkedHashMap<>();
summary.put("total", remotes.size());
summary.put("created", created);
summary.put("updated", updated);
summary.put("disabled", disabled);
summary.put("reenabled", reenabled);
log.info("casdoor sync done: {}", summary);
return summary;
} finally {
running.set(false);
}
}
/** 定时触发(仅 accountOnly 生效) */
void scheduledSync() {
if (!props.isEnabled() || !props.isAccountOnly()) {
return;
}
try {
syncFull()
.doOnError(e -> log.error("casdoor scheduled sync failed: {}", e.getMessage()))
.onErrorResume(e -> Mono.just(Map.of("error", String.valueOf(e.getMessage()))))
.subscribe();
} catch (Exception e) {
log.error("casdoor scheduled sync rejected: {}", e.getMessage());
}
}
private User createUser(CasdoorUserDto r, String username) {
User user = new User();
user.setUsername(username);
user.setEmail(r.getEmail());
user.setFullName(r.getName());
user.setPasswordHash(new BCryptPasswordEncoder(10).encode(UUID.randomUUID().toString()));
user.setEnabled(!r.isDisabledOrForbidden());
user.setUserGroups(r.getGroups());
user.setRole(roleResolver.resolveRole(username, r.getGroups()));
return user;
}
/** 定时任务注册:间隔取 syncIntervalMinutes(避免 fixedDelayString 无法读 @ConfigurationProperties) */
@Configuration
@EnableScheduling
static class SyncScheduling implements SchedulingConfigurer {
private final CasdoorAccountSyncService service;
private final CasdoorProperties props;
SyncScheduling(CasdoorAccountSyncService service, CasdoorProperties props) {
this.service = service;
this.props = props;
}
@Override
public void configureTasks(ScheduledTaskRegistrar registrar) {
long intervalMillis = Math.max(1, props.getSyncIntervalMinutes()) * 60_000L;
registrar.addFixedDelayTask(service::scheduledSync, intervalMillis);
}
}
}
@@ -1,162 +0,0 @@
package com.deepdata.gateway.infrastructure.casdoor;
import com.deepdata.gateway.infrastructure.casdoor.dto.CasdoorUserDto;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import io.netty.channel.ChannelOption;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.client.reactive.ReactorClientHttpConnector;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.netty.http.client.HttpClient;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.atomic.AtomicReference;
/**
* Casdoor 管理 API 客户端(M3:账号全量同步)。
*
* <p>鉴权两档:
* <ul>
* <li>{@code api-token} 配置了 → 直接 Bearer(新版 Casdoor API Tokens);</li>
* <li>否则用 {@code sync-username}/{@code sync-password} 走 deepdata 应用的
* password grant 换短期 access_token(本 31.23 实例为老版,无 API Tokens 功能),
* 并按 expires_in 缓存复用,过期前 60s 刷新。</li>
* </ul>
*
* <p>用户列表端点:老版实例无 {@code /api/get-accounts},实测用
* {@code /api/get-users?owner=...}(2026-08-31 验证)。
*
* @since 2026/8/31
*/
@Slf4j
@Component
public class CasdoorAdminClient {
private static final ObjectMapper MAPPER = new ObjectMapper();
private static final String TOKEN_PATH = "/api/login/oauth/access_token";
private final CasdoorProperties props;
private final CasdoorClient casdoorClient;
private final WebClient webClient;
private final AtomicReference<CachedToken> tokenCache = new AtomicReference<>();
public CasdoorAdminClient(CasdoorProperties props, CasdoorClient casdoorClient) {
this.props = props;
this.casdoorClient = casdoorClient;
HttpClient httpClient = HttpClient.create()
.option(ChannelOption.CONNECT_TIMEOUT_MILLIS, props.getConnectTimeoutMillis())
.responseTimeout(Duration.ofMillis(props.getReadTimeoutMillis()));
this.webClient = WebClient.builder()
.clientConnector(new ReactorClientHttpConnector(httpClient))
.build();
}
/**
* 拉取组织全量用户(含组与禁用标志)。
*
* @return 用户列表;Casdoor 异常时以 error Mono 暴露
*/
public Mono<List<CasdoorUserDto>> listUsers() {
return adminToken().flatMap(token -> webClient.get()
.uri(apiBase() + "/api/get-users?owner=" + enc(props.getOrgOwner()))
.header("Authorization", "Bearer " + token)
.retrieve()
.bodyToMono(String.class)
.map(this::parseUsers)
.doOnNext(users -> log.info("casdoor sync: fetched {} users from org [{}]",
users.size(), props.getOrgOwner())));
}
/** 取管理令牌:apiToken 直用;否则 password grant + 缓存 */
private Mono<String> adminToken() {
if (hasText(props.getApiToken())) {
return Mono.just(props.getApiToken());
}
long now = System.currentTimeMillis();
CachedToken cached = tokenCache.get();
if (cached != null && cached.expiresAtMillis() - 60_000L > now) {
return Mono.just(cached.token());
}
if (!hasText(props.getSyncUsername()) || !hasText(props.getSyncPassword())) {
return Mono.error(new CasdoorAdminException(
"casdoor sync not configured: set api-token or sync-username/sync-password"));
}
return casdoorClient.passwordGrant(props.getSyncUsername(), props.getSyncPassword())
.map(r -> {
long ttlMillis = (r.getExpiresIn() != null ? r.getExpiresIn() : 3600L) * 1000L;
CachedToken next = new CachedToken(r.getAccessToken(), System.currentTimeMillis() + ttlMillis);
tokenCache.set(next);
log.info("casdoor sync token refreshed: ttl={}s", ttlMillis / 1000);
return next.token();
});
}
/** apiBase:tokenUri 去掉 /api/login/oauth/access_token 后缀得实例根 */
private String apiBase() {
String tokenUri = props.getTokenUri();
if (tokenUri != null && tokenUri.endsWith(TOKEN_PATH)) {
return tokenUri.substring(0, tokenUri.length() - TOKEN_PATH.length());
}
return tokenUri;
}
private List<CasdoorUserDto> parseUsers(String body) {
try {
JsonNode root = MAPPER.readTree(body);
if (!"ok".equalsIgnoreCase(root.path("status").asText())) {
throw new CasdoorAdminException("get-users returned status=" + root.path("status").asText()
+ " msg=" + root.path("msg").asText());
}
List<CasdoorUserDto> users = new ArrayList<>();
for (JsonNode node : root.path("data")) {
CasdoorUserDto u = new CasdoorUserDto();
u.setOwner(node.path("owner").asText(null));
u.setName(node.path("name").asText(null));
u.setEmail(node.path("email").asText(null));
u.setIsForbidden(node.path("isForbidden").isBoolean() ? node.path("isForbidden").asBoolean() : null);
u.setIsDisabled(node.path("isDisabled").isBoolean() ? node.path("isDisabled").asBoolean() : null);
u.setLegacyDisabled(node.path("is_disabled").isBoolean() ? node.path("is_disabled").asBoolean() : null);
List<String> groups = new ArrayList<>();
for (JsonNode g : node.path("groups")) {
if (g.isTextual() && !g.asText().isBlank()) {
groups.add(g.asText());
}
}
u.setGroups(groups.isEmpty() ? null : groups);
users.add(u);
}
return users;
} catch (CasdoorAdminException e) {
throw e;
} catch (Exception e) {
throw new CasdoorAdminException("failed to parse get-users response", e);
}
}
private static String enc(String s) {
return URLEncoder.encode(s == null ? "" : s, StandardCharsets.UTF_8);
}
private static boolean hasText(String s) {
return s != null && !s.isBlank();
}
private record CachedToken(String token, long expiresAtMillis) {}
/** Casdoor 管理 API 调用失败 */
public static class CasdoorAdminException extends RuntimeException {
public CasdoorAdminException(String message) {
super(message);
}
public CasdoorAdminException(String message, Throwable cause) {
super(message, cause);
}
}
}
@@ -86,38 +86,6 @@ public class CasdoorProperties {
/** Flow B:/api/user/login 是否优先走 Casdoor password grant */
private PasswordGrant passwordGrant = new PasswordGrant();
// ---------- M3:账权全面迁入 Casdoor(Casdoor 为账号唯一数据源) ----------
/**
* 账号唯一数据源开关:true 时
* <ul>
* <li>/api/user/login、/api/user/signup 返回 403(本地账密登录/注册关闭);</li>
* <li>定时全量同步 Casdoor 用户 → 本地 users(状态/组/角色),Casdoor 中不存在的本地账号置为禁用。</li>
* </ul>
* 前端据 /sso/casdoor/config 的 accountOnly 隐藏本地登录/注册入口。
*/
private boolean accountOnly = false;
/**
* Casdoor 管理 API Token(Bearer 调用 get-users 全量同步)。
* 新版 Casdoor 可在控制台「API Tokens」创建(owner 建议 built-in);
* 老版无此功能时留空,改走 {@link #syncUsername}/{@link #syncPassword}
* password grant 换短期令牌。
*/
private String apiToken;
/** 同步认证用户名(password grant;apiToken 为空时使用) */
private String syncUsername;
/** 同步认证密码(password grant;apiToken 为空时使用) */
private String syncPassword;
/** 同步的组织 owner(Casdoor 用户所属组织) */
private String orgOwner = "built-in";
/** 定时同步间隔(分钟) */
private int syncIntervalMinutes = 5;
// ---------- M2:组驱动角色 ----------
/** id_token 中组列表的 claim 名(Casdoor 应用 tokenFields=["Groups"] 时携带) */
@@ -1,49 +0,0 @@
package com.deepdata.gateway.infrastructure.casdoor.dto;
import com.fasterxml.jackson.annotation.JsonIgnoreProperties;
import com.fasterxml.jackson.annotation.JsonProperty;
import lombok.Data;
import java.util.List;
/**
* Casdoor 用户(M3 全量同步用),对应 {@code /api/get-users?owner=...} 返回项。
*
* <p>只映射同步所需字段;老版 Casdoor 无 {@code isDisabled},禁用标志为
* {@code isForbidden}(2026-08-31 实测 31.23 实例),两字段都解析。
*
* @since 2026/8/31
*/
@Data
@JsonIgnoreProperties(ignoreUnknown = true)
public class CasdoorUserDto {
/** 组织 owner,如 built-in */
private String owner;
/** Casdoor 用户名 */
private String name;
private String email;
/** Casdoor 组列表,如 ["built-in/deepdata-admin", "built-in/deepdata-user"] */
private List<String> groups;
/** 老版禁用标志(forbidden) */
@JsonProperty("isForbidden")
private Boolean isForbidden;
/** 新版禁用标志 */
@JsonProperty("isDisabled")
private Boolean isDisabled;
@JsonProperty("is_disabled")
private Boolean legacyDisabled;
/** 任一禁用标志为 true → 视为禁用 */
public boolean isDisabledOrForbidden() {
return Boolean.TRUE.equals(isForbidden)
|| Boolean.TRUE.equals(isDisabled)
|| Boolean.TRUE.equals(legacyDisabled);
}
}
@@ -1,102 +1,42 @@
package com.deepdata.gateway.interfaces.rest;
import com.deepdata.common.infrastructure.common.Response;
import com.deepdata.gateway.common.config.RoleResolver;
import com.deepdata.gateway.domain.service.UserService;
import com.deepdata.gateway.infrastructure.casdoor.CasdoorAccountSyncService;
import com.deepdata.gateway.infrastructure.casdoor.CasdoorProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.server.ServerWebExchange;
import reactor.core.publisher.Mono;
import java.util.LinkedHashMap;
import java.util.Map;
/**
* Casdoor SSO 前端配置端点(M2)+ M3 账号同步管理端点。
* Casdoor SSO 前端配置端点(M2)。
*
* <p>前端(登录页 / 菜单壳)通过 {@code GET /sso/casdoor/config} 获取 SSO 开关、
* accountOnly 与登录/登出入口,无需感知后端细节。始终注册(不走
* {@code @ConditionalOnProperty}):enabled=false 时 {@code enabled:false},
* 前端自然隐藏 SSO 入口。
*
* <p>M3 {@code POST /sync}:手动触发一次全量账号同步,仅 admin 角色可调
* (JWT sub → 本地用户 → 主角色=admin 或存量 role=ADMIN)。
* <p>前端(登录页 / 菜单壳)通过 {@code GET /sso/casdoor/config} 获取 SSO 开关与登录/登出入口,
* 无需感知后端细节。始终注册(不走 {@code @ConditionalOnProperty}):enabled=false 时
* {@code enabled:false},前端自然隐藏 SSO 入口 —— 这是与 OauthController
* 「关了就 404」的差异点,接入文档已注明。
*
* <p>敏感字段(clientSecret 等)绝不返回给浏览器。
*
* @since 2026/8/30
*/
@Slf4j
@RestController
@RequestMapping("/sso/casdoor")
@RequiredArgsConstructor
public class CasdoorSsoConfigController {
private final CasdoorProperties props;
private final CasdoorAccountSyncService syncService;
private final UserService userService;
private final RoleResolver roleResolver;
@GetMapping("/config")
public Response<Map<String, Object>> config() {
Map<String, Object> cfg = new LinkedHashMap<>();
cfg.put("enabled", props.isEnabled());
cfg.put("accountOnly", props.isAccountOnly());
cfg.put("loginUrl", "/api/user/oauth/login");
cfg.put("logoutUrl", "/api/user/oauth/logout");
cfg.put("issuer", props.getIssuer());
cfg.put("clientId", props.getClientId());
return Response.ok(cfg);
}
/**
* 手动全量同步(admin only)。WebFlux 事件循环线程不可 block,返回 Mono。
*/
@PostMapping("/sync")
public Mono<ResponseEntity<Response<Map<String, Object>>>> sync(ServerWebExchange exchange) {
if (!props.isEnabled() || !props.isAccountOnly()) {
return Mono.just(ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new Response<>("casdoor.disabled", "Casdoor 账号同步未启用", null)));
}
String username = currentUsername(exchange);
if (username == null || !isAdmin(username)) {
log.warn("casdoor sync rejected: caller={}", username);
return Mono.just(ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new Response<>("casdoor.forbidden", "仅管理员可触发账号同步", null)));
}
return syncService.syncFull()
.map(summary -> ResponseEntity.ok(Response.ok(summary)))
.onErrorResume(e -> {
log.error("casdoor manual sync failed: {}", e.getMessage());
return Mono.just(ResponseEntity.status(HttpStatus.BAD_GATEWAY)
.body(new Response<>("casdoor.sync_failed",
"同步失败: " + e.getMessage(), null)));
});
}
private String currentUsername(ServerWebExchange exchange) {
String authHeader = exchange.getRequest().getHeaders().getFirst("Authorization");
if (authHeader != null && authHeader.startsWith("Bearer ")) {
return userService.validateToken(authHeader.substring(7));
}
return null;
}
private boolean isAdmin(String username) {
return userService.findUserByUsername(username).map(u -> {
if (u.getRole() != null && "ADMIN".equalsIgnoreCase(u.getRole())) {
return true;
}
String primary = roleResolver.primaryRole(u.getUserGroups());
return "admin".equalsIgnoreCase(primary);
}).orElse(false);
}
}
@@ -8,7 +8,6 @@ import com.deepdata.gateway.common.config.RoleResolver;
import com.deepdata.gateway.domain.service.UserService;
import com.deepdata.gateway.infrastructure.client.OmsExtensionService;
import com.deepdata.gateway.infrastructure.client.OmsService;
import com.deepdata.gateway.infrastructure.casdoor.CasdoorProperties;
import com.deepdata.gateway.interfaces.dto.LoginRequest;
import com.deepdata.gateway.interfaces.dto.LoginResponse;
import com.deepdata.gateway.interfaces.dto.RegisterRequest;
@@ -47,8 +46,6 @@ public class UserController {
private final OmsExtensionService omsExtensionService;
/** M2:Casdoor 组 → 主角色 解析 */
private final RoleResolver roleResolver;
/** M3:accountOnly 时本地登录/注册一律 403 */
private final CasdoorProperties casdoorProperties;
@Value("${deepdata.jwt.enable:false}")
private Boolean jwtEnable;
@@ -92,12 +89,6 @@ public class UserController {
@PostMapping("/login")
@IgnoreResponseWrap
public ResponseEntity<Response<LoginResponse>> login(@Valid @RequestBody LoginRequest loginRequest) {
// M3:Casdoor 为账号唯一数据源时,本地账号密码登录一律 403
if (casdoorProperties.isEnabled() && casdoorProperties.isAccountOnly()) {
log.warn("Local login rejected (accountOnly): user={}", loginRequest.getUsername());
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new Response<>("casdoor.account_only", "本地登录已关闭,请使用 Casdoor 单点登录", null));
}
return userApplicationService.login(loginRequest)
.map(response -> ResponseEntity.ok(Response.ok(response)))
.orElseGet(() -> ResponseEntity.status(HttpStatus.UNAUTHORIZED)
@@ -107,12 +98,6 @@ public class UserController {
@PostMapping("/signup")
@IgnoreResponseWrap
public ResponseEntity<Response<LoginResponse>> register(@Valid @RequestBody RegisterRequest registerRequest) {
// M3:Casdoor 为账号唯一数据源时,本地注册一律 403
if (casdoorProperties.isEnabled() && casdoorProperties.isAccountOnly()) {
log.warn("Local signup rejected (accountOnly): user={}", registerRequest.getUsername());
return ResponseEntity.status(HttpStatus.FORBIDDEN)
.body(new Response<>("casdoor.account_only", "本地注册已关闭,账号由 Casdoor 统一管理", null));
}
return userApplicationService.register(registerRequest)
.map(response -> ResponseEntity.ok(Response.ok(response)))
.orElseGet(() -> ResponseEntity.status(HttpStatus.BAD_REQUEST)
+15 -27
View File
@@ -46,14 +46,6 @@ export function Header() {
const [currentUser, setCurrentUser] = useState<UserResponse | null>(null);
const [authMode, setAuthMode] = useState<'SSO' | 'JWT' | 'NONE'>('NONE');
const [userLoading, setUserLoading] = useState(true);
// M3:accountOnly(Casdoor 为账号唯一数据源)时隐藏注册入口,登录统一走登录弹窗(仅 Casdoor SSO)
const [accountOnly, setAccountOnly] = useState(false);
useEffect(() => {
get("/sso/casdoor/config")
.then((res: any) => setAccountOnly(!!res?.data?.accountOnly))
.catch(() => {});
}, []);
const handleLogin = async (values: { username: string; password: string }) => {
try {
@@ -258,8 +250,7 @@ export function Header() {
: t('user.actions.login'),
icon: <LogIn className="h-4 w-4" />,
onClick: () => {
// M3:accountOnly 时始终打开登录弹窗(内含 Casdoor 单点登录按钮),不走旧 OMS 登录页
if (!accountOnly && (authMode === 'SSO' || isSSOAvailable())) {
if (authMode === 'SSO' || isSSOAvailable()) {
// SSO 模式:跳转到 ME 登录
window.location.href = `${ME_LOGIN_URL}?redirect=${encodeURIComponent(window.location.href)}`;
} else {
@@ -268,24 +259,21 @@ export function Header() {
}
},
},
// M3:accountOnly 时隐藏「注册」入口(后端 /api/user/signup 亦已 403)
...(accountOnly ? [] : [
{
type: 'divider',
{
type: 'divider',
},
{
key: 'register',
label: t('user.actions.register'),
icon: <UserPlus className="h-4 w-4" />,
onClick: () => {
if (authMode === 'SSO' || isSSOAvailable()) {
message.info(t('user.messages.useSSO'));
} else {
setSignupOpen(true);
}
},
{
key: 'register',
label: t('user.actions.register'),
icon: <UserPlus className="h-4 w-4" />,
onClick: () => {
if (authMode === 'SSO' || isSSOAvailable()) {
message.info(t('user.messages.useSSO'));
} else {
setSignupOpen(true);
}
},
},
]),
},
];
return (
+4 -17
View File
@@ -14,7 +14,6 @@ interface LoginDialogProps {
interface SsoConfig {
enabled?: boolean;
accountOnly?: boolean;
loginUrl?: string;
logoutUrl?: string;
issuer?: string;
@@ -44,8 +43,6 @@ export function LoginDialog({ open, onOpenChange, onLogin, loading, onSignupClic
const [form] = Form.useForm()
const [ssoEnabled, setSsoEnabled] = useState(false)
const [ssoLoginUrl, setSsoLoginUrl] = useState("")
// M3:accountOnly 时后端已 403 本地登录/注册,前端只保留 Casdoor 单点登录
const [accountOnly, setAccountOnly] = useState(false)
useEffect(() => {
let alive = true;
@@ -53,7 +50,6 @@ export function LoginDialog({ open, onOpenChange, onLogin, loading, onSignupClic
if (!alive) return;
setSsoEnabled(!!cfg.enabled && !!cfg.loginUrl);
if (cfg.loginUrl) setSsoLoginUrl(cfg.loginUrl);
setAccountOnly(!!cfg.accountOnly);
});
return () => {
alive = false;
@@ -93,27 +89,19 @@ export function LoginDialog({ open, onOpenChange, onLogin, loading, onSignupClic
<div className="mb-4">
<Button
type="primary"
ghost
block
icon={<ShieldCheck className="w-4 h-4" />}
onClick={handleCasdoorLogin}
>
{t('user.loginDialog.casdoorLogin')}
</Button>
{!accountOnly && (
<Divider plain style={{ margin: '12px 0' }}>
{t('user.loginDialog.or')}
</Divider>
)}
<Divider plain style={{ margin: '12px 0' }}>
{t('user.loginDialog.or')}
</Divider>
</div>
)}
{accountOnly && !ssoEnabled && (
<div className="mb-4 text-sm text-amber-600">
本地登录已关闭,且 Casdoor 单点登录入口不可用,请联系管理员。
</div>
)}
{!accountOnly && (
<Form
form={form}
layout="vertical"
@@ -160,7 +148,6 @@ export function LoginDialog({ open, onOpenChange, onLogin, loading, onSignupClic
</div>
</Form.Item>
</Form>
)}
</Modal>
)
}