大家好,我是程序员柏风,AI 最全的 Vibecoding 学习地图 http://coderoadmap.cn 作者。
授权模型
这一课你将能做到
- 实现 RBAC(基于角色的访问控制),设计用户-角色-权限三层模型
- 理解 ABAC(基于属性的访问控制)和 ReBAC(基于关系的访问控制)的适用场景
- 在 Spring Security 中配置方法级和 URL 级权限控制
为什么要学
认证解决「你是谁」,授权解决「你能做什么」。一个系统里,管理员能删课程、讲师能改课程内容、学员只能看——这就是授权。授权模型选错,要么权限漏洞导致越权,要么设计过度复杂拖慢开发。
先建立图像
RBAC 模型(最常用):
┌────────┐ ┌────────┐ ┌────────┐
│ 用户 │ ───► │ 角色 │ ───► │ 权限 │
│ User │ │ Role │ │Permission│
└────────┘ └────────┘ └────────┘
张三 管理员 课程:删除
李四 讲师 课程:编辑
王五 学员 课程:查看
ABAC 模型(更灵活):
用户属性(部门=技术部, 级别=P7) + 资源属性(密级=机密) + 环境属性(时间=工作日)
│
▼
策略引擎判断:技术部 P7 以上 + 工作日 → 允许访问机密文件
ReBAC 模型(社交关系):
用户A ──关注──► 用户B ──发布──► 文章X
│
└── 因为关注关系,A 可以查看 B 的文章
跟着做
步骤 1:数据库设计(RBAC)
-- 用户表
CREATE TABLE users (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
username VARCHAR(50) UNIQUE NOT NULL,
password VARCHAR(100) NOT NULL,
email VARCHAR(100)
);
-- 角色表
CREATE TABLE roles (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
code VARCHAR(50) UNIQUE NOT NULL, -- ADMIN, INSTRUCTOR, STUDENT
name VARCHAR(100) NOT NULL
);
-- 权限表
CREATE TABLE permissions (
id BIGINT PRIMARY KEY AUTO_INCREMENT,
code VARCHAR(100) UNIQUE NOT NULL, -- lesson:read, lesson:write, lesson:delete
name VARCHAR(100) NOT NULL,
resource VARCHAR(50), -- lesson, submission, user
action VARCHAR(50) -- read, write, delete
);
-- 用户-角色关联
CREATE TABLE user_roles (
user_id BIGINT,
role_id BIGINT,
PRIMARY KEY (user_id, role_id)
);
-- 角色-权限关联
CREATE TABLE role_permissions (
role_id BIGINT,
permission_id BIGINT,
PRIMARY KEY (role_id, permission_id)
);
步骤 2:实体类
@Entity
@Table(name = "users")
public class User {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String username;
private String password;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "user_roles",
joinColumns = @JoinColumn(name = "user_id"),
inverseJoinColumns = @JoinColumn(name = "role_id"))
private Set<Role> roles = new HashSet<>();
}
@Entity
@Table(name = "roles")
public class Role {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
private String name;
@ManyToMany(fetch = FetchType.EAGER)
@JoinTable(name = "role_permissions",
joinColumns = @JoinColumn(name = "role_id"),
inverseJoinColumns = @JoinColumn(name = "permission_id"))
private Set<Permission> permissions = new HashSet<>();
}
@Entity
@Table(name = "permissions")
public class Permission {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
private String code;
private String name;
private String resource;
private String action;
}
步骤 3:Spring Security 配置
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable())
.sessionManagement(session -> session.sessionCreationPolicy(SessionCreationPolicy.STATELESS))
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/**").permitAll()
.requestMatchers(HttpMethod.GET, "/api/lessons/**").permitAll()
.requestMatchers(HttpMethod.POST, "/api/lessons").hasRole("ADMIN")
.requestMatchers(HttpMethod.PUT, "/api/lessons/**").hasAnyRole("ADMIN", "INSTRUCTOR")
.requestMatchers(HttpMethod.DELETE, "/api/lessons/**").hasRole("ADMIN")
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.anyRequest().authenticated()
)
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class);
return http.build();
}
}
步骤 4:方法级权限控制
@RestController
@RequestMapping("/api/lessons")
public class LessonController {
@GetMapping
public ApiResponse<List<Lesson>> list() {
// 任何人都能看
return ApiResponse.success(lessonService.findAll());
}
@PostMapping
@PreAuthorize("hasRole('ADMIN')")
public ApiResponse<Lesson> create(@RequestBody @Valid LessonRequest request) {
// 只有管理员能创建
return ApiResponse.success(lessonService.create(request));
}
@PutMapping("/{id}")
@PreAuthorize("hasAnyRole('ADMIN', 'INSTRUCTOR')")
public ApiResponse<Lesson> update(@PathVariable Long id,
@RequestBody @Valid LessonRequest request) {
// 管理员和讲师都能改
return ApiResponse.success(lessonService.update(id, request));
}
@DeleteMapping("/{id}")
@PreAuthorize("hasRole('ADMIN')")
public ApiResponse<Void> delete(@PathVariable Long id) {
// 只有管理员能删
lessonService.delete(id);
return ApiResponse.success(null);
}
@GetMapping("/{id}/submissions")
@PreAuthorize("hasAnyRole('ADMIN', 'INSTRUCTOR') or @submissionService.isOwner(#id, authentication.name)")
public ApiResponse<List<Submission>> getSubmissions(@PathVariable Long id,
Authentication authentication) {
// 管理员、讲师、或提交者本人能看
return ApiResponse.success(submissionService.findByLessonId(id));
}
}
步骤 5:自定义权限评估器(处理复杂逻辑)
@Component("lessonSecurity")
public class LessonSecurityService {
public boolean canEdit(Long lessonId, Authentication authentication) {
// 管理员直接放行
if (authentication.getAuthorities().stream()
.anyMatch(a -> a.getAuthority().equals("ROLE_ADMIN"))) {
return true;
}
// 讲师只能改自己负责的课程
Lesson lesson = lessonRepository.findById(lessonId).orElse(null);
if (lesson == null) {
return false;
}
return lesson.getInstructorId().equals(getCurrentUserId(authentication));
}
public boolean canViewSubmissions(Long lessonId, Authentication authentication) {
// 同 canEdit,或增加「已购买该课程」的判断
return canEdit(lessonId, authentication);
}
}
@PutMapping("/{id}")
@PreAuthorize("@lessonSecurity.canEdit(#id, authentication)")
public ApiResponse<Lesson> update(@PathVariable Long id,
@RequestBody @Valid LessonRequest request) {
return ApiResponse.success(lessonService.update(id, request));
}
步骤 6:数据级权限(行级安全)
// 只返回当前用户有权限的数据
@Query("SELECT l FROM Lesson l WHERE " +
"(:isAdmin = true) OR " +
"(l.visibility = 'PUBLIC') OR " +
"(l.instructorId = :userId)")
List<Lesson> findVisibleLessons(@Param("userId") Long userId,
@Param("isAdmin") boolean isAdmin);
讲清楚要点
- RBAC 是默认选择:用户-角色-权限三层,覆盖 80% 的场景。角色是权限的集合,用户通过角色获得权限。好处是简单直观,好维护。
- ABAC 适合复杂策略:当权限取决于「时间、地点、数据属性、用户属性」的组合时,RBAC 力不从心。比如「工作日的 9-18 点,技术部 P7 以上可以访问生产环境日志」。ABAC 用策略语言(如 XACML)表达规则。
- ReBAC 适合社交网络:权限取决于「关系」,比如「好友可见」「关注后才能看」。微信、微博都用 ReBAC。
- 权限控制分三层:URL 级(粗粒度,如
/api/admin/**需要 ADMIN)、方法级(中粒度,如@PreAuthorize)、数据级(细粒度,如只返回自己的数据)。三层结合最安全。 - 最小权限原则:默认拒绝,按需开通。新用户注册后没有任何权限,由管理员分配角色。不要给「所有人都能看」的默认权限。
你可能会懵的地方
-
RBAC 角色和权限有什么区别? 角色是「职位」(如管理员、讲师),权限是「动作」(如删除课程、编辑课程)。角色是权限的集合。好处:给用户分配角色比逐个分配权限简单。
-
什么时候用 ABAC? 当权限规则涉及「动态属性」时。比如「只能修改自己创建的课程」——RBAC 做不到(课程创建者不是固定角色),需要 ABAC(资源创建者 == 当前用户)。
-
Spring Security 的 hasRole 和 hasAuthority 区别?
hasRole('ADMIN')自动加ROLE_前缀,匹配ROLE_ADMIN;hasAuthority('ADMIN')直接匹配ADMIN。建议统一用hasAuthority,更灵活。 -
数据级权限怎么做? 方案一:SQL 里加条件(如上面的
@Query);方案二:MyBatis 拦截器自动拼接条件;方案三:数据库视图或行级安全策略(如 PostgreSQL RLS)。推荐方案一,简单可控。 -
微服务架构下授权怎么做? 方案一:每个服务自己校验(去中心化,性能好但规则分散);方案二:统一授权服务(如 OPA、Casbin,集中管理但增加调用);方案三:网关层粗粒度 + 服务内细粒度。推荐方案三。
错法对照
| 错法 | 问题 | 正解 |
|---|---|---|
| 前端隐藏按钮就当权限控制了 | 前端可被绕过,直接调 API | 后端必须校验权限 |
| 所有权限都写成硬编码 if-else | 新增权限要改代码,难维护 | 数据库存权限,动态加载 |
| 用户直接关联权限,不通过角色 | 权限分配太琐碎,难管理 | RBAC 三层模型 |
| 只控制 URL,不控制方法 | GET /api/lessons 公开,POST /api/lessons 也要公开? | 按 HTTP 方法区分权限 |
| 管理员权限给所有运营人员 | 误操作风险大 | 拆分角色:内容管理员、用户管理员、超级管理员 |
卡点急救
@PreAuthorize不生效:检查是否加了@EnableMethodSecurity(Spring Boot 2.x 是@EnableGlobalMethodSecurity),以及方法是否是 public。- 权限不足返回 403 但不知道原因:开启 Spring Security 调试日志
logging.level.org.springframework.security=DEBUG。 - 多角色用户权限混乱:设计角色时考虑「权限并集」,一个用户有多个角色时,权限是叠加的(除非显式做互斥设计)。
面试常问
-
RBAC 和 ABAC 的区别? RBAC 基于角色(静态),ABAC 基于属性(动态)。RBAC 简单,适合组织架构稳定的系统;ABAC 灵活,适合规则复杂、属性多变的场景。能各举一个实际场景。
-
怎么实现「只能修改自己创建的资源」? 方案一:Service 层判断
resource.creatorId == currentUserId;方案二:自定义@PreAuthorize表达式 + 权限评估器;方案三:数据库行级安全。能讲出优缺点。 -
Spring Security 的授权流程? Filter 链 → SecurityContextHolder 取 Authentication → AccessDecisionManager 决策 → 通过/拒绝。能讲出
FilterSecurityInterceptor、MethodSecurityInterceptor的区别。
自测三问
- 设计一个「在线文档协作系统」的权限模型,要求支持「公开可读、邀请可编辑、仅自己可见」三种分享模式。
- 如果角色有 100 个,每个角色平均 20 个权限,数据库查询会有什么问题?怎么优化?
- 什么时候需要在 API 响应里做「字段级权限」(如普通用户看不到手机号,管理员能看到)?
小结一句话
授权的核心是「最小权限 + 分层控制」:RBAC 管角色,ABAC 管策略,ReBAC 管关系;URL 级、方法级、数据级三层防护缺一不可。
延伸
- Spring Security 授权文档
- OPA(Open Policy Agent)
- 下一课:API 安全——OWASP API Top 10 与防护实战。