Skip to content

请求参数加密

请求参数加密通过 @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

配置项说明

配置项类型默认值必填说明
enabledbooleanfalse是否启用参数加密
typeenumRSA加密类型:AES 或 RSA
keyString-解密密钥:RSA 为私钥,AES 为口令(经 AesUtil 派生)
algorithmStringAES/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);
    }
}

注意

  • 如果未显式指定 @EncryptedParamvalue 参数,框架会自动使用方法参数名作为请求参数名
  • 需要确保编译时包含参数名信息(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: 因为参数解析器是针对单个参数设计的。如需对象加密,建议:

  1. 使用混合加密(推荐)
  2. 分别加密对象的每个字段

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

相关工具

参考资料