spring boot 授权控制 + JPA 排序分页 ------16
「Spring Security 授权」和「JPA 数据处理」两大模块,完全兼容你现有项目结构,代码遵循企业级最佳实践。
一、核心概念前置(对应 164~167 集)
1. 认证 vs 授权(165 集)
- 认证(Authentication):你是谁?验证账号密码是否正确,确认身份。
- 授权(Authorization):你能做什么?身份确认后,判断你有没有权限访问某个接口、操作某条数据。
2. GrantedAuthority 权限载体(166 集)
Spring Security 内部所有权限 / 角色都封装成
GrantedAuthority 对象,存在安全上下文中,用来做权限校验。3. ROLE 与 AUTHORITY 的区别(167 集)
表格
| 类型 | 命名规范 | 校验方式 | 适用场景 |
|---|---|---|---|
| Role 角色 | 必须以 ROLE_ 前缀开头,如 ROLE_ADMIN |
hasRole("ADMIN") 自动补前缀 |
粗粒度权限,比如管理员、普通用户 |
| Authority 权限 | 任意字符串,如 user:delete |
hasAuthority("user:delete") |
细粒度权限,精确到单个操作 |
最佳实践:中小项目用角色即可;大型系统用「角色 + 权限」组合,角色作为权限集合。
二、授权实战:接口权限控制 + 自定义错误(168~169 集)
1. 自定义安全异常响应
让 401(未登录)、403(无权限)也返回和业务接口一致的 JSON 格式,而不是默认的白页错误。
未登录异常处理器 security/RestAuthenticationEntryPoint.java
java
运行
package com.example.jobportal.security; import com.example.jobportal.common.Result; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; import org.springframework.stereotype.Component; import java.io.IOException; /** * 处理未登录/认证失败的场景,返回统一JSON * 对应169集:自定义Security错误 */ @Component public class RestAuthenticationEntryPoint implements AuthenticationEntryPoint { private final ObjectMapper objectMapper = new ObjectMapper(); @Override public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpServletResponse.SC_UNAUTHORIZED); // 401 Result<Void> result = Result.fail(401, "未登录或登录已过期,请先登录"); response.getWriter().write(objectMapper.writeValueAsString(result)); } }
无权限异常处理器 security/RestAccessDeniedHandler.java
java
运行
package com.example.jobportal.security; import com.example.jobportal.common.Result; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; import org.springframework.stereotype.Component; import java.io.IOException; /** * 处理已登录但无权限的场景 * 对应169集:自定义Security错误 */ @Component public class RestAccessDeniedHandler implements AccessDeniedHandler { private final ObjectMapper objectMapper = new ObjectMapper(); @Override public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException { response.setContentType("application/json;charset=UTF-8"); response.setStatus(HttpServletResponse.SC_FORBIDDEN); // 403 Result<Void> result = Result.fail(403, "权限不足,无法访问该资源"); response.getWriter().write(objectMapper.writeValueAsString(result)); } }
2. 升级安全配置 SecurityConfig.java
整合角色权限规则 + 自定义异常处理器
java
运行
package com.example.jobportal.config; import com.example.jobportal.security.CustomAuthenticationProvider; import com.example.jobportal.security.RestAccessDeniedHandler; import com.example.jobportal.security.RestAuthenticationEntryPoint; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; import org.springframework.web.cors.CorsConfiguration; import org.springframework.web.cors.CorsConfigurationSource; import org.springframework.web.cors.UrlBasedCorsConfigurationSource; import java.util.List; @Configuration @EnableWebSecurity @RequiredArgsConstructor public class SecurityConfig { private final CustomAuthenticationProvider customAuthenticationProvider; private final RestAuthenticationEntryPoint authenticationEntryPoint; private final RestAccessDeniedHandler accessDeniedHandler; @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .csrf(csrf -> csrf.disable()) .cors(cors -> cors.configurationSource(corsConfigurationSource())) // ========== 授权规则(对应168集:hasRole保护接口) ========== .authorizeHttpRequests(auth -> auth .requestMatchers("/api/auth/register", "/api/public/**").permitAll() // 管理员接口:必须有ADMIN角色 .requestMatchers("/api/admin/**").hasRole("ADMIN") // 普通用户接口:USER角色即可 .requestMatchers("/api/user/**").hasAnyRole("USER", "ADMIN") .anyRequest().authenticated() ) // ========== 自定义异常处理(对应169集) ========== .exceptionHandling(ex -> ex .authenticationEntryPoint(authenticationEntryPoint) // 未登录 .accessDeniedHandler(accessDeniedHandler) // 无权限 ) .formLogin(form -> form.disable()) .httpBasic(basic -> {}); http.authenticationProvider(customAuthenticationProvider); return http.build(); } @Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } @Bean public CorsConfigurationSource corsConfigurationSource() { CorsConfiguration config = new CorsConfiguration(); config.setAllowedOriginPatterns(List.of("*")); config.setAllowedHeaders(List.of("*")); config.setAllowedMethods(List.of("*")); config.setAllowCredentials(true); UrlBasedCorsConfigurationSource source = new UrlBasedCorsConfigurationSource(); source.registerCorsConfiguration("/**", config); return source; } }
三、JPA 排序(对应 170~171 集)
Spring Data JPA 内置排序能力,无需手写 SQL,两种常用方式:
- 方法参数传入
Sort对象 - 分页时通过
Pageable携带排序规则
排序接口示例
java
运行
package com.example.jobportal.controller; import com.example.jobportal.entity.Job; import com.example.jobportal.repository.JobRepository; import lombok.RequiredArgsConstructor; import org.springframework.data.domain.Sort; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController @RequestMapping("/api/jobs") @RequiredArgsConstructor public class JobController { private final JobRepository jobRepository; /** * 全量岗位列表 + 排序 * 对应170、171集:JPA Sort排序 * @param sortField 排序字段,如 salary、id * @param sortType 排序方向:asc升序 / desc降序 */ @GetMapping("/list") public List<Job> getJobList( @RequestParam(defaultValue = "id") String sortField, @RequestParam(defaultValue = "desc") String sortType) { // 构建排序对象 Sort sort = sortType.equalsIgnoreCase("asc") ? Sort.by(sortField).ascending() : Sort.by(sortField).descending(); // 多字段排序示例:先按薪资降序,再按id升序 // Sort multiSort = Sort.by(Sort.Order.desc("salary"), Sort.Order.asc("id")); return jobRepository.findAll(sort); } }
最佳实践:排序字段必须做白名单校验,禁止前端传任意字段,防止 SQL 注入风险。
四、JPA 分页 + 排序结合(对应 172~173 集)
1. 通用分页结果封装
java
运行
package com.example.jobportal.common; import lombok.Data; import org.springframework.data.domain.Page; import java.util.List; /** * 统一分页返回结果 */ @Data public class PageResult<T> { private List<T> list; // 当前页数据 private long total; // 总条数 private int pageNum; // 当前页码 private int pageSize; // 每页大小 private int totalPages; // 总页数 public static <T> PageResult<T> of(Page<T> page) { PageResult<T> result = new PageResult<>(); result.setList(page.getContent()); result.setTotal(page.getTotalElements()); result.setPageNum(page.getNumber()); result.setPageSize(page.getSize()); result.setTotalPages(page.getTotalPages()); return result; } }
2. 分页接口(带排序、带条件)
java
运行
import org.springframework.data.domain.Page; import org.springframework.data.domain.PageRequest; import org.springframework.data.domain.Pageable; // 在 JobController 中新增 @GetMapping("/page") public PageResult<Job> getJobPage( @RequestParam(defaultValue = "0") int pageNum, @RequestParam(defaultValue = "10") int pageSize, @RequestParam(defaultValue = "id") String sortField, @RequestParam(defaultValue = "desc") String sortType, @RequestParam(required = false) String location) { // 构建排序 Sort sort = sortType.equalsIgnoreCase("asc") ? Sort.by(sortField).ascending() : Sort.by(sortField).descending(); // 构建分页对象:页码、每页大小、排序 Pageable pageable = PageRequest.of(pageNum, pageSize, sort); // 执行分页查询 Page<Job> page; if (location != null && !location.isBlank()) { // 带条件分页 page = jobRepository.findByLocation(location, pageable); } else { // 全量分页 page = jobRepository.findAll(pageable); } return PageResult.of(page); }
3. Repository 带条件分页方法
java
运行
// JobRepository 中新增 Page<Job> findByLocation(String location, Pageable pageable);
说明:
- 页码默认从 0 开始,前端如果习惯从 1 开始,后端要做 -1 处理
- 生产环境建议限制最大 pageSize,防止一次性查几万条拖垮数据库
五、更新留言状态(对应 174 集)
1. 升级 Contact 实体,新增状态字段
java
运行
package com.example.jobportal.entity; import jakarta.persistence.*; import lombok.Data; import org.springframework.data.annotation.CreatedBy; import org.springframework.data.annotation.CreatedDate; import org.springframework.data.annotation.LastModifiedBy; import org.springframework.data.annotation.LastModifiedDate; import org.springframework.data.jpa.domain.support.AuditingEntityListener; import java.time.LocalDateTime; @Data @Entity @Table(name = "contact_messages") @EntityListeners(AuditingEntityListener.class) public class Contact { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; @Column(columnDefinition = "TEXT") private String message; // 留言状态:NEW-新提交,PROCESSING-处理中,CLOSED-已关闭 private String status = "NEW"; @CreatedDate @Column(updatable = false) private LocalDateTime createdAt; @CreatedBy @Column(updatable = false) private String createdBy; @LastModifiedDate private LocalDateTime updatedAt; @LastModifiedBy private String updatedBy; }
2. Service 层新增更新状态方法
java
运行
// ContactService 中新增 public Contact updateStatus(Long id, String status) { Contact contact = contactRepository.findById(id) .orElseThrow(() -> new BusinessException("留言记录不存在")); // 状态合法性校验 if (!List.of("NEW", "PROCESSING", "CLOSED").contains(status)) { throw new BusinessException("非法的状态值"); } contact.setStatus(status); return contactRepository.save(contact); }
3. Controller 新增更新接口
java
运行
// ContactController 中新增 @PutMapping("/{id}/status") @Operation(summary = "更新留言状态", description = "管理员修改留言处理状态") public Result<Contact> updateStatus(@PathVariable Long id, @RequestParam String status) { Contact contact = contactService.updateStatus(id, status); return Result.success(contact); }
六、课程对应表
表格
| 集数 | 核心内容 | 对应代码 |
|---|---|---|
| 164 | 章节概览:授权与数据处理 | 全文整体结构 |
| 165 | 认证 vs 授权概念 | 前置概念说明 |
| 166 | GrantedAuthority 内部存储机制 | 前置概念 + 用户认证逻辑 |
| 167 | Role 与 Authority 选型 | 概念对比表 |
| 168 | hasRole 接口鉴权实战 | SecurityConfig 权限规则 |
| 169 | 自定义安全异常响应 | 两个异常处理器 + 配置 |
| 170、171 | JPA Sort 排序原理与实战 | JobController 排序接口 |
| 172、173 | JPA 分页原理与实战 | PageResult + 分页接口 |
| 174 | 更新留言状态 | Contact 实体 + 更新接口 |
七、最佳实践总结
授权部分
- 优先用角色做粗粒度控制,细粒度权限再引入权限点
- 安全异常必须统一 JSON 格式,和业务接口响应保持一致,方便前端处理
- 权限配置集中管理,不要散落在各个业务代码里
分页排序部分
- 排序字段做白名单校验,禁止直接透传前端字段
- 限制最大每页条数,默认值合理设置
- 大数据量场景不建议用 offset 分页,性能较差,可考虑游标分页
- 分页查询优先走索引字段排序,避免全表排序

浙公网安备 33010602011771号