欧美bbbwbbbw肥妇,免费乱码人妻系列日韩,一级黄片

Springboot實(shí)現(xiàn)接口加密的示例代碼

 更新時(shí)間:2025年08月27日 11:43:01   作者:小u  
Springboot實(shí)現(xiàn)一個(gè)接口加密 首先來看效果 這個(gè)主要是為了防止篡改請求的。 我們這里采用的是一個(gè)AOP的攔截,在有需要這樣的接口上添加了加密處理。 下面是一些功能 防篡改 HMAC-SHA25

Springboot實(shí)現(xiàn)一個(gè)接口加密

首先來看效果

這個(gè)主要是為了防止篡改請求的。

我們這里采用的是一個(gè)AOP的攔截,在有需要這樣的接口上添加了加密處理。

下面是一些功能

防篡改HMAC-SHA256 參數(shù)簽名密鑰僅客戶端 & 服務(wù)器持有
防重放秒級時(shí)間戳 + 有效窗口校驗(yàn)默認(rèn)允許 ±5 分鐘
防竊聽AES/CBC/PKCS5Padding 加密業(yè)務(wù)體對稱密鑰 16/24/32 字符
最小侵入Spring AOP + 自定義注解@SecureApi 一行即可啟用

前后端交互流程

  1. 前端:在請求攔截器里自動(dòng)

    • 生成 timestamp
    • 將業(yè)務(wù) JSON → AES 加密得到 data
    • 按字典序拼接 timestamp=data,用 HMAC-SHA256 生成 sign
  2. 后端切面:僅攔截被 @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]));
?
        // 如果沒有參數(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)可以參考我的開源項(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) // 秒級時(shí)間戳,和后端配置一致
?
  // 若開啟加密,將 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í)很簡單:

我們不希望別人偽造請求或者直接看到請求內(nèi)容。尤其是在登錄、提交表單這種接口上,如果不做處理,參數(shù)一旦被篡改或者被抓包,后果可能挺嚴(yán)重。

所以我們在請求中加了一些“安全三件套”:

第一是簽名。前端每次發(fā)請求的時(shí)候,會(huì)把參數(shù)(主要是 timestamp 和加密后的 data)按字典序拼起來,然后用我們雙方約定好的一個(gè)密鑰生成一個(gè)簽名(HMAC-SHA256 算法)。后端拿到請求后,同樣的算法再生成一遍簽名,兩個(gè)對不上就直接拒絕。這個(gè)方式能有效防止參數(shù)被篡改。

第二是時(shí)間戳。我們不允許別人把一兩分鐘前抓到的請求再發(fā)一次,所以前端在請求里帶上當(dāng)前時(shí)間(秒級)。后端檢查這個(gè)時(shí)間是否還在允許的時(shí)間窗口(比如前后 5 分鐘)內(nèi),超了就拒絕。這個(gè)能防止重放攻擊。

第三是加密。我們不希望別人看到業(yè)務(wù)參數(shù),比如手機(jī)號、密碼、驗(yàn)證碼這類字段,所以前端用 AES(CBC 模式)把整個(gè)業(yè)務(wù)數(shù)據(jù) JSON 加密成密文,后端收到后再解密拿出真實(shí)參數(shù)。密鑰是我們自己設(shè)定的,別人拿不到。

整套邏輯通過 Spring AOP 實(shí)現(xiàn),不需要每個(gè)接口去寫重復(fù)代碼,只要在 Controller 上加一個(gè) @SecureApi 注解就行了。請求數(shù)據(jù)校驗(yàn)通過后,解密出來的原始 JSON 會(huì)通過 request.setAttribute("secureData", plaintext) 注入進(jìn)去,業(yè)務(wù)代碼直接拿就行。

整體上,這個(gè)方案是為了在不增加太多開發(fā)成本的前提下,做到參數(shù)不可篡改、請求不可復(fù)用、敏感數(shù)據(jù)不可明文傳輸。

流程圖

到此這篇關(guān)于Springboot實(shí)現(xiàn)接口加密的示例代碼的文章就介紹到這了,更多相關(guān)Springboot 接口加密內(nèi)容請搜索腳本之家以前的文章或繼續(xù)瀏覽下面的相關(guān)文章希望大家以后多多支持腳本之家!

相關(guān)文章

  • SpringBoot項(xiàng)目部署到服務(wù)器的兩種方式

    SpringBoot項(xiàng)目部署到服務(wù)器的兩種方式

    目前,前后端分離的架構(gòu)已成主流,而使用SpringBoot構(gòu)建Web應(yīng)用是非??焖俚?項(xiàng)目發(fā)布到服務(wù)器上的時(shí)候,只需要打成一個(gè)jar包,然后通過命令 : java -jar jar包名稱即可啟動(dòng)服務(wù)了,本文介紹了SpringBoot項(xiàng)目部署到服務(wù)器的兩種方式,需要的朋友可以參考下
    2024-10-10
  • Java利用Dijkstra算法求解拓?fù)潢P(guān)系最短路徑

    Java利用Dijkstra算法求解拓?fù)潢P(guān)系最短路徑

    迪杰斯特拉算法(Dijkstra)是由荷蘭計(jì)算機(jī)科學(xué)迪家迪杰斯特拉于1959年提出的,因此又叫狄克斯特拉算法。本文將利用迪克斯特拉(Dijkstra)算法求拓?fù)潢P(guān)系最短路徑,感興趣的可以了解一下
    2022-07-07
  • Java編程獲取文件列表及子文件目錄的方法(非遞歸)

    Java編程獲取文件列表及子文件目錄的方法(非遞歸)

    這篇文章主要介紹了Java編程獲取文件列表及子文件目錄的方法(非遞歸),具有一定參考價(jià)值,需要的朋友可以了解下。
    2017-10-10
  • Java?Web項(xiàng)目中如何添加Tomcat的Servlet-api.jar包(基于IDEA)

    Java?Web項(xiàng)目中如何添加Tomcat的Servlet-api.jar包(基于IDEA)

    servlet-api.jar是在編寫servlet必須用到的jar包下面這篇文章主要給大家介紹了基于IDEAJava?Web項(xiàng)目中如何添加Tomcat的Servlet-api.jar包的相關(guān)資料,文中通過圖文介紹的非常詳細(xì),需要的朋友可以參考下
    2024-04-04
  • Spring MVC利用Swagger2如何構(gòu)建動(dòng)態(tài)RESTful API詳解

    Spring MVC利用Swagger2如何構(gòu)建動(dòng)態(tài)RESTful API詳解

    這篇文章主要給大家介紹了關(guān)于在Spring MVC中利用Swagger2如何構(gòu)建動(dòng)態(tài)RESTful API的相關(guān)資料,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面來一起學(xué)習(xí)學(xué)習(xí)吧。
    2017-10-10
  • SpringBoot實(shí)現(xiàn)阿里云快遞物流查詢的示例代碼

    SpringBoot實(shí)現(xiàn)阿里云快遞物流查詢的示例代碼

    本文將基于springboot實(shí)現(xiàn)快遞物流查詢,物流信息的獲取通過阿里云第三方實(shí)現(xiàn),具有一定的參考價(jià)值,感興趣的可以了解一下
    2021-10-10
  • JAVA如何按字節(jié)截取字符串

    JAVA如何按字節(jié)截取字符串

    這篇文章主要介紹了JAVA如何按字節(jié)截取字符串,文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友可以參考下
    2020-01-01
  • Java web velocity分頁宏示例

    Java web velocity分頁宏示例

    這篇文章主要介紹了Java web velocity分頁宏示例,需要的朋友可以參考下
    2014-03-03
  • Java實(shí)現(xiàn)員工管理系統(tǒng)

    Java實(shí)現(xiàn)員工管理系統(tǒng)

    這篇文章主要為大家詳細(xì)介紹了Java實(shí)現(xiàn)員工管理系統(tǒng),具有一定的參考價(jià)值,感興趣的小伙伴們可以參考一下
    2018-01-01
  • 詳解Spring Boot最核心的27個(gè)注解,你了解多少?

    詳解Spring Boot最核心的27個(gè)注解,你了解多少?

    這篇文章主要介紹了詳解Spring Boot最核心的27個(gè)注解,你了解多少?文中通過示例代碼介紹的非常詳細(xì),對大家的學(xué)習(xí)或者工作具有一定的參考學(xué)習(xí)價(jià)值,需要的朋友們下面隨著小編來一起學(xué)習(xí)學(xué)習(xí)吧
    2019-08-08

最新評論