Skip to content

Commit 7e649d9

Browse files
committed
logout when restart the server
1 parent ea7e29b commit 7e649d9

File tree

8 files changed

+249
-3
lines changed

8 files changed

+249
-3
lines changed

gateway-ha/src/main/java/io/trino/gateway/ha/resource/LoginResource.java

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -185,4 +185,26 @@ else if (oauthManager != null) {
185185
}
186186
return Response.ok(Result.ok("Ok", loginType)).build();
187187
}
188+
189+
@POST
190+
@Path("serverInfo")
191+
@Consumes(MediaType.APPLICATION_JSON)
192+
@Produces(MediaType.APPLICATION_JSON)
193+
public Response serverInfo()
194+
{
195+
long serverStartTime = System.currentTimeMillis();
196+
if (formAuthManager != null) {
197+
// Get server start time from form auth manager
198+
try {
199+
java.lang.reflect.Field field = formAuthManager.getClass().getDeclaredField("SERVER_START_TIME");
200+
field.setAccessible(true);
201+
serverStartTime = (Long) field.get(null);
202+
}
203+
catch (Exception e) {
204+
log.error(e, "Could not get server start time");
205+
}
206+
}
207+
Map<String, Object> serverInfo = Map.of("serverStart", serverStartTime);
208+
return Response.ok(Result.ok(serverInfo)).build();
209+
}
188210
}

gateway-ha/src/main/java/io/trino/gateway/ha/security/LbFormAuthManager.java

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,7 +26,9 @@
2626
import io.trino.gateway.ha.domain.request.RestLoginRequest;
2727
import io.trino.gateway.ha.security.util.BasicCredentials;
2828

29+
import java.time.Instant;
2930
import java.util.Collections;
31+
import java.util.Date;
3032
import java.util.List;
3133
import java.util.Map;
3234
import java.util.Optional;
@@ -38,6 +40,8 @@
3840
public class LbFormAuthManager
3941
{
4042
private static final Logger log = Logger.get(LbFormAuthManager.class);
43+
private static final long SERVER_START_TIME = System.currentTimeMillis();
44+
private static final int SESSION_TIMEOUT_MINUTES = 15;
4145
/**
4246
* Cookie key to pass the token.
4347
*/
@@ -105,6 +109,21 @@ public Optional<Map<String, Claim>> getClaimsFromIdToken(String idToken)
105109
DecodedJWT jwt = JWT.decode(idToken);
106110

107111
if (LbTokenUtil.validateToken(idToken, lbKeyProvider.getRsaPublicKey(), jwt.getIssuer(), Optional.empty())) {
112+
// Check if token was issued before server restart
113+
Claim serverStartClaim = jwt.getClaim("server_start");
114+
if (serverStartClaim != null && !serverStartClaim.isNull()) {
115+
long tokenServerStart = serverStartClaim.asLong();
116+
if (tokenServerStart != SERVER_START_TIME) {
117+
log.info("Token invalidated due to server restart");
118+
return Optional.empty();
119+
}
120+
}
121+
// Check token expiration
122+
Date expiresAt = jwt.getExpiresAt();
123+
if (expiresAt != null && expiresAt.before(new Date())) {
124+
log.info("Token expired");
125+
return Optional.empty();
126+
}
108127
return Optional.of(jwt.getClaims());
109128
}
110129
}
@@ -124,10 +143,16 @@ private String getSelfSignedToken(String username)
124143

125144
Map<String, Object> headers = Map.of("alg", "RS256");
126145

146+
Instant now = Instant.now();
147+
Instant expiration = now.plusSeconds(SESSION_TIMEOUT_MINUTES * 60);
148+
127149
token = JWT.create()
128150
.withHeader(headers)
129151
.withIssuer(SessionCookie.SELF_ISSUER_ID)
130152
.withSubject(username)
153+
.withIssuedAt(Date.from(now))
154+
.withExpiresAt(Date.from(expiration))
155+
.withClaim("server_start", SERVER_START_TIME)
131156
.sign(algorithm);
132157
}
133158
catch (JWTCreationException exception) {

gateway-ha/src/main/java/io/trino/gateway/ha/security/SessionCookie.java

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,7 +30,7 @@ public static NewCookie getTokenCookie(String token)
3030
.path("/")
3131
.domain("")
3232
.comment("")
33-
.maxAge(60 * 60 * 24)
33+
.maxAge(60 * 15) // 15 minutes session timeout
3434
.secure(true)
3535
.build();
3636
}

webapp/src/App.tsx

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ import { useEffect } from 'react';
1616
import { getCSSVar } from './utils/utils';
1717
import { IllustrationIdle, IllustrationIdleDark } from '@douyinfe/semi-illustrations';
1818
import Cookies from 'js-cookie';
19+
import { SessionManager } from './utils/session';
1920

2021
function App() {
2122
return (
@@ -40,6 +41,18 @@ function Screen() {
4041
access.updateToken(token);
4142
Cookies.remove('token');
4243
}
44+
// Initialize session management
45+
const sessionManager = SessionManager.getInstance();
46+
sessionManager.setSessionExpiredCallback(() => {
47+
access.logout();
48+
});
49+
50+
// Check token validity on app start
51+
access.checkTokenValidity().catch(console.error);
52+
53+
return () => {
54+
sessionManager.clearTimeout();
55+
};
4356
}, [])
4457
return (
4558
<>

webapp/src/api/base.ts

Lines changed: 33 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,12 @@
11
import { useAccessStore } from "../store";
22
import Locale, { getServerLang } from "../locales";
33
import { Toast } from "@douyinfe/semi-ui";
4+
import { SessionManager } from "../utils/session";
45

56
export class ClientApi {
67
async get(url: string, params: Record<string, any> = {}): Promise<any> {
8+
// Check token validity before making request
9+
await this.validateTokenBeforeRequest(url);
710
let queryString = "";
811
if (Object.keys(params).length > 0) {
912
queryString = "?" + new URLSearchParams(params).toString();
@@ -40,6 +43,8 @@ export class ClientApi {
4043
}
4144

4245
async post(url: string, body: Record<string, any> = {}): Promise<any> {
46+
// Check token validity before making request
47+
await this.validateTokenBeforeRequest(url);
4348
const res: Response = await fetch(
4449
this.path(url),
4550
{
@@ -76,6 +81,8 @@ export class ClientApi {
7681
}
7782

7883
async postForm(url: string, formData: FormData = new FormData()): Promise<any> {
84+
// Check token validity before making request
85+
await this.validateTokenBeforeRequest(url);
7986
const res: Response = await fetch(
8087
this.path(url),
8188
{
@@ -104,6 +111,26 @@ export class ClientApi {
104111
return resJson.data;
105112
}
106113

114+
private async validateTokenBeforeRequest(url: string): Promise<void> {
115+
// Skip validation for login-related endpoints to avoid infinite loops
116+
if (url.includes('/login') || url.includes('/serverInfo') || url.includes('/loginType')) {
117+
return;
118+
}
119+
120+
const accessStore = useAccessStore.getState();
121+
if (accessStore.token) {
122+
try {
123+
const isValid = await accessStore.checkTokenValidity();
124+
if (!isValid) {
125+
throw new Error('Token validation failed');
126+
}
127+
} catch (error) {
128+
// Token validation failed, user will be logged out
129+
throw error;
130+
}
131+
}
132+
}
133+
107134
path(path: string): string {
108135
const proxyPath = import.meta.env.VITE_PROXY_PATH;
109136
return [proxyPath, path].join("");
@@ -134,7 +161,12 @@ export function getHeaders(): Record<string, string> {
134161
const validString = (x: string) => x && x.length > 0;
135162

136163
if (validString(accessStore.token)) {
137-
headers.Authorization = makeBearer(accessStore.token);
164+
// For synchronous header generation, we'll do basic token validation
165+
// The async server restart check will happen in the session manager
166+
const sessionManager = SessionManager.getInstance();
167+
if (!sessionManager.isTokenExpired(accessStore.token)) {
168+
headers.Authorization = makeBearer(accessStore.token);
169+
}
138170
}
139171

140172
return headers;

webapp/src/api/webapp/login.ts

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,3 +23,7 @@ export async function loginTypeApi(): Promise<any> {
2323
export async function getUIConfiguration(): Promise<any> {
2424
return api.get('/webapp/getUIConfiguration')
2525
}
26+
27+
export async function serverInfoApi(): Promise<any> {
28+
return api.post('/serverInfo', {})
29+
}

webapp/src/store/access.ts

Lines changed: 49 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,8 @@
11
import { create } from "zustand";
22
import { persist } from "zustand/middleware";
33
import { StoreKey } from "../constant";
4-
import { getInfoApi } from "../api/webapp/login";
4+
import { getInfoApi, serverInfoApi } from "../api/webapp/login";
5+
import { SessionManager } from "../utils/session";
56

67
export enum Role {
78
ADMIN = "ADMIN",
@@ -28,6 +29,8 @@ export interface AccessControlStore {
2829
getUserInfo: (_?: boolean) => void;
2930
hasRole: (role: Role) => boolean;
3031
hasPermission: (permission: string | undefined) => boolean;
32+
logout: () => void;
33+
checkTokenValidity: () => Promise<boolean>;
3134
}
3235

3336
let fetchState: number = 0; // 0 not fetch, 1 fetching, 2 done
@@ -78,6 +81,51 @@ export const useAccessStore = create<AccessControlStore>()(
7881
const permissions = get().permissions
7982
return permission == undefined || permissions == null || permissions.length == 0 || permissions.includes(permission);
8083
},
84+
logout() {
85+
const sessionManager = SessionManager.getInstance();
86+
sessionManager.clearTimeout();
87+
set(() => ({
88+
token: "",
89+
userId: "",
90+
userName: "",
91+
nickName: "",
92+
userType: "",
93+
email: "",
94+
phonenumber: "",
95+
sex: "",
96+
avatar: "",
97+
permissions: [],
98+
roles: [],
99+
}));
100+
fetchState = 0;
101+
},
102+
async checkTokenValidity() {
103+
const token = get().token;
104+
if (!token) return false;
105+
106+
const sessionManager = SessionManager.getInstance();
107+
108+
// Check if token is expired
109+
if (sessionManager.isTokenExpired(token)) {
110+
get().logout();
111+
return false;
112+
}
113+
114+
// Check for server restart
115+
try {
116+
const serverInfo = await serverInfoApi();
117+
if (sessionManager.checkServerRestart(token, serverInfo.serverStart)) {
118+
console.log('Server restart detected, logging out');
119+
get().logout();
120+
return false;
121+
}
122+
} catch (error) {
123+
console.error('Error checking server info:', error);
124+
// Don't logout on API error, just continue
125+
}
126+
127+
return true;
128+
},
81129
}),
82130
{
83131
name: StoreKey.Access,

webapp/src/utils/session.ts

Lines changed: 102 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,102 @@
1+
export class SessionManager {
2+
private static instance: SessionManager;
3+
private timeoutId: number | null = null;
4+
private readonly TIMEOUT_MINUTES = 15;
5+
private readonly CHECK_INTERVAL = 60000; // Check every minute
6+
private lastActivity: number = Date.now();
7+
private onSessionExpired?: () => void;
8+
9+
private constructor() {
10+
this.setupActivityListeners();
11+
this.startTimeoutCheck();
12+
}
13+
14+
public static getInstance(): SessionManager {
15+
if (!SessionManager.instance) {
16+
SessionManager.instance = new SessionManager();
17+
}
18+
return SessionManager.instance;
19+
}
20+
21+
public setSessionExpiredCallback(callback: () => void): void {
22+
this.onSessionExpired = callback;
23+
}
24+
25+
public resetTimeout(): void {
26+
this.lastActivity = Date.now();
27+
}
28+
29+
public clearTimeout(): void {
30+
if (this.timeoutId) {
31+
clearInterval(this.timeoutId);
32+
this.timeoutId = null;
33+
}
34+
}
35+
36+
private setupActivityListeners(): void {
37+
const events = ['mousedown', 'mousemove', 'keypress', 'scroll', 'touchstart', 'click'];
38+
39+
events.forEach(event => {
40+
document.addEventListener(event, () => {
41+
this.resetTimeout();
42+
}, true);
43+
});
44+
}
45+
46+
private startTimeoutCheck(): void {
47+
this.timeoutId = setInterval(() => {
48+
const now = Date.now();
49+
const timeSinceLastActivity = now - this.lastActivity;
50+
const timeoutMs = this.TIMEOUT_MINUTES * 60 * 1000;
51+
52+
if (timeSinceLastActivity >= timeoutMs) {
53+
this.handleSessionExpired();
54+
}
55+
}, this.CHECK_INTERVAL);
56+
}
57+
58+
private handleSessionExpired(): void {
59+
this.clearTimeout();
60+
if (this.onSessionExpired) {
61+
this.onSessionExpired();
62+
}
63+
}
64+
65+
public isTokenExpired(token: string): boolean {
66+
if (!token) return true;
67+
68+
try {
69+
// Decode JWT token to check expiration
70+
const payload = JSON.parse(atob(token.split('.')[1]));
71+
const currentTime = Math.floor(Date.now() / 1000);
72+
73+
// Check if token has expired
74+
if (payload.exp && payload.exp < currentTime) {
75+
return true;
76+
}
77+
78+
return false;
79+
} catch (error) {
80+
console.error('Error decoding token:', error);
81+
return true;
82+
}
83+
}
84+
85+
public checkServerRestart(token: string, currentServerStart: number): boolean {
86+
if (!token) return false;
87+
88+
try {
89+
const payload = JSON.parse(atob(token.split('.')[1]));
90+
91+
// Check if token was issued before server restart
92+
if (payload.server_start && payload.server_start !== currentServerStart) {
93+
return true;
94+
}
95+
96+
return false;
97+
} catch (error) {
98+
console.error('Error checking server restart:', error);
99+
return false;
100+
}
101+
}
102+
}

0 commit comments

Comments
 (0)