Spring Boot(五)之跨域、自定义查询及分页

发布时间 - 2026-01-11 00:49:57    点击率:

跨域

前面我们初步做出了一个可以实现受保护的 REST API,但是我们没有涉及一个前端领域很重要的问题,那就是跨域请求( cross-origin HTTP request )。先来回顾一些背景知识:

跨域请求

定义:当我们从本身站点请求不同域名或端口的服务所提供的资源时,就会发起跨域请求。

例如最常见的我们很多的 css 样式文件是会链接到某个公共 CDN 服务器上,而不是在本身的服务器上,这其实就是典型的一个跨域请求。但浏览器由于安全原因限制了在脚本( script )中发起的跨域 HTTP 请求。也就是说 XMLHttpRequest 和 Fetch 等是遵循“同源规则”的,即只能访问自己服务器的指定端口的资源(同一服务器不同端口也会视为跨域)。但这种限制在今天,我们的应用需要访问多种外部 API 或 资源的时候就不能满足开发者的需求了,因此就产生了若干对于跨域的解决方案,JSONP 是其中一种,但在今天来看主流的更彻底的解决方案是 CORS ( Cross-Origin Resource Sharing )。

跨域资源共享 ( CORS )

这种机制将跨域的访问控制权交给服务器,这样可以保证安全的跨域数据传输。现代浏览器一般会将 CORS 的支持封装在 HTTP API 之中( 比如 XMLHttpRequest 和 Fetch ),这样可以有效控制使用跨域请求的风险,因为你绕不过去,总得要使用 API 吧。

概括来说,这个机制是增加一系列的 HTTP 头来让服务器可以描述哪些源是允许使用浏览器来访问资源的。而且对于简单的请求和复杂请求,处理机制是不一样的。

简单请求仅允许三个 HTTP 方法:GET,POST 以及 HEAD,另外只能支持若干 header 参数:Accept , Accept-Language , Content-Language , Content-Type (值只能是 application/x-www-form-urlencoded、multipart/form-data 和 text/plain), DPR , Downlink , Save-Data , Viewport-Width 和 Width。

对于简单请求来说,比如下面这样一个简单的GET请求:从 http://me.domain 发起到 http://another.domain/data/blablabla 的资源请求

GET /data/blablabla/ HTTP/1.1
// 请求的域名
Host: another.domain
...//省略其它部分,重点是下面这句,说明了发起请求者的来源
Origin: http://me.domain

应用了 CORS 的对方服务器返回的响应应该像下面这个样子,当然这里 Access-Control-Allow-Origin: * 中的 * 表示任何网站都可以访问该资源,如果要限制只能从 me.domain 访问,那么需要改成 Access-Control-Allow-Origin: http://me.domain

HTTP/1.1 200 OK
...//省略其它部分
Access-Control-Allow-Origin: *
...//省略其它部分
Content-Type: application/json

那么对于复杂请求怎么办呢?这需要一次预检请求和一次实际的请求,也就是说需要两次和对方服务器的请求/响应。预检请求是以 OPTION 方法进行的,因为 OPTION 方法不会改变任何资源,所以这个预检请求是安全的,它的职责在于发送实际请求将会使用的 HTTP 方法以及将要发送的 HEADER 中将携带哪些内容,这样对方服务器可以根据预检请求的信息决定是否接受。

// 预检请求
OPTIONS /resources/post/ HTTP/1.1
Host: another.domain
...// 省略其它部分
Origin: http://me.domain
Access-Control-Request-Method: POST
Access-Control-Request-Headers: Content-Type

服务器对预检请求的响应如下:

HTTP/1.1 200 OK
// 省略其它部分
Access-Control-Allow-Origin: http://me.domain
Access-Control-Allow-Methods: POST, GET, OPTIONS
Access-Control-Allow-Headers: Content-Type
Access-Control-Max-Age: 86400
// 省略其它部分
Content-Type: text/plain

接下来的正式请求就和上面的简单请求差不多了,就不赘述了。

在 Spring Boot 中如何启用 CORS

啰嗦了这么多,终于进入正题,但我一直觉得不能光知其然而不知其所以然,所以各位就忍了吧。加入 CORS 的支持在 Spring Boot 中简单到不忍直视,添加一个配置类即可:

import org.springframework.boot.web.servlet.FilterRegistrationBean;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
 @Bean
 public FilterRegistrationBean corsFilter() {
  UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource();
  CorsConfiguration config = new CorsConfiguration();
  config.setAllowCredentials(true);
  // 设置你要允许的网站域名,如果全允许则设为 *
  config.addAllowedOrigin("http://localhost:4200");
  // 如果要限制 HEADER 或 METHOD 请自行更改
  config.addAllowedHeader("*");
  config.addAllowedMethod("*");
  source.registerCorsConfiguration("/**", config);
  FilterRegistrationBean bean = new FilterRegistrationBean(new CorsFilter(source));
  // 这个顺序很重要哦,为避免麻烦请设置在最前
  bean.setOrder(0);
  return bean;
 }
}

如果我们使用 POSTMAN 访问一下 API,会发现得到一个 Invalid CORS request 的响应,因为我们的 API 只授权给了 localhost:4200

用 POSTMAN 无法得到请求结果

当然,如果我们使用 CURL 的话是可以访问的,这是因为 CURL 不是浏览器。

嗯嗯,这样就结束了,这节好水,但就是这么简单啊。

自定义查询

我们回过头来再来看看数据查询,大部分情况下 Spring Data 提供的按方法名进行查询的方式足够简单也足够强大,但总归还是有很多局限。为了说明这个问题,也顺便为我的新 Angular 项目打造一个 API,我们把 API 的需求改一下。现在我们要做的不是一个简单的 Todo 了,而是类似 Teambition 或 Worktile 那样的企业协作平台,当然我们不会做的那么复杂,是个简化版本。那么这时我们的对象模型是这样的:

主要的领域对象

我们首先看一下 Project 这个对象,我们来构建它的 API,增删改没啥可讲。但是查询上会有点不一样,首先我们并不希望把所有的 Project 都查出来,而是该用户参与的项目要提供一个 API 给客户端。

Project 和 User 按关系型数据库的看法是个多对多的关系,在MongoDB中这其实有多种做法,可以在 User 对象中设置一个 Project 的集合属性,也可以在 Project 中设置 User 的集合属性 (在我们的例子里是 memebers ),还可以两者结合,就是在 User 和 Projet 中互相有对方的集合属性。具体采用哪种需要看业务场景和性能需求,比如如果任何一个项目的成员数如果不会很大,那么在 Project 中嵌入 User 集合就比较划算;如果项目的成员较多,但一个成员归属的项目不会很多的情况下,就可以把 Project 的集合嵌入到 User 中。我们这里采用了第一种做法。

那么接下来我们来写该用户参与的项目的查询。当然我们可以按照 Spring Data 强大的按方法名称来生成对应查询的方式来做:寻找 members 属性中包含该用户的集合

Set<Project> findByMembersContaining(User user)

看起来还可以,挺简单,但是如果我们说再加两个条件要筛选 project.enabled == true (我们不会物理删除项目,而是设置其标志位 enabled,所以这就是筛选未删除的项目) 和 project.archived == false (项目完结后需要归档,这就是筛选未归档的)。这两个条件一加上,好家伙,我们的方法名变成了下面这个样子,不忍直视啊:

Set<Project> findByMembersContainingAndEnabledAndArchived(User user, boolean enabled, boolean archived)

当然好用还是好用了,但是这个方法名也太长了,好在 Spring Data 中提供很多种方式自定义查询,我们介绍一种相对简单的:利用 @Query 注解来进行查询,方法名字就没有那么雷人了:

@Query("{'owner.$id': ?#{[0]}, 'enabled': ?#{[1]}, 'archived': ?#{[2]}}")
Set<Project> findRelated(User user, boolean enabled, boolean archived)

这个注解中的内容是一个 JSON 对象,就和我们在 MongoDB 的控制台查询的find()中的内容是一样的,只不过将双引号换成单引号,将需要变量用 [0]、[1] 和 [2] 的形式表示第一、第二和第三个参数。那么 ?#{} 是表示里面的内容是个 SpEL ( Spring 的表达式语言) 表达式。

所以实践中,我们可以在 MongoDB 的控制台去实验语句是否好用,然后在 Spring 中编写表达式。

db.project.find(
 {
  "owner.$id": ObjectId("58f5a904edc76ab0e033cfc3"),  
  "enabled": true, 
  "archived": false
 })

在MongoDB的console查询

数据的分页

很多时候我们希望 API 返回的数据是可以分页的,这个分页问题在 Spring Boot 有怎样便捷的方法呢?我们是否需要再定义一堆什么 pageSize,pageCount,start, off 的参数呢?答案是完全没必要,分页这个事情对于 Spring Boot 来说很简单,只需改变各层级原有方法中返回的 List 或 Set 对象为 Page 对象,传入参数多一个 Pageable 类型的参数即可。

// Controller
@RequestMapping(method = RequestMethod.GET)
public Page<Project> findRelated(
  @RequestHeader(value = "userId") String userId,
  @RequestParam(value = "enabled", defaultValue = "true", required = false) boolean enabled,
  @RequestParam(value = "archived", defaultValue = "false", required = false) boolean archived,
  Pageable pageable) {
 return service.findRelated(userId, enabled, archived, pageable);
}
// Repository
@Query("{'owner.$id': ?#{[0]}, 'enabled': ?#{[1]}, 'archived': ?#{[2]}}")
Page<Project> findRelated(ObjectId userId, boolean enabled, boolean archived, Pageable pageable);

现在呢,我们就可以这样使用了 GET http://localhost:8090/projects/?page=0&size=3 表示取每页三个数据取第一页。

本章代码:https://github.com/wpcfan/spring-boot-tut/tree/chap05


# spring  # boot  # 跨域  # boot自定义查询  # boot分页  # springboot使用JPA时间类型进行模糊查询的方法  # 详解Spring Data Jpa 模糊查询的正确用法  # 基于SpringMVC+Bootstrap+DataTables实现表格服务端分页、模糊查询  # 使用Springboot注解形式进行模糊查询  # 分页  # 是个  # 该用户  # 还可以  # 就不  # 这就是  # 我们可以  # 很重要  # 自定义  # 好用  # 就可以  # 是一个  # 也就是说  # 情况下  # 器上  # 就会  # 是在  # 不知其所以然  # 也会  # 出了 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: 网站制作价目表怎么做,珍爱网婚介费用多少?  如何构建满足综合性能需求的优质建站方案?  Bootstrap整体框架之JavaScript插件架构  Python文件异常处理策略_健壮性说明【指导】  专业型网站制作公司有哪些,我设计专业的,谁给推荐几个设计师兼职类的网站?  Laravel如何为API编写文档_Laravel API文档生成与维护方法  js实现获取鼠标当前的位置  公司网站制作需要多少钱,找人做公司网站需要多少钱?  Swift中switch语句区间和元组模式匹配  javascript中数组(Array)对象和字符串(String)对象的常用方法总结  PHP 500报错的快速解决方法  如何在Windows服务器上快速搭建网站?  Swift开发中switch语句值绑定模式  如何正确选择百度移动适配建站域名?  如何快速建站并高效导出源代码?  Laravel Eloquent访问器与修改器是什么_Laravel Accessors & Mutators数据处理技巧  合肥制作网站的公司有哪些,合肥聚美网络科技有限公司介绍?  中国移动官方网站首页入口 中国移动官网网页登录  电商网站制作价格怎么算,网上拍卖流程以及规则?  网站设计制作书签怎么做,怎样将网页添加到书签/主页书签/桌面?  悟空识字怎么关闭自动续费_悟空识字取消会员自动扣费步骤  常州企业网站制作公司,全国继续教育网怎么登录?  进行网站优化必须要坚持的四大原则  简单实现jsp分页  Laravel如何使用Facades(门面)及其工作原理_Laravel门面模式与底层机制  Laravel如何生成API文档?(Swagger/OpenAPI教程)  如何用AWS免费套餐快速搭建高效网站?  如何用免费手机建站系统零基础打造专业网站?  Android使用GridView实现日历的简单功能  Laravel如何使用Livewire构建动态组件?(入门代码)  Laravel怎么集成Log日志记录_Laravel单文件与每日日志配置及自定义通道【详解】  Win11怎么设置虚拟桌面 Win11新建多桌面切换操作【技巧】  如何用ChatGPT准备面试 模拟面试问答与职场话术练习教程  如何在阿里云服务器自主搭建网站?  打开php文件提示内存不足_怎么调整php内存限制【解决方案】  Laravel API资源(Resource)怎么用_格式化Laravel API响应的最佳实践  简历在线制作网站免费版,如何创建个人简历?  Laravel N+1查询问题如何解决_Eloquent预加载(Eager Loading)优化数据库查询  Laravel如何集成微信支付SDK_Laravel使用yansongda-pay实现扫码支付【实战】  如何在香港服务器上快速搭建免备案网站?  Laravel怎么连接多个数据库_Laravel多数据库连接配置  如何基于云服务器快速搭建个人网站?  Laravel怎么配置S3云存储驱动_Laravel集成阿里云OSS或AWS S3存储桶【教程】  如何制作新型网站程序文件,新型止水鱼鳞网要拆除吗?  在线教育网站制作平台,山西立德教育官网?  免费制作统计图的网站有哪些,如何看待现如今年轻人买房难的情况?  laravel怎么配置Redis作为缓存驱动_laravel Redis缓存配置教程  Windows Hello人脸识别突然无法使用  如何快速搭建高效简练网站?  Google浏览器为什么这么卡 Google浏览器提速优化设置步骤【方法】