CodeRoad Map 程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。
由 程序员柏风 独立维护 · 关于作者
学习 更多
身份与访问 IAM、RBAC 与 Zero Trust。
身份与访问
这一课你将能做到
准确区分认证(AuthN)与授权(AuthZ),并解释 RBAC 与最小权限原则
用 Spring Security 实现一套 RBAC:JWT 登录、角色注解鉴权、方法级权限控制
说出 Zero Trust 的三个核心假设,并能给本站(Nginx + Spring Boot + MySQL + Redis)设计最小权限账号体系
为什么要学
「你是谁」和「你能干什么」是两件事,但无数系统把它们混为一谈:登录成功就放行一切,或者只在菜单上隐藏按钮、后端接口裸奔——这就是越权漏洞(IDOR/水平越权/垂直越权)的温床,常年霸榜 OWASP。
人话:AuthN 是门卫查身份证(确认你是张三);AuthZ 是门禁系统查你的卡能刷哪几层楼(张三只能去 3 楼)。最危险的 bug 不是「没登录能访问」,而是「登录了 A 账号能改 B 的数据」——后端没校验资源归属。
先建立图像
一次请求的身份与权限旅程:
客户端 服务端
│ POST /login (账号密码)
│ ──────────────────────→ AuthN: 校验 BCrypt 密码
│ │
│ ←──── JWT (含 userId, roles)
│
│ GET /api/admin/users
│ Header: Authorization: Bearer <JWT>
│ ──────────────────────→ ① 验签 JWT(身份有效)
│ ② 提取 roles
│ ③ AuthZ: 该接口要求 ADMIN
│ ④ 方法内校验: 资源属于该用户?
│ │
│ ←──── 200 / 403 / 401
RBAC 模型(本站简化版):
用户表 user 角色表 role 权限表 permission
┌────────┐ ┌──────────┐ ┌─────────────┐
│ id │──┐ │ id │──┐ │ code │
│ name │ │ │ code │ │ │ track:read │
└────────┘ │ └──────────┘ │ │ track:write │
│ │ │ admin:* │
user_role ──────────────────┘ └─────────────┘
(多对多中间表) role_permission (多对多)
跟着做
步骤 1:JWT 登录签发
@Service
public class AuthService {
private final JwtEncoder jwtEncoder; // Spring Security 提供
private final BCryptPasswordEncoder encoder = new BCryptPasswordEncoder(12);
public String login(String username, String rawPassword) {
User user = userRepository.findByUsername(username)
.orElseThrow(() -> new BadCredentialsException("账号或密码错误"));
// 注意:报错不区分「账号不存在」和「密码错误」,防账号枚举
if (!encoder.matches(rawPassword, user.getPasswordHash())) {
throw new BadCredentialsException("账号或密码错误");
}
Instant now = Instant.now();
JwtClaimsSet claims = JwtClaimsSet.builder()
.subject(user.getId().toString())
.claim("roles", user.getRoleCodes()) // ["USER","ADMIN"]
.issuedAt(now)
.expiresAt(now.plus(30, ChronoUnit.MINUTES)) // 短 TTL
.build();
return jwtEncoder.encode(JwtEncoderParameters.from(claims)).getTokenValue();
}
}
步骤 2:SecurityConfig 配置鉴权规则 @Configuration
@EnableWebSecurity
@EnableMethodSecurity // 开启 @PreAuthorize 方法级鉴权
public class SecurityConfig {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http
.csrf(csrf -> csrf.disable()) // 纯 JWT 无 Cookie 可关 CSRF
.sessionManagement(s ->
s.sessionCreationPolicy(STATELESS)) // 无状态,不建 Session
.authorizeHttpRequests(auth -> auth
.requestMatchers("/api/auth/login").permitAll()
.requestMatchers("/api/admin/**").hasRole("ADMIN")
.requestMatchers(HttpMethod.GET, "/api/tracks/**").permitAll()
.anyRequest().authenticated()
)
.oauth2ResourceServer(o -> o.jwt(withDefaults()));
return http.build();
}
}
步骤 3:方法级 + 资源归属校验(防越权关键) @RestController
@RequestMapping("/api/progress")
public class ProgressController {
// 方法级:必须是 USER 角色
@PreAuthorize("hasRole('USER')")
@PostMapping("/{trackId}/complete")
public void complete(@PathVariable Long trackId, Authentication auth) {
Long currentUserId = Long.valueOf(auth.getName());
// 资源归属:进度记录只能写到自己名下
// 千万不要从请求体里读 userId 然后直接用!
progressService.markComplete(currentUserId, trackId);
}
@PreAuthorize("hasRole('USER')")
@GetMapping("/my")
public List<Progress> myProgress(Authentication auth) {
Long currentUserId = Long.valueOf(auth.getName());
// 查询范围锁定当前用户,杜绝 ?userId=别人 的水平越权
return progressService.findByUserId(currentUserId);
}
}
步骤 4:数据库与基础设施的最小权限 -- 错误:应用直接用 root
-- 正确:应用账号只有需要的库、需要的操作
CREATE USER 'app'@'%' IDENTIFIED BY '强随机密码';
GRANT SELECT, INSERT, UPDATE, DELETE ON coderoadmap.* TO 'app'@'%';
-- 不给 DROP、不给 FILE、不给 SUPER
-- 再单独建个只读账号给数据分析/报表用
CREATE USER 'app_readonly'@'%' IDENTIFIED BY '另一个强密码';
GRANT SELECT ON coderoadmap.* TO 'app_readonly'@'%';
# Redis 6+ ACL:应用账号禁掉危险命令
user app on >强密码 ~coderoadmap:* +get +set +del +expire -flushall -flushdb -config -keys
步骤 5:管理后台的二次防护 # 管理接口额外加 IP 白名单,双保险
location /api/admin/ {
allow 203.0.113.0/24; # 办公网出口
deny all;
proxy_pass http://127.0.0.1:8080;
}
讲清楚要点
认证≠授权 :认证回答「你是谁」(密码/短信/JWT 验签),授权回答「你能做什么」(角色/权限/资源归属)。两个都过了请求才能执行,缺一不可。
最小权限原则(Least Privilege) :任何账号(人、应用、服务)只给完成工作所必需的最小权限、最短时间。应用数据库账号不给 DROP;CI 部署密钥不给生产写权限;JWT 不给永不过期。
越权是后端问题 :前端隐藏按钮只是体验,不是安全。所有权限校验必须在服务端做,且每个接口默认拒绝(anyRequest().authenticated() 兜底),而不是默认放行、逐个补 @PreAuthorize。
防水平越权靠「归属校验」 :角色相同的两用户 A、B,A 不能读 B 的数据。做法:资源查询永远以「当前登录人」作为过滤条件,不信任前端传的 userId。
Zero Trust(零信任) :核心三假设——网络内不等于可信(内网也要认证授权)、永不信任持续验证(每次请求都验,不是登录一次管一年)、假设已被攻破(最小权限+微分段控制爆炸半径)。
你可能会懵的地方
Session 和 JWT 选哪个?
单体 + 需要强制下线/踢人:Session(存 Redis)好管。多端 + 微服务 + 无状态扩容:JWT。JWT 的硬伤是「签出去收不回」——用短 TTL(30 分钟)+ refresh token 缓解,或维护黑名单(那就退化成有状态了)。
角色和权限什么区别?
权限是原子动作(track:write),角色是权限的打包(EDITOR 含 track:write、track:read)。直接给用户绑角色,需求变了改角色定义即可,不用改每个用户。
hasRole('ADMIN') 和 hasAuthority('ROLE_ADMIN') 一样吗?
hasRole("X") 内部会拼成 ROLE_X 再比对。混用会导致 403 玄学问题,团队内统一用一种。
接口返回 401 和 403 有什么区别?
401 = 没认证(没登录/token 无效);403 = 认证了但没权限。语义分清,前端才能正确跳登录页还是提示无权限。
内部服务之间要鉴权吗?
Zero Trust 的答案是要。即使都在内网,Spring Boot 调另一个内部服务也应带凭证(mTLS 或内部 token),防止「打进一台等于打进全部」。
错法对照 错法 问题 正解 只在前端隐藏管理菜单 直接调 API 照样通 服务端每个接口鉴权 updateProgress(req.userId, ...) 用前端传的 ID水平越权改任何人数据 从 Authentication 取当前用户 应用连库用 root 被注入后 DROP 全库 专用账号最小授权 JWT 有效期 30 天 泄漏后长期可用 30 分钟 + refresh token 登录报「用户不存在」/「密码错误」 账号枚举攻击 统一报「账号或密码错误」 admin 接口只靠角色 内鬼/被盗号即翻车 角色 + IP 白名单 + 操作审计
卡点急救
明明带了 token 却 401 :按序排查——Authorization: Bearer 前缀拼写和空格;token 是否过期(看 exp);验签密钥和签发密钥是否一致(换环境后常见);Nginx 是否把 Authorization 头吞了(加 proxy_set_header Authorization $http_authorization;)。
登录正常但所有接口 403 :多半是 hasRole("ADMIN") 与权限里存的 ADMIN(没 ROLE_ 前缀)不匹配。打日志看 Authentication.getAuthorities() 的实际值。
跨域请求预检 OPTIONS 被 401 拦 :浏览器先发不带凭证的 OPTIONS 预检。解法:CORS 配置里放行 OPTIONS 并确保 SecurityFilterChain 中 CORS 在鉴权之前生效(http.cors(withDefaults()))。
面试常问
RBAC 是什么?怎么落地?
基于角色的访问控制:用户←多对多→角色←多对多→权限。落地:数据库三张主表两张中间表;Spring Security 中角色放入 Authentication 的 authorities,用 hasRole/@PreAuthorize 在 URL 层和方法层双重校验。好处:权限调整只改角色定义,不动用户。
如何防止越权访问?
分两类。垂直越权(普通用户调管理接口):服务端接口级角色校验 + 默认拒绝。水平越权(改 ID 看别人的数据):资源归属校验——所有查询以当前登录人为过滤条件,不信任客户端传的用户标识;配合不可猜测的 ID(UUID/雪花)做第二层缓解。
什么是 Zero Trust?
零信任架构:默认不信任任何网络位置(内网≠安全),每次访问都认证+授权+加密;最小权限+动态策略;假设系统已被攻破来设计(微分段、持续验证、完整审计)。落地手段:mTLS、微服务间鉴权、SDP、持续风险评估。
自测三问
AuthN 和 AuthZ 各在本站登录流程的哪一步发生?
为什么「前端隐藏按钮」不算权限控制?
应用数据库账号应该被授予哪些权限?哪些绝不能给?
小结一句话 身份与访问的铁律:服务端强制校验、最小权限、资源归属归当前用户——认证解决你是谁,授权决定你能碰什么,两者都在后端做。
延伸