SpringBoot uses custom annotations to encrypt and decrypt simple parameters (annotation + HandlerMethodArgumentResolver)

Preface

Huang Hansan has returned and has not updated his blog for nearly half a year. This half year's experience is really not easy.
At the moment of the epidemic, the company I interned with did not share difficulties with the employees and directly dismissed many people.
As an intern, I was also relentlessly dismissed.So I have to get ready for a job again.
No, I'm sorry. I thought it was sent yesterday, but it was a clear day and a period of mourning stayed until today.

Say nothing but get to the point.It is really difficult for me to write about this period and the school has not started yet.
Originally, I just wanted to go back to school and settle in my "old age". After all, I could find a job again, but after graduation, few friends and relatives would graduate.
So relatives, we must cherish the people around us.

Because this post is written on the local typora before leaving the blog park, the format is a bit inconsistent
The blog Park markdown editor is not very useful yet, which is a bit of a headache
Another problem is the code format, and copying to markdown is messy again
I cried, I was messy, and when the blog space was squeezed, the blog was messy
markdown will be used more and more in the future, so typesetting will be more and more beautified

Readers of this article will be able to learn the following

  • Simple use and analysis of notes
  • Some knowledge about HandlerMethodArgumentResolver

cause

Write it out, we'll set up the backstage this week, and the applet end hasn't started yet.As mentioned in the title, SpringBoot is used for backend building.
Then, of course, the RESTful style is applied, and when I have a url of / initJson/{id}, I pass in the user ID directly.
I want to be able to simply encrypt the ID in the front end, at least I can't watch the ID pass directly to the back end.Although it's just a setup,
But let's do it a little bit. If so, I'll choose Base64.
I want to transfer some simple parameters from the front-end to the back-end and decrypt them to avoid plain text transmission.
In a real environment, of course, there must be a better solution.It's just a thought or a scene.
After giving you an example, you can start with a few more tips.

process

1. Front End

Encrypt when front-end parameters are passed

 // Encoder is a Base64 encryption method, you can use it yourself
 data.password = encode(pwd);
 data.username= encode(username);

In this way, the front end was ciphertext.

2. Backend

Once the parameters are passed to the backend, you want to parse the ciphertext back to plaintext, and then this is the main point of this article.
When decrypting, I first decrypted it in the interface.

/**
  *  At this point the parameter accepts ciphertext
  */
String login(String username, String password) {
       username =  Base64Util.decode(username);
       password=  Base64Util.decode(password);
}

It looks like nothing, but in case there are many parameters, or interfaces, do you want each interface to write a lot of decrypted code like this?
Obviously, there is still room for improvement. What can I do?I think of notes, or try with them, so that I can also learn more about them.

2.1 Comments

Note this thing, I thought how it worked when I was studying, it was customizable (laughing and crying).
Let's take a brief look at the notes in this article. If you need to, I can update a post about them later.
Or the reader can learn to understand it by himself. Speaking of this, the reason why I write a blog is that I don't have it on the internet, or when I find something on the Internet that is different from what I need, I will write a blog.
Some words do not write, in order to avoid the same thing, so I do not update many blogs, basically a long time ago.
But it doesn't seem right to think that blogging, whatever it is, is not only convenient for you to learn but also for others.
So the frequency should be better in the future, hopefully.

Back to the point, notes have three main things

  • Annotation Definition
  • Note Type
  • RetentionPolicy

Let's look at the definition of the note first. It's easy

// The main point is that @interface uses the type it defines as an annotation, just as the type defined by the class is a class.
public @interface Base64DecodeStr {
    /**
     * Here's where you can put something you need to annotate
     * The count() below means the number of decryptions, defaulting to one
     */
    int count() default 1;
}

Then look at note types

// Annotation type is actually where annotation declarations are made
public enum ElementType {
    TYPE,               /* Class, interface (including comment type) or enumeration declaration  */
    FIELD,              /* Field declarations (including enumeration constants)  */
    METHOD,             /* Method declaration  */
    PARAMETER,          /* Parameter declaration  */
    CONSTRUCTOR,        /* Construction method declaration  */
    LOCAL_VARIABLE,     /* Local variable declaration  */
    ANNOTATION_TYPE,    /* Annotation type declaration  */
    PACKAGE             /* Package declaration  */
}

// That's how this Target works
// Now this comment, I hope it can only declare that there are parameters in the method, otherwise it will be wrong
@Target({ElementType.METHOD, ElementType.PARAMETER})
public @interface Base64DecodeStr {
    int count() default 1;
}

Finally, let's look at the annotation strategy

public enum RetentionPolicy {
    SOURCE,  /* Annotation Information only exists during compiler processing and there is no Annotation information after the compiler has finished processing*/
    CLASS,   /* The compiler stores the Annotation in the.Class file corresponding to the class.Default behavior  */
    RUNTIME  /* The compiler stores the Annotation in a class file and can be read in by the JVM */
}

// Usually a third, RUNTIME, is used so that the program can run as well
@Target({ElementType.METHOD, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
public @interface Base64DecodeStr {
    int count() default 1;
}

So far, a comment is defined.But when does it work, we need to write an explanation of this note.
Then think about the purpose of defining this annotation: to use parameters directly in an interface is plain text, you should decrypt the ciphertext back into plain text and put it back into the parameters before entering the interface.
What's a good way to do this? Now it's the next protagonist's turn to come on. It's HandlerMethodArgumentResolver.

2.2 HandlerMethodArgumentResolver

Officially, this is how HandlerMethodArgumentResolver functions and parses

/**
 * Strategy interface for resolving method parameters into argument values in
 * the context of a given request.
 * Translated once
 * Policy interface for parsing method parameters into parameter values in the context of a given request
 * @author Arjen Poutsma
 * @since 3.1
 * @see HandlerMethodReturnValueHandler
 */
public interface HandlerMethodArgumentResolver {

        /**
         * MethodParameter Refers to the parameters of the controller layer method
         * Is this interface supported
         * ture The following method will be executed to resolve
         */
	boolean supportsParameter(MethodParameter parameter);

        /**
         * A common way to write is to process the parameters of the front end and copy them to the parameters of the controller method
         */
	@Nullable
	Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception;
}

So it's important to think about why SpringMVC receives parameters by writing a few comments on the controller.
A common @PathVariable is implemented using this interface.

I understand that by implementing this interface, methods and parameters can be handled between front-end and back-end interfaces, so it just meets the above requirements.
In fact, this interface is also a common one in SpringMVC source code, readers can still understand it by themselves.
At present, I am not ready to write an article about Spring reading source code, because I have not seen it systematically, maybe I will update the blog after reading it.

Continue, with such an interface, you can write and parse custom annotations. Careful students can find that you can write annotation parsing here.
So this annotation only works at the control level, not at the service level or even at the DAO level, so if you want to use it globally,
What I thought was that I could use AOP to cut everything I needed.

Implement the HandlerMethodArgumentResolver interface to write resolution.

public class Base64DecodeStrResolver implements HandlerMethodArgumentResolver {

    private static final transient Logger log = LogUtils.getExceptionLogger();

    /**
     * Parsing is supported if there is a custom annotation Base64DecodeStr on the parameter
     */
    @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 {
        /**
         * Because this comment is used on methods and parameters, it is case-sensitive
         */
        int count = parameter.hasMethodAnnotation(Base64DecodeStr.class)
                ? parameter.getMethodAnnotation(Base64DecodeStr.class).count()
                : parameter.getParameterAnnotation(Base64DecodeStr.class).count();
        /**
         * If it is an entity class parameter, construct the parameters passed from the front end into an entity class
         * I inherited all the entity classes from BaseEntity in the system
         */
            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("Error decoding parameters", e);
                    }
                });
                // Here return assigns the converted parameters to the controller's method parameters
                return obj;
                // If it is a non-set class, decode it directly and return it
            } else if (!Iterable.class.isAssignableFrom(parameter.getParameterType())) {
                return decodeStr(webRequest.getParameter(parameter.getParameterName()), count);
            }
        return null;
    }

    /**
     * Base64 Restore plaintext by number of times
     *
     * @param str   Base64 Encrypt the ciphertext after *
     * @param count *second
     * @return Clear text
     */
    public static String decodeStr(String str, int count) {
        for (int i = 0; i < count; i++) {
            str = Base64.decodeStr(str);
        }
        return str;
    }
}

Then register this custom Relver.
No profile registration is required here

@Configuration
public class WebConfig extends WebMvcConfigurationSupport {
    //region Register Custom HandlerMethodArgumentResolver
    @Override
    public void addArgumentResolvers(List<HandlerMethodArgumentResolver> resolvers) {
        resolvers.add(base64DecodeStrResolver());
    }

    @Bean
    public Base64DecodeStrResolver base64DecodeStrResolver() {
        return new Base64DecodeStrResolver();
    }
    //endregion
}

Use annotations at the controller level.

/**
 * Try annotating the method first
 */
@Base64DecodeStr 
public void login(@NotBlank(message = "User name cannot be empty")  String username,
                   @NotBlank(message = "Password cannot be empty") String password) {
            System.out.println(username);
            System.out.println(password);
    }

See the effect

  • Front-end value transfer
  • Backend Receive

Now that the entire functionality is implemented, let's look at the key api

// This is a parameter, the method parameter of the control layer
MethodParameter parameter
    // common method
    hasMethodAnnotation()  Is there a way to comment
    hasParameterAnnotation()  Is there a parameter comment
    getMethodAnnotation()  Get method annotations(afferent Class Can be specified)
    getParameterAnnotation() Get parameter annotations(afferent Class Can be specified)
    getParameterType()  Get the parameter type


// This can be interpreted as something passed from the front-end, in which you can get the cipher passed from the front-end, that is, the initial value, which has not been processed.
NativeWebRequest webRequest
    // Common methods These are actually the same map-based operations
    getParameter()  
    getParameterMap()
    getParameterNames()
    getParameterValues()

2.3 Deep Exploration

The example above shows how annotations work. Next, try annotating parameters.

/**
 * Annotate a parameter
 */
public void login(@NotBlank(message = "User name cannot be empty") @Base64DecodeStr  String username,
                   @NotBlank(message = "Password cannot be empty") String password) {
            System.out.println(username);
            System.out.println(password);
}
/*****************Output **********************************/
username
WTBkR2VtTXpaSFpqYlZFOQ==

/**
 * Annotate two parameters
 */
public void login(@NotBlank(message = "User name cannot be empty") @Base64DecodeStr  String username,
                   @NotBlank(message = "Password cannot be empty") @Base64DecodeStr String password) {
            System.out.println(username);
            System.out.println(password);
}
/*****************Output **********************************/
username
password

Notes can also be used on parameters, so let's take a look next, and also on methods and parameters, think about it.
Assuming that the annotations on the method take precedence and the parameters take precedence, will they be resolved twice or not?
That is, ciphertext is first parsed into plain text by method annotations, and then parsed into something else by parameter annotations.

/**
 * Annotation Method Annotation Parameters
 */
@Base64DecodeStr
public void login(@NotBlank(message = "User name cannot be empty") @Base64DecodeStr  String username,
                   @NotBlank(message = "Password cannot be empty") @Base64DecodeStr String password) {
            System.out.println(username);
            System.out.println(password);
}
/*****************Output **********************************/
username
password

The output is the correct plain text, that is, the above assumptions are not valid, let us Kangkang where the problem is.

Recall that when we parse, we all use the getParameter of the webRequest, and the values inside the webRequest are taken from the front end.
So decodeStr decryption is all about decrypting the front-end values, and of course returns the correct content (plain text), so even if the method annotation is decrypted first, it decrypts the front-end values.
Then to the attribute annotation, which decrypts the front-end value, there will be no content decrypted by the attribute annotation is the content decrypted by the method annotation.
From this point of view, this is really the case, so even if method and parameter annotations are used together, there will be no effect of repeated decryption.

But that's just one reason. I didn't think about it at first, and then I interrupted to track down the source code.

@Override
@Nullable
public Object resolveArgument(MethodParameter parameter, @Nullable ModelAndViewContainer mavContainer,NativeWebRequest webRequest, @Nullable WebDataBinderFactory binderFactory) throws Exception {
                 // Gets the resolver of the parameter, the positioning of the parameter is the controller.method.parameter position, so each parameter is unique
                 // As for heavy duty, I don't know it's not tried. You can try it, 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) {
                // The argumentResolverCache is a cache, map,
                // As you can see here, the parameters of each controller method are cached.
		HandlerMethodArgumentResolver result = this.argumentResolverCache.get(parameter);
		if (result == null) {
			for (HandlerMethodArgumentResolver resolver : this.argumentResolvers) {
                            // Call supportsParameter to see if it supports
	                    if (resolver.supportsParameter(parameter)) {
				result = resolver;
                                // A parameter can have multiple Resolvers 
	        		this.argumentResolverCache.put(parameter, result);
				break;
				}
			}
		}
		return result;          
}


So let's go a little further and call getArgumentResolver() several times when we annotate both methods and parameters.
For ease of observation, I will annotate different parameters.
Before that, let's start with a little episode, which is the problem we found while debugging

/**
 * Annotation Method
 */
@Base64DecodeStr( count = 10)
public void login(@NotBlank(message = "User name cannot be empty") String username,
                   @NotBlank(message = "Password cannot be empty")  String password) {
            System.out.println(username);
            System.out.println(password);
}

Before entering

The parameter cannot get this custom comment on the method.
Go down the code until you reach supportsParameter

Now there is another, silent.
I can't find any reason at this time.

To get it right, let's continue debugging

/**
 * Annotation Method Annotate All Parameters
 */
@Base64DecodeStr( count = 30)
public void login(@NotBlank(message = "User name cannot be empty") @Base64DecodeStr(count = 10)  String username,
                   @NotBlank(message = "Password cannot be empty") @Base64DecodeStr(count =20) String password) {
            System.out.println(username);
            System.out.println(password);
}

See if you want to start with method or parameter annotations.

  • First time in

    You can see that this is the first parameter, username
  • Second Entry

    Still the first parameter username
  • Come in for the third time

    See the second parameter password
  • Fourth Entry

    Is also the second parameter password

So you can see that there are no method annotations at all, or method annotations will go twice, parameter annotations will go once, so there are four times in total, which is no problem.
What's wrong with this?If I don't follow the method comment, how does it work? I find the reason later

  /**
   * Originally because here, although not because of method annotations, the value of method annotations is preferred here.
   * So if you want attribute annotations to take precedence, just change here
   */
  int count = parameter.hasMethodAnnotation(Base64DecodeStr.class)
                ? parameter.getMethodAnnotation(Base64DecodeStr.class).count()
                : parameter.getParameterAnnotation(Base64DecodeStr.class).count();

So the truth is clear, getArgumentResolver() will be executed four times if both method and attribute annotations are added.
Only supportsParameter() is called twice because each parameter is taken directly from the map the second time and supportsParameter() is no longer used.

End

So far we have completed this journey from the front end to the back end.
To summarize briefly.

  • annotation
    • Definition: @interface
    • Types: TYPE, FIELD, METHOD, PARAMETER, CONSTRUCTOR, LOCAL_VARIABLE, ANNOTATION_TYPE, PACKAGE
    • Policy: SOURCE, CLASS, RUNTIME
  • HandlerMethodArgumentResolver
    • Role: Like an interceptor, level from front to back
    • Two methods
      • supportsParameter: Supports the use of this Resolver
      • resolveArgument: What Resolver wants to do

Then the comment parsing section is not perfect, such as what to do if the parameter is a set type, which is follow-up.

This article is a record of my real problems, from the beginning when I want to encrypt encryption parameters to how I can achieve this function.
Such a way of thinking, I hope to give new people some inspiration, of course, I also need to keep learning, or I can not find a job, I can only busy set aside time to review.
People are more melancholy, hey, no more verbose, it is two o'clock at night, ready to sleep.

Keywords: Java Attribute SpringBoot jvm Spring

Added by valen53 on Mon, 06 Apr 2020 08:09:19 +0300