Spring @CrossOrigin 注解原理实现

现实开发中,我们难免遇到跨域问题,以前笔者只知道jsonp这种解决方式,后面听说spring只要加入@CrossOrigin即可解决跨域问题。本着好奇的心里,笔者看了下@CrossOrigin 作用原理,写下这篇博客。

先说原理:其实很简单,就是利用spring的拦截器实现往response里添加 Access-Control-Allow-Origin等响应头信息,我们可以看下spring是怎么做的

注:这里使用的spring版本为5.0.6

我们可以先往RequestMappingHandlerMapping 的initCorsConfiguration方法打一个断点,发现方法调用情况如下

如果controller在类上标了@CrossOrigin或在方法上标了@CrossOrigin注解,则spring 在记录mapper映射时会记录对应跨域请求映射,代码如下

 
  1. RequestMappingHandlerMapping
  2. protected CorsConfiguration initCorsConfiguration(Object handler, Method method, RequestMappingInfo mappingInfo) {
  3. HandlerMethod handlerMethod = createHandlerMethod(handler, method);
  4. Class<?> beanType = handlerMethod.getBeanType();
  5. //获取handler上的CrossOrigin 注解
  6. CrossOrigin typeAnnotation = AnnotatedElementUtils.findMergedAnnotation(beanType, CrossOrigin.class);
  7. //获取handler 方法上的CrossOrigin 注解
  8. CrossOrigin methodAnnotation = AnnotatedElementUtils.findMergedAnnotation(method, CrossOrigin.class);
  9. if (typeAnnotation == null && methodAnnotation == null) {
  10. //如果类上和方法都没标CrossOrigin 注解,则返回一个null
  11. return null;
  12. }
  13. //构建一个CorsConfiguration 并返回
  14. CorsConfiguration config = new CorsConfiguration();
  15. updateCorsConfig(config, typeAnnotation);
  16. updateCorsConfig(config, methodAnnotation);
  17. if (CollectionUtils.isEmpty(config.getAllowedMethods())) {
  18. for (RequestMethod allowedMethod : mappingInfo.getMethodsCondition().getMethods()) {
  19. config.addAllowedMethod(allowedMethod.name());
  20. }
  21. }
  22. return config.applyPermitDefaultValues();
  23. }

将结果返回到了AbstractHandlerMethodMapping#register,主要代码如下

 
  1. CorsConfiguration corsConfig = initCorsConfiguration(handler, method, mapping);
  2. if (corsConfig != null) {
  3. //会保存handlerMethod处理跨域请求的配置
  4. this.corsLookup.put(handlerMethod, corsConfig);
  5. }

当一个跨域请求过来时,spring在获取handler时会判断这个请求是否是一个跨域请求,如果是,则会返回一个可以处理跨域的handler

 
  1. AbstractHandlerMapping#getHandler
  2. HandlerExecutionChain executionChain = getHandlerExecutionChain(handler, request);
  3. //如果是一个跨域请求
  4. if (CorsUtils.isCorsRequest(request)) {
  5. //拿到跨域的全局配置
  6. CorsConfiguration globalConfig = this.globalCorsConfigSource.getCorsConfiguration(request);
  7. //拿到hander的跨域配置
  8. CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
  9. CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
  10. //处理跨域(即往响应头添加Access-Control-Allow-Origin信息等),并返回对应的handler对象
  11. executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
  12. }

我们可以看下如何判定一个请求是一个跨域请求,

 
  1. public static boolean isCorsRequest(HttpServletRequest request) {
  2. //判定请求头是否有Origin 属性即可
  3. return (request.getHeader(HttpHeaders.ORIGIN) != null);
  4. }

再看下getCorsHandlerExecutionChain 是如何获取一个handler

 
  1. protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
  2. HandlerExecutionChain chain, @Nullable CorsConfiguration config) {
  3. if (CorsUtils.isPreFlightRequest(request)) {
  4. HandlerInterceptor[] interceptors = chain.getInterceptors();
  5. chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
  6. }
  7. else {
  8. //只是给执行器链添加了一个拦截器
  9. chain.addInterceptor(new CorsInterceptor(config));
  10. }
  11. return chain;
  12. }

也就是在调用目标方法前会先调用CorsInterceptor#preHandle,我们观察得到其也是调用了corsProcessor.processRequest方法,我们往这里打个断点

processRequest方法的主要逻辑如下

 
  1. public boolean processRequest(@Nullable CorsConfiguration config, HttpServletRequest request,
  2. HttpServletResponse response) throws IOException {
  3. //....
  4. //调用了自身的handleInternal方法
  5. return handleInternal(serverRequest, serverResponse, config, preFlightRequest);
  6. }
  7. protected boolean handleInternal(ServerHttpRequest request, ServerHttpResponse response,
  8. CorsConfiguration config, boolean preFlightRequest) throws IOException {
  9. String requestOrigin = request.getHeaders().getOrigin();
  10. String allowOrigin = checkOrigin(config, requestOrigin);
  11. HttpHeaders responseHeaders = response.getHeaders();
  12. responseHeaders.addAll(HttpHeaders.VARY, Arrays.asList(HttpHeaders.ORIGIN,
  13. HttpHeaders.ACCESS_CONTROL_REQUEST_METHOD, HttpHeaders.ACCESS_CONTROL_REQUEST_HEADERS));
  14. if (allowOrigin == null) {
  15. logger.debug("Rejecting CORS request because '" + requestOrigin + "' origin is not allowed");
  16. rejectRequest(response);
  17. return false;
  18. }
  19. HttpMethod requestMethod = getMethodToUse(request, preFlightRequest);
  20. List<HttpMethod> allowMethods = checkMethods(config, requestMethod);
  21. if (allowMethods == null) {
  22. logger.debug("Rejecting CORS request because '" + requestMethod + "' request method is not allowed");
  23. rejectRequest(response);
  24. return false;
  25. }
  26. List<String> requestHeaders = getHeadersToUse(request, preFlightRequest);
  27. List<String> allowHeaders = checkHeaders(config, requestHeaders);
  28. if (preFlightRequest && allowHeaders == null) {
  29. logger.debug("Rejecting CORS request because '" + requestHeaders + "' request headers are not allowed");
  30. rejectRequest(response);
  31. return false;
  32. }
  33. //设置响应头
  34. responseHeaders.setAccessControlAllowOrigin(allowOrigin);
  35. if (preFlightRequest) {
  36. responseHeaders.setAccessControlAllowMethods(allowMethods);
  37. }
  38. if (preFlightRequest && !allowHeaders.isEmpty()) {
  39. responseHeaders.setAccessControlAllowHeaders(allowHeaders);
  40. }
  41. if (!CollectionUtils.isEmpty(config.getExposedHeaders())) {
  42. responseHeaders.setAccessControlExposeHeaders(config.getExposedHeaders());
  43. }
  44. if (Boolean.TRUE.equals(config.getAllowCredentials())) {
  45. responseHeaders.setAccessControlAllowCredentials(true);
  46. }
  47. if (preFlightRequest && config.getMaxAge() != null) {
  48. responseHeaders.setAccessControlMaxAge(config.getMaxAge());
  49. }
  50. //刷新
  51. response.flush();
  52. return true;
  53. }

至此@CrossOrigin的使命就完成了,说白了就是用拦截器给response添加响应头信息而已

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值