feat(登录安全): 实现 RSA 密码加密传输
后端: - 新增 RsaEncryptionUtil 工具类,支持 RSA 2048 位加解密 - 新增 RsaKeyRotationTask 定时任务,每月 1 日凌晨 2 点自动更换密钥 - 新增 EncryptedLoginRequest 和 PublicKeyResponse DTO - AuthController 添加 /public-key 和 /login/encrypted 接口 前端: - 添加 jsencrypt 依赖用于 RSA 加密 - 新增 encryption.ts 工具函数 - auth.ts 添加 getPublicKey 和 loginEncrypted API - user.ts 修改 login 函数使用 RSA 加密流程 feat(操作日志): 添加请求参数和请求接口字段 - 数据库迁移 V50 添加 request_uri 字段 - LogAspect 记录请求 URI - OperationLogResponse 新增 requestParams 和 requestUri 字段 - 前端 OperationLogView 详情弹窗展示新字段 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
parent
1038a70d92
commit
c935988188
@ -61,3 +61,63 @@
|
|||||||
| `AdminTenantController.java` | 修改详情查询方法 |
|
| `AdminTenantController.java` | 修改详情查询方法 |
|
||||||
| `admin.ts` | 扩展类型定义 |
|
| `admin.ts` | 扩展类型定义 |
|
||||||
| `TenantListView.vue` | 更新详情展示 |
|
| `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` | 详情展示新增字段 |
|
||||||
|
|||||||
6
reading-platform-frontend/package-lock.json
generated
6
reading-platform-frontend/package-lock.json
generated
@ -18,6 +18,7 @@
|
|||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
|
"jsencrypt": "^3.3.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lucide-vue-next": "^0.575.0",
|
"lucide-vue-next": "^0.575.0",
|
||||||
"pdfjs-dist": "^3.11.174",
|
"pdfjs-dist": "^3.11.174",
|
||||||
@ -4463,6 +4464,11 @@
|
|||||||
"js-yaml": "bin/js-yaml.js"
|
"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": {
|
"node_modules/jsesc": {
|
||||||
"version": "3.1.0",
|
"version": "3.1.0",
|
||||||
"resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
|
"resolved": "https://registry.npmmirror.com/jsesc/-/jsesc-3.1.0.tgz",
|
||||||
|
|||||||
@ -26,6 +26,7 @@
|
|||||||
"axios": "^1.6.7",
|
"axios": "^1.6.7",
|
||||||
"dayjs": "^1.11.10",
|
"dayjs": "^1.11.10",
|
||||||
"echarts": "^6.0.0",
|
"echarts": "^6.0.0",
|
||||||
|
"jsencrypt": "^3.3.2",
|
||||||
"lodash-es": "^4.17.21",
|
"lodash-es": "^4.17.21",
|
||||||
"lucide-vue-next": "^0.575.0",
|
"lucide-vue-next": "^0.575.0",
|
||||||
"pdfjs-dist": "^3.11.174",
|
"pdfjs-dist": "^3.11.174",
|
||||||
|
|||||||
@ -1,4 +1,5 @@
|
|||||||
import { http } from './index';
|
import { http } from './index';
|
||||||
|
import type { EncryptedLoginParams, PublicKeyResponse } from '@/utils/encryption';
|
||||||
|
|
||||||
export interface LoginParams {
|
export interface LoginParams {
|
||||||
account: string;
|
account: string;
|
||||||
@ -78,3 +79,25 @@ export function changePassword(oldPassword: string, newPassword: string): Promis
|
|||||||
params: { oldPassword, newPassword },
|
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,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|||||||
@ -860,6 +860,8 @@ export interface OperationLog {
|
|||||||
oldValue: string | null;
|
oldValue: string | null;
|
||||||
newValue: string | null;
|
newValue: string | null;
|
||||||
ipAddress: string | null;
|
ipAddress: string | null;
|
||||||
|
requestParams?: string; // 请求参数 JSON
|
||||||
|
requestUri?: string; // 请求接口路径
|
||||||
createdAt: string;
|
createdAt: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -3,6 +3,7 @@ import { ref, computed } from 'vue';
|
|||||||
import { message } from 'ant-design-vue';
|
import { message } from 'ant-design-vue';
|
||||||
import { router } from '@/router';
|
import { router } from '@/router';
|
||||||
import * as authApi from '@/api/auth';
|
import * as authApi from '@/api/auth';
|
||||||
|
import { rsaEncrypt } from '@/utils/encryption';
|
||||||
|
|
||||||
export interface User {
|
export interface User {
|
||||||
id: number;
|
id: number;
|
||||||
@ -26,10 +27,25 @@ export const useUserStore = defineStore('user', () => {
|
|||||||
const isLoggedIn = computed(() => !!token.value);
|
const isLoggedIn = computed(() => !!token.value);
|
||||||
const userRole = computed(() => user.value?.role || null);
|
const userRole = computed(() => user.value?.role || null);
|
||||||
|
|
||||||
// 登录
|
// 登录(使用 RSA 加密)
|
||||||
async function login(account: string, password: string, role: string) {
|
async function login(account: string, password: string, role: string) {
|
||||||
try {
|
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, userId, username, name, role, tenantId }
|
||||||
token.value = data.token;
|
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;
|
||||||
|
}
|
||||||
@ -118,6 +118,12 @@
|
|||||||
<a-descriptions-item label="操作描述" :span="2">
|
<a-descriptions-item label="操作描述" :span="2">
|
||||||
{{ selectedLog.description }}
|
{{ selectedLog.description }}
|
||||||
</a-descriptions-item>
|
</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">
|
<a-descriptions-item label="变更前数据" :span="2">
|
||||||
<pre class="json-data">{{ formatJson(selectedLog.oldValue) }}</pre>
|
<pre class="json-data">{{ formatJson(selectedLog.oldValue) }}</pre>
|
||||||
</a-descriptions-item>
|
</a-descriptions-item>
|
||||||
|
|||||||
@ -111,6 +111,7 @@ public class LogAspect {
|
|||||||
// 记录请求信息
|
// 记录请求信息
|
||||||
operationLog.setIpAddress(getIpAddress(request));
|
operationLog.setIpAddress(getIpAddress(request));
|
||||||
operationLog.setUserAgent(request.getHeader("User-Agent"));
|
operationLog.setUserAgent(request.getHeader("User-Agent"));
|
||||||
|
operationLog.setRequestUri(request.getRequestURI());
|
||||||
|
|
||||||
// 记录请求参数
|
// 记录请求参数
|
||||||
if (logAnnotation.recordParams()) {
|
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.TenantMapper;
|
||||||
import com.reading.platform.common.mapper.TeacherMapper;
|
import com.reading.platform.common.mapper.TeacherMapper;
|
||||||
import com.reading.platform.common.response.Result;
|
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.LoginRequest;
|
||||||
import com.reading.platform.dto.request.UpdateProfileRequest;
|
import com.reading.platform.dto.request.UpdateProfileRequest;
|
||||||
import com.reading.platform.dto.response.LoginResponse;
|
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.TokenResponse;
|
||||||
import com.reading.platform.dto.response.UpdateProfileResponse;
|
import com.reading.platform.dto.response.UpdateProfileResponse;
|
||||||
import com.reading.platform.dto.response.UserInfoResponse;
|
import com.reading.platform.dto.response.UserInfoResponse;
|
||||||
@ -38,6 +41,37 @@ public class AuthController {
|
|||||||
private final TeacherMapper teacherMapper;
|
private final TeacherMapper teacherMapper;
|
||||||
private final ParentMapper parentMapper;
|
private final ParentMapper parentMapper;
|
||||||
private final AdminUserMapper adminUserMapper;
|
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 = "用户登录")
|
@Operation(summary = "用户登录")
|
||||||
@Log(module = LogModule.AUTH, type = LogOperationType.OTHER, description = "用户登录")
|
@Log(module = LogModule.AUTH, type = LogOperationType.OTHER, description = "用户登录")
|
||||||
|
|||||||
@ -108,6 +108,8 @@ public class SchoolOperationLogController {
|
|||||||
.details(log.getDetails())
|
.details(log.getDetails())
|
||||||
.ipAddress(log.getIpAddress())
|
.ipAddress(log.getIpAddress())
|
||||||
.userAgent(log.getUserAgent())
|
.userAgent(log.getUserAgent())
|
||||||
|
.requestParams(log.getRequestParams())
|
||||||
|
.requestUri(log.getRequestUri())
|
||||||
.createdAt(log.getCreatedAt())
|
.createdAt(log.getCreatedAt())
|
||||||
.build();
|
.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,12 @@ public class OperationLogResponse {
|
|||||||
@Schema(description = "用户代理")
|
@Schema(description = "用户代理")
|
||||||
private String userAgent;
|
private String userAgent;
|
||||||
|
|
||||||
|
@Schema(description = "请求参数(JSON 格式)")
|
||||||
|
private String requestParams;
|
||||||
|
|
||||||
|
@Schema(description = "请求接口路径")
|
||||||
|
private String requestUri;
|
||||||
|
|
||||||
@Schema(description = "创建时间")
|
@Schema(description = "创建时间")
|
||||||
private LocalDateTime createdAt;
|
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 = "用户代理")
|
@Schema(description = "用户代理")
|
||||||
private String userAgent;
|
private String userAgent;
|
||||||
|
|
||||||
@Schema(description = "请求参数(JSON 格式,不返回给前端)")
|
@Schema(description = "请求参数(JSON 格式)")
|
||||||
private String requestParams;
|
private String requestParams;
|
||||||
|
|
||||||
|
@Schema(description = "请求接口路径")
|
||||||
|
private String requestUri;
|
||||||
|
|
||||||
}
|
}
|
||||||
|
|||||||
@ -0,0 +1,19 @@
|
|||||||
|
-- =====================================================
|
||||||
|
-- 操作日志表添加请求接口字段
|
||||||
|
-- 版本:V49
|
||||||
|
-- 创建时间: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