【Kotlin + Spring Boot 4 从零到架构师】第 28 篇:CORS 与文件上传
本系列定位:零基础入门,从 Kotlin 语法一路到 Spring Boot 4 高级架构(DDD + Modulith),适合 Java 开发者转型,也适合纯新手系统学习。
本篇你将学到
- CORS 跨域问题的原因与解决方案
- Spring Boot 全局 CORS 配置
- 文件上传:
MultipartFile - 文件下载:
ResponseEntity+ 流
学完本篇,你将能为 mini-shop 添加跨域支持和商品图片上传功能。
一、CORS 跨域
下面是 CORS 跨域请求的完整流程示意图:
1.1 什么是跨域
当浏览器的前端页面和后端 API 不在同一个「源」(协议+域名+端口)时,浏览器会阻止请求:
前端:http://localhost:3000 (Vite/Vue 开发服务器) 后端:http://localhost:8080 (Spring Boot) 协议相同(http)、域名相同(localhost)、端口不同(3000 vs 8080) → 浏览器判定为跨域 → 默认阻止请求1.2 错误现象
浏览器控制台报错:
Access to XMLHttpRequest at 'http://localhost:8080/api/products' from origin 'http://localhost:3000' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource.1.3 解决方案:全局 CORS 配置
packagecom.example.minishop.configimportorg.springframework.context.annotation.Configurationimportorg.springframework.web.servlet.config.annotation.CorsRegistryimportorg.springframework.web.servlet.config.annotation.WebMvcConfigurer@ConfigurationclassCorsConfig:WebMvcConfigurer{overridefunaddCorsMappings(registry:CorsRegistry){registry.addMapping("/api/**") // 对 /api/ 下所有接口生效 .allowedOrigins( // 允许的前端源 "http://localhost:3000", // Vue/Vite 开发服务器 "http://localhost:5173", // Vue 默认端口 "http://localhost:4200" // Angular 默认端口 ) .allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") .allowedHeaders("*") // 允许所有请求头 .allowCredentials(true) // 允许携带 Cookie .maxAge(3600) // 预检请求缓存 1 小时 } }1.4 生产环境 CORS
@ConfigurationclassCorsConfig:WebMvcConfigurer{overridefunaddCorsMappings(registry:CorsRegistry){registry.addMapping("/api/**") // 生产环境只允许前端域名 .allowedOrigins( "https://www.mini-shop.com", "https://mini-shop.com" ) .allowedMethods("GET", "POST", "PUT", "DELETE") .allowedHeaders("*") .allowCredentials(true) .maxAge(3600) } }安全提示:不要在生产环境用
allowedOrigins("*")+allowCredentials(true),这会被浏览器拒绝。如果允许任意域名,用allowedOriginPatterns("*")。
1.5 单个接口的 CORS
如果只有个别接口需要跨域:
@RestController@RequestMapping("/api/products")@CrossOrigin(origins=["http://localhost:3000"])// 类级别classProductController{@GetMapping("/{id}")@CrossOrigin(origins=["http://localhost:5173"])// 方法级别fungetById(@PathVariableid:Long):Product{...}}二、文件上传
下面是文件上传的完整处理流程图:
2.1 配置上传限制
spring:servlet:multipart:enabled:truemax-file-size:10MB# 单个文件最大 10MBmax-request-size:50MB# 单次请求最大 50MB2.2 文件上传接口
packagecom.example.minishop.controllerimportcom.example.minishop.exception.BusinessRuleExceptionimportcom.example.minishop.dto.ApiResponseimportorg.springframework.beans.factory.annotation.Valueimportorg.springframework.web.bind.annotation.*importorg.springframework.web.multipart.MultipartFileimportjava.nio.file.Filesimportjava.nio.file.Pathimportjava.nio.file.Pathsimportjava.nio.file.StandardCopyOptionimportjava.util.UUID@RestController@RequestMapping("/api/files")classFileController(// 从配置文件读取上传目录@Value("\${mini-shop.upload-dir:uploads}")privatevaluploadDir:String){@PostMapping("/upload")funupload(@RequestParam("file")file:MultipartFile,@RequestParam(defaultValue="product")type:String):ApiResponse<Map<String,String>>{// 1. 校验文件if(file.isEmpty){throwBusinessRuleException("文件不能为空")}valoriginalFilename=file.originalFilename?:throwBusinessRuleException("文件名不能为空")// 2. 校验文件类型valallowedExtensions=setOf("jpg","jpeg","png","gif","webp")valextension=originalFilename.substringAfterLast('.',"").lowercase()if(extension!inallowedExtensions){throwBusinessRuleException("不支持的文件类型:.$extension")}// 3. 生成唯一文件名(防止覆盖)valnewFilename="${type}/${UUID.randomUUID()}.$extension"// 4. 创建目录并保存valtargetPath:Path=Paths.get(uploadDir,newFilename)Files.createDirectories(targetPath.parent)// 5. 写入文件file.inputStream.use{input->Files.copy(input,targetPath,StandardCopyOption.REPLACE_EXISTING)}// 6. 返回文件信息returnApiResponse.success(mapOf("filename"tonewFilename,"originalName"tooriginalFilename,"size"tofile.size.toString(),"url"to"/uploads/$newFilename"))}}2.3 上传测试
curl-XPOST http://localhost:8080/api/files/upload\-F"file=@keyboard.jpg"\-F"type=product"响应:
{"code":200,"message":"success","data":{"filename":"product/550e8400-e29b-41d4-a716-446655440000.jpg","originalName":"keyboard.jpg","size":"245678","url":"/uploads/product/550e8400-e29b-41d4-a716-446655440000.jpg"}}2.4 静态资源映射
上传的文件需要能被访问到,配置静态资源映射:
@ConfigurationclassWebConfig:WebMvcConfigurer{overridefunaddResourceHandlers(registry:ResourceHandlerRegistry){// 把 /uploads/** URL 映射到本地文件目录registry.addResourceHandler("/uploads/**") .addResourceLocations("file:uploads/") } }三、文件下载
下面是文件下载的流程示意图:
@GetMapping("/download/{filename}")fundownload(@PathVariablefilename:String,response:HttpServletResponse){valfilePath:Path=Paths.get(uploadDir,filename)if(!Files.exists(filePath)){throwResourceNotFoundException("文件",filename)}valcontentType=Files.probeContentType(filePath)?:"application/octet-stream"response.contentType=contentType response.setHeader("Content-Disposition","attachment; filename=\"$filename\"")Files.copy(filePath,response.outputStream)}本篇小结
| 知识点 | 核心内容 |
|---|---|
| CORS | 浏览器安全策略,阻止跨源请求 |
| 全局 CORS 配置 | WebMvcConfigurer.addCorsMappings() |
@CrossOrigin | 单个接口的跨域配置 |
allowedOrigins | 允许的前端域名 |
allowCredentials | 允许携带 Cookie |
| 文件上传 | @RequestParam("file") MultipartFile |
| 上传限制 | spring.servlet.multipart.max-file-size |
| 文件名防覆盖 | UUID 生成唯一文件名 |
| 文件下载 | Files.copy(path, response.outputStream) |
| 静态资源映射 | addResourceHandler("/uploads/**") |
下篇预告
第 29 篇:SpringDoc OpenAPI 3 — API 文档
一行代码不写就能生成漂亮的 API 文档?下一篇集成 SpringDoc,为 mini-shop 生成交互式 API 文档。
如果本篇内容对你有帮助,欢迎点赞收藏!有任何疑问,欢迎在评论区交流。