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,两种常用方式:
  1. 方法参数传入 Sort 对象
  2. 分页时通过 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 实体 + 更新接口

七、最佳实践总结

授权部分

  1. 优先用角色做粗粒度控制,细粒度权限再引入权限点
  2. 安全异常必须统一 JSON 格式,和业务接口响应保持一致,方便前端处理
  3. 权限配置集中管理,不要散落在各个业务代码里

分页排序部分

  1. 排序字段做白名单校验,禁止直接透传前端字段
  2. 限制最大每页条数,默认值合理设置
  3. 大数据量场景不建议用 offset 分页,性能较差,可考虑游标分页
  4. 分页查询优先走索引字段排序,避免全表排序
posted @ 2026-08-01 01:59  漫漫长路</>  阅读(4)  评论(0)    收藏  举报