# 大屏 — 免密登录 — 技术方案

> 供 `ruoyi-screen` 等大屏前端**自动登录**：无需账号密码与验证码，通过 RSA 加密 UUID 换取若依 JWT，再访问各 `bigScreen:*` 看板接口。

---

## 1. 技术架构

| 项 | 说明 |
| --- | --- |
| **后端** | RuoYi **v3.9.2**（springboot2）：JDK 8、Spring MVC |
| **代码包** | `com.ruoyi.web.modules.screen`（`ScreenLoginController` / `ScreenLoginService` / `ScreenLoginRsaSupport` / `ScreenLoginProperties`） |
| **鉴权** | 登录接口 `@Anonymous` 免鉴权；登录成功后与普通若依接口一致，请求头 `Authorization: Bearer <token>` |
| **专用账号** | `sys_user` 预置用户（默认 `screenviewer`）；校验存在且未删除、未停用 |
| **菜单权限** | 登录成功后在 JWT 中注入 `bigScreen:*:*`，前端默认拥有全部大屏菜单与看板接口权限，**无需**在若依中单独授权 |
| **加密** | `RSA/ECB/PKCS1Padding`，与若依后台 `jsencrypt` 密钥格式兼容 |
| **客户端 IP** | **不校验**白名单/黑名单；仅写入登录日志 |

**流程**

```mermaid
sequenceDiagram
    participant FE as ruoyi-screen
    participant API as baqing-admin
    FE->>API: GET /bigScreen/publicKey
    API-->>FE: data.publicKey
    FE->>FE: 生成 UUID，JSEncrypt 公钥加密
    FE->>API: POST /bigScreen/login（X-Screen-Token 或 body.token）
    API->>API: 私钥解密，校验 UUID 格式
    API-->>FE: token（JWT）
    FE->>API: GET /bigScreen/home/dashboard（Authorization）
```

---

## 2. 配置

### 2.1 `application.yml`

```yaml
bigscreen:
  login:
    enabled: false          # 生产按需 true
    rsa-public-key: ""      # Base64 公钥（可含换行，服务端会去除空白）
    rsa-private-key: ""     # Base64 私钥（仅服务端，勿下发前端）
    username: screenviewer  # 专用 sys_user 登录名
```

| 项 | 说明 |
| --- | --- |
| `enabled` | `false` 时公钥接口与登录接口均返回「大屏免密登录未启用」 |
| `rsa-public-key` / `rsa-private-key` | 须成对；开发环境可与 `ruoyi-ui/src/utils/jsencrypt.js` 中示例密钥一致 |
| `username` | 对应 `sys_user.user_name`；不存在、已删除或已停用时登录失败；**无需**在若依中配置大屏菜单权限 |

**密钥生成**：可使用 [在线 RSA 密钥对工具](http://web.chacuo.net/netrsakeypair)（JSEncrypt 格式）；生产建议 **2048 位**独立密钥对。

---

## 3. 接口

**Base Path**：`/bigScreen`（含 `server.servlet.context-path`、网关前缀须补齐）。

| 序号 | 说明 | 方法 | 路径 | 鉴权 |
| --- | --- | --- | --- | --- |
| 3.1 | 获取 RSA 公钥 | GET | `/bigScreen/publicKey` | 无（`@Anonymous`） |
| 3.2 | 免密登录换 Token | POST | `/bigScreen/login` | 无（`@Anonymous`） |

### 3.1 获取 RSA 公钥

**请求**：无 Body。

**成功响应**（`AjaxResult`）：

```json
{
  "code": 200,
  "msg": "操作成功",
  "data": {
    "publicKey": "MFwwDQYJKoZIhvcNAQEBBQADSwAwSAJBAKoR8mX0rGKLqzcWmOzbfj64K8ZIgOdH..."
  }
}
```

| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `data.publicKey` | string | Base64 RSA 公钥，供 JSEncrypt `setPublicKey` |

**失败**：未启用或未配置密钥时 `ServiceException`，统一错误提示。

### 3.2 免密登录

**认证载荷**（二选一，优先请求头）：

| 方式 | 说明 |
| --- | --- |
| 请求头 | `X-Screen-Token: <RSA 密文 Base64>` |
| Body | `{ "token": "<RSA 密文 Base64>" }` |

**前端步骤**：

1. 调用 **§3.1** 获取 `publicKey`
2. 生成标准 UUID（如 `crypto.randomUUID()`）
3. 使用 JSEncrypt 以公钥加密 UUID 字符串（明文 36 字符）
4. 将 JSEncrypt 返回的 Base64 密文作为 `token` 提交

**成功响应**（与 `POST /login` 一致）：

```json
{
  "code": 200,
  "msg": "操作成功",
  "token": "eyJhbGciOiJIUzI1NiJ9..."
}
```

后续看板请求：`Authorization: Bearer <token>`。

**失败场景**

| 场景 | 提示（示例） |
| --- | --- |
| 未启用 | 大屏免密登录未启用 |
| 未配置密钥 | 大屏免密登录未配置 RSA 密钥 |
| 密文为空 / 解密失败 / 明文非 UUID | 大屏登录认证失败 |
| 专用账号不存在 | 大屏专用账号不存在 |
| 账号已删除 / 已停用 | 若依标准 `user.password.delete` / `user.blocked` |

---

## 4. 实现要点

| 类 | 职责 |
| --- | --- |
| `ScreenLoginController` | 路由、`X-Screen-Token` / Body 取值 |
| `ScreenLoginService` | 启用与密钥校验、解密验 UUID、校验专用用户状态、注入 `bigScreen:*:*`、签发 JWT |
| `ScreenLoginPermissions` | 大屏免密登录固定权限集 |
| `ScreenLoginRsaSupport` | RSA 加解密、`UUID.fromString` 格式校验 |
| `ScreenLoginProperties` | 绑定 `bigscreen.login.*` |
| `ScreenLoginConfig` | `@EnableConfigurationProperties` |

**UUID 校验**：解密明文经 `UUID.fromString` 解析，须为标准 8-4-4-4-12 十六进制格式（大小写均可）。

**权限说明**：

- 签发 Token 时写入 `permissions = ["bigScreen:*:*"]`，与若依 `SecurityUtils.hasPermi` 通配规则一致。
- `ruoyi-screen` 路由/导航为静态配置，登录成功即可访问全部大屏页面，**不依赖** `getInfo` / `getRouters`。
- 若调用 `GET /getInfo`，若依会按数据库角色刷新权限；大屏场景建议**仅使用免密登录返回的 Token**，勿混用普通登录。

**安全说明**：

- 每次登录应生成**新 UUID** 再加密，避免重放固定密文（服务端当前仅校验格式，不存 nonce）。
- 私钥仅存服务端配置，禁止写入前端或版本库（生产用环境变量/密钥管理）。
- 大屏看板接口依赖有效 JWT；权限由免密登录注入，**不要求** `screenviewer` 在若依菜单中逐项授权。

---

## 5. 前端示例（JSEncrypt）

```javascript
import JSEncrypt from 'jsencrypt/bin/jsencrypt.min'

async function screenAutoLogin(apiBase) {
  const keyRes = await fetch(`${apiBase}/bigScreen/publicKey`).then(r => r.json())
  const uuid = crypto.randomUUID()
  const encryptor = new JSEncrypt()
  encryptor.setPublicKey(keyRes.data.publicKey)
  const cipher = encryptor.encrypt(uuid)
  if (!cipher) throw new Error('RSA encrypt failed')

  const loginRes = await fetch(`${apiBase}/bigScreen/login`, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json' },
    body: JSON.stringify({ token: cipher })
  }).then(r => r.json())

  return loginRes.token
}
```

---

## 6. 交付清单

- [x] `ScreenLoginController`：`GET /bigScreen/publicKey`、`POST /bigScreen/login`
- [x] `ScreenLoginService` + `ScreenLoginRsaSupport` + `ScreenLoginProperties`
- [x] `application.yml` → `bigscreen.login.*`
- [x] 单元测试：`ScreenLoginRsaSupportTest`、`ScreenLoginServiceTest`、`ScreenLoginControllerApiTest`
- [x] `ruoyi-screen` 对接自动登录（按本方案调用公钥 + 登录接口）

---

## 7. 与各看板模块关系

| 模块 | 看板 Base Path | 登录后权限示例 |
| --- | --- | --- |
| 首页 | `/bigScreen/home` | `bigScreen:home:query` |
| 交易销售 | `/bigScreen/tradeSales` | `bigScreen:tradeSales:query` |
| 疫病风险 | `/bigScreen/epidemicRisk` | `bigScreen:epidemicRisk:query` |
| 畜牧资源 | `/bigScreen/livestockResource` | `bigScreen:livestockResource:query` |
| 共同富裕 | `/bigScreen/commonProsperity` | `bigScreen:commonProsperity:query` |

免密登录**不替代** JWT 鉴权；专用账号须在 `sys_user` 中存在且状态正常，**无需**配置大屏菜单权限。
