99%的焦虑都来自于虚度时间和没有好好做事,所以唯一的解决办法就是行动起来,认真做完事情,战胜焦虑,战胜那些心里空荡荡的时刻,而不是选择逃避。不要站在原地想象困难,行动永远是改变现状的最佳方式
写在前面
- 工作中遇到,简单整理
- 博文内容涉及 Web接口内容 类似
https
的加密和防篡改校验
- 以及具体Java Springboot 项目中如何编码。
- 理解不足小伙伴帮忙指正 :),生活加油
99%的焦虑都来自于虚度时间和没有好好做事,所以唯一的解决办法就是行动起来,认真做完事情,战胜焦虑,战胜那些心里空荡荡的时刻,而不是选择逃避。不要站在原地想象困难,行动永远是改变现状的最佳方式
持续分享技术干货,感兴趣小伙伴可以关注下 ^_^
在讲这部分内容之前,先看几个问题:
Q: 有了 https
为什么还需要接口 RSA+AES 加密+HMAC 校验
?
A:https
是通信加密
,而 接口的 RSA+AES
加密+ HMAC
校验 属于内容加密
,HTTPS 加密的是传输过程中的数据,确保数据在客户端和服务器之间传输时不会被窃听或篡改。而接口加密是对接口内容的加密,即报文实体加密
Q:用了 https
做通信加密,为什么本地抓包或者说浏览器还是可以看到报文内容 ?
A:浏览器是客户端,看的时候已经发生的解密,抓包工具利用根证书伪造来解密报文。
Q:那么为什么解密不发生在代码层面,而且在客户端就解密了? 这样就可以避免抓包工具解密了
A:换一种角度考虑,在客户端和服务端属于同一级的高信任区域,因为被信任所以可以看到报文数据,而客户端和服务端之间的链路属于低信任区域,所以加密。既解密和加密是对同一信任度的区域而言。加密和解密发生在由低信任度到高信任度之间
,是对称的,在服务端高信任区域加密数据到链路的低信任区域,所以同样需要在链路的低信任区域到客户端的高信任区域解密
。所以不在代码层面解密,因为加密发生着服务器端的同级信任区。
Q: 如果希望在代码层面解密,应该如何处理,即为什么需要做内容加密?
A: 通过 https
实现了通信加密,但是对于客户端本地来讲,还是可以利用浏览器或者抓包工具获取实际的报文数据,为了避免敏感数据的泄露,把解密控制在代码层面,我们就需要在 客户端和服务端的信任区域上面在加高一级别信任度的信任区域,这个区域就是代码级别的信任区,之前的 https 对全部报文做了加密,现在我们只对交互的报文实体做加密,这也就是内容加密,所以内容加密是为了数据到达客户端(如浏览器)
后会被解密出实际响应报文
后响应实体
仍然处于加密状态,防止通过浏览器或者抓包工具直接获取数据。
通过上面的问题,我们可以对内容加密有一定的认知。下面我们看一下如何对内容加密
接口内容加密方案简单介绍
先来简单看下 https 的加密原理:
https
实际上是 http + SSL/TSL(加密认证,防篡改) = https
是在 HTTP 协议的基础上,通过 SSL/TLS 协议对数据进行加密和认证,确保通信的安全性和完整性。
SSL/TLS 的核心功能 SSL/TLS 提供了以下核心功能:
1. 加密:对传输的数据进行加密,防止窃听。
2. 认证:验证服务器的身份,防止中间人攻击。
3. 完整性:确保数据在传输过程中未被篡改。
HTTPS 的工作流程: HTTPS 的加密原理主要依赖于 SSL/TLS
协议,其工作流程如下:
- 建立 TCP 连接: 客户端与服务器通过 TCP 三次握手建立连接。
- TLS 握手:
- 客户端 Hello:客户端发送支持的 TLS 版本、加密套件列表和一个随机数。
- 服务器 Hello:服务器选择 TLS 版本、加密套件,并返回自己的随机数和证书(包含公钥)。
- 证书验证:客户端验证服务器证书的有效性(是否由可信 CA 签发,是否过期等),防止中间人攻击
- 密钥交换:客户端生成一个
预主密钥(Pre-Master Secret)
,用服务器的公钥
非对称加密后发送给服务器。
- 生成会话密钥:客户端和服务器使用预主密钥和随机数生成
对称加密密钥(Session Key)
,用于后续通信。
加密通信
- 客户端和服务器使用对称加密密钥对 HTTP 数据进行加密和解密。
- 每次通信都会使用 HMAC 或 AEAD 模式验证数据的完整性。
HTTP 像寄明信片,内容公开,容易被偷看或篡改。HTTPS 像寄加密信件,内容被锁在保险箱里,只有收件人有钥匙打开,确保安全性和完整性。 而我们要做的内容加密是在 HTTPS 的基础上,对明信片上面的内容进行加密处理,收件人用钥匙打开之后,明信片上面是密文,还需要用约定的密码来解密出明文
这里的内容加密
也使用上面 https 加密的方案,当然还有其他的方案,不同的是 CA证书的获取和认证,即从获取非对称加密的公钥开始,所有的加解密是发生的代码层级的。
常见的加密方案(RSA + AES + HMAC
TLS 1.2)
对称加密(如 AES)
:加密和解密使用相同的密钥,速度快,但密钥分发不安全,用于加密实际传输的数据,保证高效性。
非对称加密(如 RSA)
:加密和解密使用不同的密钥,安全性高,但速度慢,用于加密 AES 密钥,解决密钥分发问题
消息认证码(如 HMAC)
:用于验证数据的完整性和真实性,生成签名。
对于一个完整的接口内容加密流程
客户端请求,加密报文过程:
密钥生成,类似 SSL
1 2 3 4
| 1. 获取非对称的公钥: MIIBIjAN................kqUXgQntOo3HOuzW9pqwIDAQAB 2. 生成使用的对称的密钥: buLZ...CsBsEcd 3. 通过生成的对称密钥对请求报文进行加密:bodySt......14g== 4. 对称密钥通过非对称公钥加密生成传输的密钥: NDI3q..................p86SyQ==
|
签名的生成,这里使用的是 HMAC ,也可以考虑使用 AEAD
1 2 3 4 5 6 7 8
| 签名需要的数据 ================================== message : 接口类型: POST 接口地址:/hotel/web/threePartyI。。。。。。。。。。。nfoAnonymous 对称密钥通过非对称公钥加密后的密钥(X-Secret 报文头): NDI3qtS...................6SyQ== 随机字符串(X-Nonce 报文头):M2jmJm6Yo9 时间戳(X-Timestamp 报文头):1738897905 请求报文对称加密后的密文哈希值: 6fb2a0229959e25706a7fc50b888f82dbe8688bc
|
上面的签名数据组合在通过哈希算法和对称密钥做种子生成签名
1
| 生成的签名(X-Signature 报文头) signature:cdd5bab8e.......73d61753d546b
|
调用接口:
1 2 3 4
| 11:11:46.081 [main] INFO ......- crm url:https:.....nymous method: POST body: {"gCertNo":"220882199608126526","gMobile":"18147405370"} 实际的请求报文:bodyStr{"gCertNo":"220882199608126526","gMobile":"18147405370"} 加密后的报文 bodyStroZtulHZvJrRN2sfBs6MvLOCRaLbIh8jgpGHIsE9DFwHBGGp0vuNylE/OOAeo6pYGRP/kkpL8DZPXOMapTbX14g== .......................................
|
服务端收到报文,对请求报文做解密处理,同时对响应报文做加密处理
- 加载本地的非对称加密的私钥
- 判断报文头数据是否存在,同时从报文头获取需要的数据(X-Secret,X-Nonce,X-Timestamp,X-Signature)
- 判断时间戳是否符合要求
- 通过非对称加密的私钥对对称加密的密钥进行解密,获取对称加密的密钥
HMAC
校验,重新生成签名判断是否一致
- 解密请求报文给后端接口处理
- 接口处理完成返回响应,通过 对称加密的密钥对响应报文进行加密
1 2 3 4
| 11:11:46.433 [main] DEBUG org.springframework.web.client.RestTemplate - Response 200 OK 11:11:46.434 [main] DEBUG org.springframework.web.client.RestTemplate - Reading to [java.lang.String] as "application/json;charset=UTF-8" ================ 返回的消息:<200,auePdYEQfYaQgBt3RPMOGLXTxXxO72t8nSk6CQ5aqKXzZ+GWXoxml7pv9+OhwlAE,[vary:"Origin,Access-Control-Request-Method,Access-Control-Request-Headers", x-content-type-options:"nosniff", x-xss-protection:"1; mode=block", strict-transport-security:"max-age=31536000 ; includeSubDomains", x-frame-options:"SAMEORIGIN", content-type:"application/json;charset=UTF-8", content-length:"64", date:"Fri, 07 Feb 2025 03:11:46 GMT", x-envoy-upstream-service-time:"17", server:"istio-envoy"]>
|
客户端收到响应报文,通过上面请求报文生成的对称加密密钥对响应报文进行解密处理,获取实际的响应报文
1 2
| 返回的报文:auePdYEQfYaQgBt......Xoxml7pv9+OhwlAE 解密后的报文=>>>{"msg":"......","code":200}
|
代码实现
下面是一个 SpringBoot
项目的接口加密实现服务端的编码
- 在
Spring-Security
添加对应的过滤器,用于处理服务端的请求解密,响应加密:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16
|
@Autowired private SecurityFilter securityFilter;
..................... .addFilterBefore(authenticationTokenFilter, UsernamePasswordAuthenticationFilter.class) .addFilterBefore(corsFilter, JwtAuthenticationTokenFilter.class) .addFilterBefore(corsFilter, LogoutFilter.class) .addFilterBefore(securityFilter, JwtAuthenticationTokenFilter.class); 。。。。。。。。
|
- 处理请求报文的解密,响应报文的加密
过滤器方法,这里利用 Java Web 的过滤器链,doFilterInternal
为核心的方法,用于对请求的报文进行解密,然后给传递给其他的过滤器,最后到实际的路由地址,处理完请求的返回响应在对响应报文进行加密给客户端返回数据。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79
|
@SneakyThrows @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) { String requestUri = request.getRequestURI(); if (!requestUri.startsWith(API_PATH_PATTERN)) { filterChain.doFilter(request, response); return; } String bodyStr = getRequestBody(request); String secretHeader = request.getHeader("X-Secret"); String nonceHeader = request.getHeader("X-Nonce"); String timestampHeader = request.getHeader("X-Timestamp"); String signatureHeader = request.getHeader("X-Signature"); if (secretHeader == null || nonceHeader == null || timestampHeader == null || signatureHeader == null) { log.error("请求报文不符合要求!"); response.sendError(HttpStatus.BAD_REQUEST.value(), "signature verification error"); return; } PrivateKey privateKey = loadPrivateKey(privateKeyStr); System.out.println("================= 加密的密钥:"+ secretHeader); String decrypt = RSAFacade.decrypt(CipherTypeEnums.RSA_PKCS1, secretHeader, privateKey); System.out.println("================= 解密的密钥:"+ decrypt); long timestamp = Long.parseLong(timestampHeader); long currTimestamp = Instant.now().getEpochSecond(); if (currTimestamp - timestamp > REQUEST_EXPIRY_TIME) { log.error("时间戳异常!"); response.sendError(HttpStatus.BAD_REQUEST.value(), "expired request"); return; } String message = buildMessage(request, secretHeader, nonceHeader, timestampHeader,bodyStr); System.out.println("=========================: 消息"+ message); String computedSignature = null; computedSignature = toHMAC_SHA256( message,decrypt); System.out.println("========================== 生成的签名:" + computedSignature); if (!computedSignature.equals(signatureHeader)) { log.error("签名不一致!"); response.sendError(HttpStatus.BAD_REQUEST.value(), "signature verification error"); return; } System.out.println("================加密的报文数据 requestBody: " + bodyStr); String encryptedBody = null; AES aes = AES.getInstance(CipherTypeEnums.AES_CBC_PKCS7, decrypt, decrypt); encryptedBody = aes.decrypt(bodyStr); System.out.println("===================== 获取到的报文数据:"+ encryptedBody); CustomHttpServletRequestWrapper wrappedRequest = new CustomHttpServletRequestWrapper(request, encryptedBody.getBytes()); EncryptingResponseWrapper wrappedResponse = new EncryptingResponseWrapper(response); filterChain.doFilter(wrappedRequest, wrappedResponse); String string = wrappedResponse.getResponseData(); System.out.println("====================返回的报文数据:" + string); bodyStr = aes.encrypt(string); System.out.println("====================返回的加密报文数据:" + bodyStr); response.getOutputStream().write(bodyStr.getBytes(StandardCharsets.UTF_8)); }
|
需要注意的问题:
request.getInputStream()
只能读一次,并且在过滤器链里面使用,处理完请求报文,还要在塞回去。
- 请求报文和响应报文的二次封装通过内部类
EncryptingResponseWrapper
和 CustomHttpServletRequestWrapper
实现
- 对于加密和解密使用的RSA,AES以及计算哈希值的算法,服务端和客户端要保证使用一致的密钥格式,即 加密算法(RSA, AES),加密模式(ECB,CBC等),加密补码方式(PKCS1_PADDING, PKCS5_PADDING) 要保持一致,开发中出现的加解密错误大都是这里的问题。
下面为过滤器中处理加密解密完整的代码. 关于算法部分这里没有展示
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 380 381 382 383 384 385 386 387 388 389 390 391
| import com.ruoyi.framework.security.filter.tool.AES; import com.ruoyi.framework.security.filter.tool.CipherTypeEnums; import com.ruoyi.framework.security.filter.tool.RSAFacade; import lombok.SneakyThrows; import lombok.extern.slf4j.Slf4j; import org.apache.commons.codec.binary.Hex; import org.springframework.http.HttpStatus; import org.springframework.stereotype.Component; import org.springframework.web.filter.OncePerRequestFilter;
import javax.crypto.Cipher; import javax.crypto.Mac; import javax.crypto.spec.SecretKeySpec; import javax.servlet.*; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletRequestWrapper; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpServletResponseWrapper; import java.io.*; import java.nio.charset.StandardCharsets; import java.security.*; import java.time.Instant;
@Component @Slf4j public class SecurityFilter extends OncePerRequestFilter {
private static final String API_PATH_PATTERN = "/hot......eePartyInterfaceCRM/";
private static final String RSA_PRIVATE_KEY = "MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQDaEFdIynY8WbH8" + "...................0W";
private static final long REQUEST_EXPIRY_TIME = 30;
@SneakyThrows @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) {
String requestUri = request.getRequestURI(); String bodyStr = getRequestBody(request);
if (!requestUri.startsWith(API_PATH_PATTERN)) { filterChain.doFilter(request, response); return; }
String secretHeader = request.getHeader("X-Secret"); String nonceHeader = request.getHeader("X-Nonce"); String timestampHeader = request.getHeader("X-Timestamp"); String signatureHeader = request.getHeader("X-Signature");
if (secretHeader == null || nonceHeader == null || timestampHeader == null || signatureHeader == null) { log.error("请求报文不符合要求!"); response.sendError(HttpStatus.BAD_REQUEST.value(), "signature verification error"); return; } PrivateKey privateKey = loadPrivateKey(RSA_PRIVATE_KEY); System.out.println("================= 加密的密钥:"+ secretHeader); String decrypt = RSAFacade.decrypt(CipherTypeEnums.RSA_PKCS1, secretHeader, privateKey); System.out.println("================= 解密的密钥:"+ decrypt);
long timestamp = Long.parseLong(timestampHeader); long currTimestamp = Instant.now().getEpochSecond();
if (currTimestamp - timestamp > REQUEST_EXPIRY_TIME) { log.error("时间戳异常!"); response.sendError(HttpStatus.BAD_REQUEST.value(), "expired request"); return; }
String message = buildMessage(request, secretHeader, nonceHeader, timestampHeader,bodyStr); System.out.println("=========================: 消息"+ message); String computedSignature = null; computedSignature = toHMAC_SHA256( message,decrypt); System.out.println("========================== 生成的签名:" + computedSignature);
if (!computedSignature.equals(signatureHeader)) { log.error("签名不一致!"); response.sendError(HttpStatus.BAD_REQUEST.value(), "signature verification error"); return; }
System.out.println("================加密的报文数据 requestBody: " + bodyStr); String encryptedBody = null; AES aes = AES.getInstance(CipherTypeEnums.AES_CBC_PKCS7, decrypt, decrypt); encryptedBody = aes.decrypt(bodyStr); System.out.println("===================== 获取到的报文数据:"+ encryptedBody);
CustomHttpServletRequestWrapper wrappedRequest = new CustomHttpServletRequestWrapper(request, encryptedBody.getBytes()); EncryptingResponseWrapper wrappedResponse = new EncryptingResponseWrapper(response);
filterChain.doFilter(wrappedRequest, wrappedResponse);
String string = wrappedResponse.getResponseData();
System.out.println("====================返回的报文数据:" + string); bodyStr = aes.encrypt(string); System.out.println("====================返回的加密报文数据:" + bodyStr); response.getOutputStream().write(bodyStr.getBytes(StandardCharsets.UTF_8)); }
private PrivateKey loadPrivateKey(String privateKeyString) throws Exception { System.out.println("==============================加载的私钥:"+ privateKeyString ); return RSAFacade.getPrivateKey(CipherTypeEnums.RSA_PKCS1, privateKeyString);
}
private String buildMessage(HttpServletRequest request, String secret, String nonce, String timestamp, String bodyStr) throws IOException, NoSuchAlgorithmException { String method = request.getMethod();
String url = request.getRequestURI().toString(); String bodyHash = getBodyHash(bodyStr);
return method + "\n" + url + "\n" + secret + "\n" + nonce + "\n" + timestamp + "\n" + bodyHash + "\n"; }
private String getBodyHash(String request) throws IOException, NoSuchAlgorithmException { MessageDigest md = MessageDigest.getInstance("SHA-1"); md.update(request.getBytes()); byte[] hash = md.digest(); return Hex.encodeHexString(hash); }
private String toHMAC_SHA256(String str, String key) throws Exception { byte[] secret = key.getBytes(StandardCharsets.UTF_8); SecretKeySpec secretKey = new SecretKeySpec(secret, "HmacSHA256"); Mac mac = Mac.getInstance(secretKey.getAlgorithm()); mac.init(secretKey); byte[] macData = mac.doFinal(str.getBytes(StandardCharsets.UTF_8)); byte[] hex = (new Hex()).encode(macData); return new String(hex, StandardCharsets.UTF_8); }
public static String getRequestBody(HttpServletRequest request) throws IOException { StringBuilder stringBuilder = new StringBuilder(); ServletInputStream inputStream = null; BufferedReader bufferedReader = null;
try { inputStream = request.getInputStream(); bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
char[] charBuffer = new char[128]; int bytesRead;
while ((bytesRead = bufferedReader.read(charBuffer)) != -1) { stringBuilder.append(charBuffer, 0, bytesRead); } } finally { if (bufferedReader != null) { bufferedReader.close(); } if (inputStream != null) { inputStream.close(); } }
return stringBuilder.toString(); }
private static class EncryptingResponseWrapper extends HttpServletResponseWrapper { private ByteArrayOutputStream outputStream = new ByteArrayOutputStream(); private PrintWriter writer = new PrintWriter(outputStream);
public EncryptingResponseWrapper(HttpServletResponse response) { super(response); }
@Override public ServletOutputStream getOutputStream() throws IOException { return new EncryptingServletOutputStream(outputStream); }
@Override public PrintWriter getWriter() throws IOException { return writer; }
public String getResponseData() throws IOException { writer.flush(); return outputStream.toString(StandardCharsets.UTF_8.name()); } }
private static class EncryptingServletOutputStream extends ServletOutputStream { private final ByteArrayOutputStream byteArrayOutputStream;
public EncryptingServletOutputStream(ByteArrayOutputStream byteArrayOutputStream) { this.byteArrayOutputStream = byteArrayOutputStream; }
@Override public void write(int b) throws IOException { byteArrayOutputStream.write(b); }
@Override public void write(byte[] b) throws IOException { byteArrayOutputStream.write(b); }
@Override public void write(byte[] b, int off, int len) throws IOException { byteArrayOutputStream.write(b, off, len); }
@Override public void flush() throws IOException { byteArrayOutputStream.flush(); }
@Override public void close() throws IOException { byteArrayOutputStream.close(); }
@Override public boolean isReady() { return false; }
@Override public void setWriteListener(WriteListener writeListener) {
} }
public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private final ServletInputStream inputStream;
public CustomHttpServletRequestWrapper(HttpServletRequest request, byte[] body) throws IOException { super(request); this.inputStream = new ByteArrayServletInputStream(body); }
@Override public ServletInputStream getInputStream() throws IOException { return inputStream; }
}
public class ByteArrayServletInputStream extends ServletInputStream {
private final ByteArrayInputStream byteArrayInputStream;
public ByteArrayServletInputStream(byte[] data) { this.byteArrayInputStream = new ByteArrayInputStream(data); }
@Override public int read() throws IOException { return byteArrayInputStream.read(); }
@Override public int read(byte[] b) throws IOException { return byteArrayInputStream.read(b); }
@Override public long skip(long n) throws IOException { return byteArrayInputStream.skip(n); }
@Override public int available() throws IOException { return byteArrayInputStream.available(); }
@Override public void close() throws IOException { byteArrayInputStream.close(); }
@Override public synchronized void mark(int readlimit) { byteArrayInputStream.mark(readlimit); }
@Override public synchronized void reset() throws IOException { byteArrayInputStream.reset(); }
@Override public boolean markSupported() { return byteArrayInputStream.markSupported(); }
@Override public boolean isFinished() { return false; }
@Override public boolean isReady() { return false; }
@Override public void setReadListener(ReadListener readListener) {
} }
}
|
博文部分内容参考
© 文中涉及参考链接内容版权归原作者所有,如有侵权请告知 :)
© 2018-至今 liruilonger@gmail.com, 保持署名-非商用-相同方式共享(CC BY-NC-SA 4.0)