Springboot實(shí)現(xiàn)接口加密的示例代碼
Springboot實(shí)現(xiàn)一個(gè)接口加密
首先來(lái)看效果

這個(gè)主要是為了防止篡改請(qǐng)求的。
我們這里采用的是一個(gè)AOP的攔截,在有需要這樣的接口上添加了加密處理。
下面是一些功能
| 防篡改 | HMAC-SHA256 參數(shù)簽名 | 密鑰僅客戶端 & 服務(wù)器持有 |
|---|---|---|
| 防重放 | 秒級(jí)時(shí)間戳 + 有效窗口校驗(yàn) | 默認(rèn)允許 ±5 分鐘 |
| 防竊聽(tīng) | AES/CBC/PKCS5Padding 加密業(yè)務(wù)體 | 對(duì)稱密鑰 16/24/32 字符 |
| 最小侵入 | Spring AOP + 自定義注解 | @SecureApi 一行即可啟用 |
前后端交互流程
前端:在請(qǐng)求攔截器里自動(dòng)
- 生成
timestamp - 將業(yè)務(wù) JSON → AES 加密得到
data - 按字典序拼接
timestamp=data,用 HMAC-SHA256 生成sign
- 生成
后端切面:僅攔截被
@SecureApi標(biāo)記的方法/類- 解析三字段 → 校驗(yàn)時(shí)間窗口
- 移除
sign再驗(yàn)簽 - 成功后解密
data→ 注入request.setAttribute("secureData", plaintext)
源碼部分
首先是定義一個(gè)注解。
/**
* 在 Controller 方法或類上添加該注解后,將啟用參數(shù)簽名、時(shí)間戳校驗(yàn)和 AES 解密校驗(yàn)。
*/
@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;
?
/**
* 安全校驗(yàn)切面
*/
@Aspect
@Component
public class SecureAspect {
?
private static final Logger log = LoggerFactory.getLogger(SecureAspect.class);
?
@Autowired
private 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();
?
Map<String, String> params = extractParams(request);
?
// 1. 時(shí)間戳校驗(yàn)
validateTimestamp(params.get("timestamp"));
?
// 2. 簽名校驗(yàn)
validateSign(params);
?
// 3. AES 解密 data 字段
if (params.containsKey("data")) {
String plaintext = AESUtil.decrypt(params.get("data"), properties.getAesKey());
// 把解密后的內(nèi)容放到 request attribute,方便業(yè)務(wù)層讀取
request.setAttribute("secureData", plaintext);
}
?
return pjp.proceed();
}
?
private Map<String, String> extractParams(HttpServletRequest request) throws IOException {
Map<String, String[]> parameterMap = request.getParameterMap();
Map<String, String> params = new HashMap<>();
parameterMap.forEach((k, v) -> params.put(k, v[0]));
?
// 如果沒(méi)有參數(shù),但可能是 JSON body,需要讀取 body
if (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();
Map<String, 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(Map<String, String> params) {
String sign = params.remove("sign");
if (sign == null) {
throw new SecureException("sign missing");
}
// 排序
Map<String, 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自動(dòng)配置
@Configuration
@ConditionalOnClass(WebMvcConfigurer.class)
@AutoConfigureAfter(name = "org.springframework.boot.autoconfigure.web.servlet.WebMvcAutoConfiguration")
public class SecureAutoConfiguration {
?
@Bean
@ConditionalOnMissingBean
public SecureProperties secureProperties() {
return new SecureProperties();
}
}
動(dòng)態(tài)配置 當(dāng)然也可以用靜態(tài)的
/**
* 安全模塊配置
*/
@ConfigurationProperties(prefix = "secure")
public class SecureProperties {
?
/**
* AES 密鑰(16/24/32 位)
*/
// 默認(rèn) 16 字符,避免 InvalidKeyException
private String aesKey = "xiaou-secure-123";
?
/**
* 簽名密鑰
*/
private String signSecret = "xiaou-sign-secret";
?
/**
* 允許的時(shí)間差 (秒),默認(rèn) 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 的參數(shù) map,已按字典序排序
* @param secret 秘鑰
*/
public static String sign(Map<String, 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);
}
}
以上就是全部源碼
如果想要看具體的一個(gè)實(shí)現(xiàn)可以參考我的開(kāi)源項(xiàng)目里面的xiaou-common-secure模塊 github.com/xiaou61/U-s…
使用流程
在需要的接口上添加注解
@SecureApi // 生效!
@PostMapping("/student/save")
public R<Void> saveStudent(HttpServletRequest request) {
String json = (String) request.getAttribute("secureData"); // 解密后明文
StudentDTO dto = JSON.parseObject(json, StudentDTO.class);
//其他業(yè)務(wù)操作
return R.ok();
}
}
前端接入
1. 安裝依賴
npm i crypto-js
2. 編寫工具 (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 重新實(shí)現(xiàn):封裝 { timestamp, data: cipher, sign }
?
export async function securePost (url, bizData = {}, { encrypt = true } = {}) {
const timestamp = Math.floor(Date.now() / 1000) // 秒級(jí)時(shí)間戳,和后端配置一致
?
// 若開(kāi)啟加密,將 bizData 加密為 Base64 字符串
const cipherText = encrypt ? aesEncrypt(bizData) : JSON.stringify(bizData)
?
// 組裝待簽名參數(shù)
const payload = {
timestamp,
data: cipherText
}
?
// 生成簽名
payload.sign = genSign(payload)
?
// 發(fā)送 JSON
return http.post(url, payload, {
headers: {
'Content-Type': 'application/json'
}
})
}
?
// 向后兼容:導(dǎo)出舊別名
export { securePost as securePostV2 }
調(diào)用
export const login = (data) => {
// 學(xué)生登錄接口使用新的 securePost (AES/CBC + HMAC-SHA256)
return securePost('/student/auth/login', data)
}
原理解析
這個(gè)接口加密機(jī)制的出發(fā)點(diǎn)其實(shí)很簡(jiǎn)單:
我們不希望別人偽造請(qǐng)求或者直接看到請(qǐng)求內(nèi)容。尤其是在登錄、提交表單這種接口上,如果不做處理,參數(shù)一旦被篡改或者被抓包,后果可能挺嚴(yán)重。
所以我們?cè)谡?qǐng)求中加了一些“安全三件套”:
第一是簽名。前端每次發(fā)請(qǐng)求的時(shí)候,會(huì)把參數(shù)(主要是 timestamp 和加密后的 data)按字典序拼起來(lái),然后用我們雙方約定好的一個(gè)密鑰生成一個(gè)簽名(HMAC-SHA256 算法)。后端拿到請(qǐng)求后,同樣的算法再生成一遍簽名,兩個(gè)對(duì)不上就直接拒絕。這個(gè)方式能有效防止參數(shù)被篡改。
第二是時(shí)間戳。我們不允許別人把一兩分鐘前抓到的請(qǐng)求再發(fā)一次,所以前端在請(qǐng)求里帶上當(dāng)前時(shí)間(秒級(jí))。后端檢查這個(gè)時(shí)間是否還在允許的時(shí)間窗口(比如前后 5 分鐘)內(nèi),超了就拒絕。這個(gè)能防止重放攻擊。
第三是加密。我們不希望別人看到業(yè)務(wù)參數(shù),比如手機(jī)號(hào)、密碼、驗(yàn)證碼這類字段,所以前端用 AES(CBC 模式)把整個(gè)業(yè)務(wù)數(shù)據(jù) JSON 加密成密文,后端收到后再解密拿出真實(shí)參數(shù)。密鑰是我們自己設(shè)定的,別人拿不到。
整套邏輯通過(guò) Spring AOP 實(shí)現(xiàn),不需要每個(gè)接口去寫重復(fù)代碼,只要在 Controller 上加一個(gè) @SecureApi 注解就行了。請(qǐng)求數(shù)據(jù)校驗(yàn)通過(guò)后,解密出來(lái)的原始 JSON 會(huì)通過(guò) request.setAttribute("secureData", plaintext) 注入進(jìn)去,業(yè)務(wù)代碼直接拿就行。
整體上,這個(gè)方案是為了在不增加太多開(kāi)發(fā)成本的前提下,做到參數(shù)不可篡改、請(qǐng)求不可復(fù)用、敏感數(shù)據(jù)不可明文傳輸。
流程圖


到此這篇關(guān)于Springboot實(shí)現(xiàn)接口加密的示例代碼的文章就介紹到這了,更多相關(guān)Springboot 接口加密內(nèi)容請(qǐng)搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!
相關(guān)文章
SpringBoot自定義路由覆蓋實(shí)現(xiàn)流程詳解
這篇文章主要介紹了SpringBoot自定義路由覆蓋實(shí)現(xiàn)流程,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來(lái)一起學(xué)習(xí)吧2023-01-01
SpringBoot整合Elasticsearch實(shí)現(xiàn)全文搜索功能
在現(xiàn)代應(yīng)用程序中,對(duì)于大量數(shù)據(jù)的高效管理和快速檢索是至關(guān)重要的,Elasticsearch作為一款開(kāi)源的全文搜索引擎,為開(kāi)發(fā)者提供了強(qiáng)大而靈活的搜索解決方案,本文將介紹如何通過(guò)Spring Boot框架整合Elasticsearch,實(shí)現(xiàn)高效的全文搜索功能,需要的朋友可以參考下2025-12-12
Java內(nèi)存模型之happens-before概念詳解
happens-before原則非常重要,它是判斷數(shù)據(jù)是否存在競(jìng)爭(zhēng)、線程是否安全的主要依據(jù),依靠這個(gè)原則,我們解決在并發(fā)環(huán)境下兩操作之間是否可能存在沖突的所有問(wèn)題。下面我們就一個(gè)簡(jiǎn)單的例子稍微了解下happens-before知識(shí),感興趣的朋友一起看看吧2021-06-06
Java基礎(chǔ)學(xué)習(xí)之標(biāo)簽
在Java中,標(biāo)簽必須在循環(huán)之前使用, 一個(gè)循環(huán)之中嵌套另一個(gè)循環(huán)的開(kāi)關(guān),從多重嵌套中continue或break,該文詳細(xì)介紹了標(biāo)簽的相關(guān)知識(shí),對(duì)正在學(xué)習(xí)java基礎(chǔ)的小伙伴們還很有幫助,需要的朋友可以參考下2021-05-05
解決Mybatis一對(duì)多/多對(duì)多查詢時(shí)只查出了一條數(shù)據(jù)問(wèn)題
在使用MyBatis進(jìn)行一對(duì)多查詢時(shí),若兩張表的ID字段名稱相同,則會(huì)導(dǎo)致多的一方數(shù)據(jù)只查詢出來(lái)一條,解決方法是修改ID字段名稱,保持不一致,此經(jīng)驗(yàn)供參考,希望對(duì)大家有所幫助2026-05-05
SpringBoot實(shí)現(xiàn)接口加密的五大技巧分享
為了有效應(yīng)對(duì)接口安全面臨的挑戰(zhàn),接口加密作為一種至關(guān)重要的手段,能夠?qū)⒃紨?shù)據(jù)轉(zhuǎn)化為密文進(jìn)行傳輸,從而確保數(shù)據(jù)在傳輸過(guò)程中的機(jī)密性和完整性,本文將深入探討SpringBoot接口加密的最佳實(shí)踐,需要的朋友可以參考下2025-07-07
Spring Boot企業(yè)常用的starter示例詳解
這篇文章主要給大家介紹了關(guān)于Spring Boot企業(yè)常用starter的相關(guān)資料,文中通過(guò)示例代碼介紹的非常詳細(xì),對(duì)大家學(xué)習(xí)或者使用Spring Boot具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來(lái)一起學(xué)習(xí)學(xué)習(xí)吧2019-12-12

