Compare commits
3 Commits
c1ee18ca97
...
4d97e1cbf9
| Author | SHA1 | Date | |
|---|---|---|---|
| 4d97e1cbf9 | |||
| 6f47a07401 | |||
| c935988188 |
@ -61,3 +61,172 @@
|
||||
| `AdminTenantController.java` | 修改详情查询方法 |
|
||||
| `admin.ts` | 扩展类型定义 |
|
||||
| `TenantListView.vue` | 更新详情展示 |
|
||||
|
||||
---
|
||||
|
||||
## 工作内容 - 学校端操作日志请求参数与接口展示
|
||||
|
||||
### 背景
|
||||
在学校端操作日志页面,需要记录并返回以下三个信息:
|
||||
1. **操作人** - 谁执行的操作(已有 `userId`、`userRole`)
|
||||
2. **请求参数** - 操作时传入的参数(`requestParams`)
|
||||
3. **请求接口** - 执行的具体 API 接口路径(`requestUri`)
|
||||
|
||||
当前后端已通过 `LogAspect` 自动记录这些信息到数据库,但未返回给前端展示。
|
||||
|
||||
### 修改内容
|
||||
|
||||
#### 1. 数据库迁移
|
||||
**文件**: `V50__add_request_uri_to_operation_log.sql`
|
||||
- 添加 `request_uri` 字段 - VARCHAR(500),记录请求接口路径
|
||||
|
||||
#### 2. 后端实体修改
|
||||
**文件**: `OperationLog.java`
|
||||
- 新增 `requestUri` 字段
|
||||
|
||||
#### 3. 后端切面修改
|
||||
**文件**: `LogAspect.java`
|
||||
- 在 `before()` 方法中记录 `requestURI`
|
||||
|
||||
#### 4. 后端 Response DTO 修改
|
||||
**文件**: `OperationLogResponse.java`
|
||||
- 新增 `requestParams` 字段 - 请求参数 JSON
|
||||
- 新增 `requestUri` 字段 - 请求接口路径
|
||||
|
||||
#### 5. 后端 Controller 修改
|
||||
**文件**: `SchoolOperationLogController.java`
|
||||
- 在 `convertToResponse()` 方法中添加字段映射
|
||||
|
||||
#### 6. 前端类型定义更新
|
||||
**文件**: `school.ts`
|
||||
- `OperationLog` 接口添加 `requestParams` 和 `requestUri` 字段
|
||||
|
||||
#### 7. 前端页面修改
|
||||
**文件**: `OperationLogView.vue`
|
||||
- 详情弹窗新增"请求接口"展示项
|
||||
- 详情弹窗新增"请求参数"展示项(JSON 格式化)
|
||||
|
||||
### 验证步骤
|
||||
1. ✅ 后端编译成功
|
||||
2. ⏳ 数据库迁移待执行
|
||||
3. ⏳ 启动服务测试
|
||||
|
||||
### 文件清单
|
||||
| 文件 | 修改内容 |
|
||||
|------|---------|
|
||||
| `V50__add_request_uri_to_operation_log.sql` | 新增数据库迁移 |
|
||||
| `OperationLog.java` | 新增 `requestUri` 字段 |
|
||||
| `LogAspect.java` | 记录 `requestURI` |
|
||||
| `OperationLogResponse.java` | 新增返回字段 |
|
||||
| `SchoolOperationLogController.java` | 字段映射 |
|
||||
| `school.ts` | 类型定义更新 |
|
||||
| `OperationLogView.vue` | 详情展示新增字段 |
|
||||
|
||||
---
|
||||
|
||||
## 工作内容 - RSA 密码加密传输实现
|
||||
|
||||
### 背景
|
||||
当前登录系统密码明文传输,存在安全风险。需要实现 RSA 非对称加密,确保密码在传输过程中加密。
|
||||
|
||||
### 实现方案
|
||||
使用简化 RSA 方案:
|
||||
1. 后端生成 RSA 密钥对,提供公钥获取接口
|
||||
2. 前端登录前获取公钥,用 RSA 加密密码
|
||||
3. 后端用私钥解密密码,继续原有登录流程
|
||||
|
||||
### 修改内容
|
||||
|
||||
#### 后端部分
|
||||
|
||||
**1. 新建 RSA 加密工具类** (`RsaEncryptionUtil.java`)
|
||||
- 生成 RSA 2048 位密钥对
|
||||
- 提供公钥获取方法
|
||||
- 提供私钥解密方法
|
||||
- 支持密钥版本管理
|
||||
- 密钥仅保存在内存中,不持久化
|
||||
|
||||
**2. 新建密钥轮换任务** (`RsaKeyRotationTask.java`)
|
||||
- 每月 1 日凌晨 2 点自动更换 RSA 密钥
|
||||
- 使用 `@Scheduled(cron = "0 0 2 1 * ?")` 定时执行
|
||||
- 支持日志记录密钥版本变更
|
||||
|
||||
**3. 新建加密登录请求 DTO** (`EncryptedLoginRequest.java`)
|
||||
- `username` - 登录账号(明文)
|
||||
- `encryptedPassword` - RSA 加密后的密码(Base64)
|
||||
- `role` - 登录角色
|
||||
- `keyVersion` - 密钥版本号(可选)
|
||||
|
||||
**4. 新建公钥响应 DTO** (`PublicKeyResponse.java`)
|
||||
- `publicKey` - RSA 公钥(Base64)
|
||||
- `keyVersion` - 当前密钥版本号
|
||||
|
||||
**5. 修改认证控制器** (`AuthController.java`)
|
||||
- 新增 `GET /api/v1/auth/public-key` - 获取 RSA 公钥
|
||||
- 新增 `POST /api/v1/auth/login/encrypted` - 加密登录接口
|
||||
- 注入 `RsaEncryptionUtil` 进行加解密操作
|
||||
|
||||
#### 前端部分
|
||||
|
||||
**6. 添加依赖** (`package.json`)
|
||||
- 添加 `jsencrypt@^3.3.2` - RSA 加密库
|
||||
|
||||
**7. 新建加密工具** (`encryption.ts`)
|
||||
- `rsaEncrypt(data, publicKey)` - RSA 加密函数
|
||||
- 导出 `PublicKeyResponse` 和 `EncryptedLoginParams` 类型
|
||||
|
||||
**8. 修改认证 API** (`auth.ts`)
|
||||
- 新增 `getPublicKey()` - 获取公钥
|
||||
- 新增 `loginEncrypted(params)` - 加密登录
|
||||
|
||||
**9. 修改用户 Store** (`user.ts`)
|
||||
- 修改 `login()` 函数,改为 RSA 加密流程:
|
||||
1. 获取 RSA 公钥
|
||||
2. 加密密码
|
||||
3. 调用加密登录接口
|
||||
|
||||
### 登录流程变更
|
||||
|
||||
**原流程**:
|
||||
```
|
||||
明文密码 → POST /api/v1/auth/login → 后端验证
|
||||
```
|
||||
|
||||
**新流程**:
|
||||
```
|
||||
1. GET /api/v1/auth/public-key → 获取 RSA 公钥
|
||||
2. 前端用公钥加密密码
|
||||
3. POST /api/v1/auth/login/encrypted → 后端私钥解密 → 验证登录
|
||||
```
|
||||
|
||||
### 安全特性
|
||||
|
||||
- RSA 2048 位加密,安全性足够
|
||||
- **密钥每月自动更换** - 每月 1 日凌晨 2 点自动更换
|
||||
- 密钥版本管理 - 便于排查问题
|
||||
- 私钥仅保存在后端内存中,不持久化
|
||||
- 必须配合 HTTPS 使用
|
||||
|
||||
### 验证步骤
|
||||
|
||||
1. 安装前端依赖:`cd reading-platform-frontend && npm install`
|
||||
2. 启动后端服务,测试 `GET /api/v1/auth/public-key` 返回公钥
|
||||
3. 启动前端,打开浏览器开发者工具
|
||||
4. 登录时检查 Network 面板:
|
||||
- `/v1/auth/public-key` 返回公钥
|
||||
- `/v1/auth/login/encrypted` 请求的 `encryptedPassword` 为密文
|
||||
5. 验证登录功能正常
|
||||
|
||||
### 文件清单
|
||||
|
||||
| 文件 | 操作 | 说明 |
|
||||
|------|------|------|
|
||||
| `RsaEncryptionUtil.java` | 新建 | RSA 加密工具类 |
|
||||
| `RsaKeyRotationTask.java` | 新建 | 密钥轮换定时任务 |
|
||||
| `EncryptedLoginRequest.java` | 新建 | 加密登录请求 DTO |
|
||||
| `PublicKeyResponse.java` | 新建 | 公钥响应 DTO |
|
||||
| `AuthController.java` | 修改 | 添加公钥和加密登录接口 |
|
||||
| `package.json` | 修改 | 添加 jsencrypt 依赖 |
|
||||
| `encryption.ts` | 新建 | 前端 RSA 加密工具 |
|
||||
| `auth.ts` | 修改 | 添加加密登录 API |
|
||||
| `user.ts` | 修改 | 使用 RSA 加密登录 |
|
||||
|
||||
6
reading-platform-frontend/package-lock.json
generated
6
reading-platform-frontend/package-lock.json
generated
@ -18,6 +18,7 @@
|
||||
"axios": "^1.6.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^6.0.0",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-vue-next": "^0.575.0",
|
||||
"pdfjs-dist": "^3.11.174",
|
||||
@ -4463,6 +4464,11 @@
|
||||
"js-yaml": "bin/js-yaml.js"
|
||||
}
|
||||
},
|
||||
"node_modules/jsencrypt": {
|
||||
"version": "3.5.4",
|
||||
"resolved": "https://registry.npmmirror.com/jsencrypt/-/jsencrypt-3.5.4.tgz",
|
||||
"integrity": "sha512-kNjfYEMNASxrDGsmcSQh/rUTmcoRfSUkxnAz+MMywM8jtGu+fFEZ3nJjHM58zscVnwR0fYmG9sGkTDjqUdpiwA=="
|
||||
},
|
||||
"node_modules/jsesc": {
|
||||
"version": "3.1.0",
|
||||
"resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
|
||||
|
||||
@ -26,6 +26,7 @@
|
||||
"axios": "^1.6.7",
|
||||
"dayjs": "^1.11.10",
|
||||
"echarts": "^6.0.0",
|
||||
"jsencrypt": "^3.3.2",
|
||||
"lodash-es": "^4.17.21",
|
||||
"lucide-vue-next": "^0.575.0",
|
||||
"pdfjs-dist": "^3.11.174",
|
||||
|
||||
@ -1,4 +1,5 @@
|
||||
import { http } from './index';
|
||||
import type { EncryptedLoginParams, PublicKeyResponse } from '@/utils/encryption';
|
||||
|
||||
export interface LoginParams {
|
||||
account: string;
|
||||
@ -78,3 +79,25 @@ export function changePassword(oldPassword: string, newPassword: string): Promis
|
||||
params: { oldPassword, newPassword },
|
||||
});
|
||||
}
|
||||
|
||||
// ========== RSA 加密登录相关 API ==========
|
||||
|
||||
/**
|
||||
* 获取 RSA 公钥
|
||||
*/
|
||||
export function getPublicKey(): Promise<PublicKeyResponse> {
|
||||
return http.get('/v1/auth/public-key');
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA 加密登录
|
||||
* @param params 加密后的登录参数
|
||||
*/
|
||||
export function loginEncrypted(params: EncryptedLoginParams): Promise<LoginResponse> {
|
||||
return http.post('/v1/auth/login/encrypted', {
|
||||
username: params.username,
|
||||
encryptedPassword: params.encryptedPassword,
|
||||
role: params.role,
|
||||
keyVersion: params.keyVersion,
|
||||
});
|
||||
}
|
||||
|
||||
@ -184,6 +184,11 @@ export function getTeacherSourceCourses() {
|
||||
return api.teacherSchoolCourseControllerGetSourceCourses() as any;
|
||||
}
|
||||
|
||||
// 获取学校端可创建校本课程包的源课程列表
|
||||
export function getSourceCourses() {
|
||||
return api.schoolCourseControllerGetSourceCourses() as any;
|
||||
}
|
||||
|
||||
// 获取教师端校本课程包详情
|
||||
export function getTeacherSchoolCourseDetail(id: number) {
|
||||
return api.teacherSchoolCourseControllerFindOne(id) as any;
|
||||
|
||||
@ -860,6 +860,8 @@ export interface OperationLog {
|
||||
oldValue: string | null;
|
||||
newValue: string | null;
|
||||
ipAddress: string | null;
|
||||
requestParams?: string; // 请求参数 JSON
|
||||
requestUri?: string; // 请求接口路径
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@ -886,6 +888,9 @@ export const getOperationLogs = (params?: {
|
||||
...log,
|
||||
userType: (log as any).userRole, // 后端字段 userRole 映射为前端 userType
|
||||
description: (log as any).details, // 后端字段 details 映射为前端 description
|
||||
createdBy: (log as any).createBy, // 操作人
|
||||
requestParams: (log as any).requestParams, // 请求参数
|
||||
requestUri: (log as any).requestUri, // 请求接口
|
||||
oldValue: null, // 后端暂未支持
|
||||
newValue: null, // 后端暂未支持
|
||||
})),
|
||||
|
||||
@ -3,6 +3,7 @@ import { ref, computed } from 'vue';
|
||||
import { message } from 'ant-design-vue';
|
||||
import { router } from '@/router';
|
||||
import * as authApi from '@/api/auth';
|
||||
import { rsaEncrypt } from '@/utils/encryption';
|
||||
|
||||
export interface User {
|
||||
id: number;
|
||||
@ -26,10 +27,25 @@ export const useUserStore = defineStore('user', () => {
|
||||
const isLoggedIn = computed(() => !!token.value);
|
||||
const userRole = computed(() => user.value?.role || null);
|
||||
|
||||
// 登录
|
||||
// 登录(使用 RSA 加密)
|
||||
async function login(account: string, password: string, role: string) {
|
||||
try {
|
||||
const data = await authApi.login({ account, password, role });
|
||||
// 1. 获取 RSA 公钥
|
||||
const { publicKey, keyVersion } = await authApi.getPublicKey();
|
||||
|
||||
// 2. 使用 RSA 公钥加密密码
|
||||
const encryptedPassword = rsaEncrypt(password, publicKey);
|
||||
if (!encryptedPassword) {
|
||||
throw new Error('密码加密失败');
|
||||
}
|
||||
|
||||
// 3. 使用加密后的密码登录
|
||||
const data = await authApi.loginEncrypted({
|
||||
username: account,
|
||||
encryptedPassword,
|
||||
role,
|
||||
keyVersion,
|
||||
});
|
||||
|
||||
// 后端返回格式: { token, userId, username, name, role, tenantId }
|
||||
token.value = data.token;
|
||||
|
||||
37
reading-platform-frontend/src/utils/encryption.ts
Normal file
37
reading-platform-frontend/src/utils/encryption.ts
Normal file
@ -0,0 +1,37 @@
|
||||
import { JSEncrypt } from 'jsencrypt';
|
||||
|
||||
/**
|
||||
* RSA 加密工具函数
|
||||
* 使用 jsencrypt 库进行 RSA 加密
|
||||
*/
|
||||
|
||||
/**
|
||||
* 使用 RSA 公钥加密数据
|
||||
* @param data 要加密的明文数据
|
||||
* @param publicKey RSA 公钥(Base64 编码的 PEM 格式)
|
||||
* @returns 加密后的密文(Base64 编码)
|
||||
*/
|
||||
export function rsaEncrypt(data: string, publicKey: string): string {
|
||||
const encrypt = new JSEncrypt();
|
||||
encrypt.setPublicKey(publicKey);
|
||||
const result = encrypt.encrypt(data);
|
||||
return result || '';
|
||||
}
|
||||
|
||||
/**
|
||||
* RSA 公钥响应
|
||||
*/
|
||||
export interface PublicKeyResponse {
|
||||
publicKey: string;
|
||||
keyVersion: number;
|
||||
}
|
||||
|
||||
/**
|
||||
* 加密登录参数
|
||||
*/
|
||||
export interface EncryptedLoginParams {
|
||||
username: string;
|
||||
encryptedPassword: string;
|
||||
role: string;
|
||||
keyVersion?: number;
|
||||
}
|
||||
@ -98,7 +98,7 @@
|
||||
>
|
||||
<a-descriptions :column="2" bordered size="small" v-if="selectedLog">
|
||||
<a-descriptions-item label="操作用户">
|
||||
{{ selectedLog.userType }} (ID: {{ selectedLog.userId }})
|
||||
{{ selectedLog.createdBy || selectedLog.userType }} (ID: {{ selectedLog.userId }})
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="操作时间">
|
||||
{{ formatDateTime(selectedLog.createdAt) }}
|
||||
@ -118,6 +118,12 @@
|
||||
<a-descriptions-item label="操作描述" :span="2">
|
||||
{{ selectedLog.description }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="请求接口" :span="2">
|
||||
{{ selectedLog.requestUri || '-' }}
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="请求参数" :span="2">
|
||||
<pre class="json-data">{{ formatJson(selectedLog.requestParams) }}</pre>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item label="变更前数据" :span="2">
|
||||
<pre class="json-data">{{ formatJson(selectedLog.oldValue) }}</pre>
|
||||
</a-descriptions-item>
|
||||
@ -150,6 +156,9 @@ interface OperationLog {
|
||||
oldValue: string | null;
|
||||
newValue: string | null;
|
||||
ipAddress: string | null;
|
||||
createdBy?: string;
|
||||
requestParams?: string;
|
||||
requestUri?: string;
|
||||
createdAt: string;
|
||||
}
|
||||
|
||||
@ -185,10 +194,12 @@ const pagination = reactive({
|
||||
|
||||
// 表格列
|
||||
const columns = [
|
||||
{ title: '操作人', dataIndex: 'createdBy', key: 'createdBy', width: 120 },
|
||||
{ title: '用户类型', key: 'userType', width: 100 },
|
||||
{ title: '模块', key: 'module', width: 120 },
|
||||
{ title: '操作', key: 'action', width: 120 },
|
||||
{ title: '描述', dataIndex: 'description', key: 'description', ellipsis: true },
|
||||
{ title: '请求接口', dataIndex: 'requestUri', key: 'requestUri', width: 180, ellipsis: true },
|
||||
{ title: 'IP地址', dataIndex: 'ipAddress', key: 'ipAddress', width: 130 },
|
||||
{ title: '时间', key: 'createdAt', width: 180 },
|
||||
{ title: '操作', key: 'actions', width: 80 },
|
||||
|
||||
@ -111,6 +111,7 @@ public class LogAspect {
|
||||
// 记录请求信息
|
||||
operationLog.setIpAddress(getIpAddress(request));
|
||||
operationLog.setUserAgent(request.getHeader("User-Agent"));
|
||||
operationLog.setRequestUri(request.getRequestURI());
|
||||
|
||||
// 记录请求参数
|
||||
if (logAnnotation.recordParams()) {
|
||||
|
||||
@ -0,0 +1,36 @@
|
||||
package com.reading.platform.common.task;
|
||||
|
||||
import com.reading.platform.common.util.RsaEncryptionUtil;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* RSA 密钥定时轮换任务
|
||||
* 每月自动更换 RSA 密钥对,增强安全性
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class RsaKeyRotationTask {
|
||||
|
||||
private final RsaEncryptionUtil rsaEncryptionUtil;
|
||||
|
||||
/**
|
||||
* 每月 1 日凌晨 2 点自动更换 RSA 密钥
|
||||
* cron 表达式: 秒 分 时 日 月 周
|
||||
*/
|
||||
@Scheduled(cron = "0 0 2 1 * ?")
|
||||
public void rotateRsaKey() {
|
||||
try {
|
||||
log.info("开始执行 RSA 密钥定时更换任务");
|
||||
long oldVersion = rsaEncryptionUtil.getKeyVersion();
|
||||
rsaEncryptionUtil.generateNewKeyPair();
|
||||
long newVersion = rsaEncryptionUtil.getKeyVersion();
|
||||
log.info("RSA 密钥定时更换任务执行完成,版本: {} -> {}", oldVersion, newVersion);
|
||||
} catch (Exception e) {
|
||||
log.error("RSA 密钥定时更换任务执行失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,132 @@
|
||||
package com.reading.platform.common.util;
|
||||
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
import javax.crypto.Cipher;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.security.KeyPair;
|
||||
import java.security.KeyPairGenerator;
|
||||
import java.security.PrivateKey;
|
||||
import java.security.PublicKey;
|
||||
import java.util.Base64;
|
||||
import java.util.concurrent.atomic.AtomicLong;
|
||||
|
||||
/**
|
||||
* RSA 加密工具类
|
||||
* 支持密钥对生成、加密解密、密钥版本管理
|
||||
*/
|
||||
@Slf4j
|
||||
@Component
|
||||
public class RsaEncryptionUtil {
|
||||
|
||||
private static final String ALGORITHM = "RSA";
|
||||
private static final int KEY_SIZE = 2048;
|
||||
|
||||
private volatile KeyPair keyPair;
|
||||
private volatile String currentPublicKey;
|
||||
private final AtomicLong keyVersion = new AtomicLong(0);
|
||||
|
||||
/**
|
||||
* 初始化时生成密钥对
|
||||
*/
|
||||
@PostConstruct
|
||||
public void init() throws Exception {
|
||||
generateNewKeyPair();
|
||||
log.info("RSA 加密工具初始化完成,当前密钥版本: {}", keyVersion.get());
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成新的 RSA 密钥对
|
||||
* 每月定时执行,也可手动调用
|
||||
*/
|
||||
public synchronized void generateNewKeyPair() throws Exception {
|
||||
try {
|
||||
KeyPairGenerator generator = KeyPairGenerator.getInstance(ALGORITHM);
|
||||
generator.initialize(KEY_SIZE);
|
||||
this.keyPair = generator.generateKeyPair();
|
||||
this.currentPublicKey = Base64.getEncoder().encodeToString(
|
||||
keyPair.getPublic().getEncoded()
|
||||
);
|
||||
keyVersion.incrementAndGet();
|
||||
log.info("RSA 密钥对已更换,新版本号: {},密钥长度: {} 位", keyVersion.get(), KEY_SIZE);
|
||||
} catch (Exception e) {
|
||||
log.error("生成 RSA 密钥对失败", e);
|
||||
throw e;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前公钥(Base64 编码)
|
||||
*/
|
||||
public String getPublicKey() {
|
||||
return currentPublicKey;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取当前密钥版本号
|
||||
*/
|
||||
public long getKeyVersion() {
|
||||
return keyVersion.get();
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用私钥解密数据
|
||||
*
|
||||
* @param encryptedData Base64 编码的加密数据
|
||||
* @return 解密后的明文
|
||||
*/
|
||||
public String decrypt(String encryptedData) throws Exception {
|
||||
if (encryptedData == null || encryptedData.isEmpty()) {
|
||||
throw new IllegalArgumentException("加密数据不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
cipher.init(Cipher.DECRYPT_MODE, keyPair.getPrivate());
|
||||
byte[] decrypted = cipher.doFinal(
|
||||
Base64.getDecoder().decode(encryptedData)
|
||||
);
|
||||
return new String(decrypted, StandardCharsets.UTF_8);
|
||||
} catch (Exception e) {
|
||||
log.error("RSA 解密失败", e);
|
||||
throw new Exception("密码解密失败,请重试", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用公钥加密数据(主要用于测试)
|
||||
*
|
||||
* @param plainText 明文
|
||||
* @param publicKeyBase64 Base64 编码的公钥
|
||||
* @return Base64 编码的密文
|
||||
*/
|
||||
public String encrypt(String plainText, String publicKeyBase64) throws Exception {
|
||||
if (plainText == null || plainText.isEmpty()) {
|
||||
throw new IllegalArgumentException("明文不能为空");
|
||||
}
|
||||
|
||||
try {
|
||||
byte[] keyBytes = Base64.getDecoder().decode(publicKeyBase64);
|
||||
java.security.spec.X509EncodedKeySpec spec = new java.security.spec.X509EncodedKeySpec(keyBytes);
|
||||
java.security.KeyFactory keyFactory = java.security.KeyFactory.getInstance(ALGORITHM);
|
||||
PublicKey publicKey = keyFactory.generatePublic(spec);
|
||||
|
||||
Cipher cipher = Cipher.getInstance(ALGORITHM);
|
||||
cipher.init(Cipher.ENCRYPT_MODE, publicKey);
|
||||
byte[] encrypted = cipher.doFinal(plainText.getBytes(StandardCharsets.UTF_8));
|
||||
return Base64.getEncoder().encodeToString(encrypted);
|
||||
} catch (Exception e) {
|
||||
log.error("RSA 加密失败", e);
|
||||
throw new Exception("密码加密失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 使用当前公钥加密数据(用于测试)
|
||||
*/
|
||||
public String encryptWithCurrentKey(String plainText) throws Exception {
|
||||
return encrypt(plainText, currentPublicKey);
|
||||
}
|
||||
}
|
||||
@ -8,9 +8,12 @@ import com.reading.platform.common.mapper.ParentMapper;
|
||||
import com.reading.platform.common.mapper.TenantMapper;
|
||||
import com.reading.platform.common.mapper.TeacherMapper;
|
||||
import com.reading.platform.common.response.Result;
|
||||
import com.reading.platform.common.util.RsaEncryptionUtil;
|
||||
import com.reading.platform.dto.request.EncryptedLoginRequest;
|
||||
import com.reading.platform.dto.request.LoginRequest;
|
||||
import com.reading.platform.dto.request.UpdateProfileRequest;
|
||||
import com.reading.platform.dto.response.LoginResponse;
|
||||
import com.reading.platform.dto.response.PublicKeyResponse;
|
||||
import com.reading.platform.dto.response.TokenResponse;
|
||||
import com.reading.platform.dto.response.UpdateProfileResponse;
|
||||
import com.reading.platform.dto.response.UserInfoResponse;
|
||||
@ -38,6 +41,37 @@ public class AuthController {
|
||||
private final TeacherMapper teacherMapper;
|
||||
private final ParentMapper parentMapper;
|
||||
private final AdminUserMapper adminUserMapper;
|
||||
private final RsaEncryptionUtil rsaEncryptionUtil;
|
||||
|
||||
@Operation(summary = "获取 RSA 公钥")
|
||||
@GetMapping("/public-key")
|
||||
public Result<PublicKeyResponse> getPublicKey() {
|
||||
PublicKeyResponse response = new PublicKeyResponse();
|
||||
response.setPublicKey(rsaEncryptionUtil.getPublicKey());
|
||||
response.setKeyVersion(rsaEncryptionUtil.getKeyVersion());
|
||||
return Result.success(response);
|
||||
}
|
||||
|
||||
@Operation(summary = "加密登录(RSA)")
|
||||
@Log(module = LogModule.AUTH, type = LogOperationType.OTHER, description = "用户加密登录")
|
||||
@PostMapping("/login/encrypted")
|
||||
public Result<LoginResponse> loginEncrypted(@Valid @RequestBody EncryptedLoginRequest request) {
|
||||
// 解密密码
|
||||
String decryptedPassword;
|
||||
try {
|
||||
decryptedPassword = rsaEncryptionUtil.decrypt(request.getEncryptedPassword());
|
||||
} catch (Exception e) {
|
||||
return Result.error("密码解密失败,请重试");
|
||||
}
|
||||
|
||||
// 构造普通登录请求
|
||||
LoginRequest loginRequest = new LoginRequest();
|
||||
loginRequest.setUsername(request.getUsername());
|
||||
loginRequest.setPassword(decryptedPassword);
|
||||
loginRequest.setRole(request.getRole());
|
||||
|
||||
return Result.success(authService.login(loginRequest));
|
||||
}
|
||||
|
||||
@Operation(summary = "用户登录")
|
||||
@Log(module = LogModule.AUTH, type = LogOperationType.OTHER, description = "用户登录")
|
||||
|
||||
@ -108,6 +108,9 @@ public class SchoolOperationLogController {
|
||||
.details(log.getDetails())
|
||||
.ipAddress(log.getIpAddress())
|
||||
.userAgent(log.getUserAgent())
|
||||
.requestParams(log.getRequestParams())
|
||||
.requestUri(log.getRequestUri())
|
||||
.createBy(log.getCreateBy())
|
||||
.createdAt(log.getCreatedAt())
|
||||
.build();
|
||||
}
|
||||
|
||||
@ -0,0 +1,29 @@
|
||||
package com.reading.platform.dto.request;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* 加密登录请求 DTO
|
||||
* 用于 RSA 加密传输的登录请求
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "加密登录请求")
|
||||
public class EncryptedLoginRequest {
|
||||
|
||||
@NotBlank(message = "账号不能为空")
|
||||
@Schema(description = "登录账号(明文)")
|
||||
private String username;
|
||||
|
||||
@NotBlank(message = "密码不能为空")
|
||||
@Schema(description = "RSA 加密后的密码(Base64 编码)")
|
||||
private String encryptedPassword;
|
||||
|
||||
@NotBlank(message = "角色不能为空")
|
||||
@Schema(description = "登录角色")
|
||||
private String role;
|
||||
|
||||
@Schema(description = "密钥版本号(可选,用于兼容性)")
|
||||
private Long keyVersion;
|
||||
}
|
||||
@ -48,6 +48,15 @@ public class OperationLogResponse {
|
||||
@Schema(description = "用户代理")
|
||||
private String userAgent;
|
||||
|
||||
@Schema(description = "请求参数(JSON 格式)")
|
||||
private String requestParams;
|
||||
|
||||
@Schema(description = "请求接口路径")
|
||||
private String requestUri;
|
||||
|
||||
@Schema(description = "创建人")
|
||||
private String createBy;
|
||||
|
||||
@Schema(description = "创建时间")
|
||||
private LocalDateTime createdAt;
|
||||
}
|
||||
|
||||
@ -0,0 +1,18 @@
|
||||
package com.reading.platform.dto.response;
|
||||
|
||||
import io.swagger.v3.oas.annotations.media.Schema;
|
||||
import lombok.Data;
|
||||
|
||||
/**
|
||||
* RSA 公钥响应
|
||||
*/
|
||||
@Data
|
||||
@Schema(description = "RSA 公钥响应")
|
||||
public class PublicKeyResponse {
|
||||
|
||||
@Schema(description = "RSA 公钥(Base64 编码)")
|
||||
private String publicKey;
|
||||
|
||||
@Schema(description = "密钥版本号")
|
||||
private Long keyVersion;
|
||||
}
|
||||
@ -44,7 +44,10 @@ public class OperationLog extends BaseEntity {
|
||||
@Schema(description = "用户代理")
|
||||
private String userAgent;
|
||||
|
||||
@Schema(description = "请求参数(JSON 格式,不返回给前端)")
|
||||
@Schema(description = "请求参数(JSON 格式)")
|
||||
private String requestParams;
|
||||
|
||||
@Schema(description = "请求接口路径")
|
||||
private String requestUri;
|
||||
|
||||
}
|
||||
|
||||
@ -0,0 +1,19 @@
|
||||
-- =====================================================
|
||||
-- 操作日志表添加请求接口字段
|
||||
-- 版本:V50
|
||||
-- 创建时间:2026-03-24
|
||||
-- 描述:在 operation_log 表中添加 request_uri 字段,用于记录请求接口路径
|
||||
-- =====================================================
|
||||
|
||||
-- -----------------------------------------------------
|
||||
-- 1. 添加 request_uri 字段
|
||||
-- -----------------------------------------------------
|
||||
ALTER TABLE `operation_log`
|
||||
ADD COLUMN `request_uri` VARCHAR(500) COMMENT '请求接口路径' AFTER `request_params`;
|
||||
|
||||
-- -----------------------------------------------------
|
||||
-- 说明:
|
||||
-- - request_uri 字段用于存储请求的接口路径(如:/api/v1/school/teachers)
|
||||
-- - 该字段与 request_params 配合使用,完整记录操作请求信息
|
||||
-- - 由 LogAspect 自动记录,通过 @Log 注解标记的操作会自动保存
|
||||
-- -----------------------------------------------------
|
||||
Loading…
Reference in New Issue
Block a user