前言

我黄汉三又回来了,快半年没更新博客了,这半年来的经历实属不易,

疫情当头,本人实习的公司没有跟员工共患难,直接辞掉了很多人。

作为一个实习生,本人也被无情开除了。所以本人又得重新准备找工作了。

算了,感慨一下,本来想昨天发的,但昨天是清明,哀悼时期,就留到了今天发。

话不多说,直接进入正题吧。这段时间本人在写毕设,学校也迟迟没有开学的消息,属实难顶。

本来被开了本人只想回学校安度"晚年"算了,毕竟工作可以再找,但亲朋好友以后毕业了就很少见了。

所以亲们,一定要珍惜身边的人噢。

因为这篇博文是现在本地typora上面写好再放过博客园的,格式有点不统一

博客园的markdown编辑器还不是很好用,这点有点头疼

还有一点是代码格式问题,复制到markdown又变乱了

我哭了,本来就乱了,再加上博客篇幅的问题一挤压,博文就乱完了

以后更文都用markdown了,所以关于排版的问题会越来越美化一下

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

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

起因

写毕设,这周才把后台搭好,还有小程序端还没开始。如题目所说,用了SpringBoot做后端搭建。

然后也当然应用了RESTful风格,当本人有一个url是/initJson/{id}的时候,直接就把用户ID传过来了。

本人就想能不能在前端简单把ID加密一下,起码不能眼睁睁看着ID直接就传到后端。虽然只是一个毕设,

但还是稍微处理一下吧,处理的话我选择用Base64好了。

本人现在是想把前端传的一些简单参数,用密文传到后端再解密使用,避免明文传输。

当然在真正的环境中,肯定是使用更好的方案的。这里只是说有那么一种思路或者说那么一种场景。

给大家举个例子之后可以抛砖引玉。

过程

1.前端

前端传参的时候,加密

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

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

2.后端

当参数传到后端之后,想要把密文解析回明文,然后接下来就是本文的主旨所在了。

解密的时候,本人一开始是在接口里面解密的。

  1. /**
  2. * 此时参数接受到的内容是密文
  3. */
  4. String login(String username, String password) {
  5. username = Base64Util.decode(username);
  6. password= Base64Util.decode(password);
  7. }

看起来也没啥是吧,但是万一参数很多,或者说接口多,难道要每个接口都这么写一堆解密的代码吗。

显然还可以改造,怎么做?本人想到了注解,或者说想用注解试试,这样自己也能加深对注解的学习。

2.1 注解

注解这个东西,本人当时学习的时候还以为是怎么起作用的,原来是可以自定义的(笑哭)。

我们在本文简单了解下注解吧,如果有需要,后面本人可以更新一篇关于注解的博文。

或者读者可以自行学习了解一下,说到这里,本人写博客的理由是,网上没有,或者网上找到的东西跟本人需要的不一样时才会写博客。

有的话就不写了,以免都是同样的东西,所以本人更新的博客并不算多,基本很久才一篇。

但好像这样想并不对,写博客无论是什么内容,不仅方便自己学习也可以方便他人,

所以以后应该更新频率会好点吧希望。

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

  • 注解定义(Annotation)
  • 注解类型(ElementType)
  • 注解策略(RetentionPolicy)

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

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

然后再来看看注解类型

  1. // 注解类型其实就是注解声明在什么地方
  2. public enum ElementType {
  3. TYPE, /* 类、接口(包括注释类型)或枚举声明 */
  4. FIELD, /* 字段声明(包括枚举常量) */
  5. METHOD, /* 方法声明 */
  6. PARAMETER, /* 参数声明 */
  7. CONSTRUCTOR, /* 构造方法声明 */
  8. LOCAL_VARIABLE, /* 局部变量声明 */
  9. ANNOTATION_TYPE, /* 注释类型声明 */
  10. PACKAGE /* 包声明 */
  11. }
  12. // 这个Target就是这么使用的
  13. // 现在这个注解,本人希望它只能声明在方法上还有参数上,别的地方声明就会报错
  14. @Target({ElementType.METHOD, ElementType.PARAMETER})
  15. public @interface Base64DecodeStr {
  16. int count() default 1;
  17. }

最后再来看看注解策略

  1. public enum RetentionPolicy {
  2. SOURCE, /* Annotation信息仅存在于编译器处理期间,编译器处理完之后就没有该Annotation信息了*/
  3. CLASS, /* 编译器将Annotation存储于类对应的.class文件中。默认行为 */
  4. RUNTIME /* 编译器将Annotation存储于class文件中,并且可由JVM读入 */
  5. }
  6. // 一般用第三个,RUNTIME,这样的话程序运行中也可以使用
  7. @Target({ElementType.METHOD, ElementType.PARAMETER})
  8. @Retention(RetentionPolicy.RUNTIME)
  9. public @interface Base64DecodeStr {
  10. int count() default 1;
  11. }

到此为止,一个注解就定义好了。但是在什么时候工作呢,这时我们就需要写这个注解的解析了。

然后想想,定义这个注解的目的是,想直接在接口使用参数就是明文,所以应该在进入接口之前就把密文解密回明文并放回参数里。

这一步有什么好办法呢,这时候就轮到下一个主角登场了,它就是HandlerMethodArgumentResolver

2.2 HandlerMethodArgumentResolver

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

  1. /**
  2. * Strategy interface for resolving method parameters into argument values in
  3. * the context of a given request.
  4. * 翻译了一下
  5. * 策略接口,用于在给定请求的上下文中将方法参数解析为参数值
  6. * @author Arjen Poutsma
  7. * @since 3.1
  8. * @see HandlerMethodReturnValueHandler
  9. */
  10. public interface HandlerMethodArgumentResolver {
  11. /**
  12. * MethodParameter指的是控制器层方法的参数
  13. * 是否支持此接口
  14. * ture就会执行下面的方法去解析
  15. */
  16. boolean supportsParameter(MethodParameter parameter);
  17. /**
  18. * 常见的写法就是把前端的参数经过处理再复制给控制器方法的参数
  19. */
  20. @Nullable
  21. Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
  22. }

所以这个接口,是很重要的,想想SpringMVC为何在控制器写几个注解,就能接收到参数,这个接口就是功不可没的。

像常见的@PathVariable 就是用这个接口实现的。



本人的理解是,实现这个接口,就能在前端到后端接口之间处理方法和参数,所以刚好满足上面的需求。

其实这个接口也是属于SpringMVC源码里面常见的一个,读者依然也可自行了解下,

目前本人还没有准备要写Spring读源码的文章,因为本人也还没系统的去看过,或许以后本人看了就会更新有关博客。

继续,有了这样的接口就可以用来写解析自定义注解了,细心的同学可以发现,在这里写注解解析,

那么这个注解就只能是在控制层起作用了,在服务层甚至DAO层都用不了,所以如果想全局用的话,

本人想到的是可以用AOP切一下,把需要用到的地方都切起来就可以了。

实现HandlerMethodArgumentResolver接口来写解析。

  1. public class Base64DecodeStrResolver implements HandlerMethodArgumentResolver {
  2. private static final transient Logger log = LogUtils.getExceptionLogger();
  3. /**
  4. * 如果参数上有自定义注解Base64DecodeStr的话就支持解析
  5. */
  6. @Override
  7. public boolean supportsParameter(MethodParameter parameter) {
  8. return parameter.hasParameterAnnotation(Base64DecodeStr.class)
  9. || parameter.hasMethodAnnotation(Base64DecodeStr.class);
  10. }
  11. @Override
  12. public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer mavContainer,
  13. NativeWebRequest webRequest, WebDataBinderFactory binderFactory) throws Exception {
  14. /**
  15. * 因为这个注解是作用在方法和参数上的,所以要分情况
  16. */
  17. int count = parameter.hasMethodAnnotation(Base64DecodeStr.class)
  18. ? parameter.getMethodAnnotation(Base64DecodeStr.class).count()
  19. : parameter.getParameterAnnotation(Base64DecodeStr.class).count();
  20. /**
  21. * 如果是实体类参数,就把前端传过来的参数构造成一个实体类
  22. * 在系统中本人把所有实体类都继承了BaseEntity
  23. */
  24. if (BaseEntity.class.isAssignableFrom(parameter.getParameterType())) {
  25. Object obj = parameter.getParameterType().newInstance();
  26. webRequest.getParameterMap().forEach((k, v) -> {
  27. try {
  28. BeanUtils.setProperty(obj, k, decodeStr(v[0], count));
  29. } catch (Exception e) {
  30. log.error("参数解码有误", e);
  31. }
  32. });
  33. // 这里的return就会把转化过的参数赋给控制器的方法参数
  34. return obj;
  35. // 如果是非集合类,就直接解码返回
  36. } else if (!Iterable.class.isAssignableFrom(parameter.getParameterType())) {
  37. return decodeStr(webRequest.getParameter(parameter.getParameterName()), count);
  38. }
  39. return null;
  40. }
  41. /**
  42. * Base64根据次数恢复明文
  43. *
  44. * @param str Base64加密*次之后的密文
  45. * @param count *次
  46. * @return 明文
  47. */
  48. public static String decodeStr(String str, int count) {
  49. for (int i = 0; i < count; i++) {
  50. str = Base64.decodeStr(str);
  51. }
  52. return str;
  53. }
  54. }

然后注册一下这个自定义的Resolver。

这里就不用配置文件注册了

  1. @Configuration
  2. public class WebConfig extends WebMvcConfigurationSupport {
  3. //region 注册自定义HandlerMethodArgumentResolver
  4. @Override
  5. public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
  6. resolvers.add(base64DecodeStrResolver());
  7. }
  8. @Bean
  9. public Base64DecodeStrResolver base64DecodeStrResolver() {
  10. return new Base64DecodeStrResolver();
  11. }
  12. //endregion
  13. }

在控制器层使用注解。

  1. /**
  2. * 先试试给方法加注解
  3. */
  4. @Base64DecodeStr
  5. public void login(@NotBlank(message = "用户名不能为空") String username,
  6. @NotBlank(message = "密码不能为空") String password) {
  7. System.out.println(username);
  8. System.out.println(password);
  9. }

看看效果

  • 前端传值

  • 后端接收

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

  1. // 这个就是一个参数,控制层的方法参数
  2. MethodParameter parameter
  3. // 常用方法
  4. hasMethodAnnotation() 是否有方法注解
  5. hasParameterAnnotation() 是否有参数注解
  6. getMethodAnnotation() 获取方法注解(传入Class可以指定)
  7. getParameterAnnotation() 获取参数注解(传入Class可以指定)
  8. getParameterType() 获取参数类型
  9. // 这个可以理解为是前端传过来的东西,里面可以拿到前端传过来的密文,也就是初始值,没有被处理过的
  10. NativeWebRequest webRequest
  11. // 常用方法 其实这几个都是同一个 基于map的操作
  12. getParameter()
  13. getParameterMap()
  14. getParameterNames()
  15. getParameterValues()

2.3 深入探讨

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

  1. /**
  2. * 注解一个参数
  3. */
  4. public void login(@NotBlank(message = "用户名不能为空") @Base64DecodeStr String username,
  5. @NotBlank(message = "密码不能为空") String password) {
  6. System.out.println(username);
  7. System.out.println(password);
  8. }
  9. /*****************输出******************************/
  10. username
  11. WTBkR2VtTXpaSFpqYlZFOQ==
  12. /**
  13. * 注解两个参数
  14. */
  15. public void login(@NotBlank(message = "用户名不能为空") @Base64DecodeStr String username,
  16. @NotBlank(message = "密码不能为空") @Base64DecodeStr String password) {
  17. System.out.println(username);
  18. System.out.println(password);
  19. }
  20. /*****************输出******************************/
  21. username
  22. password

可见注解在参数上也能用,接下来再来看看,同时注解在方法上和参数上,想一下。

假设方法上的注解优先,参数上的注解其次,会不会被解析两次,

也就是说,密文先被方法注解解析成明文,然后之后被参数注解再次解析成别的东西。

  1. /**
  2. * 注解方法 注解参数
  3. */
  4. @Base64DecodeStr
  5. public void login(@NotBlank(message = "用户名不能为空") @Base64DecodeStr String username,
  6. @NotBlank(message = "密码不能为空") @Base64DecodeStr String password) {
  7. System.out.println(username);
  8. System.out.println(password);
  9. }
  10. /*****************输出******************************/
  11. username
  12. password

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

回想一下,在解析的时候,我们都是用的webRequest的getParameter,而webRequest里面的值是从前端拿过来的,

所以decodeStr解密都是对前端的值解密,当然会返回正确的内容(明文),所以即使是方法注解先解密了,它解密的是前端的值,

然后再到属性注解,它解密的也是前端的值,不会出现属性注解解密的内容是方法注解解密出来的内容。

从这点来看,确实是这么一回事,所以即使方法注解和参数注解一起用也不会出现重复解密的效果。

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

  1. @Override
  2. @Nullable
  3. public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
  4. // 获取参数的resolver,参数的定位是控制器.方法.参数位置 ,所以每个parameter都是唯一的
  5. // 至于重载的啊,不知道没试过,你们可以试下,XD
  6. HandlerMethodArgumentResolver resolver = getArgumentResolver(parameter);
  7. if (resolver == null) {
  8. throw new IllegalArgumentException("Unsupported parameter type [" +
  9. parameter.getParameterType().getName() + "]. supportsParameter should be called first.");
  10. }
  11. return resolver.resolveArgument(parameter, mavContainer, webRequest, binderFactory);
  12. }
  13. @Nullable
  14. private HandlerMethodArgumentResolver getArgumentResolver(MethodParameter parameter) {
  15. // argumentResolverCache是一个缓存,map,
  16. // 从这里可以看出,每个控制器方法的参数都会被缓存起来,
  17. HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
  18. if (result == null) {
  19. for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
  20. // 调用supportsParameter看看是否支持
  21. if (resolver.supportsParameter(parameter)) {
  22. result = resolver;
  23. // 一个参数可以有多个resolver
  24. this.argumentResolverCache.put(parameter, result);
  25. break;
  26. }
  27. }
  28. }
  29. return result;
  30. }

所以问题再细化一点,当我们同时注解方法和参数的时候,会调用几次getArgumentResolver()呢,

为了便于观察,本人将注解传不同的参数。

在那之前,先放点小插曲,就是在调试的时候发现的问题

  1. /**
  2. * 注解方法
  3. */
  4. @Base64DecodeStr( count = 10)
  5. public void login(@NotBlank(message = "用户名不能为空") String username,
  6. @NotBlank(message = "密码不能为空") String password) {
  7. System.out.println(username);
  8. System.out.println(password);
  9. }

进去前



parameter是获取不到方法上这个自定义注解的。

当代码往下走,走到supportsParameter的时候



此时又有了,无语。

什么原因本人暂时没找到。

言归正传,我们继续调试

  1. /**
  2. * 注解方法 注解全部参数
  3. */
  4. @Base64DecodeStr( count = 30)
  5. public void login(@NotBlank(message = "用户名不能为空") @Base64DecodeStr(count = 10) String username,
  6. @NotBlank(message = "密码不能为空") @Base64DecodeStr(count =20) String password) {
  7. System.out.println(username);
  8. System.out.println(password);
  9. }

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

  • 第一次进来



    可以看到是第一个参数username
  • 第二次进来



    依然是第一个参数username
  • 第三次进来



    看到是第二个参数password
  • 第四次进来



    也是第二个参数password

所以可以看到,根本就没有走方法注解,或者说方法注解会走两次,参数注解一个一次,所以总共四次,这也没问题。

这是怎么回事呢。要是不走方法注解,那方法注解怎么会生效呢,后面我找到了原因

  1. /**
  2. * 原来是因为这里,虽然不是因为方法注解进来的,但是这里优先取的是方法注解的值,
  3. * 所以如果想让属性注解优先的话这里改一下就行
  4. */
  5. int count = parameter.hasMethodAnnotation(Base64DecodeStr.class)
  6. ? parameter.getMethodAnnotation(Base64DecodeStr.class).count()
  7. : 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想要做的事

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

本篇内容都是本人真实遇到的问题并记录下来,从开始想要加密加密参数到想办法去实现这个功能,

这么一种思路,希望能给新人一点启示,当然本人本身也还需要不断学习,不然都找不到工作了,我只能边忙毕设边挤时间复习了。

人一惆怅话就多了,嘿嘿,不啰嗦了,现在是夜里两点,准备睡了。

Springboot使用自定义注解实现简单参数加密解密(注解+HandlerMethodArgumentResolver)的更多相关文章

  1. Java实现一个简单的加密解密方法

    Crypto是Java语言写的一个简单的加密解密方法. 使用方法: 加密方法 String cipherte=Enande.encrypt(content, pass): 解密方法 Enande.de ...

  2. SpringBoot框架中,使用过滤器进行加密解密操作(一)

    一.基本说明 1.请求方式:POST请求.注解@PostMapping 2.入参格式:json串 3.出参格式:json串(整体加密) 4.使用Base64进行加密解密.具体的加密方式,可以根据需求自 ...

  3. PHP 简单的加密解密算法

    <?php /** * * @创建时间:2015-3-12 下午2:07:33 * @作者:YuMing * @描述:异或加密解密类 */ class Yihuo extends CI_Cont ...

  4. DES对 json 、http参数加密解密算法

    网上众多大神们的众多方式实现加解密操作及保障数据安全性.今天无意中发现一篇以 DES加密解密算法.摘抄如下 工具类: import java.security.SecureRandom; import ...

  5. SpringBoot中自定义properties文件配置参数并带有输入提示

    1. 创建配置类 在项目中创建一个参数映射类如下 @ConfigurationProperties(prefix = "user.info") public class MyPro ...

  6. URL参数加密解密

    /// <summary>        /// DES加密字符串        /// </summary>        /// <param name=" ...

  7. ASP.NET - URL中参数加密解密操作

    效果: 代码: using System; using System.Text; using System.IO; using System.Security.Cryptography; public ...

  8. PHP 简单的加密解密方法

    本算法的基础:给定字符A B,A^B=C,C^B=A,即两次异或运算可得到原字符.实现代码如下: /** * @desc加密 * @param string $str 待加密字符串 * @param ...

  9. JS 浏览器地址栏传递参数,参数加密/解密(编码/解码)

    我们有时候在JS里进行页面跳转,并且传递了参数(AppName),如下: window.location = "../../views/form/edit.html?AppName=新增&q ...

随机推荐

  1. 吐槽苹果开放接口のappleid登陆

    这里吐槽一下苹果的开发文档,一切源于前段时间,公司的产品app(某知名资讯app)要接入苹果登陆(ios13发布以来,apple就流氓要求新上线的app,如果有第三方登陆的话,必须要接入appleid ...

  2. JUC常用同步工具类——CountDownLatch,CyclicBarrier,Semaphore

    在 JUC 下包含了一些常用的同步工具类,今天就来详细介绍一下,CountDownLatch,CyclicBarrier,Semaphore 的使用方法以及它们之间的区别. 一.CountDownLa ...

  3. 我厌倦了 Redux,那就造个轮子 Rectx:第三集

    仓库:215566435/rectx 前言 麻烦快去我的仓库里面喷: 老子学不动了,求不要更新. 呵呵,你没想到吧,这玩意儿竟然有第三集!我靠,我自己都没想到,让我们悄悄的回顾一下前两集完全没想到,竟 ...

  4. RuntimeError: No application found. Either work inside a view function or push an application context.

    记录: 遇到这种报错信息: 在create_all()生成数据表的时候,添加app=app,指明app对象即可-----> create_all(app=app)

  5. Yuchuan_Linux_C编程之一 Vim编辑器的使用

    一.整体大纲 二.Vim 编辑器的使用 vi -- vim    vim是从vi发展过来的一款文本编辑器    vi a.txt    前提: 安装了vim软件 工作模式: 1. 命令模式 -- 打开 ...

  6. SSL/TLS 协议运行机制概述(二)

    SSL/TLS 协议运行机制概述(二) 在SSL/TLS 协议运行机制概述(一)中介绍了TLS 1.2 的运行机制,现在我们来看年 TLS 1.3 的运行机制.会涉及到SSL/TLS 协议运行机制概述 ...

  7. 如何使用Kibana

    目录 前言 一.安装 二.加载自定义索引 三.如何搜索数据 四.如何切换中文 五.如何使用控制台 六.可视化图表 七.使用仪表盘 前言 Kibana 是为 Elasticsearch设计的开源分析和可 ...

  8. Python3 面向对象之:多继承

    多继承 class ShenXian: # 神仙 def fei(self): print("神仙都会⻜") class Monkey: # 猴 def chitao(self): ...

  9. C++ 动态链接库 dll的加载

    //首先生成一个my.dll项目,在cpp中添加如下代码 //导出函数 _declspec(dllexport) int test(int a, int b) { return a + b; } // ...

  10. Python 小技巧:如何实现操作系统兼容性打包?

    有一个这样的问题:现要用 setuptools 把一个项目打包成 whl 文件,然后 pip install 在 Windows/Linux 两种操作系统上,但是该项目中有一些依赖库只有 Window ...