Franz`s blog

接口AES+RSA加密实践

对于需要加密的接口使用aes+rsa加密

首先需要明确aes是一种对称加密算法,rsa是一种非对称加密算法。对于这两种加密算法的概念可以自行了解。

rsa加密的作用是为了加密aes密钥

这里提到的加密是对于请求数据和接口返回内容都进行加密

基本流程

  1. 创建一个rsa密钥对
  2. 前端生成aes密钥
  3. 前端获取生成的rsa公钥
  4. 使用rsa公钥加密aes密钥
  5. 当前端请求加密接口时使用aes密钥加密请求数据,并且把rsa加密后的aes密钥放在请求头Sign的位置。
  6. 后端解密aes密钥,再使用解密后的aes密钥解密请求数据
  7. 后端使用aes密钥加密后返回数据
  8. 前端收到数据,使用aes密钥解密

前端代码

封装aes加解密过程

使用crypto-js进行aes加解密

jsencrypt进行rsa的加解密

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
import CryptoJS from "crypto-js";
import JSEncrypt from "jsencrypt";

const iv = CryptoJS.enc.Utf8.parse("4382822409223508");

export function Encrypt(data: string, k: string): string {
return CryptoJS.AES.encrypt(
CryptoJS.enc.Utf8.parse(data),
CryptoJS.enc.Utf8.parse(k),
{
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
}
).toString()
}
export function Decrypt(data: string, k: string): string {
return CryptoJS.enc.Utf8.stringify(CryptoJS.AES.decrypt(
data,
CryptoJS.enc.Utf8.parse(k),
{
iv: iv,
mode: CryptoJS.mode.CBC,
padding: CryptoJS.pad.Pkcs7,
}
))
}

这里面的iv为混淆向量,可以自己随意更改,但是要跟后端保持一致

注意这里使用的aes模式是CBC/Pkcs7模式

获取rsa公钥,生成本地aes密钥

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
export async function initEnc() {
if (useKeyStore().getKey().aesKey.length > 0) {
return true;
}
if (localStorage.getItem("e")) {
let { aesKey, rsaPublicKey, encAesKey } = JSON.parse(localStorage.getItem("e") as string)
useKeyStore().setKey(aesKey, rsaPublicKey, encAesKey)
return true;
}
let aesKey = generateKey()
let rsaPublicKey = // 这里替换成自己获取rsa公钥逻辑
let encrypt = new JSEncrypt()
encrypt.setPublicKey(rsaPublicKey)
let encAesKey = encrypt.encrypt(aesKey)
if (!encAesKey) {
throw new Error("加密失败")
}
//存储到store
useKeyStore().setKey(aesKey, encrypt.getPublicKeyB64(), encAesKey)
// 持久化到localStorage
localStorage.setItem("e", JSON.stringify(useKeyStore().getKey()))
return true;
}

export function generateKey(): string {
return randomString(16);
}
function randomString(length: number): string {
const characters =
"ABCDEFGHJKMNPQRSTWXYZabcdefhijkmnprstwxyz2345678";
const charactersLength = characters.length;
let result = "";
for (let i = 0; i < length; i++) {
result += characters.charAt(Math.floor(Math.random() * charactersLength));
}
return result;
}

在应用初始化的地方调用initEnc方法即可。

这里用到了pinia做状态管理,存储aesKey,加密后的aesKey,rsa公钥

定义如下

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
import { defineStore } from "pinia";

export const useKeyStore = defineStore("keyStore", {
state: () => ({
aesKey:"",
rsaPublicKey:"",
encAesKey:"",
}),
actions: {
setKey(aesKey: string,rsaPublicKey:string,encAesKey:string) {
this.aesKey = aesKey;
this.rsaPublicKey = rsaPublicKey;
this.encAesKey = encAesKey;
},
setKeyObj(p:any) {
this.aesKey = p["aesKey"];
this.rsaPublicKey = p["rsaPublicKey"];
this.encAesKey = p["encAesKey"];
},
getKey() {
return this
}
}
})

对于axios进行封装,export一个request函数

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
const request = <ResponseType = unknown>(
url: string,
options?: AxiosRequestConfig,
encReq: boolean = false,
encRes: boolean = false,
): Promise<ResponseType> => {
const aesKey = useKeyStore().getKey().aesKey
const encAesKey = useKeyStore().getKey().encAesKey
if (encReq && options?.data) {
options.data = Encrypt(JSON.stringify(options.data), aesKey)
}
if (encReq || encRes) {
options = options == null ?
{ headers: { 'sign': encAesKey } } :
{ ...options, headers: { ...options.headers, 'sign': encAesKey } }
}
return new Promise((resolve, reject) => {
axiosInstance({
url,
...options
})
.then(
(res: AxiosResponse) => {
if (!res.headers.enc) {
resolve(res.data.data)
return
}
const aesKey = useKeyStore().getKey().aesKey
res.data = JSON.parse(Decrypt(res.data, aesKey))
resolve(res.data.data)
}
)
.catch(
(err) => reject(err)
)
})

}

假设有一个加密接口,只需要这样调用即可

1
2
3
4
5
6
7
8
export async function getXXXXDetail(id: number) {
return request<any>('/XXXX/detail', {
params:{
id
}
},true,true)
}

后端代码

思路主要是使用RequestBodyAdviceAdapter和ResponseBodyAdvice进行aop的解密和加密

对于要加密的接口在方法上标注EncryptController注解

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
@Inherited
@Documented
@Target({ElementType.FIELD, ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptController{

/**
* 传入参数是否加密
*/
boolean requestEncrypt() default false;

/**
* 返回参数是否加密
*/
boolean responseEncrypt() default false;

}

加密接口定义

1
2
3
4
5
@PutMapping
@EncryptController(requestEncrypt = true, responseEncrypt = true)
public Boolean userRegister(@RequestBody UserDto dto) {
return userService.userRegister(dto);
}

请求解密

hutool使用PKCS7时要添加另外的pom依赖

1
2
3
4
5
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk15to18</artifactId>
<version>1.68</version>
</dependency>
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

/**
* @author:Franz Li
* 请求参数解密
*/
@ControllerAdvice
@Slf4j
public class RequestEncryptAdvice extends RequestBodyAdviceAdapter {

@Resource
RSA rsa;

@Resource
RedisTemplate redisTemplate;

@Override
public boolean supports(MethodParameter methodParameter, Type targetType, Class<? extends HttpMessageConverter<?>> converterType) {
EncryptController methodAnnotation = methodParameter.getMethodAnnotation(EncryptController.class);
return methodAnnotation != null && methodAnnotation.requestEncrypt();
}

@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage inputMessage,
MethodParameter parameter,
Type targetType,
Class<? extends HttpMessageConverter<?>> converterType) throws IOException {
if (inputMessage.getHeaders().get(EncConstant.AES_HEADER).size() > 0){
return decryptBody(inputMessage);
}
return inputMessage;
}


private HttpInputMessage decryptBody(HttpInputMessage inputMessage){
// 用rsa解密aesKey
String aesKey = Objects.requireNonNull(inputMessage.getHeaders().get(EncConstant.AES_HEADER)).get(0);
//redis优化
if (Boolean.TRUE.equals(redisTemplate.hasKey(EncConstant.AES_MAP_PREFIX + aesKey))) {
aesKey = (String) redisTemplate.opsForValue().get(EncConstant.AES_MAP_PREFIX + aesKey);
} else {
aesKey = rsa.decryptStr(aesKey, KeyType.PrivateKey);
redisTemplate.opsForValue().set(EncConstant.AES_MAP_PREFIX + aesKey, aesKey);
}
try{
byte[] body = new byte[inputMessage.getBody().available()];
inputMessage.getBody().read(body);
String bodyStr = new String(body);
AES aes = new AES("CBC",
"PKCS7Padding",
aesKey.getBytes(StandardCharsets.UTF_8),
"4382822409223508".getBytes(StandardCharsets.UTF_8));
log.debug("bodyStr:{}", bodyStr);
bodyStr = aes.decryptStr(bodyStr,CharsetUtil.CHARSET_UTF_8);
byte[] decrypt = bodyStr.getBytes(StandardCharsets.UTF_8);
final ByteArrayInputStream bais = new ByteArrayInputStream(decrypt);
return new HttpInputMessage() {
@Override
public InputStream getBody() {
return bais;
}
@Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
};
}catch (Exception e){
log.error("解密失败 {}",e);
throw new BusinessException(50000,"加密参数异常");
}
}

}

响应加密

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

@Order(2)
@RestControllerAdvice
public class ResponseEncryptAdvice implements ResponseBodyAdvice<Object> {

@Resource
RSA rsa;

@Override
public boolean supports(MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
EncryptController methodAnnotation = returnType.getMethodAnnotation(EncryptController.class);
return methodAnnotation != null && methodAnnotation.responseEncrypt();
}

@Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> selectedConverterType,
ServerHttpRequest request,
ServerHttpResponse response) {
response.getHeaders().set("enc","true");
String sign = request.getHeaders().get(EncConstant.AES_HEADER).get(0);
sign = rsa.decryptStr(sign, KeyType.PrivateKey);
AES aes = new AES("CBC",
"PKCS7Padding",
sign.getBytes(StandardCharsets.UTF_8),
"4382822409223508".getBytes(StandardCharsets.UTF_8));
String content = JSONUtil.toJsonStr(body);
return aes.encryptBase64(content, StandardCharsets.UTF_8);
}
}

有多个ResponseBodyAdvice时注意Order的顺序

上文中注入的RSA对象可以自己按需求注入自己的对象。