SpringBoot自定義註解使用AOP實現請求參數解密以及響應數據加密

SpringBoot自定義註解使用AOP實現請求參數解密以及響應數據加密

一、前言

本篇文章將依託與SpringBoot平臺,自定義註解用來標識接口請求是否實現加密解密。使用AOP切面來具體操作解密加密,實現對源代碼的低耦合,不在原基礎上做很大的改動。

本篇文章的所有示例,都上傳到我的github中,歡迎大家拉取測試,歡迎star github

實現要求:

  1. 自定義一個註解@Secret,用來標識需要實現加密解密
    • 作用在Controller類上,表示此Controller類的所有接口都實現加密解密
    • 作用來單一方法上,表示此接口方法需要實現加密解密
  2. 使用AOP切面編程實現
    • 在接口方法執行之前將前端的加密參數解密並重新賦給接口參數
    • 在接口方法響應之後,將返回的數據進行加密返回
  3. 在配置文件中配置,是否開啓全局的加密解密操作

實現流程:
在這裏插入圖片描述

  1. 前端請求的接口將請求參數json通過AES加密生成加密字符串,然後將加密字符串通過名爲encyptStr字段傳遞給後端。
  2. AOP前置方法攔截,將encyptStr字符串通過AES解密得到原始請求參數json,將json映射爲請求方法的參數對象User。
  3. 接口通過參數成功響應,並將響應數據直接返回。
  4. AOP後置方式攔截,將響應參數data字段裏的數據AES加密,並返回給前端
  5. 前端收到請求響應,通過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後纔會起作用。

  1. @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;
}
  1. @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;
    }
}
  1. 定義字段isSecret通過@Value從配置文件中注入true/false,來規定是否執行全局的AOP加密解密,在開發測試環境我們可以配置爲不加密解密,方便查找錯誤。當項目上線運行時,可以配置true,實現加密操作。默認不配置爲true

application.yml

# 配置是否開啓AOP參數加密解密,不配置默認爲true
isSecret: true
  1. 定義了AOP的切入點@Pointcut("@within(Secret)||@annotation(Secret)"),@Within表示匹配類上的指定註解,@annotation表示匹配方法上的指定註解
  2. 使用環繞通知切面來實現加密和解密。point.proceed(args);表示執行請求方法,此方法之前表示前置切點,此方法之後表示後置切點。args是已經解密後的參數重新賦值傳入。
  3. 通過被代理的接口方法反射找到@Secret註解是在類上還是在方法上,並獲取@Secret註解類對象,找到傳入的value和encryptStrName
  4. 我默認只有是單一參數的接口使用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結尾的請求文件,我們可以像編輯配置文件一樣,編輯請求,然後點擊執行!!每個請求以三個#好分割,並可以分別執行。
在這裏插入圖片描述

  1. 編寫好我的請求文件,執行addUser方法試試。
    在這裏插入圖片描述
    執行成功!看到沒,請求傳遞的加密參數,經過AOP解密傳遞給addUser方法,並返回給AOP加密給前端,並不是簡單的直接傳遞噢,中間經歷瞭解密再加密的過程。

  2. 執行addDept方法測試一下,這個接口我們吧@Secret註解在了接口方法上,並傳遞了自定義的加密字段encryptJson。所以前端傳遞的加密參數名應該爲encryptJson
    在這裏插入圖片描述

AESUtils就是一個普通的AES加密工具,在此沒有展示出來,需要的可以去我的 github 獲取,歡迎star

三、總結

  1. 此篇文章實現,使用了自定註解的功能,其實自定義註解無非就是這樣用,通過反射來獲取操作和標識作用。
  2. AOP面向切面,其實SpringBoot的自動配置就是使用的AOP切面來實現的,我們通過自己實現一個切面,可以瞭解到整個執行流程和反射的應用。
  3. 建議需要加密操作的接口參數都用vo對象來接收,因爲切面中只能獲取接口參數,不能獲取到接口不存在的參數名。所以前端在傳遞加密參數名的時候,一定要保證接口參數具有相同的名稱來接收,解密後再賦值其他參數。
  4. AOP切面還有很多的實現,比如@Before @After @AfterReturning等,在此不過多講解,本片只使用了@Around環繞通知。你也可以將解密加密分別拆分到@Before和@After中去執行。
  5. rest-api.http文件是idea HTTPClient工具生成的api測試文件,默認沒保存在項目中,我已經將此文件放在了test目錄下,大家可以查看。
  6. 本片文章參考了博客 https://blog.csdn.net/lmb55/article/details/82470388
  7. 本篇文章的所有示例,都上傳到我的github中,歡迎大家拉取測試,歡迎star github
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章