Skip to content

Commit 90f6f9d

Browse files
soso
authored andcommitted
Fix stale sessions after container restart
1 parent 0e42de6 commit 90f6f9d

4 files changed

Lines changed: 63 additions & 9 deletions

File tree

docs/changelog.md

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,28 @@
11
# 版本更新日志
22

3+
## v0.0.3
4+
5+
发布时间:2026-05-24
6+
7+
### 修复
8+
9+
- 修复 Docker 容器重启后旧页面继续轮询导致 `invalid or expired session` 反复刷错误日志的问题。
10+
- 面板接口返回登录过期后,前端会自动清理本地登录状态、停止轮询并回到登录页。
11+
- 后端将登录过期类请求从 `ERROR` 降为 `INFO`,避免正常会话失效刷屏污染错误日志。
12+
13+
- 修复容器重启后旧页面可能使用旧 RSA 公钥提交登录或配置请求,导致 `failed to decrypt request key: decryption error` 的问题。
14+
- 前端每次提交加密请求前都会重新获取当前后端公钥。
15+
- `/api/public-key` 返回 `Cache-Control: no-store`,避免浏览器或中间代理缓存旧公钥。
16+
- 解密失败这类请求格式问题调整为 `WARN` 日志。
17+
18+
### 验证
19+
20+
- `cargo fmt --check`
21+
- `cargo clippy --all-targets -- -D warnings`
22+
- `cargo test`
23+
- `cd frontend && npm run build`
24+
- `cargo build --release --locked`
25+
326
## v0.0.2
427

528
发布时间:2026-05-24

frontend/src/App.vue

Lines changed: 20 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -974,10 +974,28 @@ async function api<T>(path: string, init: RequestInit = {}): Promise<T> {
974974
headers.set('Content-Type', 'application/json')
975975
if (token.value) headers.set('Authorization', `Bearer ${token.value}`)
976976
const response = await fetch(path, { ...init, headers })
977-
if (!response.ok) throw new Error(await response.text())
977+
if (!response.ok) {
978+
const message = await response.text()
979+
if (response.status === 401 && token.value && !isAuthBootstrapPath(path)) {
980+
handleAuthExpired()
981+
}
982+
throw new Error(message)
983+
}
978984
return response.json() as Promise<T>
979985
}
980986
987+
function isAuthBootstrapPath(path: string) {
988+
return path === '/api/login' || path === '/api/setup' || path === '/api/setup-status'
989+
}
990+
991+
function handleAuthExpired() {
992+
token.value = ''
993+
clearStoredToken()
994+
stopDashboardPolling()
995+
mode.value = 'login'
996+
error.value = '登录已过期,请重新登录'
997+
}
998+
981999
async function fetchPublicKey() {
9821000
const response = await api<PublicKeyResponse>('/api/public-key')
9831001
if (!hasWebCrypto()) {
@@ -999,7 +1017,7 @@ async function fetchPublicKey() {
9991017
}
10001018
10011019
async function encryptPayload(name: string, value: unknown) {
1002-
if (!publicKey.value) publicKey.value = await fetchPublicKey()
1020+
publicKey.value = await fetchPublicKey()
10031021
const aesKey = randomBytes(32)
10041022
const fieldName = randomFieldName()
10051023
const iv = randomBytes(12)

src/crypto_api.rs

Lines changed: 15 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,12 @@ use aes_gcm::{
44
Aes256Gcm, Nonce,
55
aead::{Aead, KeyInit},
66
};
7-
use axum::{Json, extract::State};
7+
use axum::{
8+
Json,
9+
extract::State,
10+
http::header,
11+
response::{IntoResponse, Response},
12+
};
813
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
914
use rsa::{
1015
Oaep, RsaPrivateKey, RsaPublicKey,
@@ -112,11 +117,15 @@ struct PlainField {
112117
value: Value,
113118
}
114119

115-
pub async fn public_key(State(state): State<AppState>) -> Json<PublicKeyResponse> {
116-
Json(PublicKeyResponse {
117-
algorithm: "RSA-OAEP-256/AES-256-GCM",
118-
public_key_pem: (*state.crypto_keys.public_key_pem).clone(),
119-
})
120+
pub async fn public_key(State(state): State<AppState>) -> Response {
121+
(
122+
[(header::CACHE_CONTROL, "no-store")],
123+
Json(PublicKeyResponse {
124+
algorithm: "RSA-OAEP-256/AES-256-GCM",
125+
public_key_pem: (*state.crypto_keys.public_key_pem).clone(),
126+
}),
127+
)
128+
.into_response()
120129
}
121130

122131
fn decode_b64(value: &str) -> AppResult<Vec<u8>> {

src/error.rs

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -63,7 +63,11 @@ impl AppError {
6363

6464
impl IntoResponse for AppError {
6565
fn into_response(self) -> Response {
66-
tracing::error!(error = %self, "request failed");
66+
match &self {
67+
Self::Unauthorized(_) => tracing::info!(error = %self, "request unauthorized"),
68+
Self::Validation(_) => tracing::warn!(error = %self, "request validation failed"),
69+
_ => tracing::error!(error = %self, "request failed"),
70+
}
6771
(self.status(), self.client_message()).into_response()
6872
}
6973
}

0 commit comments

Comments
 (0)