注解实现接口加密
- 1、前言
- 1.1、前端必看
- 1.2、后端必看
- 2、后端注解实现
- 2.1、实现流程
- 2.2、开始实现
- 2.2.1、 pom
- 2.2.2、 注解
- 2.2.3、 加密工具类
- 2.2.3、 定义切面(注意切点包名)
- 2.2.4、 定义加密基类与各种入参VO
- 2.2.5、写两个Controller
- 3、参考文章
1、前言
起因是公司给人开发的内网系统登录接口采用明文传输,本来用着没啥问题(其实不太好!),毕竟只是内网使用。后面需要外网访问,需要提高安全等级,禁止密码明文传输。于是就有了这篇文章。本文采用:对称加密
有问题联系Qq:1101165230
1.1、前端必看
需要说明的是,加密传输是需要前端配合的,毕竟请求接口的是前端。所以前端也需要知道加密模式、填充方式与偏移量等基础知识。同时需要商定是对称加密还是非对称加密。
前端加密快速上手
1.2、后端必看
点开链接:按照目录浏览
后端加密快速上手
2、后端注解实现
好的,你已经了解了上面的基础知识,接下来你准备好为你项目编写一个牛x的功能了吗?
2.1、实现流程
2.2、开始实现
开始前我们现在application.yml 中增加一个配置
# 配置是否开启AOP参数加密解密,不配置默认为true
MySecret:
isSecret: true
2.2.1、 pom
<!-- AOP切面依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<!-- lombok工具 -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- json操作类 -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>1.2.52.sec06</version>
</dependency>
<!-- String工具包 -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
</dependency>
2.2.2、 注解
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Secret {
Class value();
// 参数类中传递加密数据的属性名,默认encryptStr
String encryptStrName() default "encryptStr";
}
2.2.3、 加密工具类
注意AES_KEY需要更改,切记和前端商量
import com.alibaba.fastjson.JSON;
import com.example.undertwo.entity.vo.UserVO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.Getter;
import lombok.NoArgsConstructor;
import org.apache.commons.lang3.StringUtils;
import javax.crypto.Cipher;
import javax.crypto.KeyGenerator;
import javax.crypto.NoSuchPaddingException;
import javax.crypto.SecretKey;
import javax.crypto.spec.IvParameterSpec;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.security.*;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.KeySpec;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Base64.Decoder;
import java.util.Base64.Encoder;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.ConcurrentHashMap;
/**
* @author by Guoshun
* @version 1.0.0
* @description 支持AES、DES、RSA加密、数字签名以及生成对称密钥和非对称密钥对
* @date 2024/5/8 15:04
*/
public class CryptoUtils {
public static final String AES_KEY = "IQcoqkg==";
private static final Charset DEFAULT_CHARSET = StandardCharsets.UTF_8;
private static final Encoder BASE64_ENCODER = Base64.getEncoder();
private static final Decoder BASE64_DECODER = Base64.getDecoder();
private static final Map<Algorithm, KeyFactory> KEY_FACTORY_CACHE = new ConcurrentHashMap<>();
private static final Map<Algorithm, Cipher> CIPHER_CACHE = new HashMap<>();
/**
* 对称加密
*
* @param secretKey 密钥
* @param iv 加密向量,只有CBC模式才支持,如果是CBC则必传
* @param plainText 明文
* @param algorithm 对称加密算法,如AES、DES
* @return
* @throws Exception
*/
public static String encryptSymmetrically(String secretKey, String iv, String plainText, Algorithm algorithm) throws Exception {
SecretKey key = decodeSymmetricKey(secretKey, algorithm);
IvParameterSpec ivParameterSpec = StringUtils.isBlank(iv) ? null : decodeIv(iv);
byte[] plainTextInBytes = plainText.getBytes(DEFAULT_CHARSET);
byte[] ciphertextInBytes = transform(algorithm, Cipher.ENCRYPT_MODE, key, ivParameterSpec, plainTextInBytes);
return BASE64_ENCODER.encodeToString(ciphertextInBytes);
}
/**
* 对称解密
*
* @param secretKey 密钥
* @param iv 加密向量,只有CBC模式才支持,如果是CBC则必传
* @param ciphertext 密文
* @param algorithm 对称加密算法,如AES、DES
* @return
* @throws Exception
*/
public static String decryptSymmetrically(String secretKey, String iv, String ciphertext, Algorithm algorithm) throws Exception {
SecretKey key = decodeSymmetricKey(secretKey, algorithm);
IvParameterSpec ivParameterSpec = StringUtils.isBlank(iv) ? null : decodeIv(iv);
byte[] ciphertextInBytes = BASE64_DECODER.decode(ciphertext);
byte[] plainTextInBytes = transform(algorithm, Cipher.DECRYPT_MODE, key, ivParameterSpec, ciphertextInBytes);
return new String(plainTextInBytes, DEFAULT_CHARSET);
}
/**
* 将密钥进行Base64位解码,重新生成SecretKey实例
*
* @param secretKey 密钥
* @param algorithm 算法
* @return
*/
private static SecretKey decodeSymmetricKey(String secretKey, Algorithm algorithm) {
byte[] key = BASE64_DECODER.decode(secretKey);
return new SecretKeySpec(key, algorithm.getName());
}
private static IvParameterSpec decodeIv(String iv) {
byte[] ivInBytes = BASE64_DECODER.decode(iv);
return new IvParameterSpec(ivInBytes);
}
private static byte[] transform(Algorithm algorithm, int mode, Key key, IvParameterSpec iv, byte[] msg) throws Exception {
Cipher cipher = CIPHER_CACHE.get(algorithm);
// double check,减少上下文切换
if (cipher == null) {
synchronized (CryptoUtils.class) {
if ((cipher = CIPHER_CACHE.get(algorithm)) == null) {
cipher = determineWhichCipherToUse(algorithm);
CIPHER_CACHE.put(algorithm, cipher);
}
cipher.init(mode, key, iv);
return cipher.doFinal(msg);
}
}
synchronized (CryptoUtils.class) {
cipher.init(mode, key, iv);
return cipher.doFinal(msg);
}
}
private static Cipher determineWhichCipherToUse(Algorithm algorithm) throws NoSuchAlgorithmException, NoSuchPaddingException {
Cipher cipher;
String transformation = algorithm.getTransformation();
// 官方推荐的transformation使用algorithm/mode/padding组合,SunJCE使用ECB作为默认模式,使用PKCS5Padding作为默认填充
if (StringUtils.isNotEmpty(transformation)) {
cipher = Cipher.getInstance(transformation);
} else {
cipher = Cipher.getInstance(algorithm.getName());
}
return cipher;
}
/**
* 算法分为加密算法和签名算法,更多算法实现见:<br/>
* <a href="https://docs.oracle.com/javase/8/docs/technotes/guides/security/StandardNames.html#impl">jdk8中的标准算法</a>
*/
public static class Algorithm {
@Getter
private String name;
@Getter
private String transformation;
@Getter
private int keySize;
public Algorithm(String name, int keySize) {
this(name, null, keySize);
}
public Algorithm(String name, String transformation, int keySize) {
this.name = name;
this.transformation = transformation;
this.keySize = keySize;
}
public interface Encryption {
Algorithm AES_ECB_PKCS5 = new Algorithm("AES", "AES/ECB/PKCS5Padding", 128);
Algorithm AES_CBC_PKCS5 = new Algorithm("AES", "AES/CBC/PKCS5Padding", 128);
Algorithm DES_ECB_PKCS5 = new Algorithm("DES", "DES/ECB/PKCS5Padding", 56);
Algorithm DES_CBC_PKCS5 = new Algorithm("DES", "DES/CBC/PKCS5Padding", 56);
Algorithm RSA_ECB_PKCS1 = new Algorithm("RSA", "RSA/ECB/PKCS1Padding", 1024);
Algorithm DSA = new Algorithm("DSA", 1024);
}
public interface Signing {
Algorithm SHA1WithDSA = new Algorithm("SHA1withDSA", 1024);
Algorithm SHA1WithRSA = new Algorithm("SHA1WithRSA", 2048);
Algorithm SHA256WithRSA = new Algorithm("SHA256WithRSA", 2048);
}
}
@Data
@NoArgsConstructor
@AllArgsConstructor
public static class AsymmetricKeyPair {
private String publicKey;
private String privateKey;
}
/**
* 加密
* @param jsonStr
* @return
* @throws Exception
*/
public static String encrypt(String jsonStr) throws Exception {
return CryptoUtils.encryptSymmetrically(AES_KEY, null, jsonStr, Algorithm.Encryption.AES_ECB_PKCS5);
}
/**
* 解密
* @param jsonStr
* @return
* @throws Exception
*/
public static String decrypt(String jsonStr) throws Exception {
return CryptoUtils.decryptSymmetrically(AES_KEY, null, jsonStr, Algorithm.Encryption.AES_ECB_PKCS5);
}
public static void main(String[] args) throws Exception {
UserVO userVO = new UserVO();
userVO.setId(123);
userVO.setName("阿萨");
String encrypt = encrypt(JSON.toJSONString(userVO));
System.out.println("加密后:" + encrypt);
String decrypt = decrypt(encrypt);
UserVO userVO1 = JSON.parseObject(decrypt, UserVO.class);
System.out.println("解密后" + userVO1.toString());
}
2.2.3、 定义切面(注意切点包名)
import com.alibaba.fastjson.JSON;
import com.example.undertwo.config.Secret;
import com.example.undertwo.entity.result.ResultVO;
import com.example.undertwo.utils.CryptoUtils;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.lang.reflect.Type;
/**
* @author by Guoshun
* @version 1.0.0
* @description 切面加密解密
* @date 2024/5/9 14:14
*/
@Aspect
@Component
@Slf4j
public class SecretAOP {
// 是否进行加密解密,通过配置文件注入(不配置默认为true)
@Value("${MySecret.isSecret:true}")
boolean isSecret;
// 定义切点,使用了@Secret注解的类 或 使用了@Secret注解的方法
@Pointcut("@within(com.example.undertwo.config.Secret) || @annotation(com.example.undertwo.config.Secret)")
public void pointcut(){}
// 环绕切面
@Around("pointcut()")
public ResultVO around(ProceedingJoinPoint point){
ResultVO result = null;
// 获取被代理方法参数
Object[] args = point.getArgs();
// 获取被代理对象
Object target = point.getTarget();
// 获取通知签名
MethodSignature signature = (MethodSignature )point.getSignature();
try {
// 获取被代理方法
Method pointMethod = target.getClass().getMethod(signature.getName(), signature.getParameterTypes());
// 获取被代理方法上面的注解@Secret
Secret secret = pointMethod.getAnnotation(Secret.class);
// 被代理方法上没有,则说明@Secret注解在被代理类上
if(secret==null){
secret = target.getClass().getAnnotation(Secret.class);
}
if(secret!=null){
// 获取注解上声明的加解密类
Class clazz = secret.value();
// 获取注解上声明的加密参数名
String encryptStrName = secret.encryptStrName();
for (int i = 0; i < args.length; i++) {
// 如果是clazz类型则说明使用了加密字符串encryptStr传递的加密参数
if(clazz.isInstance(args[i])){
Object cast = clazz.cast(args[i]); //将args[i]转换为clazz表示的类对象
// 通过反射,执行getEncryptStr()方法,获取加密数据
Method method = clazz.getMethod(getMethedName(encryptStrName));
// 执行方法,获取加密数据
String encryptStr = (String) method.invoke(cast);
// 加密字符串是否为空
if(StringUtils.isNotBlank(encryptStr)){
// 解密
String json = CryptoUtils.decrypt(encryptStr);
// 转换vo
args[i] = JSON.parseObject(json, (Type) args[i].getClass());
}
}
// 其他类型,比如基本数据类型、包装类型就不使用加密解密了
}
}
// 执行请求
result = (ResultVO) point.proceed(args);
// 判断配置是否需要返回加密
if(isSecret){
// 获取返回值json字符串
String jsonString = JSON.toJSONString(result.getData());
// 加密
String s = CryptoUtils.encrypt(jsonString);
result.setData(s);
}
} catch (NoSuchMethodException e) {
log.error("@Secret注解指定的类没有字段:encryptStr,或encryptStrName参数字段不存在");
e.printStackTrace();
} catch (Throwable throwable) {
throwable.printStackTrace();
}
return result;
}
// 转化方法名
private String getMethedName(String name){
String first = name.substring(0,1);
String last = name.substring(1);
first = StringUtils.upperCase(first);
return "get" + first + last;
}
}
2.2.4、 定义加密基类与各种入参VO
import lombok.Data;
/**
* @author by Guoshun
* @version 1.0.0
* @description 基础的实体类
* @date 2024/5/9 14:00
*/
@Data
public class BaseVO {
private String encryptStr;
}
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author by Guoshun
* @version 1.0.0
* @description 部门类
* @date 2024/5/9 14:16
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptVO{
private Integer id;
private String deptName;
// 自己实现的一个参数,用来给前端传递加密字符串
private String encryptJson;
}
import com.example.undertwo.entity.BaseVO;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @author by Guoshun
* @version 1.0.0
* @description TODO
* @date 2024/5/9 14:05
*/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserVO extends BaseVO {
private Integer id;
private String name;
}
2.2.5、写两个Controller
import com.example.undertwo.config.Secret;
import com.example.undertwo.entity.result.ResultVO;
import com.example.undertwo.entity.vo.DeptVO;
import org.springframework.web.bind.annotation.*;
/**
* @author by Guoshun
* @version 1.0.0
* @description 部门类
* @date 2024/5/9 14:17
*/
@RestController
@RequestMapping("dept")
public class DeptController {
@GetMapping("getDeptName/{id}")
public ResultVO getDeptName(@PathVariable("id") String id){
return new ResultVO(0,"查询成功","财务部" + id);
}
// 注解在方法上,并传递了encryptStrName自己定义的加密字符串名称encryptJson
@Secret(value = DeptVO.class,encryptStrName = "encryptJson")
@PostMapping("addDept")
public ResultVO addDept(@RequestBody DeptVO dept){
return new ResultVO(0,"新增成功",dept);
}
}
import com.example.undertwo.config.Secret;
import com.example.undertwo.entity.BaseVO;
import com.example.undertwo.entity.result.ResultVO;
import com.example.undertwo.entity.vo.UserVO;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* @author by Guoshun
* @version 1.0.0
* @description 用户控制类
* @date 2024/5/9 14:10
*/
@Secret(BaseVO.class) //接口参数和返回要进行加解密
@RestController
@RequestMapping("user")
public class UserController {
//采用内部类的实例代码块方式初始化map
HashMap<Integer, UserVO> userMap = new HashMap<Integer, UserVO>(){
{
put(1,new UserVO(1,"张三"));
put(2,new UserVO(2,"李四"));
put(3,new UserVO(3,"王五"));
}
};
// 通过id查询用户
@GetMapping("getUserName/{id}")
public ResultVO getUserName(@PathVariable("id") Integer id){
return new ResultVO(0,"查询成功",userMap.get(id));
}
// 通过name查询用户id
@GetMapping("getUserId")
public ResultVO getUserId(@RequestParam String name){
Iterator<Map.Entry<Integer, UserVO>> iterator = userMap.entrySet().iterator();
UserVO u = null;
while (iterator.hasNext()){
Map.Entry<Integer, UserVO> entry = iterator.next();
if(entry.getValue().getName().equals(name)){
u = entry.getValue();
break;
}
}
return new ResultVO(0,"查询成功",u);
}
// 新增用户
@PostMapping("addUser")
public ResultVO addUser(@RequestBody UserVO user){
return new ResultVO(0,"新增成功",user);
}
// 更改用户
@PostMapping("updateUser")
public ResultVO updateUser(@RequestBody UserVO user) throws Throwable {
if(user==null||user.getId()==null){
throw new NullPointerException();
}else{
return new ResultVO(0,"修改成功",user);
}
}
}
3、参考文章
[1]:springboot 自定义注解使用AOP实现请求参数解密以及响应数据加密
[2]:前端CryptoJS和Java后端数据互相加解密(AES)