请求参数加密
请求参数加密通过 @EncryptedParam 注解实现单个参数的自动解密,适用于需要对特定参数进行加密传输的场景。
功能说明
参数加密基于 Spring MVC 参数解析器实现,在 Controller 参数绑定前自动解密,对业务代码透明。
核心特性
✅ 简单易用
- 注解驱动:只需一个 @EncryptedParam 注解
- 自动解密:参数绑定时自动解密
- 类型转换:支持所有基本类型和对象类型
✅ 灵活配置
- 选择性加密:只加密敏感参数
- 配置化密钥:密钥统一管理
- 多种算法:支持 AES、RSA
配置
基础配置
yaml
# application.yml
molandev:
encrypt:
params:
enabled: true # 启用参数加密,必填
type: RSA # 加密类型:AES 或 RSA,默认 RSA(登录场景)
key: ${RSA_PRIVATE_KEY} # 解密密钥,必填(RSA 用私钥;AES 为口令)
# algorithm 仅 type=AES 时生效,默认 AES/GCM/NoPadding
# algorithm: AES/GCM/NoPadding配置项说明
| 配置项 | 类型 | 默认值 | 必填 | 说明 |
|---|---|---|---|---|
| enabled | boolean | false | ✅ | 是否启用参数加密 |
| type | enum | RSA | ❌ | 加密类型:AES 或 RSA |
| key | String | - | ✅ | 解密密钥:RSA 为私钥,AES 为口令(经 AesUtil 派生) |
| algorithm | String | AES/GCM/NoPadding | ❌ | 仅 type=AES 时生效;默认 GCM |
TIP
项目登录场景默认使用 RSA。若改用 AES,前后端须对齐:密钥派生 SHA-256 取前 16 字节,密文 Base64(IV12 \|\| ciphertext\|\|tag)。
使用示例
示例 1:基础用法
Controller
java
import com.molandev.core.encrypt.params.EncryptedParam;
@RestController
@RequestMapping("/api/user")
public class UserController {
// ✅ 单个参数解密(显式指定参数名)
@GetMapping("/query")
public User query(@EncryptedParam("userId") Long userId) {
// userId 已自动解密
return userService.getById(userId);
}
// ✅ 参数名自动推断(推荐方式)
@GetMapping("/info")
public User info(@EncryptedParam String userId) {
// 自动使用方法参数名 userId 作为请求参数名
return userService.getById(userId);
}
// ✅ 多个参数解密
@PostMapping("/login")
public Result login(
@EncryptedParam String username, // 自动推断
@EncryptedParam String password // 自动推断
) {
// username 和 password 已自动解密
return authService.login(username, password);
}
}注意:
- 如果未显式指定
@EncryptedParam的value参数,框架会自动使用方法参数名作为请求参数名 - 需要确保编译时包含参数名信息(Maven 默认已配置
-parameters) - 当请求参数名与方法参数名不一致时,才需要显式指定
value
前端加密(AES-GCM,与后端对齐)
后端 type: AES 时,默认算法为 AES/GCM/NoPadding。前端须使用 Web Crypto AES-GCM,密文格式与 AesUtil 一致:
javascript
/**
* 与后端 AesUtil 对齐:
* - 密钥:SHA-256(password) 前 16 字节 → AES-128
* - IV:12 字节随机,前置
* - Tag:128 bit
* - 输出:Base64(IV || ciphertext||tag)
*/
async function deriveAesKey(password) {
const hash = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(password)
);
const keyBytes = new Uint8Array(hash).slice(0, 16);
return crypto.subtle.importKey(
'raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt']
);
}
function bytesToBase64(bytes) {
let s = '';
bytes.forEach((b) => { s += String.fromCharCode(b); });
return btoa(s);
}
async function aesEncrypt(text, password) {
const key = await deriveAesKey(password);
const iv = crypto.getRandomValues(new Uint8Array(12));
const cipherBuf = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
key,
new TextEncoder().encode(String(text))
);
const combined = new Uint8Array(12 + cipherBuf.byteLength);
combined.set(iv, 0);
combined.set(new Uint8Array(cipherBuf), 12);
return bytesToBase64(combined);
}
// 加密参数(password 与后端 molandev.encrypt.params.key 一致)
const encryptedUserId = await aesEncrypt('12345', 'your-param-password');
const encryptedUsername = await aesEncrypt('zhangsan', 'your-param-password');
const encryptedPassword = await aesEncrypt('password123', 'your-param-password');
// 发送请求
fetch(`/api/user/query?userId=${encodeURIComponent(encryptedUserId)}`);
fetch('/api/user/login', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({
username: encryptedUsername,
password: encryptedPassword
})
});登录场景(RSA)
项目实际登录多用 type: RSA:前端用公钥加密,后端 key 配置私钥并由 @EncryptedParam 解密。RSA 示例见下方「RSA 参数加密」。
前端加密(RSA,登录常用)
javascript
import JSEncrypt from 'jsencrypt';
function rsaEncrypt(text, publicKey) {
const encrypt = new JSEncrypt();
encrypt.setPublicKey(publicKey);
return encrypt.encrypt(text);
}
const encryptedPassword = rsaEncrypt(password, SERVER_RSA_PUBLIC_KEY);示例 2:混合使用
java
@RestController
@RequestMapping("/api/order")
public class OrderController {
@GetMapping("/query")
public Order query(
@EncryptedParam String orderId, // 自动推断:请求参数名 = orderId
@RequestParam String status, // 普通参数
@EncryptedParam("uid") Long userId // 显式指定:请求参数名 = uid
) {
// orderId 和 userId 已解密,status 保持原样
return orderService.query(orderId, status, userId);
}
}示例 3:对象类型
java
@Data
public class LoginRequest {
private String username;
private String password;
}
@RestController
public class AuthController {
// ❌ 不支持:@EncryptedParam 不能用于对象
@PostMapping("/login")
public Result login(@EncryptedParam("request") LoginRequest request) {
// 这种方式不支持
}
// ✅ 正确:分别解密每个字段
@PostMapping("/login")
public Result login(
@EncryptedParam("username") String username,
@EncryptedParam("password") String password
) {
LoginRequest request = new LoginRequest();
request.setUsername(username);
request.setPassword(password);
return authService.login(request);
}
}示例 4:封装工具类(AES-GCM)
javascript
// encrypt-utils.js — 与后端 AesUtil / params.algorithm=AES/GCM/NoPadding 对齐
export class ParamEncryptUtils {
constructor(password) {
this.password = password;
}
async #deriveKey() {
const hash = await crypto.subtle.digest(
'SHA-256',
new TextEncoder().encode(this.password)
);
const keyBytes = new Uint8Array(hash).slice(0, 16);
return crypto.subtle.importKey(
'raw', keyBytes, { name: 'AES-GCM' }, false, ['encrypt']
);
}
#bytesToBase64(bytes) {
let s = '';
bytes.forEach((b) => { s += String.fromCharCode(b); });
return btoa(s);
}
/**
* 加密单个参数 → Base64(IV12 || cipher||tag)
*/
async encrypt(value) {
if (value === null || value === undefined) {
return value;
}
const str = typeof value === 'string' ? value : String(value);
const key = await this.#deriveKey();
const iv = crypto.getRandomValues(new Uint8Array(12));
const cipherBuf = await crypto.subtle.encrypt(
{ name: 'AES-GCM', iv, tagLength: 128 },
key,
new TextEncoder().encode(str)
);
const combined = new Uint8Array(12 + cipherBuf.byteLength);
combined.set(iv, 0);
combined.set(new Uint8Array(cipherBuf), 12);
return this.#bytesToBase64(combined);
}
/**
* 批量加密参数
*/
async encryptParams(params, encryptKeys) {
const result = { ...params };
for (const key of encryptKeys) {
if (result[key] !== undefined) {
result[key] = await this.encrypt(result[key]);
}
}
return result;
}
async get(url, params, encryptKeys = []) {
const encryptedParams = await this.encryptParams(params, encryptKeys);
const queryString = new URLSearchParams(encryptedParams).toString();
const response = await fetch(`${url}?${queryString}`);
return response.json();
}
async post(url, params, encryptKeys = []) {
const encryptedParams = await this.encryptParams(params, encryptKeys);
const response = await fetch(url, {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams(encryptedParams)
});
return response.json();
}
}
// 使用
const encryptUtils = new ParamEncryptUtils('your-param-password');
await encryptUtils.get('/api/user/query', {
userId: '12345',
status: 'active'
}, ['userId']);
await encryptUtils.post('/api/user/login', {
username: 'zhangsan',
password: 'password123'
}, ['username', 'password']);示例 5:Vue.js 集成
javascript
// plugins/param-encrypt.js
import { ParamEncryptUtils } from '@/utils/encrypt-utils';
const encryptUtils = new ParamEncryptUtils(import.meta.env.VITE_PARAM_KEY);
export default {
install(app) {
app.config.globalProperties.$encryptParam = (value) => {
return encryptUtils.encrypt(value); // 返回 Promise
};
app.config.globalProperties.$axios.interceptors.request.use(async (config) => {
const encryptKeys = config.encryptParams || [];
if (encryptKeys.length > 0) {
if (config.method === 'get' && config.params) {
config.params = await encryptUtils.encryptParams(config.params, encryptKeys);
} else if (config.data) {
config.data = await encryptUtils.encryptParams(config.data, encryptKeys);
}
}
return config;
});
}
};技术细节
参数解析器
java
public class EncryptedParamHandlerMethodArgumentResolver
implements HandlerMethodArgumentResolver {
@Autowired
private EncryptProperties encryptProperties;
@Override
public boolean supportsParameter(MethodParameter parameter) {
// 支持带 @EncryptedParam 注解的参数
return parameter.hasParameterAnnotation(EncryptedParam.class);
}
@Override
public Object resolveArgument(
MethodParameter parameter,
ModelAndViewContainer mavContainer,
NativeWebRequest webRequest,
WebDataBinderFactory binderFactory
) throws Exception {
// 1. 获取注解
EncryptedParam annotation = parameter.getParameterAnnotation(EncryptedParam.class);
String paramName = annotation.value();
// 2. 获取加密的参数值
String encryptedValue = webRequest.getParameter(paramName);
if (encryptedValue == null) {
return null;
}
// 3. 解密
String decryptedValue = decrypt(encryptedValue);
// 4. 类型转换
Class<?> parameterType = parameter.getParameterType();
return convertValue(decryptedValue, parameterType);
}
private String decrypt(String encryptedValue) {
EncryptProperties.ParamsProperties config = encryptProperties.getParams();
if (config.getType() == EncryptProperties.EncryptType.AES) {
return AesUtil.decrypt(encryptedValue, config.getKey(), config.getAlgorithm());
} else {
return RsaUtil.decrypt(encryptedValue, config.getPrivateKey());
}
}
}类型转换
支持的参数类型:
- 基本类型:int, long, boolean, etc.
- 包装类型:Integer, Long, Boolean, etc.
- 字符串:String
- 日期类型:Date, LocalDateTime, etc.
java
private Object convertValue(String value, Class<?> targetType) {
if (targetType == String.class) {
return value;
} else if (targetType == Integer.class || targetType == int.class) {
return Integer.valueOf(value);
} else if (targetType == Long.class || targetType == long.class) {
return Long.valueOf(value);
} else if (targetType == Boolean.class || targetType == boolean.class) {
return Boolean.valueOf(value);
}
// ... 其他类型转换
}注意事项
⚠️ 仅支持简单类型
@EncryptedParam 只支持简单类型,不支持对象:
java
// ✅ 支持
@EncryptedParam("userId") Long userId
@EncryptedParam("username") String username
// ❌ 不支持
@EncryptedParam("user") User user⚠️ URL 编码
加密后的字符串需要 URL 编码:
javascript
const encrypted = aesEncrypt('value', key);
const encoded = encodeURIComponent(encrypted); // ✅ URL 编码
fetch(`/api/user?id=${encoded}`);⚠️ 与 @RequestParam 配合
java
// ✅ 正确:使用 @EncryptedParam
@GetMapping("/query")
public User query(@EncryptedParam("userId") Long userId) {
}
// ❌ 错误:同时使用两个注解
@GetMapping("/query")
public User query(
@EncryptedParam("userId")
@RequestParam("userId") Long userId
) {
}⚠️ POST 请求内容类型
参数加密只支持 application/x-www-form-urlencoded:
javascript
// ✅ 正确
fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/x-www-form-urlencoded' },
body: new URLSearchParams({ username: encrypted })
});
// ❌ 错误:JSON 格式不支持
fetch('/api/login', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ username: encrypted })
});如需 JSON 格式,请使用混合加密。
常见问题
Q1: 为什么不支持对象类型?
A: 因为参数解析器是针对单个参数设计的。如需对象加密,建议:
- 使用混合加密(推荐)
- 分别加密对象的每个字段
Q2: 如何与签名配合使用?
A: 加密后的参数值参与签名:
javascript
// 1. 先加密参数(AES-GCM,与后端对齐)
const encryptedUserId = await aesEncrypt('12345', paramPassword);
// 2. 用加密后的值生成签名
const params = {
userId: encryptedUserId, // 加密后的值
timestamp: Date.now(),
nonce: 'abc123'
};
params.sign = generateSign(params, signSecret);Q3: 可以混合使用加密和普通参数吗?
A: 可以。
java
@GetMapping("/query")
public Result query(
@EncryptedParam("userId") Long userId, // 加密
@RequestParam String status // 普通
) {
// userId 会解密,status 保持原样
}Q4: 性能影响大吗?
A: 影响很小。AES 解密单个参数耗时约 1-2ms,RSA 解密耗时略高但也在可接受范围内。
Q5: 测试环境如何调试?
A: 临时关闭参数加密:
yaml
# application-dev.yml
molandev:
encrypt:
params:
enabled: false