spring boot接口加密
前后端分离的项目中数据是明文暴露,没有https协议情况下,数据会被抓包存在风险。
本文主要介绍常用的加密算法包括对称加密(AES)和非对称加密(RSA),
此处只展示了后端代码,前端代码可以查看下面的链接。
源码地址:https://gitee.com/linjinpeng/spring-boot
原理分析:
前端给后端发送消息,RSA的加密步骤如下:
1.后端生成一对密钥(公钥和私钥),私钥不公开,后端自己保留,公钥发送给前端。
2.前端用后端的公钥对消息进行加密,并发送自己的公钥和加密后的key。
3.后端接收到前端加密的消息,利用自己的私钥对消息进行解密
后端回复前端消息,RSA签名步骤如下:
1.后端生成一对密钥(公钥和私钥),私钥不公开,自己保留。公钥为公开的。
2.后端用自己的私钥对消息加签,形成签名,并将加签的消息发送给前端。
3.前端收到消息后,在获取后端的公钥进行验签,如果验签出来的内容与消息本身一致,则解密成功。
一.需要用到的依赖包
<!--lombok插件,用来记录日志 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</dependency>
<!-- Base64编码需要 -->
<dependency>
<groupId>org.apache.directory.studio</groupId>
<artifactId>org.apache.commons.codec</artifactId>
<version>1.8</version>
</dependency>
<!--aop 面向切面看,拦截请求 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- thymeleaf模板 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- BouncyCastleProvider -->
<dependency>
<groupId>org.bouncycastle</groupId>
<artifactId>bcprov-jdk16</artifactId>
<version>1.46</version>
</dependency>
二.新建AES加、解密算法工具类
**
* AES加、解密算法工具类
*/
public class AesUtil {
/**
* 加密算法AES
*/
private static final String KEY_ALGORITHM = "AES";
/**
* key的长度,Wrong key size: must be equal to 128, 192 or 256
* 传入时需要16、24、36
*/
private static final Integer KEY_LENGTH = 16 * 8;
/**
* 算法名称/加密模式/数据填充方式
* 默认:AES/ECB/PKCS5Padding
*/
private static final String ALGORITHMS = "AES/ECB/PKCS5Padding";
/**
* 后端AES的key,由静态代码块赋值
*/
public static String key;
/**
* 不能在代码中创建
* JceSecurity.getVerificationResult 会将其put进 private static final Map<Provider,Object>中,导致内存缓便被耗尽
*/
private static final BouncyCastleProvider PROVIDER = new BouncyCastleProvider();
static {
key = getKey();
}
/**
* 获取key
*/
public static String getKey() {
StringBuilder uid = new StringBuilder();
//产生16位的强随机数
Random rd = new SecureRandom();
for (int i = 0; i < KEY_LENGTH / 8; i++) {
//产生0-2的3位随机数
int type = rd.nextInt(3);
switch (type) {
case 0:
//0-9的随机数
uid.append(rd.nextInt(10));
break;
case 1:
//ASCII在65-90之间为大写,获取大写随机
uid.append((char) (rd.nextInt(25) + 65));
break;
case 2:
//ASCII在97-122之间为小写,获取小写随机
uid.append((char) (rd.nextInt(25) + 97));
break;
default:
break;
}
}
return uid.toString();
}
/**
* 加密
*
* @param content 加密的字符串
* @param encryptKey key值
*/
public static String encrypt(String content, String encryptKey) throws Exception {
//设置Cipher对象
Cipher cipher = Cipher.getInstance(ALGORITHMS, PROVIDER);
cipher.init(Cipher.ENCRYPT_MODE, new SecretKeySpec(encryptKey.getBytes(), KEY_ALGORITHM));
//调用doFinal
// 转base64
return Base64.encodeBase64String(cipher.doFinal(content.getBytes(StandardCharsets.UTF_8)));
}
/**
* 解密
*
* @param encryptStr 解密的字符串
* @param decryptKey 解密的key值
*/
public static String decrypt(String encryptStr, String decryptKey) throws Exception {
//base64格式的key字符串转byte
byte[] decodeBase64 = Base64.decodeBase64(encryptStr);
//设置Cipher对象
Cipher cipher = Cipher.getInstance(ALGORITHMS,PROVIDER);
cipher.init(Cipher.DECRYPT_MODE, new SecretKeySpec(decryptKey.getBytes(), KEY_ALGORITHM));
//调用doFinal解密
return new String(cipher.doFinal(decodeBase64));
}
}
三.新建RSA加、解密算法工具类
/**
* RSA加、解密算法工具类
*/
@Slf4j
public class RsaUtil {
/**
* 加密算法AES
*/
private static final String KEY_ALGORITHM = "RSA";
/**
* 算法名称/加密模式/数据填充方式
* 默认:RSA/ECB/PKCS1Padding
*/
private static final String ALGORITHMS = "RSA/ECB/PKCS1Padding";
/**
* Map获取公钥的key
*/
private static final String PUBLIC_KEY = "publicKey";
/**
* Map获取私钥的key
*/
private static final String PRIVATE_KEY = "privateKey";
/**
* RSA最大加密明文大小
*/
private static final int MAX_ENCRYPT_BLOCK = 117;
/**
* RSA最大解密密文大小
*/
private static final int MAX_DECRYPT_BLOCK = 128;
/**
* RSA 位数 如果采用2048 上面最大加密和最大解密则须填写: 245 256
*/
private static final int INITIALIZE_LENGTH = 1024;
/**
* 后端RSA的密钥对(公钥和私钥)Map,由静态代码块赋值
*/
private static Map<String, Object> genKeyPair = new LinkedHashMap<>();
static {
try {
genKeyPair.putAll(genKeyPair());
} catch (Exception e) {
//输出到日志文件中
System.err.println(e.getMessage());
}
}
/**
* 生成密钥对(公钥和私钥)
*/
private static Map<String, Object> genKeyPair() throws Exception {
KeyPairGenerator keyPairGen = KeyPairGenerator.getInstance(KEY_ALGORITHM);
keyPairGen.initialize(INITIALIZE_LENGTH);
KeyPair keyPair = keyPairGen.generateKeyPair();
RSAPublicKey publicKey = (RSAPublicKey) keyPair.getPublic();
RSAPrivateKey privateKey = (RSAPrivateKey) keyPair.getPrivate();
Map<String, Object> keyMap = new HashMap<String, Object>(2);
//公钥
keyMap.put(PUBLIC_KEY, publicKey);
//私钥
keyMap.put(PRIVATE_KEY, privateKey);
return keyMap;
}
/**
* 私钥解密
*
* @param encryptedData 已加密数据
* @param privateKey 私钥(BASE64编码)
*/
public static byte[] decryptByPrivateKey(byte[] encryptedData, String privateKey) throws Exception {
//base64格式的key字符串转Key对象
Key privateK = KeyFactory.getInstance(KEY_ALGORITHM).generatePrivate(new PKCS8EncodedKeySpec(Base64.decodeBase64(privateKey)));
Cipher cipher = Cipher.getInstance(ALGORITHMS);
cipher.init(Cipher.DECRYPT_MODE, privateK);
//分段进行解密操作
return encryptAndDecryptOfSubsection(encryptedData, cipher, MAX_DECRYPT_BLOCK);
}
/**
* 公钥加密
*
* @param data 源数据
* @param publicKey 公钥(BASE64编码)
*/
public static byte[] encryptByPublicKey(byte[] data, String publicKey) throws Exception {
//base64格式的key字符串转Key对象
Key publicK = KeyFactory.getInstance(KEY_ALGORITHM).generatePublic(new X509EncodedKeySpec(Base64.decodeBase64(publicKey)));
Cipher cipher = Cipher.getInstance(ALGORITHMS);
cipher.init(Cipher.ENCRYPT_MODE, publicK);
//分段进行加密操作
return encryptAndDecryptOfSubsection(data, cipher, MAX_ENCRYPT_BLOCK);
}
/**
* 获取私钥
*/
public static String getPrivateKey() {
Key key = (Key) genKeyPair.get(PRIVATE_KEY);
return Base64.encodeBase64String(key.getEncoded());
}
/**
* 获取公钥
*/
public static String getPublicKey() {
Key key = (Key) genKeyPair.get(PUBLIC_KEY);
return Base64.encodeBase64String(key.getEncoded());
}
/**
* 分段进行加密、解密操作
*/
private static byte[] encryptAndDecryptOfSubsection(byte[] data, Cipher cipher, int encryptBlock) throws Exception {
int inputLen = data.length;
ByteArrayOutputStream out = new ByteArrayOutputStream();
int offSet = 0;
byte[] cache;
int i = 0;
// 对数据分段加密
while (inputLen - offSet > 0) {
if (inputLen - offSet > encryptBlock) {
cache = cipher.doFinal(data, offSet, encryptBlock);
} else {
cache = cipher.doFinal(data, offSet, inputLen - offSet);
}
out.write(cache, 0, cache.length);
i++;
offSet = i * encryptBlock;
}
out.close();
return out.toByteArray();
}
}
四.新建Result类,统一返回对象
/**
* 统一返回对象
*/
@Data
public class Result<T> implements Serializable {
/**
* 通信数据
*/
private T data;
/**
* 通信状态
*/
private boolean flag = true;
/**
* 通信描述
*/
private String msg = "操作成功";
/**
* 通过静态方法获取实例
*/
public static <T> Result<T> of(T data) {
return new Result<>(data);
}
public static <T> Result<T> of(T data, boolean flag) {
return new Result<>(data, flag);
}
public static <T> Result<T> of(T data, boolean flag, String msg) {
return new Result<>(data, flag, msg);
}
@Deprecated
public Result() {
}
private Result(T data) {
this.data = data;
}
private Result(T data, boolean flag) {
this.data = data;
this.flag = flag;
}
private Result(T data, boolean flag, String msg) {
this.data = data;
this.flag = flag;
this.msg = msg;
}
五.新建注解类 Encrypt和 Decrypt
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Decrypt {
}
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
public @interface Encrypt {
}
六.新建SafetyAspect类,AES + RSA 加解密AOP处理
/**
* AES + RSA 加解密AOP处理
*/
@Slf4j
@Aspect
@Component
public class SafetyAspect {
/**
* Pointcut 切入点
* 匹配
* cn.huanzi.qch.baseadmin.sys.*.controller、
* cn.huanzi.qch.baseadmin.*.controller包下面的所有方法
*/
@Pointcut(value = "execution(public * com.example.demo.*.*(..))")
public void safetyAspect() {}
/**
* 环绕通知
*/
@Around(value = "safetyAspect()")
public Object around(ProceedingJoinPoint pjp) {
try {
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
assert attributes != null;
//request对象
HttpServletRequest request = attributes.getRequest();
//http请求方法 post get
String httpMethod = request.getMethod().toLowerCase();
//method方法
Method method = ((MethodSignature) pjp.getSignature()).getMethod();
//method方法上面的注解
Annotation[] annotations = method.getAnnotations();
//方法的形参参数
Object[] args = pjp.getArgs();
//是否有@Decrypt
boolean hasDecrypt = false;
//是否有@Encrypt
boolean hasEncrypt = false;
for (Annotation annotation : annotations) {
if (annotation.annotationType() == Decrypt.class) {
hasDecrypt = true;
}
if (annotation.annotationType() == Encrypt.class) {
hasEncrypt = true;
}
}
//前端公钥
String publicKey = null;
//jackson
ObjectMapper mapper = new ObjectMapper();
//jackson 序列化和反序列化 date处理
mapper.setDateFormat( new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"));
//执行方法之前解密,且只拦截post请求
if ("post".equals(httpMethod) && hasDecrypt) {
//AES加密后的数据
String data = request.getParameter("data");
//后端RSA公钥加密后的AES的key
String aesKey = request.getParameter("aesKey");
//前端公钥
publicKey = request.getParameter("publicKey");
System.err.println("前端公钥:" + publicKey);
//后端私钥解密的到AES的key
byte[] plaintext = RsaUtil.decryptByPrivateKey(Base64.decodeBase64(aesKey), RsaUtil.getPrivateKey());
aesKey = new String(plaintext);
System.err.println("解密出来的AES的key:" + aesKey);
//RSA解密出来字符串多一对双引号
aesKey = aesKey.substring(1, aesKey.length() - 1);
//AES解密得到明文data数据
String decrypt = AesUtil.decrypt(data, aesKey);
System.err.println("解密出来的data数据:" + decrypt);
//设置到方法的形参中,目前只能设置只有一个参数的情况
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//注:参数最好用Vo对象来接参,单用String来接,args有长度但获取为空,很奇怪不知道为什么
if(args.length > 0){
args[0] = mapper.readValue(decrypt, args[0].getClass());
}
}
//执行并替换最新形参参数 PS:这里有一个需要注意的地方,method方法必须是要public修饰的才能设置值,private的设置不了
Object o = pjp.proceed(args);
//返回结果之前加密
if (hasEncrypt) {
mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
//每次响应之前随机获取AES的key,加密data数据
String key = AesUtil.getKey();
System.err.println("AES的key:" + key);
String dataString = mapper.writeValueAsString(o);
System.err.println("需要加密的data数据:" + dataString);
String data = AesUtil.encrypt(dataString, key);
//用前端的公钥来解密AES的key,并转成Base64
String aesKey = Base64.encodeBase64String(RsaUtil.encryptByPublicKey(key.getBytes(), publicKey));
System.err.println("publicKey:" + publicKey);
//转json字符串并转成Object对象,设置到Result中并赋值给返回值o
o = Result.of(mapper.readValue("{\"data\":\"" + data + "\",\"aesKey\":\"" + aesKey + "\"}", Object.class));
}
//返回
return o;
} catch (Throwable e) {
//输出到日志文件中
System.err.println(e.getMessage());
return Result.of(null, false, "加解密异常:\n\t" + e.getMessage());
}
}
}
七.控制器中调用加解密注释,
控制器中引入注解 @Decrypt @Encrypt 会自动加密 解密。
初次访问,前端需要拿到后端的公钥,每个请求用户的公钥都是不同的。
/**
* 跳转登录页面
*/
@RequestMapping("/")
public ModelAndView index(){
ModelAndView modelAndView = new ModelAndView("index");
//后端公钥
String publicKey = RsaUtil.getPublicKey();
System.err.println("后端公钥:" + publicKey);
modelAndView.addObject("publicKey", publicKey);
return modelAndView;
}
@Decrypt
@Encrypt
@RequestMapping("out")
public Result out(UserBean userBean){
return Result.of(userBean);
}