Skip to content

Commit 4083f92

Browse files
authored
fix: login popup when enable jwt (#463)
fix: loging popup when enable jwt
1 parent a21b750 commit 4083f92

File tree

7 files changed

+119
-56
lines changed

7 files changed

+119
-56
lines changed

backend/api-gateway/src/main/java/com/datamate/gateway/common/filter/AuthFilter.java

Lines changed: 20 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -26,15 +26,19 @@
2626
/**
2727
* 用户数据隔离过滤器
2828
*
29-
* 支持两种认证模式
30-
* 1. SSO 模式:从 OmsAuthFilter 添加的 X-User-Name header 中提取用户信息
31-
* 2. JWT 模式:从 Authorization Bearer Token 中提取用户信息
32-
*
33-
* 无论哪种模式,最终都会添加 User header 供下游服务隔离用户数据
29+
* 支持两种场景
30+
* 1. 商业场景(SSO):OmsAuthFilter 已添加 X-User-Name header,直接使用
31+
* 2. 独立场景(可选登录):
32+
* - DATAMATE_JWT_ENABLED=true:必须登录,验证 JWT token 并添加 User header
33+
* - DATAMATE_JWT_ENABLED=false:允许匿名访问,不添加 User header
3434
*
3535
* 优先级:SSO > JWT
3636
* Order: 2 (低于 OmsAuthFilter 的 Order=1)
3737
*
38+
* 环境变量:
39+
* - OMS_AUTH_ENABLED:是否启用 OmsAuthFilter(商业场景)
40+
* - DATAMATE_JWT_ENABLE:独立场景下是否要求用户登录
41+
*
3842
* @author songyongtan
3943
* @date 2026-03-30
4044
*/
@@ -57,10 +61,17 @@ public class AuthFilter implements GlobalFilter, Ordered {
5761
public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
5862
ServerHttpRequest request = exchange.getRequest();
5963
String path = request.getURI().getPath();
64+
65+
// 公开接口:直接放行
6066
if (path.equals("/api/user/login") || path.equals("/api/user/signup")) {
6167
return chain.filter(exchange);
6268
}
6369

70+
// 内部接口:/api/user/me 内部会自行验证 SSO 或 JWT,直接放行
71+
if (path.equals("/api/user/me")) {
72+
return chain.filter(exchange);
73+
}
74+
6475
try {
6576
// 优先检查 SSO 模式(OmsAuthFilter 已添加的 header)
6677
String ssoUser = request.getHeaders().getFirst("X-User-Name");
@@ -77,16 +88,16 @@ public Mono<Void> filter(ServerWebExchange exchange, GatewayFilterChain chain) {
7788
return chain.filter(mutatedExchange);
7889
}
7990

80-
// 检查 JWT 模式
91+
// 独立场景:根据 DATAMATE_JWT_ENABLE 决定是否要求登录
8192
if (!jwtEnable) {
82-
log.debug("JWT is disabled, passing request without user header");
93+
log.debug("JWT authentication is not required, passing request without user header");
8394
return chain.filter(exchange);
8495
}
8596

86-
// JWT 模式:验证 Token
97+
// JWT 模式:必须登录,验证 Token
8798
String authHeader = request.getHeaders().getFirst(AUTH_HEADER);
8899
if (authHeader == null || !authHeader.startsWith(TOKEN_PREFIX)) {
89-
log.warn("JWT enabled but no valid Authorization header found");
100+
log.warn("JWT authentication is required but no valid Authorization header found");
90101
return sendUnauthorizedResponse(exchange);
91102
}
92103

backend/api-gateway/src/main/java/com/datamate/gateway/interfaces/dto/UserResponse.java

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -42,4 +42,9 @@ public class UserResponse {
4242
* 认证模式
4343
*/
4444
private String authMode; // "SSO" | "JWT" | "NONE"
45+
46+
/**
47+
* 是否强制要求登录(由 datamate.jwt.enable 控制)
48+
*/
49+
private Boolean requireLogin;
4550
}

backend/api-gateway/src/main/java/com/datamate/gateway/interfaces/rest/UserController.java

Lines changed: 19 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
import lombok.RequiredArgsConstructor;
1616
import lombok.extern.slf4j.Slf4j;
1717
import org.apache.commons.lang3.StringUtils;
18+
import org.springframework.beans.factory.annotation.Value;
1819
import org.springframework.http.HttpStatus;
1920
import org.springframework.http.ResponseEntity;
2021
import org.springframework.http.server.reactive.ServerHttpRequest;
@@ -43,6 +44,9 @@ public class UserController {
4344
private final OmsService omsService;
4445
private final OmsExtensionService omsExtensionService;
4546

47+
@Value("${datamate.jwt.enable:false}")
48+
private Boolean jwtEnable;
49+
4650
private static final String AUTH_TOKEN_KEY = "__Host-X-Auth-Token";
4751
private static final String CSRF_TOKEN_KEY = "__Host-X-Csrf-Token";
4852

@@ -109,32 +113,32 @@ public ResponseEntity<Response<LoginResponse>> register(@Valid @RequestBody Regi
109113
*/
110114
@GetMapping("/me")
111115
public Response<UserResponse> getCurrentUser(ServerHttpRequest request) {
112-
log.info("=== /api/user/me called ===");
116+
log.debug("=== /api/user/me called ===");
113117

114118
// 优先检查 SSO 模式(从 cookies 读取 OMS token)
115119
MultiValueMap<String, HttpCookie> cookies = request.getCookies();
116120
String authToken = getToken(cookies, AUTH_TOKEN_KEY);
117121
String csrfToken = getToken(cookies, CSRF_TOKEN_KEY);
118122

119-
log.info("Cookies present - __Host-X-Auth-Token: {}, __Host-X-Csrf-Token: {}",
123+
log.debug("Cookies present - __Host-X-Auth-Token: {}, __Host-X-Csrf-Token: {}",
120124
StringUtils.isNotBlank(authToken), StringUtils.isNotBlank(csrfToken));
121125

122126
if (StringUtils.isNotBlank(authToken)) {
123127
try {
124128
// 获取真实 IP
125129
String realIp = getRealIp(request);
126-
log.info("Calling OMS service with realIp: {}", realIp);
130+
log.debug("Calling OMS service with realIp: {}", realIp);
127131

128132
// 调用 OMS 服务验证
129133
String username = omsService.getUserNameFromOms(authToken, csrfToken, realIp);
130134
if (StringUtils.isNotBlank(username)) {
131-
log.info("SSO mode: user={}", username);
135+
log.info("SSO authentication successful: user={}", username);
132136

133137
// 获取用户组 ID(可能为 null)
134138
String groupId = null;
135139
try {
136140
groupId = omsExtensionService.getUserGroupId(username);
137-
log.info("User groupId: {}", groupId);
141+
log.debug("User groupId: {}", groupId);
138142
} catch (Exception e) {
139143
log.warn("Failed to get user group ID: {}", e.getMessage());
140144
}
@@ -144,6 +148,7 @@ public Response<UserResponse> getCurrentUser(ServerHttpRequest request) {
144148
.groupId(groupId)
145149
.authenticated(true)
146150
.authMode("SSO")
151+
.requireLogin(true) // SSO 模式始终要求登录
147152
.build());
148153
} else {
149154
log.warn("OMS service returned null username");
@@ -160,20 +165,26 @@ public Response<UserResponse> getCurrentUser(ServerHttpRequest request) {
160165
String username = userService.validateToken(token);
161166

162167
if (StringUtils.isNotBlank(username)) {
163-
log.info("JWT mode: user={}", username);
168+
log.info("JWT authentication successful: user={}", username);
164169
return Response.ok(UserResponse.builder()
165170
.username(username)
166171
.authenticated(true)
167172
.authMode("JWT")
173+
.requireLogin(true) // 已登录
168174
.build());
175+
} else {
176+
log.warn("JWT token validation failed");
169177
}
170178
}
171179

172-
// 未登录
173-
log.debug("User not authenticated");
180+
// 未登录:检查是否强制要求登录
181+
boolean requireLogin = Boolean.TRUE.equals(jwtEnable);
182+
log.debug("User not authenticated, requireLogin={}, jwtEnable={}", requireLogin, jwtEnable);
183+
174184
return Response.ok(UserResponse.builder()
175185
.authenticated(false)
176186
.authMode("NONE")
187+
.requireLogin(requireLogin) // 关键字段:告诉前端是否需要登录
177188
.build());
178189
}
179190
}

frontend/src/components/ErrorBoundary.tsx

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,7 @@ export default class ErrorBoundary extends Component<
6363
this.logErrorToService(error, errorInfo);
6464

6565
// 开发环境下在控制台显示详细错误
66-
if (process.env.NODE_ENV === "development") {
66+
if (import.meta.env.DEV) {
6767
console.error("ErrorBoundary 捕获到错误:", error);
6868
console.error("错误详情:", errorInfo);
6969
}
@@ -187,7 +187,7 @@ export function withErrorBoundary(
187187
Component: React.ComponentType
188188
): React.ComponentType {
189189
return (props) => (
190-
<ErrorBoundary showDetails={process.env.NODE_ENV === "development"}>
190+
<ErrorBoundary showDetails={import.meta.env.DEV}>
191191
<Component {...props} />
192192
</ErrorBoundary>
193193
);

frontend/src/main.tsx

Lines changed: 54 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,7 @@ import { Provider } from "react-redux";
1111
import theme from "./theme";
1212
import {errorConfigStore} from "@/utils/errorConfigStore.ts";
1313
import { setCachedHomePageUrl, getCachedHomePageUrl } from "@/utils/systemParam";
14+
import { setRequireLoginMode } from "@/utils/request";
1415
import "@/i18n";
1516

1617
function showLoadingUI() {
@@ -64,11 +65,23 @@ function getAuthToken(): string | null {
6465
* 在任何渲染之前检查系统参数 sys.home.page.url,若已配置则立即跳转,确保无闪烁。
6566
* 使用原始 fetch 但携带 JWT token,避免已登录用户仍收到 401。
6667
*/
67-
async function checkHomePageRedirect(): Promise<{ redirected: boolean; authNeeded: boolean }> {
68+
async function checkHomePageRedirect(requireLogin: boolean): Promise<{ redirected: boolean; authNeeded: boolean }> {
6869
if (window.location.pathname !== '/') {
6970
return { redirected: false, authNeeded: false };
7071
}
7172

73+
// 如果需要登录,检查是否有缓存的登录页URL
74+
if (requireLogin) {
75+
const cachedUrl = getCachedHomePageUrl();
76+
if (cachedUrl) {
77+
window.location.replace(cachedUrl);
78+
return { redirected: true, authNeeded: false };
79+
}
80+
// 需要登录且没有缓存URL,显示登录框
81+
return { redirected: false, authNeeded: true };
82+
}
83+
84+
// 不需要登录时,检查自定义首页
7285
const headers: Record<string, string> = { 'Content-Type': 'application/json' };
7386
const token = getAuthToken();
7487
if (token) {
@@ -91,15 +104,6 @@ async function checkHomePageRedirect(): Promise<{ redirected: boolean; authNeede
91104
}
92105
// 参数存在但值为空 → 管理员已清除,清掉缓存
93106
setCachedHomePageUrl(null);
94-
} else if (response.status === 401) {
95-
// 未登录,尝试从缓存读取
96-
const cachedUrl = getCachedHomePageUrl();
97-
if (cachedUrl) {
98-
window.location.replace(cachedUrl);
99-
return { redirected: true, authNeeded: false };
100-
}
101-
// 未登录且无缓存,需要弹出登录框
102-
return { redirected: false, authNeeded: true };
103107
}
104108
} catch {
105109
// 网络错误等,尝试从缓存读取
@@ -109,15 +113,52 @@ async function checkHomePageRedirect(): Promise<{ redirected: boolean; authNeede
109113
return { redirected: true, authNeeded: false };
110114
}
111115
}
116+
112117
return { redirected: false, authNeeded: false };
113118
}
114119

120+
/**
121+
* 检查是否需要登录(从后端获取配置)
122+
* 这个检查应该在应用启动时总是执行,无论当前路径是什么
123+
*/
124+
async function checkRequireLoginMode(): Promise<boolean> {
125+
try {
126+
const userResponse = await fetch('/api/user/me', {
127+
method: 'GET',
128+
credentials: 'include',
129+
headers: { 'Content-Type': 'application/json' },
130+
});
131+
132+
if (userResponse.ok) {
133+
const userResult = await userResponse.json();
134+
const requireLogin = userResult?.data?.requireLogin ?? false;
135+
// 设置全局标记,供 request.ts 使用
136+
setRequireLoginMode(requireLogin);
137+
return requireLogin;
138+
} else if (userResponse.status === 401) {
139+
// /api/user/me 本身返回 401,说明需要登录
140+
setRequireLoginMode(true);
141+
return true;
142+
}
143+
} catch (e) {
144+
console.error('[bootstrap] Failed to check login requirement:', e);
145+
}
146+
147+
// 默认不需要登录
148+
setRequireLoginMode(false);
149+
return false;
150+
}
151+
115152
async function bootstrap() {
116153
const container = document.getElementById("root");
117154
if (!container) return;
118155

119-
// 在任何 UI 渲染之前检查自定义首页重定向
120-
const { redirected, authNeeded } = await checkHomePageRedirect();
156+
// 首先检查是否需要登录(无论当前路径是什么)
157+
const requireLogin = await checkRequireLoginMode();
158+
159+
// 然后检查自定义首页重定向(只在根路径时)
160+
const { redirected, authNeeded } = await checkHomePageRedirect(requireLogin);
161+
121162
if (redirected) {
122163
return;
123164
}
@@ -131,7 +172,7 @@ async function bootstrap() {
131172
}
132173

133174
const root = createRoot(container);
134-
175+
135176
root.render(
136177
<StrictMode>
137178
<Provider store={store}>

frontend/src/pages/Layout/Header.tsx

Lines changed: 8 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@ interface UserResponse {
1717
groupId?: string;
1818
authenticated: boolean;
1919
authMode: 'SSO' | 'JWT' | 'NONE';
20+
requireLogin?: boolean; // 是否强制要求登录(由 DATAMATE_JWT_ENABLE 控制)
2021
}
2122

2223
function loginUsingPost(data: any) {
@@ -32,8 +33,8 @@ function getCurrentUser() {
3233
}
3334

3435
// ME 登录 URL(根据实际环境修改)
35-
const ME_LOGIN_URL = process.env.VITE_ME_LOGIN_URL || 'https://modelengine.com/login';
36-
const OMS_LOGOUT_URL = process.env.VITE_OMS_LOGOUT_URL || 'https://oms-service/logout';
36+
const ME_LOGIN_URL = import.meta.env.VITE_ME_LOGIN_URL || 'https://modelengine.com/login';
37+
const OMS_LOGOUT_URL = import.meta.env.VITE_OMS_LOGOUT_URL || 'https://oms-service/logout';
3738

3839
export function Header() {
3940
const { t } = useTranslation();
@@ -154,18 +155,13 @@ export function Header() {
154155
setCurrentUser(response.data);
155156
setAuthMode(response.data.authMode);
156157

157-
// 如果未登录,根据模式处理
158+
// 如果未登录,根据 requireLogin 决定是否弹出登录框
158159
if (!response.data.authenticated) {
159-
if (isSSOAvailable()) {
160-
// SSO 模式:自动跳转到 ME 登录
161-
console.log('SSO mode detected, redirecting to ME login...');
162-
// 不自动跳转,等待用户点击登录按钮
163-
} else {
164-
// JWT 模式:保持未登录状态
165-
console.log('JWT mode, waiting for user to login');
160+
if (response.data.requireLogin) {
161+
// 强制要求登录:弹出登录框
162+
window.dispatchEvent(new CustomEvent('show-login'));
166163
}
167-
} else {
168-
console.log(`User authenticated via ${response.data.authMode}:`, response.data.username);
164+
// 不强制登录:允许匿名访问
169165
}
170166
}
171167
} catch (error) {

0 commit comments

Comments
 (0)