21bc4aa9创建于 2025年8月12日历史提交
<!DOCTYPE html>
<html>
    <head>
        <meta charset="utf-8"/>
        <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"/>
        <title>AJ Security 框架-API 接口数据加密</title>
        <meta name="description" content="实用的 Java Web 安全库。对 HTTP API 接口中的入参、出参进行数据加密、解密,其目的是保护敏感数据、防篡改、防抓包。"/>
        <meta name="keywords" content="security, xss, csrf, captcha, api"/>
        <meta name="viewport" content="width=device-width, initial-scale=1"/>
        <link rel="stylesheet" href="https://framework.ajaxjs.com/static/font/font.css" />
        <link rel="stylesheet" href="/asset/main.css"/>
        <link rel="icon" type="image/x-icon" href="https://framework.ajaxjs.com/aj-logo/logo.ico"/>
        <script src="https://framework.ajaxjs.com/static/aj-docs/common.js"></script>
        <script>
            // 获取用户的默认语言
            var userLang = navigator.language || navigator.userLanguage;

            // 检查是否为中文环境(包括简体和繁体)
            if (userLang.startsWith('zh') && location.pathname.indexOf('cn') == -1) {
                 confirm('欢迎!您可以改为访问中文内容。是否继续?') && location.assign('/cn');  // 如果是中文,则弹出提示
            }

            var _hmt = _hmt || [];
            (function() {
              var hm = document.createElement("script");
              hm.src = "https://hm.baidu.com/hm.js?950ba5ba1f1fe4906c3b4cf836080f03";
              var s = document.getElementsByTagName("script")[0];
              s.parentNode.insertBefore(hm, s);
            })();
        </script>
    </head>
    <body>
        <nav>
            <div>
                <div class="links">
                    <a href="/">🏠 首页</a>
                    | ⚙️ 源码:
                    <a target="_blank" href="https://github.com/lightweight-component/aj-security">Github</a>/<a target="_blank" href="https://gitcode.com/lightweight-component/aj-security">Gitcode</a>
                    |
                    <a href="/">英文版本</a>
                </div>
                <h1><img src="https://framework.ajaxjs.com/aj-logo/logo.png" style="vertical-align: middle;height: 45px;margin-bottom: 6px;" /> AJ Security</h1>
                <h3>用户手册</h3>
            </div>
        </nav>
        <div>
            <menu>
                <ul>
                    <li class="selected">
                        <a href="/cn">首页</a>
                    </li>
                    <li>
                        <a href="/install-cn">安装与配置</a>
                    </li>
                </ul>
                <h3>HTTP Web 安全</h3>
                <ul>
                    <li>
                        <a href="/http/http-referer-cn">HTTP Referer 校验</a>
                    </li>
                    <li>
                       <a href="/http/timestamp-cn">时间戳加密 Token 校验</a>
                    </li>
                     <li>
                       <a href="/http/paramssign-cn">请求参数防篡改</a>
                    </li>
                    <li>
                       <a href="/http/ip-list-cn">IP 白名单/黑名单</a>
                    </li>
                     <li>
                       <a href="/http/nonrepeatsubmit-cn">防止重复提交数据</a>
                    </li>
                </ul>
                <h3>一般性 Web 校验</h3>
                <ul>
                      <li>
                           <a href="/classic/xss-cn">防止 XSS 跨站攻击</a>
                      </li>
                      <li>
                           <a href="/classic/crlf-cn">防止 CRLF 攻击</a>
                      </li>
                </ul>

                <h3>验证码 Captcha 机制</h3>
                <ul>
                    <li><a href="/captcha/img-captcha-cn">图片验证码</a></li>
                    <li><a href="/captcha/google-cn">基于 Google 的验证码</a></li>
                    <li><a href="/captcha/cf-cn">基于 CloudFlare 的验证码</a></li>
                </ul>
                <h3>HTTP 标准认证</h3>
                <ul>
                    <li><a href="/auth/http-basic-auth-cn">HTTP Basic Auth 认证</a></li>
                    <li><a href="/auth/http-digest-auth-cn">HTTP Digest Auth 认证</a></li>
                </ul>
                <h3>API 接口功能</h3>
                <ul>
                    <li><a href="/api/limit-cn">限流限次数</a></li>
                </ul>
                <h3>其他实用功能</h3>
                <ul>
                    <li><a href="/misc/desensitize-cn">实体字段脱敏</a></li>
                    <li><a href="/misc/encryption-api-cn">API 接口加解密</a></li>
                    <li><a href="/misc/trace-id-cn">链路跟踪记录</a></li>
                </ul>
            </menu>
            <article>
                <h1>API 接口加密/解密</h1>
<p>为了安全性需要对接口的数据进行加密处理,不能明文暴露数据。为此应该对接口进行加密/解密处理,对于接口的行为,分别有:</p>
<ul>
<li>入参,对传过来的加密参数解密。接口处理客户端提交的参数时候,这里统一约定对 HTTP Raw Body 提交的数据(已加密的密文),转换为
JSON 处理,这是最常见的提交方式。其他 QueryString、标准 Form、HTTP Header 的入参则不支持。</li>
<li>出参,对返回值进行加密。接口统一返回加密后的 JSON 结果。</li>
</ul>
<p>有人把加密结果原文输出,如下图所示:</p>
<p><img src="/asset/imgs/api-encode.png" alt="">
但笔者觉得那是一种反模式,而保留原有 JSON 结构更好,如下提交的 JSON。</p>
<pre><code class="language-json">{
    &quot;errCode&quot;: &quot;0&quot;,
    &quot;data&quot;: &quot;BQduoGH4PI+6jxgu+6S2FWu5c/vHd+041ITnCH9JulUKpPX8BvRTvBNYfP7……&quot;
}
</code></pre>
<p>另外也符合既有的统一返回结果,即把<code>data</code>数据加密,其他<code>code</code><code>msg</code>等的正常显示。</p>
<p>系统要求:只支持 Spring + Jackson 的方案。</p>
<h2>加密算法</h2>
<p>加密算法需要调用方(如浏览器)与 API 接口协商好。一般采用 RSA 加密算法。虽然 RSA 没 AES 速度高,但胜在是非对称加密,AES
这种对称加密机制在这场合就不适用了(因为浏览器是不能放置任何密钥的,——除非放置非对称的公钥)。</p>
<p>当然,如果你设计的 API 接口给其他第三方调用而不是浏览器,可以保证密钥安全的话,那么使用 AES
也可以,包括其他摘要算法同理亦可,大家商定好算法(md5/sha1/sha256……)和盐值(Slat)即可。</p>
<p>该组件当前仅支持 RSA(1024bit key)。下面更多的算法在路上。</p>
<ul>
<li>RSA(512/2048……)</li>
<li>AES</li>
<li>MD5/SHA1/SHA256…… with Slat</li>
</ul>
<h1>使用方式</h1>
<h2>初始化</h2>
<p>在 YAML 配置中加入:</p>
<pre><code class="language-yaml">api:
  EncryptedBody:
    enable: true
    publicKey: MIGfMA0GCSqGSIb3DQEBAQUAA4GNADCBiQKBgQCmkKluNutOWGmAK2U……
    privateKey: MIICdgIBADANBgkqhkiG9w0BAQ……
</code></pre>
<p>主要是 RSA 的公钥/私钥。然后在 Spring 配置类<code>WebMvcConfigurer</code>中加入:</p>
<pre><code class="language-java">@Value(&quot;${api.EncryptedBody.publicKey}&quot;)
private String apiPublicKey;

@Value(&quot;${api.EncryptedBody.privateKey}&quot;)
private String apiPrivateKey;

@Value(&quot;${api.EncryptedBody.enable}&quot;)
private boolean apiEncryptedBodyEnable;

@Override
public void configureMessageConverters(List&lt;HttpMessageConverter&lt;?&gt;&gt; converters) {
    EncryptedBodyConverter converter = new EncryptedBodyConverter(apiPublicKey, apiPrivateKey);
    converter.setEnabled(apiEncryptedBodyEnable);

    converters.add(0, converter);
}
</code></pre>
<h2>配置要加密的数据</h2>
<p>使用方式很简单,其实就是添加一个 Java 注解<code>@EncryptedData</code>到你的 Java Bean 上即可。</p>
<p>不过我们还是按照正儿八经的循序渐进的方式去看看。首先是解密请求的数据,我们观察这个 Spring MVC 接口声明,与一般的 JSON
提交数据方式无异,添加了注解<code>@RequestBody</code>,其他无须修改:</p>
<pre><code class="language-java">@PostMapping(&quot;/submit&quot;)
boolean jsonSubmit(@RequestBody User user);
</code></pre>
<p>重点是 User 这个 DTO,为了标明是加密数据,需要在这个 Bean 上声明我们自定义的注解<code>@EncryptedData</code></p>
<pre><code class="language-java">package com.ajaxjs.api.encryptedbody;

@EncryptedData
public class User {
    private String name;
    private int age;

    // Getters and Setters
}
</code></pre>
<p>同时我们在客户端提交的对象不再是 User 的 JSON,而是<code>DecodeDTO</code>(虽然最终转换为<code>User</code>,成功解密的话),即:</p>
<pre><code class="language-java">package com.ajaxjs.api.encryptedbody;

import lombok.Data;

@Data
public class DecodeDTO {
    /**
     * Encrypted data
     */
    private String data;
}
</code></pre>
<p>当然你可以修改这个 DTO 为你符合的结构。一般情况下提交的样子就是像:</p>
<pre><code class="language-json">{
    &quot;data&quot;: &quot;BQduoGH4PI+6jxgu+6S2FWu5c/vHd+041ITnCH9JulUKpPX8BvRTvBNYfP7……&quot;
}
</code></pre>
<p>这个加密过的密文怎么来的?当然是你客户端加密后的结果。或者从下面小节说的方式,返回一段密文。</p>
<h2>返回加密的数据</h2>
<p>下面 Controller 方法返回一个 User 对象,没有任何修改。</p>
<pre><code class="language-java">@GetMapping(&quot;/user&quot;)
User User();

……

@Override
public User User() {
    User user = new User();
    user.setAge(1);
    user.setName(&quot;tom&quot;);
    
    return user;
}
</code></pre>
<p>我们同样需要加一个注解<code>@EncryptedData</code>即可对其加密。当前版本中暂不支持字段级别的加密,只支持整个对象加密。</p>
<p>返回结果如下:</p>
<pre><code class="language-json">{
    &quot;status&quot;: 1,
    &quot;errorCode&quot;: null,
    &quot;message&quot;: &quot;操作成功&quot;,
    &quot;data&quot;: &quot;ReSSPC34JE+O/SmLCxE5zVJb6D2tzp1f5pfQyKdjvOWkQQ+qDjcjw/2m/KPA+2+uc9kseqFryXNPIZCEfsaOCJAqzMtrXyZ0JPB1skeJxKOngS5USijsY0UZqN9hLS3O/7CBLlSGkEuyXZV//WcWDG9BpQ4TAKrlRfwM4bnCo+E=&quot;
}
</code></pre>
<h2>添加依赖</h2>
<p>哦~对了,别忘了添加依赖,——没单独搞 jar 包,直接 copy 代码吧~
才三个类:<a href="https://gitcode.com/zhangxin09/aj-framework/tree/master/aj-framework/src/main/java/com/ajaxjs/api/encryptedbody">源码</a></p>
<p>其中<code>ResponseResultWrapper</code>就是统一返回结果的类,你可以改为你项目的,——其他的没啥依赖了,——还有就是 RSA 依赖我的工具包:</p>
<pre><code class="language-xml">&lt;dependency&gt;
    &lt;groupId&gt;com.ajaxjs&lt;/groupId&gt;
    &lt;artifactId&gt;ajaxjs-util&lt;/artifactId&gt;
    &lt;version&gt;1.1.8&lt;/version&gt;
&lt;/dependency&gt;
</code></pre>
<p>很小巧的,才60kb 的 jar 包——请放心食用~</p>
<h1>实现方式</h1>
<p>这里说说实现原理,以及一些 API 设计风格的思考。</p>
<p>我们这种的用法,相当于接收了 A 对象(加密的,<code>DecodeDTO</code>),转换为 B 对象(解密的,供控制器使用)。最简单的方式就是这样的:</p>
<pre><code class="language-java">@PostMapping(&quot;/submit&quot;)
boolean jsonSubmit(@RequestBody DecodeDTO dto) {
    User user = 转换函数(dto.getData());
}
</code></pre>
<p>但是这种方法,方法数量一多则遍地<code>DecodeDTO</code>,API
文档也没法写了(破坏了代码清晰度,不能反映原来代码的意图)。为此我们应该尽量采用“非入侵”的方法,所谓非入侵,就是不修改原有的代码,只做额外的“装饰”。这种手段有很多,典型如
AOP,其他同类的开源库 <a href="https://github.com/ishuibo/rsa-encrypt-body-spring-boot">rsa-encrypt-body-spring-boot</a><a href="https://github.com/Licoy/encrypt-body-spring-boot-starter">encrypt-body-spring-boot-starter</a>
也是不约而同地使用 AOP。</p>
<p>然而笔者个人来说不太喜欢 AOP,可能也是不够熟悉吧——反正能不用则不用。如果不用 AOP 那应该如何做呢?笔者思考了几种方式例如
Filter、拦截器等,但最终把这个问题定位于 JSON 序列化/反序列化层面上,在执行这一步骤之前就可以做加密/解密操作了。开始以为可以修改
Jackson 全局序列化方式,但碍于全局的话感觉不太合理,更合适的是在介乎于 Spring 与 Jackson
结合的地方做修改。于是有了在的<code>MappingJackson2HttpMessageConverter</code>基础上扩展的 <code>EncryptedBodyConverter</code>,重写了<code>read</code>
方法,在反序列化之前先做解密操作,<code>writeInternal</code>方法亦然。</p>
<p>核心方法就一个类,不足一百行代码:</p>
<pre><code class="language-java">import com.ajaxjs.springboot.ResponseResultWrapper;
import com.ajaxjs.util.EncodeTools;
import com.ajaxjs.util.cryptography.RsaCrypto;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.http.HttpInputMessage;
import org.springframework.http.HttpOutputMessage;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.http.converter.HttpMessageNotWritableException;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;

import java.io.IOException;
import java.lang.reflect.Type;

public class EncryptedBodyConverter extends MappingJackson2HttpMessageConverter {
    public EncryptedBodyConverter(String publicKey, String privateKey) {
        super();
        this.publicKey = publicKey;
        this.privateKey = privateKey;
    }

    private final String publicKey;

    private final String privateKey;

    /**
     * 使用私钥解密字符串
     *
     * @param encryptBody 经过 Base64 编码的加密字符串
     * @param privateKey  私钥字符串,用于解密
     * @return 解密后的字符串
     */
    static String decrypt(String encryptBody, String privateKey) {
        byte[] data = EncodeTools.base64Decode(encryptBody);

        return new String(RsaCrypto.decryptByPrivateKey(data, privateKey));
    }

    /**
     * 使用公钥加密字符串
     * &lt;p&gt;
     * 该方法采用RSA加密算法,使用给定的公钥对一段字符串进行加密
     * 加密后的字节数组被转换为 Base64 编码的字符串,以便于传输和存储
     *
     * @param body      需要加密的原始字符串
     * @param publicKey 用于加密的公钥字符串
     * @return 加密后的 Base64 编码字符串
     */
    static String encrypt(String body, String publicKey) {
        byte[] encWord = RsaCrypto.encryptByPublicKey(body.getBytes(), publicKey);
        return EncodeTools.base64EncodeToString(encWord);
    }

    /**
     * 重写 read 方法以支持加密数据的读取
     *
     * @param type         数据类型,用于确定返回对象的类型
     * @param contextClass 上下文类,未在本方法中使用
     * @param inputMessage 包含加密数据的 HTTP 输入消息
     * @return 根据类型参数反序列化后的对象实例
     * @throws IOException                     如果读取或解析过程中发生 I/O 错误
     * @throws HttpMessageNotReadableException 如果消息无法解析为对象实例
     */
    @Override
    public Object read(Type type, Class&lt;?&gt; contextClass, HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
        Class&lt;?&gt; clz = (Class&lt;?&gt;) type;

        if (clz.getAnnotation(EncryptedData.class) != null) {
            ObjectMapper objectMapper = getObjectMapper();
            DecodeDTO decodeDTO = objectMapper.readValue(inputMessage.getBody(), DecodeDTO.class);
            String encryptBody = decodeDTO.getData();

            String decodeJson = decrypt(encryptBody, privateKey);

            return objectMapper.readValue(decodeJson, clz);
        }

        return super.read(type, contextClass, inputMessage);
    }

    @Override
    protected void writeInternal(Object object, Type type, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
        Class&lt;?&gt; clz = (Class&lt;?&gt;) type;

        if (object instanceof ResponseResultWrapper &amp;&amp; clz.getAnnotation(EncryptedData.class) != null) {
            ResponseResultWrapper response = (ResponseResultWrapper) object;
            Object data = response.getData();
            String json = getObjectMapper().writeValueAsString(data);
            String encryptBody = encrypt(json, publicKey);

            response.setData(encryptBody);
        }

        super.writeInternal(object, type, outputMessage);
    }
}
</code></pre>
<p>TODO</p>
<ul>
<li>增加加密解密算法</li>
<li>增加一个加密选项,说明使用公钥还是私钥。当前是公钥</li>
<li>针对字段单独的解密</li>
</ul>
<p>哈哈 最后发现也有人用<code>HttpMessageConverter</code>来做:https://blog.allbs.cn/posts/49043/。</p>

            </article>
        </div>
        <footer>
             AJ Security,开源框架 <a href="https://framework.ajaxjs.com" target="_blank">AJ-Framework</a> 的一部分。联系方式:
             frank@ajaxjs.com,<a href="https://blog.csdn.net/zhangxin09" target="_blank">作者博客</a>
             <br />
             <br />
             Copyright © 2025 Frank Cheung. All rights reserved.
         </footer>
    </body>
</html>