做设计的兼职网站有哪些,做影视网站版权问题,建筑模板915 1830重量,廊坊网站建设公司首先来看效果这个主要是为了防止篡改请求的。
我们这里采用的是一个AOP的拦截#xff0c;在有需要这样的接口上添加了加密处理。
下面是一些功能防篡改HMAC-SHA256 参数签名密钥仅客户端 服务器持有防重放秒级时间戳 有效窗口校验默认允许 5 分钟防窃听AES/CBC/PKCS5Pa…首先来看效果这个主要是为了防止篡改请求的。
我们这里采用的是一个AOP的拦截在有需要这样的接口上添加了加密处理。
下面是一些功能防篡改HMAC-SHA256 参数签名密钥仅客户端 服务器持有防重放秒级时间戳 有效窗口校验默认允许 ±5 分钟防窃听AES/CBC/PKCS5Padding 加密业务体对称密钥 16/24/32 字符最小侵入Spring AOP 自定义注解SecureApi 一行即可启用前后端交互流程
前端在请求拦截器里自动
生成 timestamp将业务 JSON → AES 加密得到 data按字典序拼接 timestampdata用 HMAC-SHA256 生成 sign
后端切面仅拦截被 SecureApi 标记的方法/类
解析三字段 → 校验时间窗口移除 sign 再验签成功后解密 data → 注入 request.setAttribute(secureData, plaintext)源码部分
首先是定义一个注解。
/*** 在 Controller 方法或类上添加该注解后将启用参数签名、时间戳校验和 AES 解密校验。*/
Target({ ElementType.METHOD, ElementType.TYPE })
Retention(RetentionPolicy.RUNTIME)
Documented
public interface SecureApi {
}最主要的拦截器
package com.xiaou.secure.aspect;import com.xiaou.secure.exception.SecureException;
import com.xiaou.secure.properties.SecureProperties;
import com.xiaou.secure.util.AESUtil;
import com.xiaou.secure.util.SignUtil;
import jakarta.servlet.http.HttpServletRequest;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;import java.io.BufferedReader;
import java.io.IOException;
import java.time.Instant;
import java.util.*;
import java.util.stream.Collectors;/*** 安全校验切面*/
Aspect
Component
public class SecureAspect {private static final Logger log LoggerFactory.getLogger(SecureAspect.class);Autowiredprivate SecureProperties properties;Around(annotation(com.xiaou.secure.annotation.SecureApi))public Object around(ProceedingJoinPoint pjp) throws Throwable {ServletRequestAttributes attrs (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();if (attrs null) {return pjp.proceed();}HttpServletRequest request attrs.getRequest();MapString, String params extractParams(request);// 1. 时间戳校验validateTimestamp(params.get(timestamp));// 2. 签名校验validateSign(params);// 3. AES 解密 data 字段if (params.containsKey(data)) {String plaintext AESUtil.decrypt(params.get(data), properties.getAesKey());// 把解密后的内容放到 request attribute方便业务层读取request.setAttribute(secureData, plaintext);}return pjp.proceed();}private MapString, String extractParams(HttpServletRequest request) throws IOException {MapString, String[] parameterMap request.getParameterMap();MapString, String params new HashMap();parameterMap.forEach((k, v) - params.put(k, v[0]));// 如果没有参数但可能是 JSON body需要读取 bodyif (params.isEmpty() request.getContentType() ! null request.getContentType().startsWith(application/json)) {String body readBody(request);if (body ! null !body.isEmpty()) {try {com.fasterxml.jackson.databind.ObjectMapper mapper new com.fasterxml.jackson.databind.ObjectMapper();MapString, Object jsonMap mapper.readValue(body, Map.class);jsonMap.forEach((k, v) - params.put(k, v null ? null : v.toString()));} catch (Exception e) {// 回退到原始 分隔的解析方式兼容 x-www-form-urlencoded 字符串Arrays.stream(body.split()).forEach(kv - {String[] kvArr kv.split(, 2);if (kvArr.length 2) {params.put(kvArr[0], kvArr[1]);}});}}}return params;}private String readBody(HttpServletRequest request) throws IOException {StringBuilder sb new StringBuilder();try (BufferedReader reader request.getReader()) {String line;while ((line reader.readLine()) ! null) {sb.append(line);}}return sb.toString();}private void validateTimestamp(String timestampStr) {if (timestampStr null) {throw new SecureException(timestamp missing);}long ts;try {ts Long.parseLong(timestampStr);} catch (NumberFormatException e) {throw new SecureException(timestamp invalid);}long now Instant.now().getEpochSecond();if (Math.abs(now - ts) properties.getAllowedTimestampOffset()) {throw new SecureException(timestamp expired);}}private void validateSign(MapString, String params) {String sign params.remove(sign);if (sign null) {throw new SecureException(sign missing);}// 排序MapString, String sorted params.entrySet().stream().sorted(Map.Entry.comparingByKey()).collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue, (a, b) - b, LinkedHashMap::new));String expected SignUtil.sign(sorted, properties.getSignSecret());if (!Objects.equals(expected, sign)) {throw new SecureException(sign invalid);}}
}配置方面
springboot自动配置
Configuration
ConditionalOnClass(WebMvcConfigurer.class)
AutoConfigureAfter(name org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration)
public class SecureAutoConfiguration {BeanConditionalOnMissingBeanpublic SecureProperties secureProperties() {return new SecureProperties();}
}动态配置 当然也可以用静态的
/*** 安全模块配置*/
ConfigurationProperties(prefix secure)
public class SecureProperties {/*** AES 密钥16/24/32 位*/// 默认 16 字符避免 InvalidKeyExceptionprivate String aesKey xiaou-secure-123;/*** 签名密钥*/private String signSecret xiaou-sign-secret;/*** 允许的时间差 (秒)默认 300 秒*/private long allowedTimestampOffset 300;public String getAesKey() {return aesKey;}public void setAesKey(String aesKey) {this.aesKey aesKey;}public String getSignSecret() {return signSecret;}public void setSignSecret(String signSecret) {this.signSecret signSecret;}public long getAllowedTimestampOffset() {return allowedTimestampOffset;}public void setAllowedTimestampOffset(long allowedTimestampOffset) {this.allowedTimestampOffset allowedTimestampOffset;}
}工具类
package com.xiaou.secure.util;import javax.crypto.Cipher;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.util.Base64;/*** AES/CBC/PKCS5Padding 工具类*/
public class AESUtil {private static final String AES_CBC_PKCS5 AES/CBC/PKCS5Padding;private static final String AES AES;private AESUtil() {}public static String encrypt(String data, String key) {try {Cipher cipher Cipher.getInstance(AES_CBC_PKCS5);SecretKeySpec skeySpec new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES);IvParameterSpec iv new IvParameterSpec(key.substring(0, 16).getBytes(StandardCharsets.UTF_8));cipher.init(Cipher.ENCRYPT_MODE, skeySpec, iv);byte[] encrypted cipher.doFinal(data.getBytes(StandardCharsets.UTF_8));return Base64.getEncoder().encodeToString(encrypted);} catch (Exception e) {throw new RuntimeException(AES encrypt error, e);}}public static String decrypt(String cipherText, String key) {try {Cipher cipher Cipher.getInstance(AES_CBC_PKCS5);SecretKeySpec skeySpec new SecretKeySpec(key.getBytes(StandardCharsets.UTF_8), AES);IvParameterSpec iv new IvParameterSpec(key.substring(0, 16).getBytes(StandardCharsets.UTF_8));cipher.init(Cipher.DECRYPT_MODE, skeySpec, iv);byte[] original cipher.doFinal(Base64.getDecoder().decode(cipherText));return new String(original, StandardCharsets.UTF_8);} catch (Exception e) {throw new RuntimeException(AES decrypt error, e);}}
}package com.xiaou.secure.util;import org.apache.commons.codec.digest.HmacAlgorithms;
import org.apache.commons.codec.digest.HmacUtils;import java.nio.charset.StandardCharsets;
import java.util.Map;
import java.util.StringJoiner;/*** 签名工具类*/
public class SignUtil {private SignUtil() {}/*** 生成签名* * param params 不包含 sign 的参数 map已按字典序排序* param secret 秘钥*/public static String sign(MapString, String params, String secret) {StringJoiner sj new StringJoiner();params.forEach((k, v) - sj.add(k v));String data sj.toString();return new HmacUtils(HmacAlgorithms.HMAC_SHA_256, secret.getBytes(StandardCharsets.UTF_8)).hmacHex(data);}
}以上就是全部源码
如果想要看具体的一个实现可以参考我的开源项目里面的xiaou-common-secure模块 https://github.com/xiaou61/U-space
使用流程
在需要的接口上添加注解SecureApi // 生效PostMapping(/student/save)public RVoid saveStudent(HttpServletRequest request) {String json (String) request.getAttribute(secureData); // 解密后明文StudentDTO dto JSON.parseObject(json, StudentDTO.class);//其他业务操作return R.ok();}
}前端接入
1. 安装依赖
npm i crypto-js2. 编写工具 (src/utils/secure.js)
import CryptoJS from crypto-js;const AES_KEY import.meta.env.VITE_AES_KEY; // 16/24/32 字符与后端保持一致
const SIGN_KEY import.meta.env.VITE_SIGN_SECRET; // 与后端 sign-secret 一致// AES/CBC/PKCS5Padding 加密 → Base64
export function aesEncrypt(plainText) {const key CryptoJS.enc.Utf8.parse(AES_KEY);const iv CryptoJS.enc.Utf8.parse(AES_KEY.slice(0, 16));const encrypted CryptoJS.AES.encrypt(plainText, key, {iv,mode: CryptoJS.mode.CBC,padding: CryptoJS.pad.Pkcs7});return encrypted.ciphertext.toString(CryptoJS.enc.Base64);
}// 生成签名字典序拼接后做 HMAC-SHA256
export function sign(params) {const sortedStr Object.keys(params).sort().map(k ${k}${params[k]}).join();return CryptoJS.HmacSHA256(sortedStr, SIGN_KEY).toString();
}封装
import http from ./request
import { aesEncrypt, sign as genSign } from ./secure// securePost 重新实现封装 { timestamp, data: cipher, sign }export async function securePost (url, bizData {}, { encrypt true } {}) {const timestamp Math.floor(Date.now() / 1000) // 秒级时间戳和后端配置一致// 若开启加密将 bizData 加密为 Base64 字符串const cipherText encrypt ? aesEncrypt(bizData) : JSON.stringify(bizData)// 组装待签名参数const payload {timestamp,data: cipherText}// 生成签名payload.sign genSign(payload)// 发送 JSONreturn http.post(url, payload, {headers: {Content-Type: application/json}})
}// 向后兼容导出旧别名
export { securePost as securePostV2 } 调用
export const login (data) {// 学生登录接口使用新的 securePost (AES/CBC HMAC-SHA256)return securePost(/student/auth/login, data)
}原理解析
这个接口加密机制的出发点其实很简单
我们不希望别人伪造请求或者直接看到请求内容。尤其是在登录、提交表单这种接口上如果不做处理参数一旦被篡改或者被抓包后果可能挺严重。
所以我们在请求中加了一些“安全三件套”
第一是签名。前端每次发请求的时候会把参数主要是 timestamp 和加密后的 data按字典序拼起来然后用我们双方约定好的一个密钥生成一个签名HMAC-SHA256 算法。后端拿到请求后同样的算法再生成一遍签名两个对不上就直接拒绝。这个方式能有效防止参数被篡改。
第二是时间戳。我们不允许别人把一两分钟前抓到的请求再发一次所以前端在请求里带上当前时间秒级。后端检查这个时间是否还在允许的时间窗口比如前后 5 分钟内超了就拒绝。这个能防止重放攻击。
第三是加密。我们不希望别人看到业务参数比如手机号、密码、验证码这类字段所以前端用 AESCBC 模式把整个业务数据 JSON 加密成密文后端收到后再解密拿出真实参数。密钥是我们自己设定的别人拿不到。
整套逻辑通过 Spring AOP 实现不需要每个接口去写重复代码只要在 Controller 上加一个 SecureApi 注解就行了。请求数据校验通过后解密出来的原始 JSON 会通过 request.setAttribute(secureData, plaintext) 注入进去业务代码直接拿就行。
整体上这个方案是为了在不增加太多开发成本的前提下做到参数不可篡改、请求不可复用、敏感数据不可明文传输。
流程图高清流程图
https://yxy7auidhk0.feishu.cn/wiki/LuXjwlXjxiFk4tkgrUEc0Ppbn4n?fromfrom_copylink