api-crypto-spring-boot
是基于 Spring Boot
开发的控制器统一注解方式自动加解密 请求体、响应体
的启动器,该组件能够提供在 接口
交互过程中数据的安全保护能力。支持常见的 加解密算法、编码、签名 等模式;
- Spring Boot 启动器组件方式,轻量级、只需要简单配置和注解即可使用;
- 支持 摘要加密、签名验签、对称性加密解密、非对称性加密解密、内容编码 等模式;
- 可以扩展 自定义实现模式、、自定义注解、自定义格式等;
- MD2、MD4、MD5
- SHA1、SHA3、SHA224、SHA256、SHA384、SHA512
- SHAKE
算法 | 模式 | 填充 |
---|---|---|
AES | ECB/CBC/CTR/OFB/CFB | PKCS7Padding/PKCS5Padding |
DES | ECB/CBC/CTR/OFB/CFB | PKCS7Padding/PKCS5Padding |
DESede | ECB/CBC/CTR/OFB/CFB | PKCS7Padding/PKCS5Padding |
算法 | 模式 | 填充 |
---|---|---|
RSA | ECB | NoPadding/PKCS1Padding/OAEPWithSHA-1AndMGF1Padding/OAEPWithSHA-256AndMGF1Padding |
RSA | None | NoPadding/PKCS1Padding |
- 数据 + 随机字符串 + 时间戳 + 秘钥
- base64、UrlBase64、hex
<dependency>
<groupId>cn.hermesdi</groupId>
<artifactId>api-crypto-spring-boot-starter</artifactId>
<version>1.0.0.RELEASE</version>
</dependency>
需要使用什么模式配置什么参数,可同时使用多个模式;
- yml 方式:
api:
crypto:
#默认字符集
charset: utf-8
#默认base64编码
encoding-type: base64
#1. 使用签名注解 (@Signature) 时,这里全局配置 或 注解中配置
signature:
#签名秘钥
secret-key: 13245678
#配置验签超时时间,默认不超时。单位(秒)
timeout: 0
#2. 使用对称性加密解密 (@Symmetric) 注解时,这里全局配置(根据使用的算法选择配置) 或 注解中配置
symmetric:
#AES 密钥长度可以是 128(bit)、192(bit)、256(bit)
AES: 1324567899999999
#DESede(3DES) 密钥长度可以是 128(bit)、192(bit)
DESede: 1324567899999999
#DES 密钥长度是 64(bit)
DES: 13245678
#3. 使用非对称性加密解密 (@Asymmetry) 注解时,这里全局配置(根据使用的算法选择配置) 或 注解中配置
asymmetry:
RSA:
#公钥
public-key: xxxx
#私钥
private-key: xxxx
- properties 方式:
#默认字符集
api.crypto.charset=utf-8
#默认base64编码
api.crypto.encoding-type=base64
#1. 使用签名注解 (@Signature) 时,这里全局配置 或 注解中配置
#签名秘钥
api.crypto.signature.secret-key=13245678
#配置验签超时时间,默认不超时。单位(秒)
api.crypto.signature.timeout=0
#2. 使用对称性加密解密 (@Symmetric) 注解时,这里全局配置(根据使用的算法选择配置) 或 注解中配置
#AES 密钥长度可以是 128(bit)、192(bit)、256(bit)
api.crypto.symmetric.AES=xx245678999999xx
#DESede(3DES) 密钥长度可以是 128(bit)、192(bit)
api.crypto.symmetric.DESede=xx245678999999xx
#DES 密钥长度是 64(bit)
api.crypto.symmetric.DES=xx2456xx
#3. 使用非对称性加密解密 (@Asymmetry) 注解时,这里全局配置(根据使用的算法选择配置) 或 注解中配置
# 公钥
api.crypto.asymmetry.RSA.private-key=xxxx
# 私钥
api.crypto.asymmetry.RSA.public-key=xxxx
// 开启 ApiCrypto 注解
@EnableApiCrypto
@SpringBootApplication
public class ApiCryptoExampleApplication {
public static void main(String[] args) {
SpringApplication.run(ApiCryptoExampleApplication.class, args);
}
}
为了减少不必要的消耗,用户可根据自身需求注册不同的模式到容器以使用。目前所有的已实现模式都实现于 ApiCryptoAlgorithm
接口,需要什么模式将该接口其对应的实现类 @Bean 注入即可。
@Configuration
public class ApiCryptoConfiguration {
/**
* 摘要加密 Bean
*/
@Bean
public ApiCryptoAlgorithm digestApiCrypto() {
return new DigestApiCrypto();
}
/**
* 对称性加密解密 Bean
*/
@Bean
public ApiCryptoAlgorithm symmetricApiCrypto() {
return new SymmetricApiCrypto();
}
/**
* 非对称性加密解密 Bean
*/
@Bean
public ApiCryptoAlgorithm asymmetryApiCrypto() {
return new AsymmetryApiCrypto();
}
/**
* 签名 Bean
*/
@Bean
public ApiCryptoAlgorithm signatureApiCrypto() {
return new SignatureApiCrypto();
}
/**
* 内容 编码、解码 Bean
*/
@Bean
public ApiCryptoAlgorithm encodingApiCrypto() {
return new EncodingApiCrypto();
}
}
- 注解放在类上:该类下所有接口方法将开启实现。
- 注解放在方法上:只有该方法开启实现。
模式(算法) | 注解 | 作用范围 |
---|---|---|
摘要 | @DigestsCrypto | 响应体加密 |
对称性 | @SymmetricCrypto | 响应体加密、请求体解密 |
非对称性 | @AsymmetryCrypto | 响应体加密、请求体解密 |
签名 | @SignatureCrypto | 响应体签名、请求体验签 |
编码 | @EncodingCrypto | 响应体编码、请求体解码 |
注意:
当同时存在 两个注解作用于一个方法或者类上和方法上都有注解 的情况下,不同注解按照注册实现Bean的顺序,相同注解则按照方法优先于类的原则。
使用于方法上,结合注解可以自由选择加密还是解密,还可以当整个类开启加密时忽略个别接口加密或解密。
@NotDecrypt
忽略 解密,就是不对请求体进行处理@NotEncrypt
忽略 加密,就是不对响应体进行处理@NotCrypto
忽略 加密、解密(上面两个注解合并)
例子:完整dome
@RestController
public class SymmetricController {
// 使用对称性加密、解密注解
@SymmetricCrypto(type = SymmetricType.AES_CFB_PKCS7_PADDING)
@PostMapping("/symmetric1")
public Object symmetric() {
return "对称性加密测试";
}
// 忽略解密(就是不对请求体进行处理)
@NotDecrypt
// 使用对称性加密、解密注解
@SymmetricCrypto(type = SymmetricType.AES_CFB_PKCS7_PADDING)
@PostMapping("/symmetric2")
public Object symmetric() {
return "对称性加密测试";
}
// 忽略加密(就是不对响应体进行处理)
@NotEncrypt
// 使用对称性加密、解密注解
@SymmetricCrypto(type = SymmetricType.AES_CFB_PKCS7_PADDING)
@PostMapping("/symmetric3")
public Object symmetric() {
return "对称性加密测试";
}
}
组件支持扩展如 自定义请求体解析、自定义请求体响应、自定义实现模式等;
可以自定义解析请求响应 body 的格式,以满足不同的场景需求。下面以注册对称性[算法Bean](#4.注册 实现模式(算法) Bean)为例:
@Configuration
public class ApiCryptoConfiguration {
/**
* 对称性加密解密 Bean
*/
@Bean
public ApiCryptoAlgorithm symmetricApiCrypto() {
SymmetricApiCrypto symmetricApiCrypto = new SymmetricApiCrypto();
// 自定义请求内容解析,格式例如:数据&偏移量
symmetricApiCrypto.setiApiRequestBody((annotation, inputStream) -> {
try {
// 转为字符串
byte[] byteArr = new byte[inputStream.available()];
inputStream.read(byteArr);
String str = new String(byteArr);
// 按照'&'符号分割
String[] strings = str.split("&");
// 封装数据
return new ApiCryptoBody().setData(strings[0]).setIv(strings[1]);
} catch (IOException e) {
e.printStackTrace();
return null;
}
});
// 自定义响应内容格式,格式例如:数据&偏移量
symmetricApiCrypto.setiApiResponseBody((annotation, cryptoBody) -> {
String data = cryptoBody.getData();
String iv = cryptoBody.getIv();
return data + "&" + iv;
});
return symmetricApiCrypto;
}
}
@RestController
public class SymmetricController {
@Resource
ObjectMapper objectMapper;
// 非对称加密、解密
@SymmetricCrypto(type = SymmetricType.AES_CFB_PKCS7_PADDING)
@PostMapping("/symmetric")
public String symmetric(@RequestBody TestBean req) throws JsonProcessingException {
System.out.println(req.toString());
TestBean testBean = new TestBean();
testBean.setAnInt(0);
testBean.setInteger(1);
testBean.setString("test string");
testBean.setStringList(Collections.singletonList("list"));
testBean.setObjectMap(Collections.singletonMap("test", "map"));
return objectMapper.writeValueAsString(testBean);
}
}
请求body为:数据&偏移量格式
1KFHb7m5f0VHCupyZmNBSmV0PpgDAmrMvmiM9bwylB1BfoXHptXbZVvtZhtuPWFdJATI1YLxxRl/102DhhO1tcKGpcnIxvpXQaXDsTz+HgU9lY2zEqfv2pvx0qieY8WN&L46gdVx6X934cpm6
响应body为:数据&偏移量格式
a9joYmcuIPEiIlGdTMhDEE+7LxPzo1DNMBlmDXGeBcG+I8qFRgdoQtuM7i+QHeNdYtz8CBUIMiC6f3nW10xW8Ok16GlBrO7ubt7cLIi/dkzhlD8Ita46ywyxJB2CeCxu&1Ed3g5Z9v6B3tpZ2
自定义实现一种模式,交互数据&md5(交互数据+盐)
例如:
要传输的数据是 :abc123abc
盐(salt)是 : 123456
最后传输数据是 : abc123abc&3bdb146d4faea7d09c0d27ec9d36cbee
这样子就可以保证数据传输安全不被篡改了。
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface CustomCrypto {
/**
* 是否请求体解密
*/
boolean isDecryption();
/**
* 是否响应体加密
*/
boolean isEncryption();
/**
* 加密的盐
*/
String salt();
}
public class CustomApiCrypto implements ApiCryptoAlgorithm {
@Autowired
private ObjectMapper objectMapper;
private static final Log logger = LogFactory.getLog(CustomApiCrypto.class);
/**
* 处理自定义 加密/解密注解 或 其他方式
*
* @param methodParameter: 方法参数
* @param requestOrResponse: 请求还是响应,true请求,false响应
* @return boolean 返回 true 将执行下面两个对应的方法,false则忽略
**/
@Override
public boolean isCanRealize(MethodParameter methodParameter, boolean requestOrResponse) {
// 获取执行 方法或类上的指定注解(注解在类上并且是被继承的无效)
CustomCrypto annotation = this.getAnnotation(methodParameter, CustomCrypto.class);
// 是否请求解密
if (annotation.isDecryption() && !requestOrResponse) {
return true;
}
// 是否响应加密
if (annotation.isEncryption() && !requestOrResponse) {
return true;
}
return false;
}
/**
* 当 isCanRealize 返回true,并且为请求时执行
*
* @param httpInputMessage: 请求内容
* @param methodParameter: 执行方法参数
* @param type: 目标类型
* @param aClass: 消息转换器
* @return org.springframework.http.HttpInputMessage 请求的数据输入流
**/
@Override
public HttpInputMessage beforeBodyRead(HttpInputMessage httpInputMessage, MethodParameter methodParameter, Type type, Class<? extends HttpMessageConverter<?>> aClass) throws IOException {
// 请求内容
byte[] byteArr = new byte[httpInputMessage.getBody().available()];
httpInputMessage.getBody().read(byteArr);
String str = new String(byteArr);
// 拆分出数据和签名
String[] strings = str.split("&");
String data = strings[0];
String md5 = strings[1];
// 获取盐
CustomCrypto annotation = this.getAnnotation(methodParameter, CustomCrypto.class);
String salt = annotation.salt();
// md5加密一下
String test = DigestUtils.md5DigestAsHex((data + salt).getBytes(StandardCharsets.UTF_8));
// 对比原来的md5加密是否一致,不一致说明被修改过请求体了
if (!test.equals(md5)) {
throw new ApiDecodeException("验证失败");
}
return this.stringToInputStream(data.getBytes(StandardCharsets.UTF_8), httpInputMessage.getHeaders(), logger);
}
/**
* 当 isCanRealize 返回 true,并且为响应时执行
*
* @param body: 方法执行后返回的参数
* @param methodParameter: 方法执行参数
* @param mediaType: 数据类型,json,text,html等等
* @param aClass: 消息转换器
* @param serverHttpRequest: 请求
* @param serverHttpResponse: 响应
* @return java.lang.Object 最终响应内容
**/
@Override
public Object responseBefore(Object body, MethodParameter methodParameter, MediaType mediaType, Class<? extends HttpMessageConverter<?>> aClass, ServerHttpRequest serverHttpRequest, ServerHttpResponse serverHttpResponse) {
CustomCrypto annotation = this.getAnnotation(methodParameter, CustomCrypto.class);
// 如果方法返回内容是字符串
if (body instanceof String) {
// md5加密一下
String md5 = DigestUtils.md5DigestAsHex((body + annotation.salt()).getBytes(StandardCharsets.UTF_8));
return body + "&" + md5;
}
// 如果方法返回内容是对象
String json = null;
try {
json = objectMapper.writeValueAsString(body);
} catch (JsonProcessingException e) {
throw new ApiEncryptException("转json字符串失败");
}
// md5加密一下
String md5 = DigestUtils.md5DigestAsHex((json + annotation.salt()).getBytes(StandardCharsets.UTF_8));
return body + "&" + md5;
}
}
@Configuration
public class ApiCryptoConfiguration {
/**
* 自定义模式 Bean
*/
@Bean
public ApiCryptoAlgorithm customApiCrypto() {
return new CustomApiCrypto();
}
}
@RestController
public class CustomController {
@CustomCrypto(isDecryption = true, isEncryption = true, salt = "123456")
@PostMapping("/custom")
public String custom(@RequestBody String text) {
System.out.println(text); // abc123abc
return "ABC123456";
}
}
请求body为:数据&md5
abc123abc&3bdb146d4faea7d09c0d27ec9d36cbee
响应body为:数据&md5
ABC123456&f929eb0889e53b3893600ba29f039ced
- 无法自动解密请求体加密内容
- 加上
@RequestBody
即可;
- 加上
- 多个算法注解同时使用时顺序问题
- 当同时存在 两个注解作用于一个方法或者类上和方法上都有注解 的情况下,不同注解按照注册实现Bean的顺序,相同注解则按照方法优先于类的原则;