欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

SpringBoot使用自定义注解实现简单参数加密解密(注解+HandlerMethodArgumentResolver)

程序员文章站 2022-04-09 14:54:02
SpringBoot 自定义注解 参数加密解密 HandlerMethodArgumentResolver ......

前言

我黄汉三又回来了,快半年没更新博客了,这半年来的经历实属不易,
疫情当头,本人实习的公司没有跟员工共患难,直接辞掉了很多人。
作为一个实习生,本人也被无情开除了。所以本人又得重新准备找工作了。
算了,感慨一下,本来想昨天发的,但昨天是清明,哀悼时期,就留到了今天发。

话不多说,直接进入正题吧。这段时间本人在写毕设,学校也迟迟没有开学的消息,属实难顶。
本来被开了本人只想回学校安度"晚年"算了,毕竟工作可以再找,但亲朋好友以后毕业了就很少见了。
所以亲们,一定要珍惜身边的人噢。

因为这篇博文是现在本地typora上面写好再放过博客园的,格式有点不统一
博客园的markdown编辑器还不是很好用,这点有点头疼
还有一点是代码格式问题,复制到markdown又变乱了
我哭了,本来就乱了,再加上博客篇幅的问题一挤压,博文就乱完了
以后更文都用markdown了,所以关于排版的问题会越来越美化一下

通过本文读者将可以学习到以下内容

  • 注解的简单使用和解析
  • handlermethodargumentresolver相关部分知识

起因

写毕设,这周才把后台搭好,还有小程序端还没开始。如题目所说,用了springboot做后端搭建。
然后也当然应用了restful风格,当本人有一个url是/initjson/{id}的时候,直接就把用户id传过来了。
本人就想能不能在前端简单把id加密一下,起码不能眼睁睁看着id直接就传到后端。虽然只是一个毕设,
但还是稍微处理一下吧,处理的话我选择用base64好了。
本人现在是想把前端传的一些简单参数,用密文传到后端再解密使用,避免明文传输。
当然在真正的环境中,肯定是使用更好的方案的。这里只是说有那么一种思路或者说那么一种场景。
给大家举个例子之后可以抛砖引玉。

过程

1.前端

前端传参的时候,加密

 // encode是base64加密的方法,可以自己随便整一个
 data.password = encode(pwd);
 data.username= encode(username);

这样子前端传过去就是密文了。

2.后端

当参数传到后端之后,想要把密文解析回明文,然后接下来就是本文的主旨所在了。
解密的时候,本人一开始是在接口里面解密的。

/**
  *  此时参数接受到的内容是密文
  */
string login(string username, string password) {
       username =  base64util.decode(username);
       password=  base64util.decode(password);
}

看起来也没啥是吧,但是万一参数很多,或者说接口多,难道要每个接口都这么写一堆解密的代码吗。
显然还可以改造,怎么做?本人想到了注解,或者说想用注解试试,这样自己也能加深对注解的学习。

2.1 注解

注解这个东西,本人当时学习的时候还以为是怎么起作用的,原来是可以自定义的(笑哭)。
我们在本文简单了解下注解吧,如果有需要,后面本人可以更新一篇关于注解的博文。
或者读者可以自行学习了解一下,说到这里,本人写博客的理由是,网上没有,或者网上找到的东西跟本人需要的不一样时才会写博客。
有的话就不写了,以免都是同样的东西,所以本人更新的博客并不算多,基本很久才一篇。
但好像这样想并不对,写博客无论是什么内容,不仅方便自己学习也可以方便他人,
所以以后应该更新频率会好点吧希望。

回到正题,注解有三个主要的东西

  • 注解定义(annotation)
  • 注解类型(elementtype)
  • 注解策略(retentionpolicy)

先来看看注解定义,很简单

// 主要的就是 @interface  使用它定义的类型就是注解了,就跟class定义的类型是类一样。
public @interface base64decodestr {
    /**
     * 这里可以放一些注解需要的东西
     * 像下面这个count()的含义是解密的次数,默认为1次
     */
    int count() default 1;
}

然后再来看看注解类型

// 注解类型其实就是注解声明在什么地方
public enum elementtype {
    type,               /* 类、接口(包括注释类型)或枚举声明  */
    field,              /* 字段声明(包括枚举常量)  */
    method,             /* 方法声明  */
    parameter,          /* 参数声明  */
    constructor,        /* 构造方法声明  */
    local_variable,     /* 局部变量声明  */
    annotation_type,    /* 注释类型声明  */
    package             /* 包声明  */
}

// 这个target就是这么使用的
// 现在这个注解,本人希望它只能声明在方法上还有参数上,别的地方声明就会报错
@target({elementtype.method, elementtype.parameter})
public @interface base64decodestr {
    int count() default 1;
}

最后再来看看注解策略

public enum retentionpolicy {
    source,  /* annotation信息仅存在于编译器处理期间,编译器处理完之后就没有该annotation信息了*/
    class,   /* 编译器将annotation存储于类对应的.class文件中。默认行为  */
    runtime  /* 编译器将annotation存储于class文件中,并且可由jvm读入 */
}

// 一般用第三个,runtime,这样的话程序运行中也可以使用
@target({elementtype.method, elementtype.parameter})
@retention(retentionpolicy.runtime)
public @interface base64decodestr {
    int count() default 1;
}

到此为止,一个注解就定义好了。但是在什么时候工作呢,这时我们就需要写这个注解的解析了。
然后想想,定义这个注解的目的是,想直接在接口使用参数就是明文,所以应该在进入接口之前就把密文解密回明文并放回参数里。
这一步有什么好办法呢,这时候就轮到下一个主角登场了,它就是handlermethodargumentresolver

2.2 handlermethodargumentresolver

关于handlermethodargumentresolver的作用和解析,官方是这么写的

/**
 * strategy interface for resolving method parameters into argument values in
 * the context of a given request.
 * 翻译了一下
 * 策略接口,用于在给定请求的上下文中将方法参数解析为参数值
 * @author arjen poutsma
 * @since 3.1
 * @see handlermethodreturnvaluehandler
 */
public interface handlermethodargumentresolver {

        /**
         * methodparameter指的是控制器层方法的参数
         * 是否支持此接口
         * ture就会执行下面的方法去解析
         */
	boolean supportsparameter(methodparameter parameter);

        /**
         * 常见的写法就是把前端的参数经过处理再复制给控制器方法的参数
         */
	@nullable
	object resolveargument(methodparameter parameter, @nullable modelandviewcontainer mavcontainer,nativewebrequest webrequest, @nullable webdatabinderfactory binderfactory) throws exception;
}

所以这个接口,是很重要的,想想springmvc为何在控制器写几个注解,就能接收到参数,这个接口就是功不可没的。
像常见的@pathvariable 就是用这个接口实现的。
SpringBoot使用自定义注解实现简单参数加密解密(注解+HandlerMethodArgumentResolver)
本人的理解是,实现这个接口,就能在前端到后端接口之间处理方法和参数,所以刚好满足上面的需求。
其实这个接口也是属于springmvc源码里面常见的一个,读者依然也可自行了解下,
目前本人还没有准备要写spring读源码的文章,因为本人也还没系统的去看过,或许以后本人看了就会更新有关博客。

继续,有了这样的接口就可以用来写解析自定义注解了,细心的同学可以发现,在这里写注解解析,
那么这个注解就只能是在控制层起作用了,在服务层甚至dao层都用不了,所以如果想全局用的话,
本人想到的是可以用aop切一下,把需要用到的地方都切起来就可以了。

实现handlermethodargumentresolver接口来写解析。

public class base64decodestrresolver implements handlermethodargumentresolver {

    private static final transient logger log = logutils.getexceptionlogger();

    /**
     * 如果参数上有自定义注解base64decodestr的话就支持解析
     */
    @override
    public boolean supportsparameter(methodparameter parameter) {
        return parameter.hasparameterannotation(base64decodestr.class) 
                || parameter.hasmethodannotation(base64decodestr.class);
    }

    @override
    public object resolveargument(methodparameter parameter, modelandviewcontainer mavcontainer,
         nativewebrequest webrequest, webdatabinderfactory binderfactory) throws exception {
        /**
         * 因为这个注解是作用在方法和参数上的,所以要分情况
         */
        int count = parameter.hasmethodannotation(base64decodestr.class)
                ? parameter.getmethodannotation(base64decodestr.class).count()
                : parameter.getparameterannotation(base64decodestr.class).count();
        /**
         * 如果是实体类参数,就把前端传过来的参数构造成一个实体类
         * 在系统中本人把所有实体类都继承了baseentity
         */
            if (baseentity.class.isassignablefrom(parameter.getparametertype())) {
                object obj = parameter.getparametertype().newinstance();
                webrequest.getparametermap().foreach((k, v) -> {
                    try {
                        beanutils.setproperty(obj, k, decodestr(v[0], count));
                    } catch (exception e) {
                        log.error("参数解码有误", e);
                    }
                });
                // 这里的return就会把转化过的参数赋给控制器的方法参数
                return obj;
                // 如果是非集合类,就直接解码返回
            } else if (!iterable.class.isassignablefrom(parameter.getparametertype())) {
                return decodestr(webrequest.getparameter(parameter.getparametername()), count);
            }
        return null;
    }

    /**
     * base64根据次数恢复明文
     *
     * @param str   base64加密*次之后的密文
     * @param count *次
     * @return 明文
     */
    public static string decodestr(string str, int count) {
        for (int i = 0; i < count; i++) {
            str = base64.decodestr(str);
        }
        return str;
    }
}

然后注册一下这个自定义的resolver。
这里就不用配置文件注册了

@configuration
public class webconfig extends webmvcconfigurationsupport {
    //region 注册自定义handlermethodargumentresolver
    @override
    public void addargumentresolvers(list<handlermethodargumentresolver> resolvers) {
        resolvers.add(base64decodestrresolver());
    }

    @bean
    public base64decodestrresolver base64decodestrresolver() {
        return new base64decodestrresolver();
    }
    //endregion
}

在控制器层使用注解。

/**
 * 先试试给方法加注解
 */
@base64decodestr 
public void login(@notblank(message = "用户名不能为空")  string username,
                   @notblank(message = "密码不能为空") string password) {
            system.out.println(username);
            system.out.println(password);
    }

看看效果

  • 前端传值
    SpringBoot使用自定义注解实现简单参数加密解密(注解+HandlerMethodArgumentResolver)
  • 后端接收
    SpringBoot使用自定义注解实现简单参数加密解密(注解+HandlerMethodArgumentResolver)

至此整个功能上已经实现了,我们来看下关键api

// 这个就是一个参数,控制层的方法参数
methodparameter parameter
    // 常用方法
    hasmethodannotation()  是否有方法注解
    hasparameterannotation()  是否有参数注解
    getmethodannotation()  获取方法注解(传入class可以指定)
    getparameterannotation() 获取参数注解(传入class可以指定)
    getparametertype()  获取参数类型


// 这个可以理解为是前端传过来的东西,里面可以拿到前端传过来的密文,也就是初始值,没有被处理过的
nativewebrequest webrequest
    // 常用方法 其实这几个都是同一个 基于map的操作
    getparameter()  
    getparametermap()
    getparameternames()
    getparametervalues()

2.3 深入探讨

上面的例子是注解在方法上的,接下来试试注解在参数上。

/**
 * 注解一个参数
 */
public void login(@notblank(message = "用户名不能为空") @base64decodestr  string username,
                   @notblank(message = "密码不能为空") string password) {
            system.out.println(username);
            system.out.println(password);
}
/*****************输出******************************/
username
wtbkr2vttxpasfpqylzfoq==

/**
 * 注解两个参数
 */
public void login(@notblank(message = "用户名不能为空") @base64decodestr  string username,
                   @notblank(message = "密码不能为空") @base64decodestr string password) {
            system.out.println(username);
            system.out.println(password);
}
/*****************输出******************************/
username
password

可见注解在参数上也能用,接下来再来看看,同时注解在方法上和参数上,想一下。
假设方法上的注解优先,参数上的注解其次,会不会被解析两次,
也就是说,密文先被方法注解解析成明文,然后之后被参数注解再次解析成别的东西。

/**
 * 注解方法 注解参数
 */
@base64decodestr
public void login(@notblank(message = "用户名不能为空") @base64decodestr  string username,
                   @notblank(message = "密码不能为空") @base64decodestr string password) {
            system.out.println(username);
            system.out.println(password);
}
/*****************输出******************************/
username
password

输出的是正确的明文,也就是说上面的假设不成立,让我们康康是哪里的问题。

回想一下,在解析的时候,我们都是用的webrequest的getparameter,而webrequest里面的值是从前端拿过来的,
所以decodestr解密都是对前端的值解密,当然会返回正确的内容(明文),所以即使是方法注解先解密了,它解密的是前端的值,
然后再到属性注解,它解密的也是前端的值,不会出现属性注解解密的内容是方法注解解密出来的内容。
从这点来看,确实是这么一回事,所以即使方法注解和参数注解一起用也不会出现重复解密的效果。

但是,这只是一个原因,一开始本人还没想到这个,然后就好奇打了断点追踪下源码。

@override
@nullable
public object resolveargument(methodparameter parameter, @nullable modelandviewcontainer mavcontainer,nativewebrequest webrequest, @nullable webdatabinderfactory binderfactory) throws exception {
                 // 获取参数的resolver,参数的定位是控制器.方法.参数位置 ,所以每个parameter都是唯一的
                 // 至于重载的啊,不知道没试过,你们可以试下,xd
		handlermethodargumentresolver resolver = getargumentresolver(parameter);
		if (resolver == null) {
		  throw new illegalargumentexception("unsupported parameter type [" +
		  parameter.getparametertype().getname() + "]. supportsparameter should be called first.");
		}
		  return resolver.resolveargument(parameter, mavcontainer, webrequest, binderfactory);
}

@nullable
private handlermethodargumentresolver getargumentresolver(methodparameter parameter) {
                // argumentresolvercache是一个缓存,map,
                // 从这里可以看出,每个控制器方法的参数都会被缓存起来,
		handlermethodargumentresolver result = this.argumentresolvercache.get(parameter);
		if (result == null) {
			for (handlermethodargumentresolver resolver : this.argumentresolvers) {
                            // 调用supportsparameter看看是否支持
	                    if (resolver.supportsparameter(parameter)) {
				result = resolver;
                                // 一个参数可以有多个resolver 
	        		this.argumentresolvercache.put(parameter, result);
				break;
				}
			}
		}
		return result;          
}


所以问题再细化一点,当我们同时注解方法和参数的时候,会调用几次getargumentresolver()呢,
为了便于观察,本人将注解传不同的参数。
在那之前,先放点小插曲,就是在调试的时候发现的问题

/**
 * 注解方法
 */
@base64decodestr( count = 10)
public void login(@notblank(message = "用户名不能为空") string username,
                   @notblank(message = "密码不能为空")  string password) {
            system.out.println(username);
            system.out.println(password);
}

进去前
SpringBoot使用自定义注解实现简单参数加密解密(注解+HandlerMethodArgumentResolver)
parameter是获取不到方法上这个自定义注解的。
当代码往下走,走到supportsparameter的时候
SpringBoot使用自定义注解实现简单参数加密解密(注解+HandlerMethodArgumentResolver)
此时又有了,无语。
什么原因本人暂时没找到。

言归正传,我们继续调试

/**
 * 注解方法 注解全部参数
 */
@base64decodestr( count = 30)
public void login(@notblank(message = "用户名不能为空") @base64decodestr(count = 10)  string username,
                   @notblank(message = "密码不能为空") @base64decodestr(count =20) string password) {
            system.out.println(username);
            system.out.println(password);
}

看看是先走方法注解还是参数注解。

  • 第一次进来
    SpringBoot使用自定义注解实现简单参数加密解密(注解+HandlerMethodArgumentResolver)
    可以看到是第一个参数username
  • 第二次进来
    SpringBoot使用自定义注解实现简单参数加密解密(注解+HandlerMethodArgumentResolver)
    依然是第一个参数username
  • 第三次进来
    SpringBoot使用自定义注解实现简单参数加密解密(注解+HandlerMethodArgumentResolver)
    看到是第二个参数password
  • 第四次进来
    SpringBoot使用自定义注解实现简单参数加密解密(注解+HandlerMethodArgumentResolver)
    也是第二个参数password

所以可以看到,根本就没有走方法注解,或者说方法注解会走两次,参数注解一个一次,所以总共四次,这也没问题。
这是怎么回事呢。要是不走方法注解,那方法注解怎么会生效呢,后面我找到了原因

  /**
   * 原来是因为这里,虽然不是因为方法注解进来的,但是这里优先取的是方法注解的值,
   * 所以如果想让属性注解优先的话这里改一下就行
   */
  int count = parameter.hasmethodannotation(base64decodestr.class)
                ? parameter.getmethodannotation(base64decodestr.class).count()
                : parameter.getparameterannotation(base64decodestr.class).count();

所以真相大白了,如果方法注解和属性注解同时加上的话,会执行四次getargumentresolver(),
其中只会调用两次supportsparameter(),因为每个参数第二次都直接从map取到值了就不再走supportsparameter()了。

结束

至此我们完成了本次从前端到后端的旅途。
简单总结一下。

  • 注解
    • 定义:@interface
    • 类型:type,field,method,parameter,constructor,local_variable,annotation_type,package
    • 策略:source,class,runtime
  • handlermethodargumentresolver
    • 作用:像拦截器一样,在前端到后端中间的关卡
    • 两个方法
      • supportsparameter:是否支持使用该resolver
      • resolveargument:resolver想要做的事

然后关于注解解析部分也不够完善,比如如果参数是集合类型的话应该怎么处理,这都是后续了。

本篇内容都是本人真实遇到的问题并记录下来,从开始想要加密加密参数到想办法去实现这个功能,
这么一种思路,希望能给新人一点启示,当然本人本身也还需要不断学习,不然都找不到工作了,我只能边忙毕设边挤时间复习了。
人一惆怅话就多了,嘿嘿,不啰嗦了,现在是夜里两点,准备睡了。