SecurityApi v1.0.1
一个基于 Java 接口参数加密框架,让接口参数加密变得简单、优雅!
文章目录
一、SecurityApi 介绍
SecurityApi 是一个基于 Java 接口参数加密框架,可以让请求参数解密,响应参数加密,目前支持AES、RSA加密模式,RSA采用分段加密的方式。
核心功能示例:
无感接收明文数据,并且可以使用 @Validated
校验参数,响应数据无需额外操作,自动加密返回。
二、SecurityApi 依赖
<dependencies>
<!-- SpringBoot2.x 依赖 -->
<!-- https://mvnrepository.com/artifact/io.github.chenhanhui/security-api-spring-boot-starter -->
<dependency>
<groupId>io.github.chenhanhui</groupId>
<artifactId>security-api-spring-boot-starter</artifactId>
<version>1.0.1</version>
</dependency>
<!-- SpringBoot3.x 依赖 -->
<!-- https://mvnrepository.com/artifact/io.github.chenhanhui/security-api-spring-boot3-starter -->
<dependency>
<groupId>io.github.chenhanhui</groupId>
<artifactId>security-api-spring-boot3-starter</artifactId>
<version>1.0.1</version>
</dependency>
</dependencies>
三、使用
1. RSA加密(非对称加密)
1.1 简单示例
在启动类中添加 @EnableSecurityParameter
注解启动 SecurityApi 功能:
@SpringBootApplication
@EnableSecurityParameter
public class SecurityApiApplication {
public static void main(String[] args) {
SpringApplication.run(SecurityApiApplication.class, args);
}
}
如果使用RSA加密,需要在 application.yml
添加以下代码:
security:
encrypt:
mode: rsa
rsa:
private-key: 'MIIEvAIBADAN...PIUg=='
client-public-key: 'MIIBIjAN...37zAEwIDAQAB'
注意:private-key
是服务器私钥,client-public-key
是客户端公钥,默认为 2048 位。
密钥是有两对,服务器公钥和私钥,客户端公钥和私钥。
公钥双方都会有(包括对方的),私钥只有自己拥有自己的,不会服务器有客户端私钥,或者客户端有服务器私钥。
- 当客户端向服务器发送数据请求时:
客户端用服务器的公钥进行数据加密,用客户端的私钥进行签名。
- 服务器接收数据后:
服务器用客户端的公钥进行验签,用服务器私钥进行数据的解密。
- 当服务器响应客户端数据结果时:
服务器是用客户端的公钥进行数据加密,用服务器私钥进行签名。
- 客户端接收数据后:
客户端就用服务器公钥进行验签,用客户端的私钥进行解密。
这一切不需要开发者关心,SecurityApi 框架已经帮你做好了。
1.2 生成RSA密钥
我们只需使用以下代码生成 RSA 公钥和私钥,需要生成两对,分别是客户端公钥和私钥,服务器公钥和私钥:
import com.chh.util.RSAUtils;
import java.util.Map;
public class RSAGenerate {
public static void main(String[] args) {
int bits = 2048;
Map<String, String> keyMap = RSAUtils.generateKeyPair(bits);
String publicKeyStr = keyMap.get("publicKey");
String privateKeyStr = keyMap.get("privateKey");
System.out.println("=======================================");
System.out.println("bits:" + bits);
System.out.println("publicKey:" + publicKeyStr);
System.out.println("privateKey:" + privateKeyStr);
System.out.println("=======================================");
}
}
在 SecurityApi 中,一行代码解决参数解密加密,只需在类或者方法上添加 @SecurityParameter
注解, 如下:
@RestController
@RequestMapping("/author")
@SecurityParameter
public class AuthorController implements SecurityBuilder {
/**
* 请求解密,响应加密
*
* @param author Author对象
* @return 返回加密后的数据 ResponseBody<SecurityResult>格式
*/
@PostMapping("/inDecodeOutEncode")
public ResponseEntity<SecurityResult> inDecodeOutEncode(@RequestBody @Validated Author author) {
author.setUrl("https://blog.csdn.net/xiaohuihui1400");
return success(author);
}
}
1.3 加签名说明
「第一个场景」B要给A传递一条加密消息
RSA的加密过程如下:
A生成一对密钥(公钥和私钥),私钥不公开,A自己保留。公钥为公开的,任何人可以获取。
A传递自己的公钥给B,B用A的公钥对消息进行加密。
A接收到B加密的消息,利用A自己的私钥对消息进行解密。
「第二个场景」B要给A传递一条加密消息,A验证B发的加密消息没有被篡改
RSA签名的过程如下:
A生成一对密钥(公钥和私钥),私钥不公开,A自己保留。公钥为公开的,任何人可以获取。
B生成一对密钥(公钥和私钥),私钥不公开,B自己保留。公钥为公开的,任何人可以获取。
A传递自己的公钥给B,B传递自己的公钥给A。
B用A的公钥对消息进行加密,再用自己的私钥对加密消息加签,形成签名,并将加签的消息和加密消息本身一起传递给A。
A收到消息后,获取B的公钥进行验签,如果验签出来的内容与消息本身一致,证明消息是B回复的。
A再用自己的私钥对加密消息解密。
综合两个场景你会发现,第一个场景主要用于防止用户抓包,虽然被截获的消息没有泄露,但是可以利用截获的公钥,将假指令进行加密,然后传递给A。
第二个场景同时使用加密和签名,A和B都有一套自己的公钥和私钥,当A要给B发送消息时,先用B的公钥对消息加密,再对加密的消息使用A的私钥加签名,达到既不泄露也不被篡改,更能保证消息的安全性。
开启和关闭RSA签名,需要在 application.yml
修改 security.encrypt.rsa.sign
的值:
security:
encrypt:
mode: rsa
rsa:
private-key: 'MIIEvAIBADAN...PIUg=='
client-public-key: 'MIIBIjAN...37zAEwIDAQAB'
sign: true # 默认开启签名
1.4 密钥传递说明
在前后端通信过程中,实现双向加密通常采用HTTPS协议来保证数据传输的安全性。HTTPS协议基于SSL/TLS协议,能够确保数据在传输过程中的加密,从而保护数据不被第三方窃取或篡改。在这种情况下,公钥的传递通常是通过SSL/TLS握手过程自动完成的,不需要开发者手动干预。
然而,如果你的场景是指在应用层面上,前后端之间需要实现额外的加密通信(例如,使用RSA加密算法),并且你想要手动控制公钥的传递过程,那么可以考虑以下几种方式:
-
通过HTTPS安全通道传递公钥:即使是应用层面的加密,也强烈建议在HTTPS协议的基础上进行。在建立了安全的通信通道后,可以将公钥以普通数据的形式发送给客户端。这种方式的优点是实现简单,缺点是每次会话都需要传递公钥。
-
公钥嵌入到客户端:在客户端(如Web前端或移动应用)发布时,可以将公钥直接嵌入到客户端代码或配置中。这样,客户端在需要加密数据时,可以直接使用嵌入的公钥进行加密。这种方式的优点是减少了公钥传递的次数,提高了效率,但缺点是更新公钥时需要更新客户端。
-
通过API接口动态获取公钥:后端提供一个API接口,用于动态获取公钥。客户端在需要进行加密通信之前,先调用这个API获取公钥。这种方式相比直接嵌入公钥,更加灵活,便于公钥的更新和管理,但会增加一次网络请求。
-
使用证书:类似于HTTPS使用的机制,可以为后端服务创建一个证书,并将公钥包含在证书中。客户端通过验证证书的合法性来获取公钥。这种方式增加了安全性,但实现起来相对复杂。
无论采用哪种方式,都需要确保公钥的传递和存储过程安全,避免公钥在传递过程中被截获或在客户端被篡改。同时,还需要注意公钥的更新和管理策略,确保加密机制的安全性和可靠性。
在 SecurityApi 框架中,主要使用以上第二种方式,并且保留了公钥的传递方式,例如服务端始终只有一对密钥,而每个不同的用户都有自己的密钥,那么服务端需要获取用户的公钥才能完成加密,在application.yml
中将 client-public-key
的删除:
security:
encrypt:
mode: rsa
rsa:
private-key: 'MIIEvAIBADAN...PIUg=='
在 Controller 中,通过查询数据库、缓存或者客户端传递的公钥在 @ModelAttribute
注解的方法中进行填充: request.setAttribute(SecurityConstant.CLIENT_PUBLIC_KEY, clientPublicKey)
,如下所示:
@RestController
@RequestMapping("/author")
public class AuthorController implements SecurityBuilder {
@ModelAttribute
public void addAttributes(HttpServletRequest request) {
String clientPublicKey = "MIIBIjAN...37zAEwIDAQAB";
request.setAttribute(SecurityConstant.CLIENT_PUBLIC_KEY, clientPublicKey);
}
/**
* 请求解密,响应加密
*
* @param author Author对象
* @return 返回加密后的数据 ResponseBody<SecurityResult>格式
*/
@PostMapping("/inDecodeOutEncode")
@SecurityParameter
public ResponseEntity<SecurityResult> inDecodeOutEncode(@RequestBody @Validated Author author) {
author.setUrl("https://blog.csdn.net/xiaohuihui1400");
return success(author);
}
}
1.5 分段加密
RSA密钥位数默认 2048 位,若使用 1024 位密钥,需要修改 key-size
,SecurityApi 框架会自动根据该值进行分段加密,分段数据使用 ;
分割。
security:
encrypt:
mode: rsa
rsa:
private-key: 'MIIEvAIBADAN...PIUg=='
client-public-key: 'MIIBIjAN...37zAEwIDAQAB'
key-size: 1024 # 未填写默认RSA密钥位数2048
2. AES加密(对称加密)
2.1 生成AES秘钥、偏移量
import com.chh.util.AESUtils;
public class AESGenerate {
public static void main(String[] args) throws Exception {
int bit = 256;
String key = AESUtils.generateKey(bit);
String iv = AESUtils.generateIv();
System.out.println("AES Key: " + key);
System.out.println("AES Iv: " + iv);
System.out.println("'123456' encrypt: " + AESUtils.encrypt("123456", key, iv));
System.out.println("'123456' encrypt not iv: " + AESUtils.encrypt("123456", key));
}
}
使用AES加密,需要在 application.yml
添加以下代码:
security:
encrypt:
mode: AES
aes:
key: 'dBV5Awv8Ji6kIE/QledA1+MHnF3up8ur'
iv: 'kYw1hVScyQOV2LO/'
iv
表示偏移量,可以使加密更加安全可靠,不易破解,如果不需要 iv
的可以将其属性删除。
3. 全局和局部解密、加密、日志打印控制
在 application.yml
可以控制全局请求参数解密、响应参数加密、解密加密日志打印:
security:
encrypt:
in-decode: true # 默认请求参数解密
out-encode: true # 默认响应参数加密
show-log: true # 默认打印请求参数、响应参数解密加密前后的数据
控制局部需要在 @SecurityParameter
注解中设置参数,如下:
@SecurityParameter(inDecode = false, outEncode = false, showLog = false)
@SecurityParameter
注解可以加在类上或者方法上,方法的优先级更高。
4. 全局和局部解密前、签名、解密后数据缓存控制
在 application.yml
可以控制全局解密前、签名、解密后数据缓存:
security:
encrypt:
cache-data: true # 默认缓存请求加密、签名数据,以及解密之后的数据到 request
控制局部需要在 @SecurityParameter
注解中设置参数,如下:
@SecurityParameter(cacheData = false)
关闭了数据缓存,未关闭日志打印,结果如下所示:
四、常见问题
1. 为什么未填写密钥而会自动加密?
在启动类中添加 @EnableSecurityParameter
注解启动加密功能之后,SecurityApi 框架会自动帮我们做初始化,如果未填写服务端私钥,会生成随机密钥并自动填充。
2. 异常:Data must not be longer than 117 bytes
security.encrypt.rsa.key-size
的值默认为 2048,当密钥的位数不等于(小于)该值,则会抛出异常,表示分段加密失败,将 security.encrypt.rsa.key-size
的值修改成和密钥位数一致即可。
3. 异常:Bad signature length: got 255 but was expecting 256
该异常非 SecurityApi 框架原因,而是前端使用了 jsencrypt 3.3.2 依赖,jsencrypt 的签名存在Bug,导致签名结果偶现异常,所以 SecurityApi 框架验签失败。
签名有时错误的原因:https://github.com/travist/jsencrypt/pull/280
五、代码托管
- GitHub:https://github.com/ChenHanHui/SecurityApi
- Gitee:https://gitee.com/chen-hanhui/SecurityApi.git
- GitCode:https://gitcode.com/xiaohuihui1400/SecurityApi.git
六、交流群
QQ交流群:982597743 点击加入
加入群聊的好处:
- 第一时间收到框架更新通知
- 第一时间收到框架 bug 通知