前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >从SpringMVC获取用户信息谈起

从SpringMVC获取用户信息谈起

原创
作者头像
A稻田守望者
修改2019-09-30 11:30:40
1.4K0
修改2019-09-30 11:30:40
举报
文章被收录于专栏:Spring点滴Spring点滴

Github地址:https://github.com/andyslin/spring-ext 编译、运行环境:JDK 8 + Maven 3 + IDEA + Lombok spring-boot:2.1.0.RELEASE(Spring:5.1.2.RELEASE) 如要本地运行github上的项目,需要安装lombok插件

上周末拜读了一位牛人的公众号文章<[Token认证,如何快速方便获取用户信息](https://mp.weixin.qq.com/s/Qi82d5xmlYwiuaGRSn54uw)>,语言风趣,引人入胜,为了表示涛涛敬仰之情,已经转载到自己的公众号了。

回顾一下文章内容,为了在Controller的方法中获取已经认证过的用户信息(比如通过JWT-JSON Web Token传输的Token),文中提供了三种方式:

  • 方式一(很挫)直接在Controller方法中获取Token头,然后解析;
  • 方式二(优雅)在过滤器Filter中验证JWT后,直接使用HttpServletRequestWrapper偷梁换柱,覆盖getHeader方法,然后在Controller方法中调用getHeader,这样就不需要再次解析了;
  • 方式三(很优雅)同样在过滤器Filter中使用HttpServletRequestWrapper,只是覆盖getParameterNamesgetParameterValues(针对表单提交)和getInputStream(针对JSON提交),然后就可以和客户端参数相同的方式获取了。

方式一需要重复解析JWT,而且控制器和Servlet API绑定,不方便测试,但是胜在简单直接。方式二和方式三虽然是一个很好的练习HttpServletRequestWrapper的示例,但是可能还算不上是优雅的获取用户信息的方式。

不妨思考一下:

  • 除了获取userId外,如果还想获取JWT中PAYLOAD的其它信息,能不能做到只修改Controller?还是需要再次修改验证JWT的过滤器Filter呢?
  • HttpServletRequestgetInpustStream()方法,Web容器实现基本都是只能调用一次的,因而方式三在扩展getInpustStream()的时候,先将其转换为byte[],然后为了添加用户信息,再将byte[]反序列化为map,添加用户信息之后又序列化为byte[],反复多次,这种方式性能怎么样?如果是文件上传,这种方式能否行得通?
  • 方式三中HttpServletRequestWrapper会无形中启到屏蔽loginUserId参数的作用,但如果客户端的的确确传入了一个loginUserId的参数(当然,这种情况还是需要尽量避免),在Controller中怎么又获取到客户端的这个参数?

有没有什么其它的方式呢?

SpringMVC中关于参数绑定有很多接口,其中很关键的一个是HandlerMethodArgumentResolver,可以通过添加新实现类来实现获取用户信息吗?当然可以,对应该接口的两个方法,首先要能够识别什么情况下需要绑定用户信息,一般来说,可以根据参数的特殊类型,也可以根据参数的特殊注解;其次要能够获取到用户信息,类似于原文中做的那样。虽然这样做也可以实现功能,但是却很繁琐。

不如抛开怎么获取用户信息不谈,先来看看SpringMVC在控制器的处理方法HandlerMethod中绑定参数是怎么做的?

熟悉SpringMVC处理流程的朋友,自然知道,主控制器是DispatcherServlet,在doDispatch()方法中根据HandlerMapping找到处理器,然后找到可以调用该处理器的HandlerAdapter,其中最常用也最核心的莫过于RequestMappingHandlerMappingHandlerMethodRequestMappingHandlerAdapter组合了。查看RequestMappingHandlerAdapter的源码,找到调用HandlerMethod的方法:

代码语言:txt
复制
@Override
protected ModelAndView handleInternal(HttpServletRequest request,
        HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

    ModelAndView mav;
    checkRequest(request);

    // Execute invokeHandlerMethod in synchronized block if required.
    if (this.synchronizeOnSession) {
        HttpSession session = request.getSession(false);
        if (session != null) {
            Object mutex = WebUtils.getSessionMutex(session);
            synchronized (mutex) {
                mav = invokeHandlerMethod(request, response, handlerMethod);
            }
        }
        else {
            // No HttpSession available -> no mutex necessary
            mav = invokeHandlerMethod(request, response, handlerMethod);
        }
    }
    else {
        // No synchronization on session demanded at all...
        mav = invokeHandlerMethod(request, response, handlerMethod);
    }

    if (!response.containsHeader(HEADER_CACHE_CONTROL)) {
        if (getSessionAttributesHandler(handlerMethod).hasSessionAttributes()) {
            applyCacheSeconds(response, this.cacheSecondsForSessionAttributeHandlers);
        }
        else {
            prepareResponse(response);
        }
    }

    return mav;
}

可以看到,真正的调用是委托给invokeHandlerMethod()方法了:

代码语言:txt
复制
@Nullable
protected ModelAndView invokeHandlerMethod(HttpServletRequest request,
        HttpServletResponse response, HandlerMethod handlerMethod) throws Exception {

    ServletWebRequest webRequest = new ServletWebRequest(request, response);
    try {
        // 创建数据绑定工厂
        WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);
        ModelFactory modelFactory = getModelFactory(handlerMethod, binderFactory);

        // 创建可调用的方法
        ServletInvocableHandlerMethod invocableMethod = createInvocableHandlerMethod(handlerMethod);
        if (this.argumentResolvers != null) {
            invocableMethod.setHandlerMethodArgumentResolvers(this.argumentResolvers);
        }
        if (this.returnValueHandlers != null) {
            invocableMethod.setHandlerMethodReturnValueHandlers(this.returnValueHandlers);
        }
        invocableMethod.setDataBinderFactory(binderFactory);
        invocableMethod.setParameterNameDiscoverer(this.parameterNameDiscoverer);

        ModelAndViewContainer mavContainer = new ModelAndViewContainer();
        mavContainer.addAllAttributes(RequestContextUtils.getInputFlashMap(request));
        modelFactory.initModel(webRequest, mavContainer, invocableMethod);
        mavContainer.setIgnoreDefaultModelOnRedirect(this.ignoreDefaultModelOnRedirect);

        // 省略异步处理相关代码

        // 这里才是真正的方法调用
        invocableMethod.invokeAndHandle(webRequest, mavContainer);
       
        // 处理返回结果
        return getModelAndView(mavContainer, modelFactory, webRequest);
    }
    finally {
        webRequest.requestCompleted();
    }
}

这个方法很关键,如果需要研读SpringMVC,可以从这个方法着手。不过由于这篇文章关注的是参数绑定,所以这里只关心WebDataBinderFactory binderFactory = getDataBinderFactory(handlerMethod);这句代码,接着看getDataBinderFactory()方法:

代码语言:txt
复制
private WebDataBinderFactory getDataBinderFactory(HandlerMethod handlerMethod) throws Exception {
    Class<?> handlerType = handlerMethod.getBeanType();
    Set<Method> methods = this.initBinderCache.get(handlerType);
    if (methods == null) {
        methods = MethodIntrospector.selectMethods(handlerType, INIT_BINDER_METHODS);
        this.initBinderCache.put(handlerType, methods);
    }
    List<InvocableHandlerMethod> initBinderMethods = new ArrayList<>();
    // Global methods first
    this.initBinderAdviceCache.forEach((clazz, methodSet) -> {
        if (clazz.isApplicableToBeanType(handlerType)) {
            Object bean = clazz.resolveBean();
            for (Method method : methodSet) {
                initBinderMethods.add(createInitBinderMethod(bean, method));
            }
        }
    });
    for (Method method : methods) {
        Object bean = handlerMethod.getBean();
        initBinderMethods.add(createInitBinderMethod(bean, method));
    }
    return createDataBinderFactory(initBinderMethods);
}

这个方法前面的代码都是一些准备工作,比如调用ControllerAdvice,最终还是调用createDataBinderFactory()方法:

代码语言:txt
复制
protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods)
			throws Exception {

    return new ServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
}

终于看到数据绑定工厂实例的创建了,方法体非常简单,只有一个new,而且非常幸运,这个方法是protected的,这说明,SpringMVC的设计者原本就预留了扩展点给我们,如果需要扩展数据绑定相关的功能,这里应该是一个不错的入口,具体做法是:

  1. 实现新的WebDataBinderFactory,当然,最好是继承这里的ServletRequestDataBinderFactory
  2. 继承RequestMappingHandlerAdapter,覆盖createDataBinderFactory()方法,返回新实现的WebDataBinderFactory实例;
  3. SpringMVC容器中使用新的RequestMappingHandlerAdapter

我们从后往前看:

有多种方式实现第3步,在SpringBoot应用中,比较简单的是通过向容器注册一个WebMvcRegistrations的实现类,这个接口定义如下:

代码语言:txt
复制
public interface WebMvcRegistrations {

	default RequestMappingHandlerMapping getRequestMappingHandlerMapping() {
		return null;
	}

	default RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
		return null;
	}

	default ExceptionHandlerExceptionResolver getExceptionHandlerExceptionResolver() {
		return null;
	}
}

实现第二个方法就可以。

第2步更简单,上面已经说明,这里就不赘述了。

再看第1步,查看ServletRequestDataBinderFactory的源码:

代码语言:txt
复制
public class ServletRequestDataBinderFactory extends InitBinderDataBinderFactory {

	public ServletRequestDataBinderFactory(@Nullable List<InvocableHandlerMethod> binderMethods,
			@Nullable WebBindingInitializer initializer) {
		super(binderMethods, initializer);
	}

	@Override
	protected ServletRequestDataBinder createBinderInstance(
			@Nullable Object target, String objectName, NativeWebRequest request) throws Exception  {

		return new ExtendedServletRequestDataBinder(target, objectName);
	}
}

除了构造函数,只定义了一个createBinderInstance()方法(一个工厂类创建一种实例,很熟悉的味道吧?),返回ExtendedServletRequestDataBinder的实例,真正的绑定逻辑在这个类里面,还需要扩展这个类:

代码语言:txt
复制
public class ExtendedServletRequestDataBinder extends ServletRequestDataBinder {

	public ExtendedServletRequestDataBinder(@Nullable Object target) {
		super(target);
	}

	public ExtendedServletRequestDataBinder(@Nullable Object target, String objectName) {
		super(target, objectName);
	}

	@Override
	@SuppressWarnings("unchecked")
	protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
		String attr = HandlerMapping.URI_TEMPLATE_VARIABLES_ATTRIBUTE;
		Map<String, String> uriVars = (Map<String, String>) request.getAttribute(attr);
		if (uriVars != null) {
			uriVars.forEach((name, value) -> {
				if (mpvs.contains(name)) {
					if (logger.isWarnEnabled()) {
						logger.warn("Skipping URI variable '" + name +
								"' because request contains bind value with same name.");
					}
				}
				else {
					mpvs.addPropertyValue(name, value);
				}
			});
		}
	}
}

要扩展一个类,首先还是找一下有哪些protected方法,可以看到有一个addBindValues()方法,然后再看这个方法被谁调用了,发现在父类ServletRequestDataBinder中有:

代码语言:txt
复制
public void bind(ServletRequest request) {
    MutablePropertyValues mpvs = new ServletRequestParameterPropertyValues(request);
    MultipartRequest multipartRequest = WebUtils.getNativeRequest(request, MultipartRequest.class);
    if (multipartRequest != null) {
        bindMultipart(multipartRequest.getMultiFileMap(), mpvs);
    }
    // 绑定前添加绑定参数
    addBindValues(mpvs, request);
    // 执行参数绑定,包括参数格式化、参数校验等
    doBind(mpvs);
    // 可以添加一些绑定之后的处理
}

至此,已经找到扩展接入点了,为了更好的对扩展开放,引入一个新的接口PropertyValuesProvider

代码语言:txt
复制
/**
 * 属性值提供器接口
 */
public interface PropertyValuesProvider {

    /**
     * 绑定前添加绑定属性,仍然需要经过参数校验
     */
    default void addBindValues(MutablePropertyValues mpvs, ServletRequest request, Object target, String name) {
    }

    /**
     * 绑定后修改目标对象,修改后的参数不需要经过参数校验
     *
     */
    default void afterBindValues(PropertyAccessor accessor, ServletRequest request, Object target, String name) {
    }
}

然后实现新的DataBinder,整个代码如下:

代码语言:txt
复制
class ArgsBindRequestMappingHandlerAdapter extends RequestMappingHandlerAdapter {

    private final List<PropertyValuesProvider> providers;

    public ArgsBindRequestMappingHandlerAdapter(List<PropertyValuesProvider> providers) {
        this.providers = providers;
    }

    @Override
    protected InitBinderDataBinderFactory createDataBinderFactory(List<InvocableHandlerMethod> binderMethods) throws Exception {
        return new ArgsBindServletRequestDataBinderFactory(binderMethods, getWebBindingInitializer());
    }

    private class ArgsBindServletRequestDataBinderFactory extends ServletRequestDataBinderFactory {

        public ArgsBindServletRequestDataBinderFactory(List<InvocableHandlerMethod> binderMethods, WebBindingInitializer initializer) {
            super(binderMethods, initializer);
        }

        @Override
        protected ServletRequestDataBinder createBinderInstance(Object target, String objectName, NativeWebRequest request) {
            return new ArgsBindServletRequestDataBinder(target, objectName);
        }
    }

    private class ArgsBindServletRequestDataBinder extends ExtendedServletRequestDataBinder {

        public ArgsBindServletRequestDataBinder(Object target, String objectName) {
            super(target, objectName);
        }

        /**
         * 属性绑定前
         */
        @Override
        protected void addBindValues(MutablePropertyValues mpvs, ServletRequest request) {
            super.addBindValues(mpvs, request);
            if (null != providers) {
                Object target = getTarget();
                String name = getObjectName();
                providers.forEach(provider -> provider.addBindValues(mpvs, request, target, name));
            }
        }

        /**
         * 属性绑定后
         */
        @Override
        public void bind(ServletRequest request) {
            super.bind(request);
            if (null != providers) {
                ConfigurablePropertyAccessor mpvs = getPropertyAccessor();
                Object target = getTarget();
                String name = getObjectName();
                providers.forEach(provider -> provider.afterBindValues(mpvs, request, target, name));
            }
        }
    }
}

最后,加上SpringBoot自动配置类:

代码语言:txt
复制
@Configuration
public class ArgsBindAutoConfiguration {

    @Bean
    @ConditionalOnBean(PropertyValuesProvider.class)
    @ConditionalOnMissingBean(ArgsBindWebMvcRegistrations.class)
    public ArgsBindWebMvcRegistrations argsBindWebMvcRegistrations(List<PropertyValuesProvider> providers) {
        return new ArgsBindWebMvcRegistrations(providers);
    }

    static class ArgsBindWebMvcRegistrations implements WebMvcRegistrations {

        private final List<PropertyValuesProvider> providers;

        public ArgsBindWebMvcRegistrations(List<PropertyValuesProvider> providers) {
            this.providers = providers;
        }

        @Override
        public RequestMappingHandlerAdapter getRequestMappingHandlerAdapter() {
            return new ArgsBindRequestMappingHandlerAdapter(providers);
        }
    }
}

好了,有了新的接口,要实现文章开始的获取用户信息的问题,也就是添加一个新接口PropertyValuesProvider的实现类,并注入到SpringMVC的容器中即可,如果需要获取PAYLOAD中的其它信息,或者有其它的自定义参数绑定逻辑,可以再加几个实现类。

在我的Github上有一个简单的测试示例,有兴趣的朋友不妨一试。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
相关产品与服务
容器服务
腾讯云容器服务(Tencent Kubernetes Engine, TKE)基于原生 kubernetes 提供以容器为核心的、高度可扩展的高性能容器管理服务,覆盖 Serverless、边缘计算、分布式云等多种业务部署场景,业内首创单个集群兼容多种计算节点的容器资源管理模式。同时产品作为云原生 Finops 领先布道者,主导开源项目Crane,全面助力客户实现资源优化、成本控制。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档