SpringBoot AOP切面编程 使用案例

参考资料

  1. Springboot AOP实现指定敏感字段数据加密 (数据加密篇 二)
  2. 【SpringBoot-3】切面AOP实现权限校验:实例演示与注解全解
  3. 【小家Spring】Spring AOP中@Pointcut切入点表达式最全面使用介绍
  4. AOP编程过程中的Signature接口
  5. Spring Boot 使用AOP切面实现后台日志管理模块

本篇文章核心思想均摘自参考资料所示的博客,详情请参阅其博客。



一. 知识储备

1.1 图示

在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

1.2 图示解释

  • Pointcut:切点,决定处理如权限校验、日志记录等在何处切入业务代码中(即织入切面)。
    切点分为execution方式和annotation方式。
    • execution可以用路径表达式指定哪些类织入切面
    • annotation可以指定被哪些注解修饰的代码织入切面。
  • Advice:处理,包括处理时机和处理内容。
    • 处理内容就是要做什么事,比如校验权限和记录日志。
    • 处理时机就是在什么时机执行处理内容,分为前置处理(即业务代码执行前)、后置处理(业务代码执行后)等。
  • Aspect:切面,即Pointcut和Advice。
  • Joint point:连接点,是程序执行的一个点。例如,一个方法的执行或者一个异常的处理。在 Spring AOP 中,一个连接点总是代表一个方法执行。
  • Weaving:织入,就是通过动态代理,在目标对象方法中执行处理内容的过程。

二. 前期准备1

2.1 POM文件

<dependency>
	<groupId>org.springframework.boot</groupId>
	<artifactId>spring-boot-starter-aop</artifactId>
</dependency>

2.2 自定义注解

  • 该自定义注解作用于实体类的属性上
  • 自定义注解和接口类似,可以在接口中定义枚举类
  • 该注解用来标记属性值被自动添加为当前Date或者登录用户ID
import java.lang.annotation.*;

@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface FieldValueAnnotation {

    Category type();
	
	// 自定义注解中定义的枚举类
    enum Category {
        dateField,
        userIdField,
    }
}

2.3 form基类

  • 基类中的属性都是各个页面共通的属性
  • 其中createDate,updateDate,loginUserId都是和数据库交互所必须的属性,其内容通过AOP切面来填充,不需要每个页面写 .set() 代码来手动填充。
  • 向前台返回json数据的时候,部分属性没必要返回,使用@JsonIgnore注解用来忽略。
import com.fasterxml.jackson.annotation.JsonIgnore;
import lombok.Data;

import java.util.Date;
import java.util.Map;

@Data
public class BaseForm {
	
	// 页面ID
    private String pageId;
	
	// 登录用户ID
    @FieldValueAnnotation(type = FieldValueAnnotation.Category.userIdField)
    private String loginUserId;
	
	// 创建日期
    // 返回前台json数据的时候,忽略此字段
    @JsonIgnore
    @FieldValueAnnotation(type = FieldValueAnnotation.Category.dateField)
    private Date createDate;
	
	// 更新日期
    @JsonIgnore
    @FieldValueAnnotation(type = FieldValueAnnotation.Category.dateField)
    private Date updateDate;
	
	// 语言code
    private String languageCode;
	
	// 画面项目Map
    private Map<String, String> itemMap;
}

2.4 提供业务数据

  • 模拟和数据库进行交互的Mapper,提供数据库数据。
import lombok.Builder;
import lombok.Data;
import org.springframework.stereotype.Service;
import org.springframework.util.ObjectUtils;

import java.util.Arrays;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
public class CommonService {

    // 支持的所有语言列表
    public List<LanguageEntity> getLanguageList() {

        return Arrays.asList(
                LanguageEntity.builder().code("jp").language("日语").build(),
                LanguageEntity.builder().code("zh").language("中文").build(),
                LanguageEntity.builder().code("en").language("英文").build()
        );
    }
    
    // 系统支持语言实体类
    @Data
    @Builder
    public static class LanguageEntity {

        private String code;

        private String language;
    }

    // 模拟数据库查询到的数据
    private Map<String, Map<String, Map<String, String>>> getItemMap() {

        // j002页面的国际化数据
        Map<String, String> zhMapJ002 = new HashMap<>() {{
            put("name", "姓名");
            put("age", "年龄");
            put("language", "语言");
            put("btnName", "提交");
        }};

        Map<String, String> jpMapJ002 = new HashMap<>() {{
            put("name", "名前");
            put("age", "年齢");
            put("language", "言語");
            put("btnName", "コミット");
        }};

        Map<String, String> enMapJ002 = new HashMap<>() {{
            put("name", "name");
            put("age", "age");
            put("language", "language");
            put("btnName", "commit");
        }};

        Map<String, Map<String, String>> j002Map = new HashMap<>() {{
            put("zh", zhMapJ002);
            put("jp", jpMapJ002);
            put("en", enMapJ002);
        }};

        // j003页面的国际化数据
        Map<String, String> zhMapJ003 = new HashMap<>() {{
            put("fruit", "苹果");
            put("drink", "可乐");
            put("language", "语言");
        }};

        Map<String, String> jpMapJ003 = new HashMap<>() {{
            put("fruit", "リンゴ");
            put("drink", "コーラ");
            put("language", "言語");
        }};

        Map<String, String> enMapJ003 = new HashMap<>() {{
            put("fruit", "apple");
            put("drink", "cola");
            put("language", "language");
        }};

        Map<String, Map<String, String>> j003Map = new HashMap<>() {{
            put("zh", zhMapJ003);
            put("jp", jpMapJ003);
            put("en", enMapJ003);
        }};

        // 最终的数据
        return new HashMap<>() {{
            put("j002", j002Map);
            put("j003", j003Map);
        }};
    }

    // 根据地区和画面id获取画面项目
    public Map<String, String> getItemByLocal(String local, String pageId) {
        return this.getItemMap().get(pageId).get(local);
    }
    
    // 模拟数据库获取下拉列表内容
    private Map<String, List<SelectItem>> getSelectList() {

        List<SelectItem> zhSelectItems = Arrays.asList(
                SelectItem.builder().code("c001").name("汽车").build(),
                SelectItem.builder().code("c002").name("电脑").build(),
                SelectItem.builder().code("c003").name("手机").build()
        );

        List<SelectItem> jpSelectItems = Arrays.asList(
                SelectItem.builder().code("c001").name("車").build(),
                SelectItem.builder().code("c002").name("パソコン").build(),
                SelectItem.builder().code("c003").name("スマホ").build()
        );

        List<SelectItem> enSelectItems = Arrays.asList(
                SelectItem.builder().code("c001").name("car").build(),
                SelectItem.builder().code("c002").name("computer").build(),
                SelectItem.builder().code("c003").name("phone").build()
        );

        return new HashMap<>(){{
            put("zh", zhSelectItems);
            put("jp", jpSelectItems);
            put("en", enSelectItems);
        }};
    }
    
    // 根据画面local地区,获取下列列表内容
    public List<SelectItem> getSelectListByLocal(String local) {
        if (ObjectUtils.isEmpty(local)) {
            local = "jp";
        }
        return this.getSelectList().get(local);
    }
    
    // 画面下拉列表实体类
    @Data
    @Builder
    public static class SelectItem {

        private String code;

        private String name;
    }
}

2.5 封装返回前台数据的实体类

  • private Object entity;属性用来存放向前台返回的数据,因为每个页面的返回的实体类的类型不同,所以此处使用了Object类型。
import lombok.Data;
import lombok.EqualsAndHashCode;

import java.io.Serializable;

@Data
@EqualsAndHashCode(callSuper = false)
public class ResultEntity implements Serializable {

    private Boolean result;

    private Object entity;

    private ResultEntity(Boolean result, Object entity) {
        this.result = result;
        this.entity = entity;
    }

    public static ResultEntity ok(Object entity) {
        return new ResultEntity(true, entity);
    }
}

三. AOP实现页面国际化 + 共通属性值自动封装案例

3.1 页面

⏹登录页面 ⇒ j001.html

<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>
    <h1>登录页面!!!</h1>
    <button id="btn">点击跳转到国际化页面</button>
</div>
</body>
<script type="text/javascript" th:src="@{/js/public/jquery-3.6.0.min.js}"></script>
<script>
    $("#btn").click(() => {
        window.location = "/j002/init";
    });
</script>
</html>

⏹登录之后显示的国际化页面 ⇒ j002.html

  • 当前页面切换下拉列表值的时候,改变页面各个项目的语言显示。
  • 提交数据到后台,后台的AOP切面会给共通属性添加属性值。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
<div>

    <label>
        <span id="language">[[${itemMap.language}]]</span>
        <select id="langList">
            <th:block th:each="content : ${langList}">
                <option th:value="${content.code}">[[${content.language}]]</option>
            </th:block>
        </select>
    </label>
    <hr>

    <span id="name">[[${itemMap["name"]}]]</span><br>
    <span id="age">[[${itemMap["age"]}]]</span>

    <hr>
    <select id="selectList" th:object="${entity}">
        <th:block th:each="selectEntity : *{selectList}">
            <option th:value="${selectEntity.code}">[[${selectEntity.name}]]</option>
        </th:block>
    </select>

    <button id="btnName">[[${itemMap["btnName"]}]]</button>
</div>
</body>
<script type="text/javascript" th:src="@{/js/public/jquery-3.6.0.min.js}"></script>
<script th:inline="javascript">
    // 打印当前页面的项目信息
    const itemInfo = [[${itemMap}]];
    console.log(itemInfo);

    // 打印当前画面的id
    const pageId = /*[[${pageId}]]*/ '';
    console.log(pageId);
</script>
<script>
    $(function() {
        eventBind();
    });

    function eventBind() {
		
		// 当语言下拉列表切换语言的时候,修改页面上的语言项目和下拉列表项目
        $("#langList").change(function({target: {value}}) {

            const data = {
                languageCode: value,
                pageId,
            };

            $.ajax({
                url: `/j002/languageChange`,
                type: 'POST',
                data: JSON.stringify(data),
                contentType: 'application/json;charset=utf-8',
                success: function (data, status, xhr) {
                    setViewItem(data);
                    setSelectList(data);
                }
            });
        });
		
		// 提交数据到后台
        $("#btnName").click(function (event) {
            $.ajax({
                url: `/j002/dataToDB`,
                type: 'POST',
                data: JSON.stringify({pageId}),
                contentType: 'application/json;charset=utf-8',
                success: function (data, status, xhr) {
                    console.log(data);
                }
            });
        });
    }

    // 选中画面上的所有项目
    function setViewItem({entity: {itemMap}}) {
        for (const [key, value] of Object.entries(itemMap)) {
            $(`#${key}`).text(value);
        }
    }

    // 设置下拉列表选纵横
    function setSelectList({entity: {selectList}}) {

        const selectObj = $("#selectList");
        const selectedCode = selectObj.val();
        selectObj.empty();

        for (const entity of selectList) {
            $("<option>", {
                value: entity.code,
                text: entity.name
            }).appendTo("#selectList");
        }
        selectObj.val(selectedCode);
    }
</script>
</html>

3.2 Controller层

⏹ 登录页面Controller

  • 一打开登录页面,就去数据库查询支持的语言,然后放到session中,其他页面初始化的时候,直接从session中获取languageList,避免和数据库交互。
  • 每个画面中所需的项目,并不需要在当前画面中自己查询,会统一在AOP切面中进行处理。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpSession;
import java.util.List;

@Controller
@RequestMapping("/j001")
public class J001LoginController {

    @Autowired
    private HttpSession session;

    @Autowired
    private CommonService commonService;

    @GetMapping("/init")
    public ModelAndView init() {
		
		// 获取整个系统支持的语言下列列表放到session中
        List<CommonService.LanguageEntity> languageEntityList = commonService.getLanguageList();
        session.setAttribute("_langList", languageEntityList);

        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("j001");
        return modelAndView;
    }
}

⏹ 国际化显示页面Controller

  • 每个页面中的各个显示项目统一放到AOP切面中处理,但是各画面中的下拉列表值需要手动获取并处理。
  • dataToDB() 方法中,需要将 J002Form 插入到数据库,其中的 createDate,updateDate,loginUserId属性值都是AOP切面为我们处理的,各页面不需要自己手动处理。
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

import java.util.List;

@Controller
@RequestMapping("/j002")
public class J002BusinessController {

    @Autowired
    private CommonService commonService;

    @Autowired
    private J002BusinessService service;

    @GetMapping("/init")
    public ModelAndView init(J002Form form) {
		
		// 将下拉列表放到前台
        J002Entity entity = new J002Entity();
        entity.setSelectList(commonService.getSelectListByLocal(form.getLanguageCode()));

        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("j002");
        modelAndView.addObject("testMsg", "测试消息");
        modelAndView.addObject("entity", entity);
        return modelAndView;
    }
	
	// 语言切换的时候,去数据库查询下拉列表的国际化数据,然后放到前台
    @PostMapping("/languageChange")
    @ResponseBody
    public ResultEntity languageChange(@RequestBody J002Form form) {

        List<CommonService.SelectItem> selectListByLocal = commonService.getSelectListByLocal(form.getLanguageCode());

        J002Entity entity = new J002Entity();
        entity.setSelectList(selectListByLocal);
        return ResultEntity.ok(entity);
    }
	
	// 插入数据到数据库
    @PostMapping("/dataToDB")
    @ResponseBody
    public void dataToDB(@RequestBody J002Form form) {
        service.insertDataToDB(form);
        service.updateDataToDB(form);
    }
}

3.3 Service层

import org.springframework.stereotype.Service;

@Service
public class J002BusinessService {

    public void insertDataToDB(J002Form form) {

        System.out.println(form);
    }

    public void updateDataToDB(J002Form form) {
        System.out.println(form);
    }
}

3.4 核心的切面编程类

  • beforeInit切面方法中,我们在每个页面初始化之前,从url中获取页面的id,并放到form中,这样各页面在无需手动设置画面id。
  • afterReturningInit方法中,我们在当前切到的方法执行之后,ModelAndView返回给前台之前,根据页面id查询画面中所有的项目,并放到ModelAndView中,这样每个页面不需要手动查询页面项目,也不需要手动向ModelAndView中塞值了。
  • postLanguageChange方法中,我们在当前切到的方法执行之后,ResultEntity返回给前台之前,从ResultEntity中获取到BaseForm ,并把查询到的国际化项目放到BaseForm 中,返回给前台。
    注意: 我们从ResultEntity获取到的其实是每个画面的Entity,而Entity继承自各画面的From,而各画面的Form又继承自BaseForm,因此可以将子类Entity强转为基类BaseFrom。
    之所以要强转为BaseForm,是因为直接获取的话到的是Object类型的数据,只能通过反射来设置属性值,如果将Object转换为各画面的Entity的话,Entity又不具有通用性,因此强转为BaseForm可以设置共通的属性值
  • beforeInsertOrUpdate方法中,我们切的是insert或者update开头的方法,并且展示了如何使用反射向Object类型的数据中设置值。其实抽取了BaseForm之后,无需使用繁琐的反射,此处只是为了展示多样的用法。
  • 我们设置了一个内部类PointcutClass ,此类的主要作用是用来聚合切面表达式。当切面表达式过多的时候,可以聚合到一个类中,然后使用时只需要引入类中的方法即可。
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.Signature;
import org.aspectj.lang.annotation.AfterReturning;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;

import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;
import org.springframework.web.servlet.ModelAndView;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpSession;
import java.lang.reflect.Field;
import java.lang.reflect.Modifier;
import java.nio.file.Path;
import java.nio.file.Paths;

import java.util.Date;
import java.util.Map;

@Aspect
@Component
public class ControllerAspect {

    @Autowired
    private HttpServletRequest request;

    @Autowired
    private HttpSession session;

    @Autowired
    private CommonService commonService;

    // 模拟当前的登录用户
    private final static String loginUserId = "jmw";

    // 只有一层包,所以是 controller.* ,如果有两层包的话,就是 controller.*.*
    @Before("execution(* com.example.jmw.aspect.controller.*.init(..)) && args(form)")
    public void beforeInit(JoinPoint join, BaseForm form) {

        // 获取当前画面的ID
        String uri = request.getRequestURI().substring(request.getContextPath().length());
        Path uriPath = Paths.get(uri);
        String pageId = uriPath.subpath(0, 1).toString();

        // 将当前的画面ID放到form中
        form.setPageId(pageId);
    }

    // 在ModelAndView返回到页面之前,向其中加入页面上的项目
    @AfterReturning(value = "execution(* com.example.jmw.aspect.controller.*.init(..)) && args(form)", argNames = "join,form,modelAndView", returning = "modelAndView")
    public void afterReturningInit(JoinPoint join, BaseForm form, ModelAndView modelAndView) {

        // 获取画面ID
        String pageId = form.getPageId();

        // 根据画面ID查询当前页面的国际化项目名称
        Map<String, String> itemMap = commonService.getItemByLocal("jp", pageId);
        modelAndView.addObject("itemMap", itemMap);

        // 将当前页面的语言list传到前台
        modelAndView.addObject("langList", session.getAttribute("_langList"));

        // 将当前页面的pageId传到前台
        modelAndView.addObject("pageId", pageId);
    }

    @AfterReturning(value = "execution(* com.example.jmw.aspect.controller.*.languageChange(..)) && args(form, ..)", argNames = "join,form,resultEntity", returning = "resultEntity")
    public void postLanguageChange(JoinPoint join, BaseForm form, ResultEntity resultEntity) {

        // 如果不是成功的响应就return
        if (!resultEntity.getResult()) {
            return;
        }

        // 因为每个画面用到的实体类都不相同,因此此处将每个画面的实体类获取出来之后,转换为父类的BaseForm,每个画面共通的字段都放到此处
        BaseForm baseForm = (BaseForm)resultEntity.getEntity();
        if (ObjectUtils.isEmpty(baseForm)) {
            baseForm = new BaseForm();
        }

        // 根据画面ID查询当前页面的国际化项目名称
        Map<String, String> itemMap = commonService.getItemByLocal(form.getLanguageCode(), form.getPageId());

        // 将国际化项目名称放到BaseForm中,也就相当于放到了每个画面自己的实体类中
        baseForm.setItemMap(itemMap);
    }

    // 切面service包下面所有的Service类里面所有以 insert 或者 update 开头的方法,并且该方法一定要有一个参数为 form
    @Before("execution(* com.example.jmw.aspect.service.*.insert*(..)) && args(form) " +
            // 可以单独定义一个类,类里面用来聚合所有的 切面表达式
            "|| com.example.jmw.common.aspect.ControllerAspect.PointcutClass.updateAspect(form)")
    public void beforeInsertOrUpdate(JoinPoint join, BaseForm form) throws Exception {

        // 获取出切面对象
        Signature signature = join.getSignature();
        this.getSignatureInfo(signature);

        // 获取出切面对象的所有参数对象
        Object[] args = join.getArgs();
        // 获取出第一个参数,并转换为父类(获取出共通的属性)
        Object objForm = args[0];
        // 通过反射来设置属性
        this.setFields(objForm);
		
		// 将子类强转换为共通的基类,基类中有我们要转换的共通的属性
        BaseForm argForm = (BaseForm)objForm;

        // 可以看到通过JoinPoint得到的参数对象和beforeInsertOrUpdate方法中的参数对象本质上是一个对象
        System.out.println(argForm == form);  // true

        // 给loginUserId添加后缀,通过这种方式设置form中的值更简洁,但是需要注意抽取基类和共通属性
        form.setLoginUserId(form.getLoginUserId() + "~我是一个后缀~");
    }
	
	// 设置Object对象中的属性
    private void setFields(Object objectForm) throws IllegalAccessException {

        // 获取当前类的class对象
        Class<?> objClass = objectForm.getClass();
        // 获取当前类的父类的class对象
        Class<?> superclass = objClass.getSuperclass();
        // 获取父类上声明的所有类型的属性
        Field[] fields = superclass.getDeclaredFields();

        for (Field field : fields) {

            if (!field.isAnnotationPresent(FieldValueAnnotation.class)) {
                continue;
            }

            // 获取field属性上标记的注解
            FieldValueAnnotation fieldAnnotation = field.getAnnotation(FieldValueAnnotation.class);
            FieldValueAnnotation.Category fieldTypeEnum = fieldAnnotation.type();
            // 获取注解上标记的类型名称
            String name = fieldTypeEnum.name();

            // 因为属性是私有属性,通过setAccessible()将其设置为允许访问
            field.setAccessible(true);

            // 根据类型设置不同的值
            if (name.equals(FieldValueAnnotation.Category.userIdField.name())) {
                field.set(objectForm, loginUserId);
            } else if (name.equals(FieldValueAnnotation.Category.dateField.name())) {
                field.set(objectForm, new Date());
            }
        }
    }

    // 可以将切面表达式聚合到一个类里面集中管理
    public class PointcutClass {

        @Pointcut("execution(* com.example.jmw.aspect.service.*.update*(..)) && args(form, ..)")
        private void updateAspect(BaseForm form){}
    }
	
	// 获取切面Signature中的信息
    private void getSignatureInfo(Signature signature) {

        // 返回此签名的标识符部分。对于方法,这将返回方法名称。
        String name = signature.getName();
        System.out.println(name);  // insertDataToDB

        // 获取被切面的对象的短名称
        String shortString = signature.toShortString();
        System.out.println(shortString);  // J002BusinessService.insertDataToDB(..)

        // 获取被切面的对象的长名称
        String longString = signature.toLongString();
        System.out.println(longString);  // public void com.example.jmw.aspect.service.J002BusinessService.insertDataToDB(com.example.jmw.aspect.form.J002Form)

        // 获取被切面方法切到的方法的修饰符
        int modifiers = signature.getModifiers();
        String modifierName = Modifier.toString(modifiers);
        System.out.println(modifierName);  // public

        Class declaringType = signature.getDeclaringType();
        System.out.println(declaringType);  // class com.example.jmw.aspect.service.J002BusinessService

        String declaringTypeName = signature.getDeclaringTypeName();
        System.out.println(declaringTypeName);  // com.example.jmw.aspect.service.J002BusinessService
    }
}

💪💪效果

在这里插入图片描述
在这里插入图片描述


四. 前期准备2

4.1 自定义注解

4.1.1 标记方法需要加密的注解

import java.lang.annotation.*;

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptInfoMethodAnnotation {

}

4.1.2 标记方法需要解密的注解

import java.lang.annotation.*;

@Documented
@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface DecryptInfoMethodAnnotation {

}

4.1.3 标记属性需要加密或者解密的注解

import java.lang.annotation.*;

@Documented
@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
public @interface EncryptDecryptFieldAnnotation {

}

五. 敏感数据加密和解密案例

5.1 前台

  • 将密码和邮箱等数据提交到数据库,密码和邮箱属于用户敏感数据,需要加密之后才能存放到数据库。
  • 查询密码和邮箱等敏感数据,返回到前台的时候,需要将加密之后的数据解密后,返回给前台。
<!DOCTYPE html>
<html lang="en" xmlns:th="http://www.thymeleaf.org">
<head>
    <meta charset="UTF-8">
    <title>Title</title>
</head>
<body>
    <div>
        <button id="insert">插入数据</button>
        <button id="select">查询数据</button>
    </div>
</body>
<script type="text/javascript" th:src="@{/js/public/jquery-3.6.0.min.js}"></script>
<script>

    $("#insert").click(function() {
        const data = {
            id: "011",
            name: "贾飞天",
            password: "pass001",
            email: "123@qq.com"
        };

        $.ajax({
            url: `/j003/insert`,
            type: 'POST',
            data: JSON.stringify(data),
            contentType: 'application/json;charset=utf-8',
            success: function (data, status, xhr) {
                console.log(data);
            }
        });
    });

    $("#select").click(function() {

        $.ajax({
            // 注意,此处的查询参数少了一个0
            url: `/j003/select?id=11`,
            type: 'GET',
            success: function (data, status, xhr) {
                console.log(data);
            }
        });
    });
</script>
</html>

5.2 前台Form

  • @EncryptDecryptFieldAnnotation注解用于标记的属性加密
import com.example.jmw.common.annotation.aspect.EncryptDecryptFieldAnnotation;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = true)
public class J003Form extends BaseForm {

    private String id;

    private String name;

    // 需要加密的字段
    @EncryptDecryptFieldAnnotation
    private String password;

    // 需要加密的字段
    @EncryptDecryptFieldAnnotation
    private String email;
}

5.3 返回数据实体类

  • 父类中的J003Form已经有了passwordemail字段。
  • 子类继承父类,并定义passwordemail字段来重写父类属性。
  • @EncryptDecryptFieldAnnotation注解用于标记的属性解密。
import com.example.jmw.common.annotation.aspect.EncryptDecryptFieldAnnotation;
import lombok.Data;
import lombok.EqualsAndHashCode;

@Data
@EqualsAndHashCode(callSuper = true)
public class J003Entity extends J003Form {

    // 需要解密的字段
    @EncryptDecryptFieldAnnotation
    private String password;

    // 需要解密的字段
    @EncryptDecryptFieldAnnotation
    private String email;
}

5.4 Mapper层

⏹接口

public interface J003Mapper {

    void insertUserInfo(J003Form form);

    J003Entity selectUserInfo(String id);
}

⏹SQL

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">

<mapper namespace="com.example.jmw.mapper.J003Mapper">

    <select id="insertUserInfo" parameterType="com.example.jmw.aspect.form.J003Form">
       INSERT INTO
        aspect_user(
          id
          , name
          , password
          , email
        )
        VALUES (
          #{id}
          , #{name}
          , #{password}
          , #{email}
        )
    </select>

    <select id="selectUserInfo" parameterType="string" resultType="com.example.jmw.aspect.entity.J003Entity">
        SELECT
          id
          , name
          , password
          , email
        FROM
          aspect_user
        WHERE
          id = #{id}
    </select>
</mapper>

5.5 Controller层

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;

@Controller
@RequestMapping("/j003")
public class J003BusinessController {

    @Autowired
    private J003BusinessService service;

    @GetMapping("/init")
    public ModelAndView init() {

        ModelAndView modelAndView = new ModelAndView();
        modelAndView.setViewName("j003");
        return modelAndView;
    }

    @PostMapping("/insert")
    @ResponseBody
    public void insertUserInfo(@RequestBody J003Form form) {
    	// 此时,form中的用户敏感数据是被加密的
        service.insertUserInfo(form);
    }

    @GetMapping("/select")
    @ResponseBody
    public J003Entity selectUserInfo(@RequestParam String id) {

        J003Entity entity = service.selectUserInfoTest(id, "");
        System.out.println(entity);
		
		// 此时,j003Entity中的敏感数据是被解密的
        J003Entity j003Entity = service.selectUserInfo(id);
        return j003Entity;
    }
}

5.6 Service层

  • @EncryptInfoMethodAnnotation@DecryptInfoMethodAnnotation注解作用于方法上,标记该方法中的参数需要加密或者解密
import org.springframework.stereotype.Service;

import javax.annotation.Resource;

@Service
public class J003BusinessService {

    @Resource
    private J003Mapper mapper;

    // 给信息加密的注解
    @EncryptInfoMethodAnnotation
    public void insertUserInfo(J003Form form) {
        mapper.insertUserInfo(form);
    }

    // 给信息解密的注解
    @DecryptInfoMethodAnnotation
    public J003Entity selectUserInfo(String id) {
        return mapper.selectUserInfo(id);
    }

    // 给信息解密的注解
    @DecryptInfoMethodAnnotation
    public J003Entity selectUserInfoTest(String id, String blank) {
        return mapper.selectUserInfo(id);
    }
}

5.7 Base64加密解密工具类

import java.nio.charset.StandardCharsets;
import java.util.Base64;

public final class Base64Utils {

    private final static Base64.Encoder encoder = Base64.getEncoder();
    private final static Base64.Decoder decoder = Base64.getDecoder();

    // 加密
    public static String encode(String text) {
        return encoder.encodeToString(text.getBytes(StandardCharsets.UTF_8));
    }

    // 解密
    public static String decode(String encodedText) {
        return new String(decoder.decode(encodedText), StandardCharsets.UTF_8);
    }
}

5.8 核心的切面编程类

  • 如果普通的@Before@AfterReturning就能解决的问题,就没有必要使用@Around了,因为@Around功能虽然强大,但通常需要在线程安全的环境下使用。 此处使用 @Around 仅为测试其作用。
  • @Around可以改变执行目标方法的参数值,也可以改变执行目标方法之后的返回值,此功能主要是通过joinPoint.proceed(args);方法来实现的。args是目前方法执行之前的参数数组对象,通过它便可以改变目标方法的参数值,只有joinPoint.proceed(args);方法执行之后,目标方法才可执行,执行之后会得到一个Object对象,既目标对象执行之后的返回值,通过操作得到的Object对象,可以改变目标方法执行之后的返回值。
  • infoEncrypt方法这种写法,无法获取出标记在方法上的自定义注解;但是infoDecrypt方法这种写法,可以获取出标记在方法上的自定义注解。
import org.apache.commons.lang3.StringUtils;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;

import org.aspectj.lang.annotation.Around;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
import org.springframework.util.ObjectUtils;

import java.lang.reflect.Field;
import java.util.Arrays;
import java.util.List;

import static java.util.stream.Collectors.toList;

@Aspect
@Component
public class EncryptAndDecryptAspect {

    /*
        功能: 获取方法上的待加密对象
        切面内容: 方法上被标记了 EncryptInfoMethodAnnotation 注解
    */
    @Before("@annotation(com.example.jmw.common.annotation.aspect.EncryptInfoMethodAnnotation)")
    public void infoEncrypt(JoinPoint joinPoint) throws Exception {

        Object form = joinPoint.getArgs()[0];
        Class<?> formClass = form.getClass();
		
		// 获取本类的所有的类型修饰符的属性
        Field[] declaredFields = formClass.getDeclaredFields();
        for (Field field : declaredFields) {
			
			// 如果Field属性上不包含EncryptDecryptFieldAnnotation加密属性,则跳过
            if (!field.isAnnotationPresent(EncryptDecryptFieldAnnotation.class) || field.getType() != String.class) {
                continue;
            }

            // 强制private属性可访问
            field.setAccessible(true);

            // 属性值进行加密
            String encodeValue = Base64Utils.encode((String) field.get(form));
            // 加密后的属性值塞到原属性中
            field.set(form, encodeValue);
        }
    }

    /*
        功能: 获取方法上的待解密对象,这种写法还能获取方法上标记的注解
        切面内容: 方法上被标记了 DecryptInfoMethodAnnotation 注解,并且方法上需要有一个参数
        注意:如果方法上标记了 DecryptInfoMethodAnnotation 注解,并且方法没有参数或者多个参数的话,不会被切面到
    */
    @Around("@annotation(decryptInfoMethodAnnotation) && args(id)")
    public Object infoDecrypt(ProceedingJoinPoint joinPoint, DecryptInfoMethodAnnotation decryptInfoMethodAnnotation, String id) throws Throwable {

        System.out.println(decryptInfoMethodAnnotation);
        System.out.println(id);  // 11

        // 对参数进行处理,如果不满3位就补0
        String leftPadStr = StringUtils.leftPad(id, 3, "0");
        Object[] args = {
            leftPadStr
        };

        /*
            👉执行切面切到的方法,获取到方法执行的返回值
            ⏹如果使用的是 joinPoint.proceed() 的话,则相当于不改变目标方法的参数值
            ⏹我们使用的是 joinPoint.proceed(args) , 系统 args 是我们改变了参数值之后的参数数组

            ❗❗如果传入的Object[]数组长度与目标方法所需要的参数个数不相等, 或者Object[]数组元素与目标方法所需参数的类型不匹配,程序就会出现异常。
        */
        Object resultObj = joinPoint.proceed(args);

        if (ObjectUtils.isEmpty(resultObj)) {
            return resultObj;
        }

        // 过滤出标记了 @EncryptDecryptFieldAnnotation 注解的属性
        Class<?> resultObjClass = resultObj.getClass();
        List<Field> fieldList = Arrays.stream(resultObjClass.getDeclaredFields()).filter(field -> field.isAnnotationPresent(EncryptDecryptFieldAnnotation.class)).collect(toList());
        if (ObjectUtils.isEmpty(fieldList)) {
            return resultObj;
        }

        for (Field field : fieldList) {
            // 强制private属性可访问
            field.setAccessible(true);

            // 对加密数据进行解密并设置到原属性中
            String encryptFieldValue = (String)field.get(resultObj);
            String decodeFieldValue = Base64Utils.decode(encryptFieldValue);
            field.set(resultObj, decodeFieldValue);
        }

        return resultObj;
    }
}

💪💪效果

在这里插入图片描述

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值