In Web projects, tokens are often used for user access verification. After obtaining tokens, if there are many places that need to obtain user information according to tokens, how will you obtain them? This article provides you with N methods. Compare them and see which Level the method used in your project belongs to. Do you need to upgrade it as soon as possible? The operations of token generation and authentication will not be involved in this paper, that is, the default token is validated. This paper will focus on the subsequent business-related processing, that is, the method of obtaining user information based on token (some methods need to be based on SpringBoot).

In Web projects, tokens are often used for user access verification. After obtaining tokens, if there are many places that need to obtain user information according to tokens, how will you obtain them?

This article provides you with N methods. Compare them and see which Level the method used in your project belongs to. Do you need to upgrade it as soon as possible?

The operations of token generation and authentication will not be involved in this paper, that is, the default token is validated. This paper will focus on the subsequent business-related processing, that is, the method of obtaining user information based on token (some methods need to be based on SpringBoot).

Level1: Manually obtained

Usually, the token is placed in the header. The lowest way to obtain the token is to directly obtain the token from the header and then obtain the userId by converting the token. The code for this example is as follows:

@GetMapping("/level1") public Integer level1(HttpServletRequest request) { String token = request.getHeader("token"); Log.info (" Level1 acquired token: {}", token); Integer userId = TokenUtil.getUserIdByToken(token); log.info("userId={}", userId); return userId; }Copy the code

This approach is the simplest and most intuitive, and can be further encapsulated, such as providing a BaseController to encapsulate the common part, the essence is the same, but also introduces inheritance. Therefore, it is usually suitable for scenarios where there are few places to use it. If you use it in a large number of places, it is cumbersome, not recommended, and not technical.

Level2: Filter token to userId

In the previous scenario, since each call required a token and userId conversion, the conversion process was handled uniformly through a filter. Get the token from the filter, convert it to a userId, and write the userId back to the header.

Define a filter first, with the following example code:

@Slf4j @Component public class ArgumentFilter implements Filter { @Override public void doFilter(ServletRequest request,  ServletResponse response, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String token = httpRequest.getHeader("token"); Integer userId = TokenUtil.getUserIdByToken(token); Log.info ("filter get userId ={}", userId); HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper(httpRequest) { @Override public String getHeader(String name) { if ("userId".equals(name)) { return userId + ""; } return super.getHeader(name); }}; filterChain.doFilter(requestWrapper, httpResponse); }}Copy the code

This is mainly through the implementation of the doFilter method of the Filter interface (JDK8 can use the interface method required by the implementation), after obtaining the token in the request, Through HttpServletRequestWrapper place after conversion userId in the header.

In the SpringBoot project, you need to configure ArgumentFilter to specify the filtered URL:

@Configuration public class FilterConfig { @Resource private ArgumentFilter argumentFilter; @Bean public FilterRegistrationBean<Filter> registerAuthFilter() { FilterRegistrationBean<Filter> registration = new FilterRegistrationBean<>(); registration.setFilter(argumentFilter); registration.addUrlPatterns("/level2"); registration.setName("authFilter"); // The smaller the value, the more advanced the Filter registration.setOrder(1); return registration; }}Copy the code

In this case, the following is used in Controller:

@GetMapping("/level2")
public Integer level2(HttpServletRequest request) {
    Integer userId = Integer.parseInt(request.getHeader("userId"));
    log.info("userId={}", userId);
    return userId;
}
Copy the code

While this approach has improved a lot, it’s still a little inconvenient to get an HttpServletRequest every time and then get a userId from it. Can you continue to improve? So let’s keep going.

Level3: Parameter matching

The previous method has already obtained the userId, so can we do more thorough, just appear on the Controller method userId, directly assign to it? Let’s look at the implementation:

@Slf4j @Component public class ArgumentParamFilter implements Filter { @Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain) throws IOException, ServletException { HttpServletRequest httpRequest = (HttpServletRequest) request; HttpServletResponse httpResponse = (HttpServletResponse) response; String token = httpRequest.getHeader("token"); Integer userId = TokenUtil.getUserIdByToken(token); Log.info ("filter get userId ={}", userId); HttpServletRequestWrapper requestWrapper = new HttpServletRequestWrapper(httpRequest) { @Override public String[] getParameterValues(String name) { if ("userId".equals(name)) { return new String[]{userId.toString()}; } return super.getParameterValues(name); } @Override public Enumeration<String> getParameterNames() { Set<String> paramNames = new LinkedHashSet<>(); paramNames.add("userId"); Enumeration<String> names = super.getParameterNames(); while (names.hasMoreElements()) { paramNames.add(names.nextElement()); } return Collections.enumeration(paramNames); }}; filterChain.doFilter(requestWrapper, httpResponse); }}Copy the code

Here we get the token from the header, convert it to a userId, and then match the parameter name of the method. If it’s a userId, then assign the converted userId to the corresponding parameter. The related filter configuration is the same as the previous method, no more code attached, let’s look at the use of Controller:

@GetMapping("/level3")
public Integer level3(Integer userId) {
    log.info("userId={}", userId);
    return userId;
}
Copy the code

Just define the userId on the method parameter in the Controller and assign the value directly. Obviously, only GET requests are supported. If the Post method is used and the argument is passed through the body (Json format), then the argument is usually an entity object, such as User. Can I inject the userId directly into the User entity?

@Data
public class User {

    private Integer userId;

    private String name;
}
Copy the code

Further modifications are needed to implement direct injection into User objects. On the filter above to add to the body body transmission mode of processing, to realize in the HttpServletRequestWrapper getInputStream method:

@Override
public ServletInputStream getInputStream() {
    byte[] requestBody;
    try {
        requestBody = StreamUtils.copyToByteArray(request.getInputStream());
        Map map = JsonUtils.toObject(Map.class, new String(requestBody));
        map.put("userId", userId);
        requestBody = JsonUtils.toJson(map).getBytes();
    } catch (IOException e) {
        throw new RuntimeException(e);
    }

    final ByteArrayInputStream bais = new ByteArrayInputStream(requestBody);
    return new ServletInputStream() {
        @Override
        public int read() {
            return bais.read();
        }

        @Override
        public boolean isFinished() {
            return false;
        }

        @Override
        public boolean isReady() {
            return true;
        }

        @Override
        public void setReadListener(ReadListener readListener) {

        }
    };
}
Copy the code

Read the (JSON format) data from the stream, parse the information into a Map, add the userId to the Map, convert it to JSON format, and finally create a stream to write it out. The corresponding Controller is implemented as follows:

@PostMapping("/level3Post")
public Integer level3Post(@RequestBody User user) {
    log.info("userId={}", user.getUserId());
    return user.getUserId();
}
Copy the code

Testing with tools like Postman shows that the User object has already been injected with a corresponding value.

Is that perfect? There seem to be some flaws.

The first one is that the userId parameter can be defined according to the convention, but it is easy to be accidentally damaged. For example, some services have their own userId, and if the name is accidentally repeated, it may be overwritten.

Second: The parameter name must be userId, and no other name can be defined flexibly.

Third, if you want to return more information, such as the User’s information, the processing becomes more complicated. Moreover, if the parameters passed by the body are complex, parsing into a Map and then encapsulating the transformation has certain risks and performance issues.

So, let’s go ahead and upgrade again. The following example is based on SpringBoot.

Level4: Method parameter parser

Spring provides a variety of resolvers, such as HandlerExceptionResolver, which is commonly used to handle exceptions uniformly. At the same time, also provides a processing method for parameters of the parser HandlerMethodArgumentResolver. It contains two methods: supportsParameter and resolveArgument. The former is used to determine whether a condition is met. If the condition is met (return true), the resolveArgument method can be used to perform specific processing operations.

Based on HandlerExceptionResolver, we can implement it in the following parts:

Custom @currentUser annotation for the User parameter on the Controller method; Custom LoginUserHandlerMethodArgumentResolver, realize HandlerMethodArgumentResolver interface, through supportsParameter inspection qualified parameters, The token is converted into a User object using the resolveArgument method and assigned to the argument. Registered HandlerMethodArgumentResolver to MVC. To see how this works, we define @currentUser as an annotation:

@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
public @interface CurrentUser {
}
Copy the code

Annotations are used to indicate that the specified parameters need to be processed. To annotate the @ CurrentUser parameters are handled by custom LoginUserHandlerMethodArgumentResolver for judgment of:

@Component public class LoginUserHandlerMethodArgumentResolver implements HandlerMethodArgumentResolver { @Override public boolean supportsParameter(MethodParameter parameter) { return parameter.hasParameterAnnotation(CurrentUser.class)  && parameter.getParameterType().isAssignableFrom(User.class); } @Override public Object resolveArgument(MethodParameter parameter, ModelAndViewContainer container, NativeWebRequest request, String token = request. GetHeader ("token"); Integer userId = TokenUtil.getUserIdByToken(token); // TODO gets the User information from the userId, omitted here, and creates a User object directly. User user = new User(); user.setName("Tom"); user.setUserId(userId); return user; }}Copy the code

The supportsParameter method filters parameters using the CurrentUser annotation and type User. Return true if the condition is met, and enter resolveArgument for processing.

In the resolveArgument, the token is obtained from the header, and the User information is obtained based on the token. You can inject UserService to obtain more User information, and then return the constructed User object. This way, you can later bind the returned User to the parameter in the Controller.

However, the custom Resolver does not take effect and needs to be added to MVC:

@Configuration public class WebConfig implements WebMvcConfigurer { @Resource private LoginUserHandlerMethodArgumentResolver loginUserHandlerMethodArgumentResolver; @Override public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) { argumentResolvers.add(loginUserHandlerMethodArgumentResolver); }}Copy the code

At this point, you can use this annotation in the Controller to get user information as follows:

@GetMapping("/level4")
public Integer level4(@CurrentUser User user) {
    log.info("userId={},username={}", user.getUserId(), user.getName());
    return user.getUserId();
}
Copy the code

Injecting a User object directly was described above. If you only need the userId, replace the User object with an Integer or Long.

This makes it easier to use, and when we need to get User information, we simply use @currentUser User in the request parameters. There is no possibility of misoperation where it is not needed, and it has full flexibility and extensibility.

Summary This paper through a scene of the business scene, from the most basic implementation of the evolution to a certain design of the implementation, involving interceptors, filters, annotations and other a series of knowledge points and actual experience. This is exactly how we evolve during project development. Which approach do you use in your projects? Need an upgrade or experience?

This article related source code address can be concerned about the public number: program new vision, reply to “1004” to obtain.

Finally, a personal video account was also opened, one minute to tell you a dry knowledge point, one minute to share a workplace experience. Welcome to follow.