SpringBoot自定義註解使用AOP實現請求參數解密以及響應數據加密
一、前言
本篇文章將依託與SpringBoot平臺,自定義註解用來標識接口請求是否實現加密解密。使用AOP切面來具體操作解密加密,實現對源代碼的低耦合,不在原基礎上做很大的改動。
本篇文章的所有示例,都上傳到我的github中,歡迎大家拉取測試,歡迎star github
實現要求:
- 自定義一個註解@Secret,用來標識需要實現加密解密
- 作用在Controller類上,表示此Controller類的所有接口都實現加密解密
- 作用來單一方法上,表示此接口方法需要實現加密解密
- 使用AOP切面編程實現
- 在接口方法執行之前將前端的加密參數解密並重新賦給接口參數
- 在接口方法響應之後,將返回的數據進行加密返回
- 在配置文件中配置,是否開啓全局的加密解密操作
實現流程:
- 前端請求的接口將請求參數json通過AES加密生成加密字符串,然後將加密字符串通過名爲encyptStr字段傳遞給後端。
- AOP前置方法攔截,將encyptStr字符串通過AES解密得到原始請求參數json,將json映射爲請求方法的參數對象User。
- 接口通過參數成功響應,並將響應數據直接返回。
- AOP後置方式攔截,將響應參數data字段裏的數據AES加密,並返回給前端
- 前端收到請求響應,通過code判斷請求是否成功,AES加密data字段得到需要的數據。
二、實現操作
1. 創建SpringBoot項目
創建一個SpringBoot項目,導入必要的maven依賴。
- 使用AOP切面需要導入AOP的啓動器
- lombok是一個通過註解簡化代碼的工具,在idea中使用需要安裝lombok插件
- json轉換工具,apache工具類
pom.xml
<!-- web依賴 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 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>
<version>3.9</version>
</dependency>
2. 自定註解@Secret
我們通過自定義的註解,來標識類或接口,告訴AOP哪些類或方法需要執行加密解密操作,更加的靈活。
Secret.java
package com.agger.springbootaopdemo.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
/**
* @classname: Secret
* @description: 自定義註解,用來標識請求類 或者方法是否使用AOP加密解密
* @author chenhx
* @date 2019-12-05 13:48:03
*/
@Target({ElementType.TYPE,ElementType.METHOD}) // 可以作用在類上和方法上
@Retention(RetentionPolicy.RUNTIME) // 運行時起作用
public @interface Secret {
// 參數類(用來傳遞加密數據,只有方法參數中有此類或此類的子類纔會執行加解密)
Class value();
// 參數類中傳遞加密數據的屬性名,默認encryptStr
String encryptStrName() default "encryptStr";
}
自定義註解很簡單,只需要確定註解的作用位置和運行時機。其中有兩個變量value和encryptStrName。
value沒有默認值,是必傳的參數,用來表示需要加解密的參數類或父類。AOP中或用到。
encryptStrName默認值爲"encryptStr",用來表示前端傳遞的加密參數名稱是什麼,value類中必須存在此字段
3. Controller中使用
定義好@Secret註解後,我們就可以在Controller中使用了,不過現在只相當於是一個標註,還沒有起任何作用,需要我們再定義好AOP後纔會起作用。
- @Secret註解作用來類上
UserController.java
package com.agger.springbootaopdemo.controller;
import com.agger.springbootaopdemo.annotation.Secret;
import com.agger.springbootaopdemo.vo.BaseVO;
import com.agger.springbootaopdemo.vo.ResultVO;
import com.agger.springbootaopdemo.vo.UserVO;
import org.springframework.web.bind.annotation.*;
import java.util.HashMap;
import java.util.Iterator;
import java.util.Map;
/**
* @program: UserController
* @description: 用戶控制類
* @author: chenhx
* @create: 2019-12-03 15:22
**/
@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);
}
}
}
@Secret(BaseVO.class)定義在了UserController類上,表示整個類下面的方法都會實現AOP加密解密。BaseVO是所有vo的基類,其中只定義了一個字段encryptStr,也就是前端傳遞的加密參數字段。
BaseVO.java
package com.agger.springbootaopdemo.vo;
import lombok.Data;
/**
* @program: BaseVO
* @description: 基類
* @author: chenhx
* @create: 2019-12-05 15:15
**/
@Data
public class BaseVO {
// 加密密文
private String encryptStr;
}
UserVO.java
package com.agger.springbootaopdemo.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @program: User
* @description: 用戶
* @author: chenhx
* @create: 2019-12-03 15:31
**/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class UserVO extends BaseVO{
private Integer id;
private String name;
}
- @Secret註解作用在方法上,表示只有此方法才需要執行AOP加密解密
DeptController.java
package com.agger.springbootaopdemo.controller;
import com.agger.springbootaopdemo.annotation.Secret;
import com.agger.springbootaopdemo.vo.DeptVO;
import com.agger.springbootaopdemo.vo.ResultVO;
import org.springframework.web.bind.annotation.*;
/**
* @program: Dept
* @description: 部門類
* @author: chenhx
* @create: 2019-12-03 15:26
**/
@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);
}
}
DeptVO類沒有繼承BaseVO類,自己寫了一個前端需要傳遞的加密字符串字段,並傳遞給註解。ResultVO爲接口響應類。
DeptVO.java
package com.agger.springbootaopdemo.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @program: Dept
* @description: 部門類
* @author: chenhx
* @create: 2019-12-03 15:32
**/
@Data
@NoArgsConstructor
@AllArgsConstructor
public class DeptVO{
private Integer id;
private String deptName;
// 自己實現的一個參數,用來給前端傳遞加密字符串
private String encryptJson;
}
ResultVO.java
package com.agger.springbootaopdemo.vo;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
/**
* @program: ResultVO
* @description: 響應類
* @author: chenhx
* @create: 2019-12-03 15:34
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class ResultVO {
private Integer code;
private String msg;
private Object data;
}
4. 定義AOP切面
萬事具備,咱只欠定義一個AOP切面來實現加密和解密操作了。
SecretAOPController.java
package com.agger.springbootaopdemo.aop;
import com.agger.springbootaopdemo.annotation.Secret;
import com.agger.springbootaopdemo.utils.AESUtils;
import com.agger.springbootaopdemo.vo.ResultVO;
import com.alibaba.fastjson.JSON;
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;
/**
* @program: SecretAOPController
* @description: 切面加密解密
* @author: chenhx
* @create: 2019-12-05 13:43
**/
@Aspect
@Component
@Slf4j
public class SecretAOPController {
// 是否進行加密解密,通過配置文件注入(不配置默認爲true)
@Value("${isSecret:true}")
boolean isSecret;
// 定義切點,使用了@Secret註解的類 或 使用了@Secret註解的方法
@Pointcut("@within(com.agger.springbootaopdemo.annotation.Secret) || @annotation(com.agger.springbootaopdemo.annotation.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 = AESUtils.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 = AESUtils.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;
}
}
- 定義字段isSecret通過@Value從配置文件中注入true/false,來規定是否執行全局的AOP加密解密,在開發測試環境我們可以配置爲不加密解密,方便查找錯誤。當項目上線運行時,可以配置true,實現加密操作。默認不配置爲true
application.yml
# 配置是否開啓AOP參數加密解密,不配置默認爲true
isSecret: true
- 定義了AOP的切入點@Pointcut("@within(Secret)||@annotation(Secret)"),@Within表示匹配類上的指定註解,@annotation表示匹配方法上的指定註解
- 使用環繞通知切面來實現加密和解密。point.proceed(args);表示執行請求方法,此方法之前表示前置切點,此方法之後表示後置切點。args是已經解密後的參數重新賦值傳入。
- 通過被代理的接口方法反射找到@Secret註解是在類上還是在方法上,並獲取@Secret註解類對象,找到傳入的value和encryptStrName
- 我默認只有是單一參數的接口使用vo接收參數的慘進行加密解密,如果是單一參數比如
getUser(Integer id) { ... }
這種形式的接口建議全部用vo來接收,並且繼承BaseVO基類,具有encryptStr字段,或者你自定義的加密字段,在你使用@Secret的時候,別忘了填寫
// 註解在方法上,並傳遞了encryptStrName自己定義的加密字符串名稱
@Secret(value = DeptVO.class,encryptStrName = "encryptJson")
5. 執行效果
我們可以通過postman或其他前端工具來調取接口,在這裏我使用的是idea自帶的接口調試工具。
選擇工具欄的Tools > HTTP Client > TestRESTfull Web Service
點擊後,就會在底部打開這個工具,跟postman一樣,可以編輯請求方法,請求參數等等。
不過,看點上面的提示沒有?This REST Client is deprecated.Try our new HTTP Client in the editor
已經過時了,請使用新的客戶端編輯,點擊右邊的按鈕,idea就會給我們生成一個.http結尾的請求文件,我們可以像編輯配置文件一樣,編輯請求,然後點擊執行!!每個請求以三個#好分割,並可以分別執行。
-
編寫好我的請求文件,執行addUser方法試試。
執行成功!看到沒,請求傳遞的加密參數,經過AOP解密傳遞給addUser方法,並返回給AOP加密給前端,並不是簡單的直接傳遞噢,中間經歷瞭解密再加密的過程。 -
執行addDept方法測試一下,這個接口我們吧@Secret註解在了接口方法上,並傳遞了自定義的加密字段encryptJson。所以前端傳遞的加密參數名應該爲encryptJson
AESUtils就是一個普通的AES加密工具,在此沒有展示出來,需要的可以去我的 github 獲取,歡迎star
三、總結
- 此篇文章實現,使用了自定註解的功能,其實自定義註解無非就是這樣用,通過反射來獲取操作和標識作用。
- AOP面向切面,其實SpringBoot的自動配置就是使用的AOP切面來實現的,我們通過自己實現一個切面,可以瞭解到整個執行流程和反射的應用。
- 建議需要加密操作的接口參數都用vo對象來接收,因爲切面中只能獲取接口參數,不能獲取到接口不存在的參數名。所以前端在傳遞加密參數名的時候,一定要保證接口參數具有相同的名稱來接收,解密後再賦值其他參數。
- AOP切面還有很多的實現,比如@Before @After @AfterReturning等,在此不過多講解,本片只使用了@Around環繞通知。你也可以將解密加密分別拆分到@Before和@After中去執行。
- rest-api.http文件是idea HTTPClient工具生成的api測試文件,默認沒保存在項目中,我已經將此文件放在了test目錄下,大家可以查看。
- 本片文章參考了博客 https://blog.csdn.net/lmb55/article/details/82470388
- 本篇文章的所有示例,都上傳到我的github中,歡迎大家拉取測試,歡迎star github