当前位置: 代码网 > it编程>编程语言>Java > Mybatis-Plus根据自定义注解实现自动加解密的示例代码

Mybatis-Plus根据自定义注解实现自动加解密的示例代码

2024年07月03日 Java 我要评论
背景我们把数据存到数据库的时候,有些敏感字段是需要加密的,从数据库查出来再进行解密。如果存在多张表或者多个地方需要对部分字段进行加解密操作,每个地方都手写一次加解密的动作,显然不是最好的选择。如果我们

背景

我们把数据存到数据库的时候,有些敏感字段是需要加密的,从数据库查出来再进行解密。如果存在多张表或者多个地方需要对部分字段进行加解密操作,每个地方都手写一次加解密的动作,显然不是最好的选择。如果我们使用的是mybatis框架,那就跟着一起探索下如何使用框架的拦截器功能实现自动加解密吧。

定义一个自定义注解

我们需要一个注解,只要实体类的属性加上这个注解,那么就对这个属性进行自动加解密。我们把这个注解定义灵活一点,不仅可以放在属性上,还可以放到类上,如果在类上使用这个注解,代表这个类的所有属性都进行自动加密。

/**
 * 加密字段
 */
@documented
@retention(retentionpolicy.runtime)
@target({elementtype.field, elementtype.annotation_type, elementtype.type})
public @interface encryptfield {

}

定义实体类

package com.wen3.demo.mybatisplus.po;

import com.baomidou.mybatisplus.annotation.*;
import com.wen3.demo.mybatisplus.encrypt.annotation.encryptfield;
import lombok.getter;
import lombok.setter;
import lombok.experimental.accessors;

@encryptfield
@getter
@setter
@accessors(chain = true)
@keysequence(value = "t_user_user_id_seq", dbtype = dbtype.postgre_sql)
@tablename("t_user")
public class userpo {

    /**
     * 用户id
     */
    @tableid(value = "user_id", type = idtype.input)
    private long userid;

    /**
     * 用户姓名
     */
    @tablefield("user_name")
    private string username;

    /**
     * 用户性别
     */
    @tablefield("user_sex")
    private string usersex;

    /**
     * 用户邮箱
     */
    @encryptfield
    @tablefield("user_email")
    private string useremail;

    /**
     * 用户账号
     */
    @tablefield("user_account")
    private string useraccount;

    /**
     * 用户地址
     */
    @tablefield("user_address")
    private string useraddress;

    /**
     * 用户密码
     */
    @tablefield("user_password")
    private string userpassword;

    /**
     * 用户城市
     */
    @tablefield("user_city")
    private string usercity;

    /**
     * 用户状态
     */
    @tablefield("user_status")
    private string userstatus;

    /**
     * 用户区县
     */
    @tablefield("user_seat")
    private string userseat;
}

拦截器

mybatis-plus有个拦截器接口com.baomidou.mybatisplus.extension.plugins.inner.innerinterceptor,但发现这个接口有一些不足

  • 必须构建一个mybatisplusinterceptor这样的bean
  • 并调用这个beanaddinnerinterceptor方法,把所有的innerinterceptor加入进去,才能生效
  • innerinterceptor只有before拦截,缺省after拦截。加密可以在before里面完成,但解密需要在after里面完成,所以这个innerinterceptor不能满足我们的要求

所以继续研究源码,发现mybatis有个org.apache.ibatis.plugin.interceptor接口,这个接口能满足我对自动加解密的所有诉求

  • 首先,实现interceptor接口,只要注册成为spring容器的bean,拦截器就能生效
  • 可以更加灵活的在beforeafter之间插入自己的逻辑

加密拦截器

创建名为encryptinterceptor的加密拦截器,对update操作进行拦截,对带@encryptfield注解的字段进行加密处理,无论是save方法还是savebatch方法都会被成功拦截到。

package com.wen3.demo.mybatisplus.encrypt.interceptor;

import com.wen3.demo.mybatisplus.encrypt.annotation.encryptfield;
import com.wen3.demo.mybatisplus.encrypt.util.fieldencryptutil;
import lombok.setter;
import lombok.extern.slf4j.slf4j;
import org.apache.commons.lang3.stringutils;
import org.apache.ibatis.executor.executor;
import org.apache.ibatis.mapping.mappedstatement;
import org.apache.ibatis.plugin.interceptor;
import org.apache.ibatis.plugin.intercepts;
import org.apache.ibatis.plugin.invocation;
import org.apache.ibatis.plugin.signature;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.stereotype.component;

import java.util.objects;

/**
 * 对update操作进行拦截,对{@link encryptfield}字段进行加密处理;
 * 无论是save方法还是savebatch方法都会被成功拦截;
 */
@slf4j
@intercepts({
        @signature(type = executor.class, method = "update", args = {mappedstatement.class, object.class})
})
@component
public class encryptinterceptor implements interceptor {

    private static final string method = "update";

    @setter(onmethod_ = {@autowired})
    private fieldencryptutil fieldencryptutil;

    @override
    public object intercept(invocation invocation) throws throwable {
        if(!stringutils.equals(method, invocation.getmethod().getname())) {
            return invocation.proceed();
        }

        // 根据update拦截规则,第0个参数一定是mappedstatement,第1个参数是需要进行判断的参数
        object param = invocation.getargs()[1];
        if(objects.isnull(param)) {
            return invocation.proceed();
        }

        // 加密处理
        fieldencryptutil.encrypt(param);

        return invocation.proceed();
    }
}

解密拦截器

创建名为decryptinterceptor的加密拦截器,对query操作进行拦截,对带@encryptfield注解的字段进行解密处理,无论是返回单个对象,还是对象的集合,都会被拦截到。

package com.wen3.demo.mybatisplus.encrypt.interceptor;

import cn.hutool.core.util.classutil;
import com.wen3.demo.mybatisplus.encrypt.annotation.encryptfield;
import com.wen3.demo.mybatisplus.encrypt.util.fieldencryptutil;
import lombok.setter;
import lombok.extern.slf4j.slf4j;
import org.apache.ibatis.executor.resultset.resultsethandler;
import org.apache.ibatis.plugin.interceptor;
import org.apache.ibatis.plugin.intercepts;
import org.apache.ibatis.plugin.invocation;
import org.apache.ibatis.plugin.signature;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.stereotype.component;

import java.sql.statement;
import java.util.collection;

/**
 * 对query操作进行拦截,对{@link encryptfield}字段进行解密处理;
 */
@slf4j
@intercepts({
        @signature(type = resultsethandler.class, method = "handleresultsets", args = statement.class)
})
@component
public class decryptinterceptor implements interceptor {

    private static final string method = "query";

    @setter(onmethod_ = {@autowired})
    private fieldencryptutil fieldencryptutil;

    @suppresswarnings("rawtypes")
    @override
    public object intercept(invocation invocation) throws throwable {
        object result = invocation.proceed();

        // 解密处理
        // 经过测试发现,无论是返回单个对象还是集合,result都是arraylist类型
        if(classutil.isassignable(collection.class, result.getclass())) {
            fieldencryptutil.decrypt((collection) result);
        } else {
            fieldencryptutil.decrypt(result);
        }

        return result;
    }
}

加解密工具类

由于加密和解密绝大部分的逻辑是相似的,不同的地方在于

  • 加密需要通过反射处理的对象,是在sql执行前,是invocation对象的参数列表中下标为1的参数;而解决需要通过反射处理的对象,是在sql执行后,对执行结果对象进行解密处理。
  • 一个是获取到字段值进行加密,一个是获取到字段值进行解密

于是把加解密逻辑抽象成一个工具类,把差异的部分做为参数传入

package com.wen3.demo.mybatisplus.encrypt.util;

import cn.hutool.core.util.classutil;
import cn.hutool.core.util.reflectutil;
import com.wen3.demo.mybatisplus.encrypt.annotation.encryptfield;
import com.wen3.demo.mybatisplus.encrypt.service.fieldencryptservice;
import lombok.setter;
import lombok.extern.slf4j.slf4j;
import org.apache.commons.lang3.stringutils;
import org.apache.commons.lang3.reflect.fieldutils;
import org.springframework.beans.factory.annotation.autowired;
import org.springframework.core.annotation.annotationutils;
import org.springframework.stereotype.component;
import org.springframework.util.collectionutils;

import java.lang.reflect.field;
import java.util.collection;
import java.util.list;
import java.util.objects;

/**
 * 加解密工具类
 */
@slf4j
@component
public class fieldencryptutil {

    @setter(onmethod_ = {@autowired})
    private fieldencryptservice fieldencryptservice;

    /**对encryptfield注解进行加密处理*/
    public void encrypt(object obj) {
        if(classutil.isprimitivewrapper(obj.getclass())) {
            return;
        }
        encryptordecrypt(obj, true);
    }

    /**对encryptfield注解进行解密处理*/
    public void decrypt(object obj) {
        encryptordecrypt(obj, false);
    }

    /**对encryptfield注解进行解密处理*/
    public void decrypt(collection list) {
        if(collectionutils.isempty(list)) {
            return;
        }
        list.foreach(this::decrypt);
    }

    /**对encryptfield注解进行加解密处理*/
    private void encryptordecrypt(object obj, boolean encrypt) {
        // 根据update拦截规则,第0个参数一定是mappedstatement,第1个参数是需要进行判断的参数
        if(objects.isnull(obj)) {
            return;
        }

        // 获取所有带加密注解的字段
        list<field> encryptfields = null;
        // 判断类上面是否有加密注解
        encryptfield encryptfield = annotationutils.findannotation(obj.getclass(), encryptfield.class);
        if(objects.nonnull(encryptfield)) {
            // 如果类上有加密注解,则所有字段都需要加密
            encryptfields = fieldutils.getallfieldslist(obj.getclass());
        } else {
            encryptfields = fieldutils.getfieldslistwithannotation(obj.getclass(), encryptfield.class);
        }

        // 没有字段需要加密,则跳过
        if(collectionutils.isempty(encryptfields)) {
            return;
        }

        encryptfields.foreach(f->{
            // 只支持string类型的加密
            if(!classutil.isassignable(string.class, f.gettype())) {
                return;
            }

            string oldvalue = (string) reflectutil.getfieldvalue(obj, f);
            if(stringutils.isblank(oldvalue)) {
                return;
            }

            string logtext = null, newvalue = null;
            if(encrypt) {
                logtext = "encrypt";
                newvalue = fieldencryptservice.encrypt(oldvalue);
            } else {
                logtext = "decrypt";
                newvalue = fieldencryptservice.decrypt(oldvalue);
            }

            log.info("{} success[{}=>{}]. before:{}, after:{}", logtext, f.getdeclaringclass().getname(), f.getname(), oldvalue, newvalue);
            reflectutil.setfieldvalue(obj, f, newvalue);
        });
    }
}

加解密算法

mybatis-plus自带了一个aes加解密算法的工具,我们只需要提供一个加密key,然后就可以完成一个加解密的业务处理了。

  • 先定义一个加解密接口
package com.wen3.demo.mybatisplus.encrypt.service;

/**
 * 数据加解密接口
 */
public interface fieldencryptservice {

    /**对数据进行加密*/
    string encrypt(string value);

    /**对数据进行解密*/
    string decrypt(string value);

    /**判断数据是否忆加密*/
    default boolean isencrypt(string value) {
        return false;
    }
}
  • 然后实现一个默认的加解密实现类
package com.wen3.demo.mybatisplus.encrypt.service.impl;

import cn.hutool.core.util.classutil;
import com.baomidou.mybatisplus.core.exceptions.mybatisplusexception;
import com.baomidou.mybatisplus.core.toolkit.aes;
import com.wen3.demo.mybatisplus.encrypt.service.fieldencryptservice;
import org.springframework.stereotype.component;

import javax.crypto.illegalblocksizeexception;

/**
 * 使用mybatis-plus自带的aes加解密
 */
@component
public class defaultfieldencryptservice implements fieldencryptservice {

    private static final string encrypt_key = "abcdefghijklmnop";

    @override
    public string encrypt(string value) {
        if(isencrypt(value)) {
            return value;
        }
        return aes.encrypt(value, encrypt_key);
    }

    @override
    public string decrypt(string value) {
        return aes.decrypt(value, encrypt_key);
    }

    @override
    public boolean isencrypt(string value) {
        // 判断是否已加密
        try {
            // 解密成功,说明已加密
            decrypt(value);
            return true;
        } catch (mybatisplusexception e) {
            if(classutil.isassignable(illegalblocksizeexception.class, e.getcause().getclass())) {
                return false;
            }
            throw e;
        }
    }
}

自动加解密单元测试

package com.wen3.demo.mybatisplus.service;

import cn.hutool.core.util.randomutil;
import com.wen3.demo.mybatisplus.mybatisplusspringboottestbase;
import com.wen3.demo.mybatisplus.encrypt.service.fieldencryptservice;
import com.wen3.demo.mybatisplus.po.userpo;
import jakarta.annotation.resource;
import org.apache.commons.lang3.randomstringutils;
import org.junit.jupiter.api.test;

import java.util.collections;
import java.util.list;
import java.util.map;

class userservicetest extends mybatisplusspringboottestbase {

    @resource
    private userservice userservice;
    @resource
    private fieldencryptservice fieldencryptservice;

    @test
    void save() {
        userpo userpo = new userpo();
        string originalvalue = randomstringutils.randomalphabetic(16);
        string encryptvalue = fieldencryptservice.encrypt(originalvalue);
        userpo.setuseremail(originalvalue);
        userpo.setusername(randomstringutils.randomalphabetic(16));
        boolean testresult = userservice.save(userpo);
        asserttrue(testresult);
        assertnotequals(originalvalue, userpo.getuseremail());
        assertequals(encryptvalue, userpo.getuseremail());

        // 测试解密: 返回单个对象
        userpo userpoquery = userservice.getbyid(userpo.getuserid());
        assertequals(originalvalue, userpoquery.getuseremail());
        // 测试解密: 返回list
        list<userpo> userpolist = userservice.listbyemail(encryptvalue);
        assertequals(originalvalue, userpolist.get(0).getuseremail());

        // 测试savebatch方法也会被拦截加密
        userpo.setuserid(null);
        testresult = userservice.save(collections.singletonlist(userpo));
        asserttrue(testresult);
        assertnotequals(originalvalue, userpo.getuseremail());
        assertequals(encryptvalue, userpo.getuseremail());
    }
}

单元测试运行截图

以上就是mybatis-plus根据自定义注解实现自动加解密的示例代码的详细内容,更多关于mybatis-plus自定义注解加解密的资料请关注代码网其它相关文章!

(0)

相关文章:

版权声明:本文内容由互联网用户贡献,该文观点仅代表作者本人。本站仅提供信息存储服务,不拥有所有权,不承担相关法律责任。 如发现本站有涉嫌抄袭侵权/违法违规的内容, 请发送邮件至 2386932994@qq.com 举报,一经查实将立刻删除。

发表评论

验证码:
Copyright © 2017-2025  代码网 保留所有权利. 粤ICP备2024248653号
站长QQ:2386932994 | 联系邮箱:2386932994@qq.com