Day 4 讲 AnchorChat 登录:OAuth 换 Token,electron-store 经 IPC 持久化,主进程 axios 自动带 Bearer。
开场:空壳能跑了,但谁都不能进
Day 2 弹窗,Day 3 穿上标题栏和托盘------老王双击图标,看见的还是 登录页。他说:「像样了,但我进不去啊。」
Day 4 解决 AnchorChat 自己的账号登录:用户名密码换 Token,Token 存哪儿、主进程翻译 API 怎么带鉴权、退出怎么清干净。
| 登录 | 是什么 | 存哪儿 |
|---|---|---|
| AnchorChat 账号 | 我们后端 OAuth,管翻译配额、工单、聊天记录上报 | electron-store |
| IM 平台账号 | Telegram / WhatsApp Web 里扫码或 Cookie | webview partition |
两套登录态 别混。客服登 AnchorChat 成功,不代表 Telegram 已在线------那是两个房间两把钥匙。

登录页:表单 → 加密 → OAuth
路由默认 / 重定向 /login。Login.vue 里就是账号密码 + 登录按钮,逻辑浓缩成:
typescript
function oauthTokenFuc() {
loginRef.value?.validate((valid) => {
if (!valid) return
loading.value = true
const params = {
username: loginForm.username,
password: encryptData(loginForm.password), // 传输前加密
tenantId: '000000',
grantType: 'password',
version: version.value,
}
userStore.oauthToken(params).then(() => {
router.replace('/main')
}).finally(() => {
loading.value = false
})
})
}
接口走 OAuth2 密码模式(示例路径 /blade-auth/oauth/v2/token),真实域名在 .env 的 VITE_BASE_URL
Pinia userStore 收到 access_token 后做三件事:
typescript
window.electronStore.setItem('userInfo', JSON.stringify(res))
setToken(res.access_token)
if (res.refresh_token) setRefreshToken(res.refresh_token)
然后 router.replace('/main')------三栏主界面我们后面边做边讲。
密钥存哪儿:electron-store + IPC
浏览器里常用 localStorage,Electron 桌面端我们更倾向 electron-store------数据落在用户目录 JSON 文件里,主进程和渲染进程都能读。
渲染进程 不能直接 require('electron-store')(安全模型不允许)。preload 暴露 electronStore:
typescript
contextBridge.exposeInMainWorld('electronStore', {
setItem: (key, value) => {
store[key] = value
ipcRenderer.send('setStore', key, value)
},
getItem: (key) => {
let value = store[key]
if (!value) {
value = ipcRenderer.sendSync('getStore', key)
store[key] = value
}
return value
},
removeItem: (key) => {
delete store[key]
ipcRenderer.send('deleteStore', key)
},
})
主进程启动时注册 IPC,并持有同一个 Store 实例:
typescript
import Store from 'electron-store'
export const store = new Store()
export function elestore() {
ipcMain.on('setStore', (_event, key, value) => {
store.set(key, value)
})
ipcMain.on('getStore', (event, key) => {
event.returnValue = store.get(key)
})
ipcMain.on('deleteStore', (_event, key) => {
store.delete(key)
})
}
app.whenReady() 里调用 elestore()------和 Day 3 的 registerWindowControls 一样,属于主进程「基础设施注册」。
渲染层 src/utils/auth.ts 封装 Token 读写,键名例如 vue_token、vue_refresh_token
主进程也要调 API:auth-ele + axios 拦截器
翻译、聊天记录上报跑在 主进程 (Node axios),不能指望渲染进程把 Token 每次 invoke 传过来------直接从 store 读:
typescript
import { store } from './elestore'
export function getToken() {
return store.get('vue_token')
}
export function setToken(token: string) {
return store.set('vue_token', token)
}
electron/utils/request.ts 请求拦截器自动带头:
typescript
if (getToken() && !isToken) {
config.headers['Blade-Auth'] = 'Bearer ' + getToken()
const refreshToken = getRefreshToken()
if (refreshToken) {
config.headers['X-Refresh-Token'] = refreshToken
}
}
响应里若后端下发新 Token(响应头 x-new-access-token),拦截器会 写回 store ------桌面端可以长时间开着,不用每天重新登录。细节实现各团队不同,AnchorChat 的思路是:一处存储,渲染 + 主进程共用。
退出登录:别留 ghost Token
标题栏退出时,除了调后端 logout,还要清本地:
electronStore里的userInfo、vue_token- Pinia 内存态
- 必要时清 IM 相关缓存
否则下次打开以为已登录,主进程 axios 却带着过期 Bearer,接口全 401------调试能调到你怀疑人生。
踩坑与思考
- 渲染进程 setToken 了,主进程读不到 :检查
setStoreIPC 是否注册、elestore()是否在whenReady里调用 - getStore 用 sendSync :首次读盘会阻塞渲染进程一小下,键别存巨型 JSON;
userInfo够用就别把整个账号列表塞进去 - 密码明文进 store :我们只存 Token 和加密后的「记住账号」可选字段,不要把明文 password 写进 electron-store
- OAuth Client Secret 别写进 preload:Basic 认证头放主进程或构建时 env
- 和 IM Cookie 混淆 :webview 里的三方 IM登录,Cookie 在
partition里,不会 自动进vue_token
明日预告
Day 5 终于进 三栏主界面 :Main.vue + El-Splitter,左账号、中聊天、右工具------登录进来不再只看登录页,而是「工作台」骨架。webview 还要再等 Day 6,别急。
你们桌面端 Token 放 electron-store 还是 keytar/系统钥匙串?欢迎评论区交流