fix: preserve login session across refreshes

This commit is contained in:
OpenClaw Agent
2026-09-22 04:48:06 +08:00
parent f8b5b585c9
commit 7b2c5f444b
3 changed files with 29 additions and 6 deletions
+13
View File
@@ -122,17 +122,30 @@ func main() {
} else if !filepath.IsAbs(userFile) { } else if !filepath.IsAbs(userFile) {
userFile = filepath.Join(root, userFile) userFile = filepath.Join(root, userFile)
} }
if err := os.MkdirAll(filepath.Dir(userFile), 0700); err != nil {
log.Fatal(err)
}
users := &auth.UserStore{Path: userFile} users := &auth.UserStore{Path: userFile}
if err := users.Load(mgr, cfg.UserHome); err != nil { if err := users.Load(mgr, cfg.UserHome); err != nil {
log.Fatal(err) log.Fatal(err)
} }
jwtSecret := cfg.JWTSecret jwtSecret := cfg.JWTSecret
if jwtSecret == "" { if jwtSecret == "" {
// Keep sessions valid across process restarts. A random in-memory secret
// would invalidate every login whenever the service is restarted.
secretPath := userFile + ".jwt-secret"
if saved, err := os.ReadFile(secretPath); err == nil && len(strings.TrimSpace(string(saved))) >= 32 {
jwtSecret = strings.TrimSpace(string(saved))
} else {
buf := make([]byte, 32) buf := make([]byte, 32)
if _, err := rand.Read(buf); err != nil { if _, err := rand.Read(buf); err != nil {
log.Fatal(err) log.Fatal(err)
} }
jwtSecret = hex.EncodeToString(buf) jwtSecret = hex.EncodeToString(buf)
if err := os.WriteFile(secretPath, []byte(jwtSecret+"\n"), 0600); err != nil {
log.Printf("warning: could not persist JWT secret at %s: %v", secretPath, err)
}
}
} }
for _, a := range mgr.Accounts { for _, a := range mgr.Accounts {
if a.Home != "" { if a.Home != "" {
+10
View File
@@ -146,11 +146,20 @@ func clientIPString(r *http.Request) string {
func writeJSON(w http.ResponseWriter, v any) { func writeJSON(w http.ResponseWriter, v any) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Vary", "Cookie")
json.NewEncoder(w).Encode(v) json.NewEncoder(w).Encode(v)
} }
func noStore(w http.ResponseWriter) {
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Vary", "Cookie")
}
func writeJSONError(w http.ResponseWriter, status int, message string) { func writeJSONError(w http.ResponseWriter, status int, message string) {
w.Header().Set("Content-Type", "application/json") w.Header().Set("Content-Type", "application/json")
w.Header().Set("Cache-Control", "no-store")
w.Header().Set("Vary", "Cookie")
w.WriteHeader(status) w.WriteHeader(status)
_ = json.NewEncoder(w).Encode(map[string]string{"error": message}) _ = json.NewEncoder(w).Encode(map[string]string{"error": message})
} }
@@ -511,6 +520,7 @@ func (s *Server) exist(w http.ResponseWriter, r *http.Request) {
} }
func (s *Server) session(w http.ResponseWriter, r *http.Request) { func (s *Server) session(w http.ResponseWriter, r *http.Request) {
noStore(w)
if r.Method == http.MethodDelete { if r.Method == http.MethodDelete {
http.SetCookie(w, &http.Cookie{Name: "JWT", Value: "", Path: "/", MaxAge: -1, Expires: time.Unix(1, 0), HttpOnly: true, SameSite: http.SameSiteLaxMode}) http.SetCookie(w, &http.Cookie{Name: "JWT", Value: "", Path: "/", MaxAge: -1, Expires: time.Unix(1, 0), HttpOnly: true, SameSite: http.SameSiteLaxMode})
http.SetCookie(w, &http.Cookie{Name: "user", Value: "", Path: "/", MaxAge: -1, Expires: time.Unix(1, 0), SameSite: http.SameSiteLaxMode}) http.SetCookie(w, &http.Cookie{Name: "user", Value: "", Path: "/", MaxAge: -1, Expires: time.Unix(1, 0), SameSite: http.SameSiteLaxMode})
+2 -2
View File
@@ -46,7 +46,7 @@ button.ghost{background:var(--surface2);color:var(--text)}button.danger{backgrou
<div class="modal" id="modal"><div class="dialog" id="dialog"></div></div> <div class="modal" id="modal"><div class="dialog" id="dialog"></div></div>
<script> <script>
const $=id=>document.getElementById(id);let current="",me=null,currentMask="",navigationRoot=""; const $=id=>document.getElementById(id);let current="",me=null,currentMask="",navigationRoot="";
async function api(url,opt={}){const r=await fetch(url,{cache:"no-store",...opt});if(!r.ok){let t="请求失败";try{t=(await r.json()).error||t}catch{};const e=Error(t);e.status=r.status;throw e}return r.status===204?null:r.json()} async function api(url,opt={}){const r=await fetch(url,{credentials:"same-origin",cache:"no-store",...opt});if(!r.ok){let t="请求失败";try{t=(await r.json()).error||t}catch{};const e=Error(t);e.status=r.status;throw e}return r.status===204?null:r.json()}
function esc(s){return String(s).replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c]))} function esc(s){return String(s).replace(/[&<>"']/g,c=>({"&":"&amp;","<":"&lt;",">":"&gt;","\"":"&quot;","'":"&#39;"}[c]))}
function fmt(n){if(n<1024)return n+" B";if(n<1048576)return (n/1024).toFixed(1)+" KB";if(n<1073741824)return (n/1048576).toFixed(1)+" MB";return (n/1073741824).toFixed(1)+" GB"} function fmt(n){if(n<1024)return n+" B";if(n<1048576)return (n/1024).toFixed(1)+" KB";if(n<1073741824)return (n/1048576).toFixed(1)+" MB";return (n/1073741824).toFixed(1)+" GB"}
async function init(){try{me=await api("/chfs/me")}catch{me={username:"",home:"",admin:false}};updateUser();current=me.home||"";navigationRoot=me.home||"";const h=new URLSearchParams(location.hash.slice(1));const shared=h.get("folder");if(shared!==null)current=shared;await refresh()} async function init(){try{me=await api("/chfs/me")}catch{me={username:"",home:"",admin:false}};updateUser();current=me.home||"";navigationRoot=me.home||"";const h=new URLSearchParams(location.hash.slice(1));const shared=h.get("folder");if(shared!==null)current=shared;await refresh()}
@@ -168,7 +168,7 @@ async function previewFile(p){const n=p.split('/').pop(),ext=n.toLowerCase().spl
function changePassword(){show('<h2>修改密码</h2><div class="field"><input id="cp" type="password" placeholder="当前密码"><input id="npw" type="password" placeholder="新密码"></div><div class="row"><button class="ghost" onclick="closeModal()">取消</button><button onclick="savePassword()">保存</button></div>')} function changePassword(){show('<h2>修改密码</h2><div class="field"><input id="cp" type="password" placeholder="当前密码"><input id="npw" type="password" placeholder="新密码"></div><div class="row"><button class="ghost" onclick="closeModal()">取消</button><button onclick="savePassword()">保存</button></div>')}
async function savePassword(){try{await api("/chfs/password",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({currentPassword:$("cp").value,password:$("npw").value})});toast("密码已更新");closeModal()}catch(e){fail(e.message)}} async function savePassword(){try{await api("/chfs/password",{method:"POST",headers:{"Content-Type":"application/json"},body:JSON.stringify({currentPassword:$("cp").value,password:$("npw").value})});toast("密码已更新");closeModal()}catch(e){fail(e.message)}}
function openLogin(){show('<h2>登录</h2><div class="field"><label>用户名</label><input id="u" autocomplete="username"></div><div class="field"><label>密码</label><input id="p" type="password" autocomplete="current-password"></div><div class="row"><button class="ghost" onclick="closeModal()">取消</button><button onclick="login()">登录</button></div>')} function openLogin(){show('<h2>登录</h2><div class="field"><label>用户名</label><input id="u" autocomplete="username"></div><div class="field"><label>密码</label><input id="p" type="password" autocomplete="current-password"></div><div class="row"><button class="ghost" onclick="closeModal()">取消</button><button onclick="login()">登录</button></div>')}
async function login(){try{await fetch("/chfs/session",{method:"POST",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({user:$("u").value,pwd:$("p").value})}).then(async r=>{if(!r.ok)throw Error((await r.json()).error||"登录失败")});closeModal();await init()}catch(e){fail(e.message)}} async function login(){try{await fetch("/chfs/session",{method:"POST",credentials:"same-origin",cache:"no-store",headers:{"Content-Type":"application/x-www-form-urlencoded"},body:new URLSearchParams({user:$("u").value,pwd:$("p").value})}).then(async r=>{if(!r.ok)throw Error((await r.json()).error||"登录失败")});closeModal();await init()}catch(e){fail(e.message)}}
async function logout(){try{await api("/chfs/session",{method:"DELETE"})}finally{me={username:"",home:"",admin:false,managed:false};current="";navigationRoot="";selected.clear();updateUser();await refresh()}} async function logout(){try{await api("/chfs/session",{method:"DELETE"})}finally{me={username:"",home:"",admin:false,managed:false};current="";navigationRoot="";selected.clear();updateUser();await refresh()}}
function toast(message,error=false){const host=$("toastHost"),el=document.createElement("div");el.className='toast'+(error?' error':'');el.textContent=message;host.appendChild(el);setTimeout(()=>el.remove(),2800)} function toast(message,error=false){const host=$("toastHost"),el=document.createElement("div");el.className='toast'+(error?' error':'');el.textContent=message;host.appendChild(el);setTimeout(()=>el.remove(),2800)}
function fail(message){toast(message,true)} function fail(message){toast(message,true)}