企业级Spring MVC高级主题与实用技术讲解
企业级Spring MVC高级主题与实用技术讲解
本手册旨在为具备Spring MVC基础的初学者,系统地讲解企业级应用开发中常用的高级主题和实用技术,涵盖RESTful API、统一异常处理、拦截器、文件处理、国际化、前端集成及Spring Security基础。内容结合JavaConfig和代码示例进行说明,并尝试与之前的图书管理系统案例和基础教程内容衔接。
1. RESTful API 设计与实践
RESTful是一种架构风格,而非强制标准。它基于HTTP协议,通过统一的接口对资源进行操作,具有无状态、客户端-服务器分离等特点。在现代企业应用中,特别是在前后端分离架构下,RESTful API是常用的后端接口风格。
核心设计原则
- 资源 (Resource): Web上的核心概念,指代某个事物(如用户、图书)。资源通过URI (统一资源标识符) 来唯一标识。
- 示例URI:
/users
,/books/123
- 示例URI:
- URI: 应简洁、直观,描述资源而非操作。使用名词复数表示集合,名词单数表示个体。避免在URI中使用动词。
- HTTP 方法 (HTTP Methods): 使用HTTP方法来表示对资源的操作:
GET
: 获取资源。安全且幂等。POST
: 创建新资源或执行非幂等操作。PUT
: 更新或替换资源。幂等。DELETE
: 删除资源。幂等。PATCH
: 部分更新资源。
- 状态码 (Status Codes): 使用标准的HTTP状态码表示请求的处理结果:
2xx
(Success):200 OK
,201 Created
,204 No Content
3xx
(Redirection):301 Moved Permanently
,302 Found
4xx
(Client Error):400 Bad Request
,401 Unauthorized
,403 Forbidden
,404 Not Found
,405 Method Not Allowed
5xx
(Server Error):500 Internal Server Error
- 表述 (Representation): 资源通过某种格式(如JSON、XML)来表述其状态。客户端和服务器通过这些表述进行数据交换。JSON是目前最流行的格式。
- 无状态 (Stateless): 服务器不存储客户端的上下文信息。每个请求都包含处理该请求所需的所有信息。
Spring 构建 RESTful 服务
Spring MVC通过一系列注解简化RESTful服务的构建。
@RestController
: 标记一个类是RESTful控制器。它是@Controller
和@ResponseBody
的组合。@ResponseBody
: 标记方法返回值直接写入HTTP响应体,不作为视图名。Spring MVC会根据Accept
头和HttpMessageConverter
将返回值转换为相应格式(如JSON)。@RequestBody
: 标记方法参数来自HTTP请求体。Spring MVC会根据Content-Type
头和HttpMessageConverter
将请求体内容转换为方法参数对象。@GetMapping
,@PostMapping
,@PutMapping
,@DeleteMapping
: 对应HTTP方法的请求映射注解,是@RequestMapping
的快捷方式。@PathVariable
: 获取URI路径中的变量。ResponseEntity
: 封装响应的完整信息,包括响应体、状态码和头部。可以在方法中返回ResponseEntity
来精确控制响应。
+-------------+ +-----------------+ +---------------------+ +-------------+
| User/Client | --> | DispatcherServlet | --> | RequestMappingHandler | --> | Controller |
| (Frontend) | +-----------------+ | Adapter | +------v------+
| | | HTTP Req | +-----------+---------+ | Process Logic
| | | | | | (Service/Repo)
| | | GET /api/books/1| | Call Method |
| | | POST /api/books | | with @RequestBody | Return Object
| | | { JSON Data } | | |
+-------------+ +-----------------+ +-----------+---------+ +------^------+| | Convert to/from JSON || Look up Handler | (@RequestBody / | HttpMessageConverter| | @ResponseBody) |v | |+-----------------+ +---------------------+ +------+------+| HandlerMapping | | HttpMessageConverter| <--> | JSON/XML |+-----------------+ +---------------------+ +------+------+| Find Handler | Serialize/Deserialize | Write to Response+----------------------+------------------------------+
代码示例 (基于图书管理系统,添加 REST API)
假设在图书管理系统中有Book
实体和BookService
。新增一个BookRestController
:
package com.yourcompany.bookmanagement.controller;import com.yourcompany.bookmanagement.entity.Book;
import com.yourcompany.bookmanagement.exception.BookNotFoundException; // 复用之前的异常
import com.yourcompany.bookmanagement.service.BookService; // 复用之前的 Service
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;import java.util.List;
import java.util.Optional;@RestController // @RestController = @Controller + @ResponseBody
@RequestMapping("/api/v1/books") // REST API 基础路径,v1 表示版本
public class BookRestController {private final BookService bookService;@Autowiredpublic BookRestController(BookService bookService) {this.bookService = bookService;}// 获取所有图书列表// GET /api/v1/books@GetMappingpublic List<Book> getAllBooks() {// 返回 List 会自动通过 HttpMessageConverter 转换为 JSON 数组return bookService.findAllBooks();}// 获取特定图书详情// GET /api/v1/books/{id}@GetMapping("/{id}")public ResponseEntity<Book> getBookById(@PathVariable Long id) {Optional<Book> book = bookService.findBookById(id);// 使用 ResponseEntity 控制状态码return book.map(value -> new ResponseEntity<>(value, HttpStatus.OK)) // 找到则返回 200 OK.orElseThrow(() -> new BookNotFoundException(id)); // 找不到则抛出异常,由全局异常处理器处理}// 创建新图书// POST /api/v1/books@PostMapping@ResponseStatus(HttpStatus.CREATED) // 创建成功返回 201 Createdpublic Book createBook(@RequestBody Book book) { // @RequestBody 将请求体 (JSON) 转换为 Book 对象// 校验等逻辑可以在 Service 层或通过 Bean Validation 实现 (需要额外配置)return bookService.saveBook(book); // 保存并返回新创建的图书 (可能包含生成的 ID)}// 更新图书// PUT /api/v1/books/{id}@PutMapping("/{id}")public ResponseEntity<Book> updateBook(@PathVariable Long id, @RequestBody Book book) {// 实际更新逻辑需要先查找,然后更新字段,最后保存Optional<Book> existingBookOptional = bookService.findBookById(id);if (existingBookOptional.isPresent()) {Book existingBook = existingBookOptional.get();// 假设只更新标题和作者,实际应根据需求更新所有字段existingBook.setTitle(book.getTitle());existingBook.setAuthor(book.getAuthor());existingBook.setIsbn(book.getIsbn());existingBook.setPublicationDate(book.getPublicationDate());Book updatedBook = bookService.saveBook(existingBook);return new ResponseEntity<>(updatedBook, HttpStatus.OK); // 返回更新后的图书和 200 OK} else {throw new BookNotFoundException(id); // 找不到则抛异常}}// 删除图书// DELETE /api/v1/books/{id}@DeleteMapping("/{id}")@ResponseStatus(HttpStatus.NO_CONTENT) // 删除成功返回 204 No Contentpublic void deleteBook(@PathVariable Long id) {// 可以先检查是否存在,再删除Optional<Book> book = bookService.findBookById(id);if (!book.isPresent()) {throw new BookNotFoundException(id); // 不存在则抛异常}bookService.deleteBookById(id); // 调用 Service 删除}
}
JSON 数据交互的最佳实践
- 一致的格式: 保持请求和响应JSON结构的命名规范、日期格式等一致。
- 合适的
Content-Type
: 请求时使用application/json
,响应时服务器返回application/json
。Spring MVC会根据produces
和consumes
参数、Accept
头自动处理。 - 字段命名: 推荐使用小驼峰命名法 (camelCase),与JavaScript习惯一致。Jackson库默认支持。
- 错误响应: 使用统一的错误响应格式,包含状态码、错误信息等(详见下一节)。
- 分页/排序: 对于列表接口,通过查询参数传递分页 (
page
,size
) 和排序 (sort
,sortBy
) 信息。Spring Data JPA的Pageable
很适合。 - 版本控制: 在URI (
/api/v1/books
) 或Header (X-API-Version
) 中体现版本,便于API演进。URI版本控制更直观常用。 - HATEOAS: (Hypermedia as the Engine of Application State) 一种更高级的RESTful实践,要求资源表述中包含指向相关资源的链接,使客户端可以无需硬编码URI就能导航API。Spring HATEOAS项目提供了支持。对于初学者,理解概念即可,实现相对复杂,通常在API成熟阶段考虑。
版本控制策略
- URI 版本 (URI Versioning): 将版本号放入URI路径中,如
/api/v1/books
。最常用且直观。缺点是URI会随着版本变化。 - Header 版本 (Header Versioning): 将版本信息放入请求头,如
Accept: application/vnd.myapi.v1+json
或自定义头X-API-Version: 1.0
。URI保持稳定,但客户端调用稍复杂。 - 参数版本 (Query Parameter Versioning): 将版本作为查询参数,如
/api/books?version=1.0
。不推荐,不符合RESTful风格。
选择哪种取决于项目需求和团队偏好,URI版本控制对初学者最友好。
2. 统一异常处理机制
良好的异常处理机制能够提升应用的健壮性和用户体验。Spring MVC提供了灵活的方式实现统一的异常处理。
使用 @ControllerAdvice
和 @ExceptionHandler
@ControllerAdvice
: 标记一个类是全局的控制器增强器。Spring会扫描这个类,并将其中的@ExceptionHandler
,@ModelAttribute
,@InitBinder
等方法应用到所有(或指定范围)的@Controller
和@RestController
上。这实现了全局性。@ExceptionHandler
: 标记一个方法用于处理特定类型的异常。当Controller方法抛出@ExceptionHandler
指定类型的异常时,Spring MVC会调用匹配的异常处理方法。这实现了针对性。
代码示例 (基于图书管理系统)
图书管理系统案例中的GlobalExceptionHandler
和BookNotFoundException
就是很好的示例。
package com.yourcompany.bookmanagement.exception;import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity; // 用于 REST API 返回 JSON
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody; // 用于 REST API
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.servlet.ModelAndView; // 用于返回视图// @ControllerAdvice 应用于所有 Controller
@ControllerAdvice
public class GlobalExceptionHandler {private static final Logger logger = LoggerFactory.getLogger(GlobalExceptionHandler.class);// --- 针对返回视图的异常处理 (如图书管理系统案例中的 HTML 页面请求) ---// 处理 BookNotFoundException 异常,返回 404 视图@ExceptionHandler(BookNotFoundException.class)@ResponseStatus(HttpStatus.NOT_FOUND) // 设置响应状态码为 404public ModelAndView handleBookNotFoundForView(BookNotFoundException ex) {logger.warn("Book not found: " + ex.getMessage());ModelAndView mav = new ModelAndView("error/404"); // 返回错误视图 error/404.htmlmav.addObject("message", ex.getMessage()); // 将错误信息添加到 Modelreturn mav;}// 处理所有其他未捕获的 Exception,返回 500 视图@ExceptionHandler(Exception.class)@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR) // 设置响应状态码为 500public ModelAndView handleAllExceptionsForView(Exception ex) {logger.error("Internal Server Error: ", ex);ModelAndView mav = new ModelAndView("error/500"); // 返回错误视图 error/500.htmlmav.addObject("message", "Internal Server Error. Please try again later.");return mav;}// --- 针对返回 JSON 的异常处理 (如 RESTful API 请求) ---// 可以定义一个返回 JSON 格式的异常处理,但需要区分请求类型// 或者更简单的做法是,如果你的 Controller 是 @RestController,异常处理方法也返回 @ResponseBody// 这里以 BookNotFoundException 为例,演示返回 JSON 错误@ExceptionHandler(BookNotFoundException.class)@ResponseStatus(HttpStatus.NOT_FOUND) // 设置响应状态码为 404@ResponseBody // 直接写入响应体,由 HttpMessageConverter 处理public ErrorResponse handleBookNotFoundForRest(BookNotFoundException ex) {logger.warn("Book not found for REST request: " + ex.getMessage());// 返回统一的 JSON 错误格式return new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage(), System.currentTimeMillis());}// 处理 @RequestBody 参数校验失败异常 (MethodArgumentNotValidException)// 通常在 REST API 中发生@ExceptionHandler(org.springframework.web.bind.MethodArgumentNotValidException.class)@ResponseStatus(HttpStatus.BAD_REQUEST) // 400 Bad Request@ResponseBodypublic ErrorResponse handleValidationExceptions(org.springframework.web.bind.MethodArgumentNotValidException ex) {// 提取所有校验错误信息String errorMessage = ex.getBindingResult().getFieldErrors().stream().map(error -> error.getField() + ": " + error.getDefaultMessage()).collect(java.util.stream.Collectors.joining(", "));logger.warn("Validation failed: " + errorMessage);return new ErrorResponse(HttpStatus.BAD_REQUEST.value(), "Validation Failed: " + errorMessage, System.currentTimeMillis());}// 统一错误响应格式 (POJO)public static class ErrorResponse {private int status;private String message;private long timestamp;public ErrorResponse(int status, String message, long timestamp) {this.status = status;this.message = message;this.timestamp = timestamp;}// Getters for Jackson serializationpublic int getStatus() { return status; }public String getMessage() { return message; }public long getTimestamp() { return timestamp; }}// 自定义异常类 (同图书管理系统案例)// package com.yourcompany.bookmanagement.exception;// public class BookNotFoundException extends RuntimeException {// public BookNotFoundException(Long id) {// super("Book not found with ID: " + id);// }// }
}
说明:
- 同一个
@ControllerAdvice
类中,可以定义多个@ExceptionHandler
方法处理不同类型的异常。 @ExceptionHandler
方法的参数可以是抛出的异常对象,返回值可以是ModelAndView
,String
(视图名或重定向),@ResponseBody
返回值,ResponseEntity
等。- 通过
@ResponseStatus
可以设置响应的HTTP状态码。 - 为了同时支持返回视图和返回JSON的异常处理,可以根据请求的
Accept
头或路径 (/api/**
) 进行区分处理,或者如示例所示,为同一种异常定义两个@ExceptionHandler
方法(Spring会根据返回类型等选择更匹配的那个,或者可以通过@RequestMapping(produces = ...)
等进一步限定)。在全RESTful API应用中,通常只返回JSON。
自定义异常类
根据业务需求定义自定义异常类,继承自RuntimeException
(非检查型异常)或Exception
(检查型异常)。非检查型异常通常用于表示编程错误或运行时环境问题,检查型异常用于表示可预期的、需要调用方显式处理的业务问题。在Spring的事务管理中,默认只对非检查型异常进行回滚。
package com.yourcompany.bookmanagement.exception;// 业务异常示例:图书库存不足
public class InsufficientStockException extends RuntimeException {private Long bookId;private int requested;private int available;public InsufficientStockException(Long bookId, int requested, int available) {super("Book " + bookId + " stock insufficient. Requested: " + requested + ", Available: " + available);this.bookId = bookId;this.requested = requested;this.available = available;}// Getters for error detailspublic Long getBookId() { return bookId; }public int getRequested() { return requested; }public int getAvailable() { return available; }
}
然后在@ControllerAdvice
中添加对应的@ExceptionHandler
处理方法。
异常处理策略
- 业务异常: 对于应用程序的正常流程中可能发生的、可预期的错误(如用户不存在、库存不足),定义特定的自定义异常。在Service层或Controller层捕获或抛出,由全局异常处理器返回友好的错误信息(给用户)和具体的错误代码(给前端/客户端)。
- 系统异常: 对于意料之外的错误(如数据库连接失败、空指针异常),通常抛出RuntimeException或其子类。全局异常处理器应记录详细日志(给开发者),并返回通用的错误提示(给用户)和500状态码。避免在生产环境泄露敏感的异常堆栈信息。
- 返回格式: 对于面向用户的Web页面,返回错误页面(如404.html, 500.html)。对于RESTful API,返回统一结构的JSON错误响应。
3. Spring MVC 拦截器 (Interceptor)
拦截器允许你在请求到达Controller之前、Controller处理之后、以及整个请求处理完成后执行自定义逻辑。它工作在DispatcherServlet内部,比Servlet Filter更靠近Spring MVC的核心流程,能够访问Handler(Controller方法)和ModelAndView等信息。
概念、生命周期与 Filter 的区别
- 概念: 拦截器是Spring MVC提供的请求处理拦截机制。
- 生命周期: 一个请求经过拦截器链的三个阶段:
preHandle()
: 在Controller方法执行之前调用。如果返回true
,继续执行后续拦截器和Controller;如果返回false
,中断整个请求处理流程。常用于认证、权限校验、日志记录。postHandle()
: 在Controller方法执行之后,视图渲染之前调用。可以访问ModelAndView,用于修改模型数据、视图名等。注意:如果Controller方法抛出异常,postHandle
不会被调用。afterCompletion()
: 在整个请求处理完成之后(包括视图渲染完成后),无论是否发生异常,都会调用。用于资源清理等。
- 与 Filter 的区别:
- Filter是Servlet规范的一部分,工作在Servlet容器层面,在DispatcherServlet之前执行,无法访问Spring MVC的上下文(如Handler)。适用于字符编码、会话管理、静态资源处理等。
- Interceptor是Spring MVC框架的一部分,工作在DispatcherServlet内部,HandlerMapping之后。能够访问Handler、ModelAndView、Spring容器中的Bean等。适用于更细粒度的、与业务逻辑关联的拦截,如认证、权限、性能监控、日志。
创建和配置拦截器
- 创建拦截器类: 实现
HandlerInterceptor
接口。该接口定义了preHandle
,postHandle
,afterCompletion
方法。或者,如果只需要实现部分方法,可以继承已废弃的HandlerInterceptorAdapter
,但现在推荐直接实现HandlerInterceptor
并使用接口的默认方法。 - 配置拦截器: 在实现
WebMvcConfigurer
接口的配置类中,通过重写addInterceptors()
方法注册拦截器。
代码示例 (基于图书管理系统)
图书管理系统案例中的AuthInterceptor
是认证拦截器的示例。
package com.yourcompany.bookmanagement.interceptor;import org.springframework.web.servlet.HandlerInterceptor; // 引入接口
import org.springframework.web.servlet.ModelAndView;import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;public class AuthInterceptor implements HandlerInterceptor { // 实现 HandlerInterceptor 接口// 在 Controller 方法执行前调用@Overridepublic boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {String requestURI = request.getRequestURI();System.out.println("AuthInterceptor: Intercepting request: " + requestURI);HttpSession session = request.getSession();Object user = session.getAttribute("loggedInUser"); // 检查 Session 中是否有名为 "loggedInUser" 的属性if (user != null) {// 用户已登录,放行System.out.println("AuthInterceptor: User is logged in.");return true; // 继续执行后续拦截器或 Controller} else {// 用户未登录,重定向到登录页面System.out.println("AuthInterceptor: User is NOT logged in. Redirecting to login page.");// 获取 contextPath,避免硬编码应用名称response.sendRedirect(request.getContextPath() + "/login");return false; // 阻止当前请求继续处理}}// 在 Controller 方法执行后,视图渲染前调用@Overridepublic void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {// 可以在这里对 Model 或 View 进行操作// System.out.println("AuthInterceptor postHandle...");}// 在整个请求处理完成后调用@Overridepublic void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {// 用于资源清理等// System.out.println("AuthInterceptor afterCompletion...");}
}
拦截器配置 (WebMvcConfig.java
):
package com.yourcompany.bookmanagement.config;import com.yourcompany.bookmanagement.interceptor.AuthInterceptor; // 引入拦截器类
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; // 引入 InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; // 引入 WebMvcConfigurer@Configuration
@EnableWebMvc
// @ComponentScan...
public class WebMvcConfig implements WebMvcConfigurer { // 实现 WebMvcConfigurer// 将拦截器注册为 Spring Bean@Beanpublic AuthInterceptor authInterceptor() {return new AuthInterceptor();}// 重写 addInterceptors 方法配置拦截器@Overridepublic void addInterceptors(InterceptorRegistry registry) {registry.addInterceptor(authInterceptor()) // 添加拦截器实例.addPathPatterns("/**") // 配置需要拦截的路径模式,"/**" 表示拦截所有请求.excludePathPatterns("/login", "/logout", "/resources/**", "/webjars/**"); // 配置排除的路径模式,登录、注销、静态资源通常需要排除}// ... 其他配置,如 ViewResolver, MultipartResolver 等
}
典型应用场景
- 日志记录: 在
preHandle
记录请求信息,在afterCompletion
记录处理时间、响应状态等。 - 权限校验: 在
preHandle
检查用户是否已登录、是否有权访问当前资源。未通过则重定向或返回错误。 - 请求预处理: 在
preHandle
或postHandle
设置一些通用的请求属性、上下文信息等。 - 性能监控: 在
preHandle
记录请求开始时间,在afterCompletion
计算并记录总耗时。
4. 文件上传与下载
Spring MVC对文件上传和下载提供了内置支持,特别是结合Servlet 3.0+的MultipartRequest API。
文件上传
- 配置
MultipartResolver
: Spring MVC需要一个MultipartResolver
Bean来解析multipart/form-data
请求。StandardServletMultipartResolver
: 基于Servlet 3.0+ 标准。推荐使用,无需额外依赖。CommonsMultipartResolver
: 基于Apache Commons FileUpload库。需要添加commons-fileupload
依赖。
- Controller 中接收文件: 在Controller方法中使用
@RequestParam MultipartFile
参数接收上传的文件。MultipartFile
接口提供了获取文件名、内容类型、文件大小、字节流等方法。 - 文件大小限制、类型校验: 可以在
MultipartResolver
中配置总大小和单个文件大小限制。文件类型校验通常在Controller或Service中根据file.getContentType()
和file.getOriginalFilename()
进行。 - 存储上传文件: 获取到
MultipartFile
后,可以使用transferTo(File dest)
方法将其保存到文件系统,或者获取getInputStream()
/getBytes()
写入数据库、云存储等。
代码示例 (文件上传)
WebMvcConfig.java
配置 StandardServletMultipartResolver
:
package com.yourcompany.bookmanagement.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.multipart.MultipartResolver; // 引入接口
import org.springframework.web.multipart.support.StandardServletMultipartResolver; // 引入 Standard 实现
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
@EnableWebMvc
// @ComponentScan...
public class WebMvcConfig implements WebMvcConfigurer {// 配置 StandardServletMultipartResolver// Servlet 3.0+ 容器会自动提供 MultipartConfigElement,StandardServletMultipartResolver 基于此工作// 文件大小等限制可以在 Servlet 注册时(例如 MyWebAppInitializer)或通过容器配置实现@Beanpublic MultipartResolver multipartResolver() {return new StandardServletMultipartResolver();}// ... 其他配置
}
如果在 MyWebAppInitializer.java
中需要配置上传属性 (如文件大小限制),可以重写 customizeRegistration
方法:
package com.yourcompany.bookmanagement.config;import org.springframework.web.servlet.support.AbstractAnnotationConfigDispatcherServletInitializer;import javax.servlet.MultipartConfigElement; // 引入
import javax.servlet.ServletRegistration; // 引入public class MyWebAppInitializer extends AbstractAnnotationConfigDispatcherServletInitializer {// ... getRootConfigClasses, getServletConfigClasses, getServletMappings methods@Overrideprotected void customizeRegistration(ServletRegistration.Dynamic registration) {// 配置文件上传属性:临时文件存放路径,最大文件大小,最大请求大小,文件阈值大小// 这些配置会传递给 StandardServletMultipartResolverString fileUploadTempDir = System.getProperty("java.io.tmpdir"); // 使用系统的临时目录long maxFileSize = 5 * 1024 * 1024; // 5MBlong maxRequestSize = 10 * 1024 * 1024; // 10MBint fileSizeThreshold = 0; // 所有文件都直接写入临时文件,而不是内存MultipartConfigElement multipartConfigElement = new MultipartConfigElement(fileUploadTempDir,maxFileSize,maxRequestSize,fileSizeThreshold);registration.setMultipartConfig(multipartConfigElement);}
}
Controller 处理文件上传:
package com.yourcompany.bookmanagement.controller;import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.servlet.mvc.support.RedirectAttributes;import java.io.IOException;
import java.nio.file.Files; // 使用 NIO.2 进行文件操作
import java.nio.file.Path;
import java.nio.file.Paths;@Controller
@RequestMapping("/files")
public class FileUploadController {// 文件保存的根目录 (请根据实际环境修改!)// 生产环境不应该硬编码在此处,应从配置读取private static final String UPLOADED_FOLDER = "/path/to/your/uploaded/files/"; // !!! 修改为你的实际路径 !!!@GetMapping("/upload")public String showUploadForm() {return "uploadForm"; // 返回上传表单视图 (如 uploadForm.html)}@PostMapping("/upload")public String handleFileUpload(@RequestParam("file") MultipartFile file,RedirectAttributes redirectAttributes) {// 简单校验:文件是否为空if (file.isEmpty()) {redirectAttributes.addFlashAttribute("message", "Please select a file to upload");return "redirect:/files/uploadStatus"; // 重定向到状态页面}try {// 获取文件名String fileName = file.getOriginalFilename();// 防止路径穿越等安全问题,实际应用中应更严格处理文件名Path path = Paths.get(UPLOADED_FOLDER + fileName);// 创建目标目录 (如果不存在)Files.createDirectories(path.getParent());// 将文件保存到目标路径Files.copy(file.getInputStream(), path); // 使用 NIO.2 Copy StreamredirectAttributes.addFlashAttribute("message", "You successfully uploaded '" + fileName + "'");} catch (IOException e) {e.printStackTrace();redirectAttributes.addFlashAttribute("message", "Failed to upload file: " + e.getMessage());}return "redirect:/files/uploadStatus"; // 重定向到状态页面显示结果}@GetMapping("/uploadStatus")public String uploadStatus() {return "uploadStatus"; // 返回上传状态视图 (如 uploadStatus.html)}
}
上传表单视图 (uploadForm.html
- Thymeleaf 示例):
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>File Upload</title>
</head>
<body><h1>Upload File</h1><!-- 注意:method 必须是 POST,enctype 必须是 multipart/form-data --><form method="POST" action="/files/upload" enctype="multipart/form-data"><div><label for="file">Select File:</label><input type="file" name="file" id="file" required/></div><div><button type="submit">Upload</button></div></form>
</body>
</html>
上传状态视图 (uploadStatus.html
- Thymeleaf 示例):
<!DOCTYPE html>
<html xmlns:th="http://www.thymeleaf.org">
<head><meta charset="UTF-8"><title>Upload Status</title>
</head>
<body><h1>Upload Status</h1><!-- 使用 th:text 获取 RedirectAttributes 中的 Flash 属性 --><div th:if="${message}"><p th:text="${message}"></p></div><p><a th:href="@{/files/upload}">Upload Another File</a></p>
</body>
</html>
文件下载
文件下载通常是通过设置HTTP响应头,让浏览器以下载方式处理响应内容。
- Controller 方法: 可以返回
ResponseEntity<Resource>
,或直接操作HttpServletResponse
。 - 设置响应头: 关键在于设置正确的
Content-Disposition
头,指定文件名并告知浏览器以下载方式处理;Content-Type
头指定文件类型;Content-Length
头指定文件大小。 - 写入响应体: 将文件内容通过流写入
HttpServletResponse
的输出流。
代码示例 (文件下载)
package com.yourcompany.bookmanagement.controller;import org.springframework.core.io.InputStreamResource; // 引入资源类型
import org.springframework.core.io.Resource; // 引入资源接口
import org.springframework.http.HttpHeaders; // 引入 HTTP 头部
import org.springframework.http.MediaType; // 引入媒体类型
import org.springframework.http.ResponseEntity; // 引入 ResponseEntity
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;import javax.servlet.http.HttpServletRequest; // 可能需要
import java.io.File;
import java.io.FileInputStream;
import java.io.IOException;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;@Controller
@RequestMapping("/files")
public class FileDownloadController {// 文件存放的根目录 (同上传示例)private static final String UPLOADED_FOLDER = "/path/to/your/uploaded/files/"; // !!! 修改为你的实际路径 !!!// 文件下载方法,返回 ResponseEntity<Resource>@GetMapping("/download/{fileName:.+}") // :.+ 匹配文件名,包括点号public ResponseEntity<Resource> downloadFile(@PathVariable String fileName, HttpServletRequest request) {Path filePath = Paths.get(UPLOADED_FOLDER).resolve(fileName).normalize(); // 构造文件路径File file = filePath.toFile();// 检查文件是否存在且可读if (!file.exists() || !file.canRead()) {// 抛出异常或返回 404 响应// return new ResponseEntity<>(HttpStatus.NOT_FOUND);throw new RuntimeException("File not found or cannot be read: " + fileName); // 示例抛出异常}try {// 确定文件的 Content-TypeString contentType = request.getServletContext().getMimeType(file.getAbsolutePath());if (contentType == null) {contentType = "application/octet-stream"; // 默认类型}// 创建 InputStreamResourceInputStreamResource resource = new InputStreamResource(new FileInputStream(file));// 构建响应return ResponseEntity.ok().contentType(MediaType.parseMediaType(contentType)) // 设置 Content-Type// 设置 Content-Disposition,inline 表示在线打开,attachment 表示下载.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"").contentLength(file.length()) // 设置 Content-Length.body(resource); // 设置响应体} catch (IOException ex) {// 记录错误日志并返回 500 响应或抛出异常ex.printStackTrace();// return new ResponseEntity<>(HttpStatus.INTERNAL_SERVER_ERROR);throw new RuntimeException("Error reading file: " + fileName, ex); // 示例抛出异常}}/*// 另一种使用 HttpServletResponse 的方式 (不推荐,因为它绕过了 Spring 的 HttpMessageConverter 等)@GetMapping("/download2/{fileName:.+}")public void downloadFile2(@PathVariable String fileName, HttpServletResponse response) throws IOException {Path filePath = Paths.get(UPLOADED_FOLDER).resolve(fileName).normalize();File file = filePath.toFile();if (!file.exists() || !file.canRead()) {response.sendError(HttpServletResponse.SC_NOT_FOUND, "File not found");return;}String contentType = request.getServletContext().getMimeType(file.getAbsolutePath());if (contentType == null) {contentType = "application/octet-stream";}response.setContentType(contentType);response.setHeader(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=\"" + file.getName() + "\"");response.setContentLength((int) file.length());try (InputStream is = new FileInputStream(file);OutputStream os = response.getOutputStream()) {byte[] buffer = new byte[1024];int len;while ((len = is.read(buffer)) != -1) {os.write(buffer, 0, len);}os.flush();} catch (IOException e) {// 错误处理e.printStackTrace();throw e; // 抛出异常让 Spring 的异常处理器处理}}*/
}
下载链接示例 (HTML):
<a th:href="@{/files/download/your_file_name.ext}">Download File</a>
5. 国际化 (i18n) 与本地化 (L10n)
国际化 (i18n) 是使应用程序能够适应不同语言和地区的过程。本地化 (L10n) 是为特定语言和地区定制应用程序的过程,包括翻译文本、调整日期格式等。Spring MVC为国际化提供了强大的支持。
Spring MVC 对国际化的支持
LocaleResolver
: 用于解析当前用户的区域设置 (Locale)。Spring提供了多种实现:AcceptHeaderLocaleResolver
(默认): 根据请求头的Accept-Language
确定Locale。SessionLocaleResolver
: 将Locale存储在HttpSession中。CookieLocaleResolver
: 将Locale存储在Cookie中。FixedLocaleResolver
: 固定使用某个Locale。
MessageSource
: 用于根据Locale加载国际化消息。它从资源文件(如.properties
文件)中读取键值对。Spring提供了ResourceBundleMessageSource
等实现。
配置资源文件和 Spring Bean
- 创建资源文件: 在
src/main/resources
目录下创建消息源文件,遵循basename_locale.properties
的命名约定。messages.properties
(默认语言)messages_en.properties
(英语)messages_zh_CN.properties
(简体中文)- 文件内容为键值对,如:
app.title=Book Management System
- 配置
MessageSource
Bean: 在Spring配置中定义MessageSource
Bean。 - 配置
LocaleResolver
Bean: 在Spring MVC配置 (WebMvcConfigurer
) 中定义LocaleResolver
Bean。 - 配置
LocaleChangeInterceptor
(可选): 如果想通过请求参数切换语言,配置LocaleChangeInterceptor
。
代码示例 (国际化)
资源文件示例 (src/main/resources/messages_zh_CN.properties
, messages_en.properties
):
messages_zh_CN.properties
:
app.title=图书管理系统
book.list.title=图书列表
book.detail.title=图书详情
book.form.add=新增图书
book.form.edit=编辑图书
book.title=标题
book.author=作者
book.isbn=ISBN
book.publicationDate=出版日期
button.save=保存
button.cancel=取消
action.details=详情
action.edit=编辑
action.delete=删除
message.save.success=图书信息保存成功!
message.delete.success=图书删除成功!
error.book.notFound=找不到ID为 {0} 的图书。
validation.NotBlank=字段不能为空
validation.Size=字段长度不符合要求
validation.Pattern=字段格式不正确
validation.PastOrPresent=日期不能晚于今天
login.title=用户登录
login.username=用户名
login.password=密码
login.button=登录
login.error=用户名或密码不正确。
logout.success=您已成功注销。
messages_en.properties
:
app.title=Book Management System
book.list.title=Book List
book.detail.title=Book Detail
book.form.add=Add Book
book.form.edit=Edit Book
book.title=Title
book.author=Author
book.isbn=ISBN
book.publicationDate=Publication Date
button.save=Save
button.cancel=Cancel
action.details=Details
action.edit=Edit
action.delete=Delete
message.save.success=Book information saved successfully!
message.delete.success=Book deleted successfully!
error.book.notFound=Book not found with ID: {0}.
validation.NotBlank=Field must not be blank
validation.Size=Field size constraints violated
validation.Pattern=Field format is incorrect
validation.PastOrPresent=Date cannot be in the future
login.title=User Login
login.username=Username
login.password=Password
login.button=Login
login.error=Invalid username or password.
logout.success=You have been logged out successfully.
WebMvcConfig.java
配置 MessageSource
和 LocaleResolver
:
package com.yourcompany.bookmanagement.config;import org.springframework.context.MessageSource; // 引入 MessageSource
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.support.ResourceBundleMessageSource; // 引入实现类
import org.springframework.web.servlet.LocaleResolver; // 引入 LocaleResolver
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry; // 引入 InterceptorRegistry
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
import org.springframework.web.servlet.i18n.CookieLocaleResolver; // 引入 CookieLocaleResolver
import org.springframework.web.servlet.i18n.LocaleChangeInterceptor; // 引入 LocaleChangeInterceptorimport java.util.Locale; // 引入 Locale@Configuration
@EnableWebMvc
// @ComponentScan...
public class WebMvcConfig implements WebMvcConfigurer {// ... MultipartResolver, AuthInterceptor 等其他 Bean// 配置 MessageSource Bean@Beanpublic MessageSource messageSource() {ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource();// 设置资源文件的 basename (不带语言和后缀)messageSource.setBasename("messages"); // 对应 src/main/resources/messages.properties, messages_en.properties 等messageSource.setDefaultEncoding("UTF-8"); // 设置编码messageSource.setUseCodeAsDefaultMessage(true); // 如果找不到对应的 code,使用 code 本身作为消息return messageSource;}// 配置 LocaleResolver Bean// 使用 CookieLocaleResolver 将用户选择的语言存储在 Cookie 中@Beanpublic LocaleResolver localeResolver() {CookieLocaleResolver localeResolver = new CookieLocaleResolver();localeResolver.setDefaultLocale(Locale.CHINA); // 设置默认区域为中国localeResolver.setCookieName("mylocale"); // 设置存储 Locale 的 Cookie 名称localeResolver.setCookieMaxAge(3600); // Cookie 有效期 (秒)return localeResolver;}// 配置 LocaleChangeInterceptor,用于通过参数切换语言@Beanpublic LocaleChangeInterceptor localeChangeInterceptor() {LocaleChangeInterceptor interceptor = new LocaleChangeInterceptor();// 设置参数名,例如通过访问 /books?lang=en 或 /books?lang=zh_CN 切换语言interceptor.setParamName("lang");return interceptor;}// 将 LocaleChangeInterceptor 注册到拦截器链中@Overridepublic void addInterceptors(InterceptorRegistry registry) {// ... 注册 AuthInterceptorregistry.addInterceptor(localeChangeInterceptor()); // 注册语言切换拦截器}// ... 其他 WebMvcConfigurer 方法
}
在视图和后端使用本地化消息
- 在视图中 (Thymeleaf): 使用
#messages
内置对象和#{...}
语法获取消息。<h1 th:text="#{book.list.title}">图书列表</h1> <p th:text="#{message.save.success}"></p> <!-- 获取带参数的消息,例如 error.book.notFound=找不到ID为 {0} 的图书。 --> <p th:text="#{error.book.notFound(${bookId})}"></p> <!-- 生成带语言切换参数的 URL --> <a th:href="@{/books(lang='en')}">English</a> | <a th:href="@{/books(lang='zh_CN')}">中文</a>
- 在视图中 (JSP): 需要配置Spring标签库,并使用
<spring:message>
标签。<%@ taglib uri="http://www.springframework.org/tags" prefix="spring" %> <spring:message code="book.list.title"/> <spring:message code="error.book.notFound" arguments="${bookId}"/> <a href="<spring:url value='/books'><spring:param name='lang' value='en'/></spring:url>">English</a>
- 在后端代码 (Controller/Service) 中: 通过注入
MessageSource
,使用getMessage()
方法获取消息。
package com.yourcompany.bookmanagement.service.impl;import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.MessageSource; // 引入 MessageSource
import org.springframework.context.i18n.LocaleContextHolder; // 引入 LocaleContextHolder
import org.springframework.stereotype.Service;import java.util.Locale; // 引入 Locale@Service
public class MyService {private final MessageSource messageSource;@Autowiredpublic MyService(MessageSource messageSource) {this.messageSource = messageSource;}public String getLocalizedGreeting(String name) {// 获取当前线程绑定的 Locale (由 LocaleResolver 决定)Locale currentLocale = LocaleContextHolder.getLocale();// 从 MessageSource 获取消息String greeting = messageSource.getMessage("greeting", new Object[]{name}, currentLocale);return greeting;}// 在异常处理中获取本地化消息// GlobalExceptionHandler.java (示例)// @ExceptionHandler(BookNotFoundException.class)// public ResponseEntity<ErrorResponse> handleBookNotFoundForRest(BookNotFoundException ex) {// Locale currentLocale = LocaleContextHolder.getLocale();// String errorMessage = messageSource.getMessage("error.book.notFound", new Object[]{ex.getBookId()}, currentLocale);// return new ResponseEntity<>(new ErrorResponse(HttpStatus.NOT_FOUND.value(), errorMessage, System.currentTimeMillis()), HttpStatus.NOT_FOUND);// }
}
6. 与前端框架集成考量
当Spring MVC作为纯后端API服务(使用@RestController
),与前端框架(Vue.js, React, Angular等)集成时,主要需要考虑数据交互格式、接口设计和跨域问题。
集成模式
- 前后端分离: 后端Spring MVC提供RESTful API,前端框架负责整个UI渲染和用户交互。两者通过HTTP请求进行通信。这是目前主流的企业级Web应用开发模式。
- 后端渲染+前端增强: 后端Spring MVC使用Thymeleaf等模板引擎渲染基础HTML页面,前端框架用于局部增强页面交互(如通过Ajax请求更新部分内容)。图书管理系统案例属于此模式。
CORS (跨域资源共享)
跨域请求指浏览器发起的,目标URL与当前页面URL的协议、域名、端口中任意一个不同的请求。出于安全考虑,浏览器会阻止非同源的HTTP请求,除非服务器明确允许。RESTful API通常部署在与前端不同的域名或端口上,因此需要处理CORS问题。
Spring MVC提供了多种方式处理CORS:
@CrossOrigin
注解: 应用在Controller类或方法上,允许来自指定来源的跨域请求。package com.yourcompany.bookmanagement.controller;import org.springframework.web.bind.annotation.CrossOrigin; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController;@RestController @RequestMapping("/api/data") // 允许来自 http://localhost:8080 和 http://example.com 的跨域请求 @CrossOrigin(origins = {"http://localhost:8080", "http://example.com"}) public class DataController {@GetMapping("/public")public String getPublicData() {return "This is public data.";}@GetMapping("/private")@CrossOrigin("http://localhost:3000") // 方法级别的 @CrossOrigin 会覆盖类级别的设置public String getPrivateData() {return "This is private data.";} }
- 全局 CORS 配置: 在
WebMvcConfigurer
中集中配置,适用于更复杂的场景或希望统一管理CORS规则。
package com.yourcompany.bookmanagement.config;import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.CorsRegistry; // 引入 CorsRegistry
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;@Configuration
@EnableWebMvc
// @ComponentScan...
public class WebMvcConfig implements WebMvcConfigurer {// ... 其他 WebMvcConfigurer 方法@Overridepublic void addCorsMappings(CorsRegistry registry) {registry.addMapping("/api/**") // 配置需要允许跨域的路径模式,例如所有 /api 下的请求.allowedOrigins("http://localhost:3000", "http://your-frontend-domain.com") // 允许的来源域名.allowedMethods("GET", "POST", "PUT", "DELETE", "OPTIONS") // 允许的 HTTP 方法.allowedHeaders("*") // 允许的请求头.allowCredentials(true) // 是否发送 Cookie 或认证信息.maxAge(3600); // 预检请求 (Preflight Request) 的缓存时间 (秒)}
}
API 接口设计对前端友好的考量
- 一致的数据格式: 前端更容易处理一致的JSON结构。保持字段命名、日期格式等规范。
- 清晰的错误处理: RESTful API应返回明确的状态码,并在响应体中包含统一结构的错误信息,便于前端解析和展示错误。
- 合理的数据结构: 根据前端页面或组件需要的数据结构来设计API响应,避免过度嵌套或返回冗余字段。
- 分页与过滤: 对于列表数据,提供分页、排序、过滤等查询参数,让前端能够灵活地获取所需数据。
- 文档: 提供清晰的API文档(如Swagger/OpenAPI),方便前端开发者理解和使用接口。
7. Spring Security 入门
Spring Security是一个强大且高度可定制的认证和授权框架。它可以轻松地为Spring应用程序提供安全性。
Spring Security 核心概念
- Authentication (认证): 验证用户身份,证明“你是谁”。通常通过用户名/密码、证书等方式。
- Authorization (授权): 在身份认证后,确定用户是否有权访问某个资源或执行某个操作。
- Principal: 当前认证用户的代表,通常包含用户名、密码、权限等信息。
- GrantedAuthority: 授予Principal的权限或角色(如ROLE_USER, read_permission)。
- AuthenticationManager: 负责处理认证请求。
- AccessDecisionManager: 负责处理授权决策。
- SecurityContextHolder: 存储当前应用程序中Principal详细信息的容器。默认使用ThreadLocal策略,确保每个线程独立。
- Filter Chain (过滤器链): Spring Security通过一系列Servlet Filter来实现各种安全功能(如认证、授权、CSRF防护)。这些Filter被组织成一个链。
DelegatingFilterProxy
是Spring Security的核心Filter,它将Servlet容器的请求委托给Spring Bean中的Security Filter Chain。
基础配置 (JavaConfig)
Spring Security通常通过JavaConfig进行配置。核心是创建一个继承自WebSecurityConfigurerAdapter
的配置类(或者使用新的SecurityFilterChain
Bean方式,取决于Spring Security版本)。
package com.yourcompany.bookmanagement.config;import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity; // 引入 HttpSecurity
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; // 启用 Spring Security
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter; // 引入适配器类
import org.springframework.security.core.userdetails.User; // 引入 UserDetails
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService; // 引入 UserDetailsService
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; // 引入密码编码器
import org.springframework.security.crypto.password.PasswordEncoder; // 引入接口
import org.springframework.security.provisioning.InMemoryUserDetailsManager; // 引入内存用户存储// 启用 Spring Security 的 Web 安全功能
@EnableWebSecurity
@Configuration // 标记为配置类
public class SecurityConfig extends WebSecurityConfigurerAdapter { // 继承 WebSecurityConfigurerAdapter (Spring Security 5.7+ 推荐使用 SecurityFilterChain Bean)// 配置密码编码器 Bean@Beanpublic PasswordEncoder passwordEncoder() {return new BCryptPasswordEncoder(); // 使用 BCrypt 强哈希算法}// 配置用户详情服务 (AuthenticationManager 的一部分)// 这里使用内存存储用户,实际应用中通常从数据库加载 (实现 UserDetailsService 接口)@Bean@Overridepublic UserDetailsService userDetailsService() {// 创建一个内存用户UserDetails user = User.builder().username("admin").password(passwordEncoder().encode("password")) // 密码必须编码.roles("USER", "ADMIN") // 设置角色.build();return new InMemoryUserDetailsManager(user);}// 配置 HTTP 请求的安全性规则@Overrideprotected void configure(HttpSecurity http) throws Exception {http// 授权配置.authorizeRequests()// /login, /resources/**, /webjars/** 路径无需认证即可访问.antMatchers("/login", "/logout", "/resources/**", "/webjars/**").permitAll()// /books/** 路径需要认证且具有 USER 或 ADMIN 角色才能访问.antMatchers("/books/**").hasAnyRole("USER", "ADMIN")// /admin/** 路径需要认证且具有 ADMIN 角色才能访问.antMatchers("/admin/**").hasRole("ADMIN")// 所有其他路径需要认证才能访问.anyRequest().authenticated().and()// 表单登录配置.formLogin().loginPage("/login") // 指定自定义的登录页面 URL.permitAll() // 登录页面允许所有用户访问.and()// 注销配置.logout().logoutUrl("/logout") // 指定注销 URL (POST 请求).logoutSuccessUrl("/login?logout") // 注销成功后重定向的 URL.permitAll(); // 注销 URL 允许所有用户访问// 禁用 CSRF 防护 (仅为简化示例,生产环境应启用并处理)// http.csrf().disable();}// Spring Security 5.7+ 推荐的配置方式 (使用 SecurityFilterChain Bean 替代 WebSecurityConfigurerAdapter)/*@Beanpublic SecurityFilterChain filterChain(HttpSecurity http) throws Exception {http// 授权配置.authorizeRequests().antMatchers("/login", "/logout", "/resources/**", "/webjars/**").permitAll().antMatchers("/books/**").hasAnyRole("USER", "ADMIN").antMatchers("/admin/**").hasRole("ADMIN").anyRequest().authenticated().and()// 表单登录配置.formLogin().loginPage("/login").permitAll().and()// 注销配置.logout().logoutUrl("/logout").logoutSuccessUrl("/login?logout").permitAll();return http.build();}*/
}
说明:
@EnableWebSecurity
注解会加载Spring Security的核心配置类。WebSecurityConfigurerAdapter
提供了一个方便的基类来定制安全性配置。configure(HttpSecurity http)
方法是配置URL授权规则、表单登录、注销等的核心。userDetailsService()
方法配置如何加载用户信息。InMemoryUserDetailsManager
用于测试,实际应用需要实现UserDetailsService
从数据库等加载。passwordEncoder()
配置密码加密器,Spring Security强制要求使用加密后的密码。BCryptPasswordEncoder
是推荐的选择。
密码加密
使用PasswordEncoder
接口对用户密码进行加密存储和比对。BCryptPasswordEncoder
使用BCrypt算法,该算法包含了随机盐值和多次哈希迭代,安全性较高。
- 存储密码时:
String encodedPassword = passwordEncoder.encode(rawPassword);
- 校验密码时:
boolean isMatch = passwordEncoder.matches(rawPassword, encodedPassword);
方法级别安全注解
除了保护URL路径,Spring Security还支持通过注解保护Controller或Service方法。
- 启用方法安全: 在任何一个
@Configuration
类上添加@EnableGlobalMethodSecurity
注解。prePostEnabled = true
: 启用@PreAuthorize
和@PostAuthorize
注解。securedEnabled = true
: 启用@Secured
注解。
package com.yourcompany.bookmanagement.config;import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.method.configuration.EnableGlobalMethodSecurity; // 引入@Configuration @EnableGlobalMethodSecurity(prePostEnabled = true, securedEnabled = true) // 启用方法安全注解 public class MethodSecurityConfig {// 此类通常是独立的,或者合并到 SecurityConfig 中 }
- 使用注解:
@PreAuthorize
: 在方法执行之前进行权限检查。可以使用Spring EL表达式。@PreAuthorize("hasRole('ADMIN')")
: 只有ADMIN角色才能访问。@PreAuthorize("hasAnyRole('USER', 'ADMIN')")
: USER或ADMIN角色都能访问。@PreAuthorize("hasPermission(#bookId, 'book', 'read')")
: 自定义权限表达式。@PreAuthorize("#username == authentication.principal.username")
: 参数username必须是当前登录用户的username。
@PostAuthorize
: 在方法执行之后进行权限检查。可以访问返回值。@PostAuthorize("returnObject.username == authentication.principal.username")
: 返回值的username必须是当前登录用户的username。
@Secured
: 基于角色的简单权限检查,不支持Spring EL。@Secured("ROLE_ADMIN")
: 只有ROLE_ADMIN角色才能访问。@Secured({"ROLE_USER", "ROLE_ADMIN"})
: ROLE_USER或ROLE_ADMIN角色都能访问。
代码示例 (方法安全)
package com.yourcompany.bookmanagement.controller;import org.springframework.security.access.annotation.Secured; // 引入 @Secured
import org.springframework.security.access.prepost.PreAuthorize; // 引入 @PreAuthorize
import org.springframework.web.bind.annotation.*;@RestController
@RequestMapping("/secure")
public class SecureController {// 需要 ADMIN 角色才能访问@GetMapping("/admin")@Secured("ROLE_ADMIN") // 或 @PreAuthorize("hasRole('ADMIN')")public String adminOnly() {return "This content is for ADMINs only!";}// 需要 USER 或 ADMIN 角色才能访问@GetMapping("/user")@PreAuthorize("hasAnyRole('USER', 'ADMIN')")public String userOrAdmin() {return "This content is for logged-in users (USER or ADMIN)!";}// 示例:基于输入参数的权限检查// 只有当请求的 username 与当前认证用户的 username 相同时才能访问@GetMapping("/profile/{username}")@PreAuthorize("#username == authentication.principal.username")public String getUserProfile(@PathVariable String username) {return "Viewing profile for user: " + username;}
}
保护URL路径访问
通过在HttpSecurity
配置中使用antMatchers()
, regexMatchers()
, anyRequest()
等匹配器结合permitAll()
, authenticated()
, hasRole()
, hasAuthority()
等方法来定义哪些URL需要哪些权限。这是最常用的保护方式。示例已包含在基础配置代码中。
运行与部署
参考图书管理系统案例中的Maven构建WAR包和部署到Servlet容器步骤。Spring Security作为Filter Chain会自动集成到请求处理流程中。
总结
通过学习这些高级主题,你将能够构建更加健壮、安全、易于维护和集成的企业级Spring MVC应用程序。RESTful API是前后端分离的基石;统一异常处理提升用户体验和开发效率;拦截器提供灵活的请求处理增强;文件处理是常见功能;国际化使应用适应全球用户;Spring Security提供全面的安全保障。
持续学习建议:
- Spring Security 深入: 用户详情服务 (
UserDetailsService
)、自定义认证提供者、OAuth2、JWT等。 - RESTful API 进阶: API文档(Swagger/OpenAPI)、API网关、更复杂的HATEOAS实现。
- 缓存: 使用Spring Cache提升数据访问性能。
- 消息队列: 集成RabbitMQ, Kafka等处理异步任务和解耦。
- 分布式系统: 了解Spring Cloud体系。
- 最重要:转向 Spring Boot! 将这些学到的原生Spring MVC知识应用到Spring Boot环境中,你会发现开发效率的巨大提升。Spring Boot基于约定大于配置的理念,自动集成了大量常用功能(包括上述大部分高级特性),让你更专注于业务逻辑。
希望这篇文章能帮助你更好地迈向Spring MVC高级开发!