现实开发中,我们难免遇到跨域问题,以前笔者只知道jsonp这种解决方式,后面听说spring只要加入@CrossOrigin即可解决跨域问题。本着好奇的心里,笔者看了下@CrossOrigin 作用原理,写下这篇博客。
先说原理:其实很简单,就是利用spring的拦截器实现往response里添加 Access-Control-Allow-Origin等响应头信息,我们可以看下spring是怎么做的
注:这里使用的spring版本为5.0.6
我们可以先往RequestMappingHandlerMapping 的initCorsConfiguration方法打一个断点,发现方法调用情况如下
如果controller在类上标了@CrossOrigin或在方法上标了@CrossOrigin注解,则spring 在记录mapper映射时会记录对应跨域请求映射,代码如下
- RequestMappingHandlerMapping
- protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
- HandlerMethod handlerMethod = createHandlerMethod(handler, method);
- Class<?> beanType = handlerMethod.getBeanType();
- //获取handler上的CrossOrigin 注解
- CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(beanType, CrossOrigin.class);
- //获取handler 方法上的CrossOrigin 注解
- CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class);
- if (typeAnnotation == null && methodAnnotation == null) {
- //如果类上和方法都没标CrossOrigin 注解,则返回一个null
- return null;
- }
- //构建一个CorsConfiguration 并返回
- CorsConfiguration config = new CorsConfiguration();
- updateCorsConfig(config, typeAnnotation);
- updateCorsConfig(config, methodAnnotation);
- if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
- for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
- config.addAllowedMethod(allowedMethod.name());
- }
- }
- return config.applyPermitDefaultValues();
- }
将结果返回到了AbstractHandlerMethodMapping#register,主要代码如下
- CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
- if (corsConfig != null) {
- //会保存handlerMethod处理跨域请求的配置
- this.corsLookup.put(handlerMethod, corsConfig);
- }
当一个跨域请求过来时,spring在获取handler时会判断这个请求是否是一个跨域请求,如果是,则会返回一个可以处理跨域的handler
- AbstractHandlerMapping#getHandler
- HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
- //如果是一个跨域请求
- if (CorsUtils.isCorsRequest(request)) {
- //拿到跨域的全局配置
- CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request);
- //拿到hander的跨域配置
- CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
- CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
- //处理跨域(即往响应头添加Access-Control-Allow-Origin信息等),并返回对应的handler对象
- executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
- }
我们可以看下如何判定一个请求是一个跨域请求,
- public static boolean isCorsRequest(HttpServletRequest request) {
- //判定请求头是否有Origin 属性即可
- return (request.getHeader(HttpHeaders.ORIGIN) != null);
- }
再看下getCorsHandlerExecutionChain 是如何获取一个handler
- protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
- HandlerExecutionChain chain, @Nullable CorsConfiguration config) {
- if (CorsUtils.isPreFlightRequest(request)) {
- HandlerInterceptor[] interceptors = chain.getInterceptors();
- chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
- }
- else {
- //只是给执行器链添加了一个拦截器
- chain.addInterceptor(new CorsInterceptor(config));
- }
- return chain;
- }
也就是在调用目标方法前会先调用CorsInterceptor#preHandle,我们观察得到其也是调用了corsProcessor.processRequest方法,我们往这里打个断点
processRequest方法的主要逻辑如下
- public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request,
- HttpServletResponse response) throws IOException {
- //....
- //调用了自身的handleInternal方法
- return handleInternal(serverRequest, serverResponse, config, preFlightRequest);
- }
- protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
- CorsConfiguration config, boolean preFlightRequest) throws IOException {
- String requestOrigin = request.getHeaders().getOrigin();
- String allowOrigin = checkOrigin(config, requestOrigin);
- HttpHeaders responseHeaders = response.getHeaders();
- responseHeaders.addAll(HttpHeaders.VARY, Arrays.asList(HttpHeaders.ORIGIN,
- HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
- if (allowOrigin == null) {
- logger.debug("Rejecting CORS request because '" + requestOrigin + "' origin is not allowed");
- rejectRequest(response);
- return false;
- }
- HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
- List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
- if (allowMethods == null) {
- logger.debug("Rejecting CORS request because '" + requestMethod + "' request method is not allowed");
- rejectRequest(response);
- return false;
- }
- List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
- List<String> allowHeaders = checkHeaders(config, requestHeaders);
- if (preFlightRequest && allowHeaders == null) {
- logger.debug("Rejecting CORS request because '" + requestHeaders + "' request headers are not allowed");
- rejectRequest(response);
- return false;
- }
- //设置响应头
- responseHeaders.setAccessControlAllowOrigin(allowOrigin);
- if (preFlightRequest) {
- responseHeaders.setAccessControlAllowMethods(allowMethods);
- }
- if (preFlightRequest && !allowHeaders.isEmpty()) {
- responseHeaders.setAccessControlAllowHeaders(allowHeaders);
- }
- if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
- responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
- }
- if (Boolean.TRUE.equals(config.getAllowCredentials())) {
- responseHeaders.setAccessControlAllowCredentials(true);
- }
- if (preFlightRequest && config.getMaxAge() != null) {
- responseHeaders.setAccessControlMaxAge(config.getMaxAge());
- }
- //刷新
- response.flush();
- return true;
- }
至此@CrossOrigin的使命就完成了,说白了就是用拦截器给response添加响应头信息而已