Files
xiaotutongxue/frontend-pc/src/utils/rsa.ts
T
lovife 5b877451eb refactor(pc): 从 Gi Admin Pro + Arco 重搭为纯 Vue 3 + Naive UI
旧模板的 193 个文件整体删除,换成纯 Vue 3.5 + Naive UI 2.42 的树。

- 去掉 auto-import(组件与 API 全显式 import)、v-hasPerm(改 user.has('码'),全库 8 处)、
  Stylelint;n-tag 的 type 统一过 utils/tag.ts 转换(Arco 色词直接透传会静默变灰)
- 图标改为构建期抽取:scripts/extract-pc-icons.py -> icons/icon-data.ts(37 个),
  内网离线部署不会白屏
- 重搭后修掉 6 类"编译 / lint / typecheck 全绿、界面上就是坏的"故障:
  ① NGrid 的 cols 写成像素值(不是断点名 -> 永远只 1 列,工作台因此错乱)
  ② n-tab-pane 用 tab 属性(只渲染空注释节点,整排 tab 一个字都不显示)
  ③ 漏 import 被当未知自定义元素(样式整块丢) ④ .n-card__content 类名不存在
  (高度链第一环失效,表格行在 DOM 里但屏幕上没有) ⑤ 填报单状态映射错位一格
  ⑥ 有标题的卡片没有上内边距(naive 的 padding-top 只给"内容区是长子"的卡片)
- 卡片式版面提成全局基线:styles/index.scss 的 xt-* 令牌 + .page-card / .toolbar /
  .section-title,页面只引用、不再各写一套
- 品牌:改名「深学在线」,文字标换成几何标(方 = 卷宗 / 环 = 在线,不用文字字形);
  index.html 的页签图标从 404 的 /favicon.ico 改指真实存在的 /favicon.svg
2026-10-10 16:48:46 +08:00

70 lines
2.6 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 登录密码加密(RSA)。
*
* 公钥从后端 `GET /auth/public-key` 拉取并缓存 —— 这样**轮换密钥不需要重新构建前端**。
* 否则"后端换了密钥、前端还是旧的"会表现为"所有人密码都错",且极难定位。
*
* ⚠️ 算法必须与服务端 `RsaCipher` 一致:**RSA/ECB/PKCS1Padding**(jsencrypt 的默认行为)。
* 换成 OAEP 会表现为"密码永远错"。
*
* ⚠️ 它不是用来替代 HTTPS 的:唯一目的是**防止明文密码进日志** ——
* 网关、Nginx、APM 都会记录请求体,明文一旦落进日志就是永久泄露。
*/
import JSEncrypt from 'jsencrypt'
// 直接用 http,不经 @/apis/xt —— 否则形成 xt → http → … → xt 的环
import http from '@/utils/http'
/** 折叠空白用(提到模块级:每次调用都新建正则纯属浪费,lint 也会拦) */
const WHITESPACE_RE = /\s+/g
let cachedKey: string | null = null
let inflight: Promise<string> | null = null
/** 拉取公钥(并发调用只会真正请求一次) */
export async function fetchPublicKey(): Promise<string> {
if (cachedKey) {
return cachedKey
}
if (inflight) {
return inflight
}
inflight = http
.get<string>('/auth/public-key')
.then((pem) => {
cachedKey = pem
return pem
})
.finally(() => {
inflight = null
})
return inflight
}
/** 加密密码,返回 Base64 密文 */
export async function encryptPassword(plain: string): Promise<string> {
const pem = await fetchPublicKey()
if (typeof pem !== 'string' || !pem.includes('BEGIN PUBLIC KEY')) {
// ⚠️ 这一条是**踩过的坑**:`/auth/public-key` 若没走到后端(例如 baseURL 少了
// `/api/v1`,请求落到 SPA 兜底上),拿到的是 **index.html** 而不是公钥。
// 此时 jsencrypt 只会返回 false,界面显示"密码加密失败" —— 排查方向会完全跑偏
// (去查 jsencrypt、去查密钥轮换,而真正的原因是请求 URL)。
// 所以这里把"到底拿到了什么"直接说出来。
throw new Error(`公钥格式不对(拿到 ${typeof pem === 'string' ? `${pem.length} 字节` : typeof pem}:`
+ `${String(pem).slice(0, 30).replace(WHITESPACE_RE, ' ')}…)`)
}
const engine = new JSEncrypt()
engine.setPublicKey(pem)
const cipher = engine.encrypt(plain)
if (!cipher) {
// jsencrypt 在密钥格式不对时返回 false 而不是抛异常,必须自己判
throw new Error('密码加密失败,请刷新页面重试')
}
return cipher
}
/** 密钥轮换后调用 */
export function resetPublicKey(): void {
cachedKey = null
}