CodeRoadMap
首页路线题库资源库文章社区
CodeRoadMap

程序员的学习成长路线图。内容免费开放,登录后可同步路线图与课时进度。

由 程序员柏风 独立维护 · 关于作者

学习

  • 路线图
  • 课程
  • 面试题
  • 文章
  • 我

更多

  • 资源
  • 社区
  • 关于作者
  • 内容说明

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
DevSecOps 入门/课时/身份与访问
RawBlame
课时目录12 节

DevSecOps 入门

  1. 01DevSecOps 概览
  2. 02网络安全基础
  3. 03密码学
  4. 04身份与访问
  5. 05应用安全
  6. 06威胁建模
  7. 07云安全
  8. 08容器安全
  9. 09供应链安全
  10. 10检测与 SIEM
  11. 11Incident 响应
  12. 12合规与扫描

身份与访问

IAM、RBAC 与 Zero Trust。

30 分钟

身份与访问

这一课你将能做到

  1. 准确区分认证(AuthN)与授权(AuthZ),并解释 RBAC 与最小权限原则
  2. 用 Spring Security 实现一套 RBAC:JWT 登录、角色注解鉴权、方法级权限控制
  3. 说出 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;
}

讲清楚要点

  1. 认证≠授权:认证回答「你是谁」(密码/短信/JWT 验签),授权回答「你能做什么」(角色/权限/资源归属)。两个都过了请求才能执行,缺一不可。

  2. 最小权限原则(Least Privilege):任何账号(人、应用、服务)只给完成工作所必需的最小权限、最短时间。应用数据库账号不给 DROP;CI 部署密钥不给生产写权限;JWT 不给永不过期。

  3. 越权是后端问题:前端隐藏按钮只是体验,不是安全。所有权限校验必须在服务端做,且每个接口默认拒绝(anyRequest().authenticated() 兜底),而不是默认放行、逐个补 @PreAuthorize。

  4. 防水平越权靠「归属校验」:角色相同的两用户 A、B,A 不能读 B 的数据。做法:资源查询永远以「当前登录人」作为过滤条件,不信任前端传的 userId。

  5. 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 白名单 + 操作审计

卡点急救

  1. 明明带了 token 却 401:按序排查——Authorization: Bearer 前缀拼写和空格;token 是否过期(看 exp);验签密钥和签发密钥是否一致(换环境后常见);Nginx 是否把 Authorization 头吞了(加 proxy_set_header Authorization $http_authorization;)。

  2. 登录正常但所有接口 403:多半是 hasRole("ADMIN") 与权限里存的 ADMIN(没 ROLE_ 前缀)不匹配。打日志看 Authentication.getAuthorities() 的实际值。

  3. 跨域请求预检 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、持续风险评估。

自测三问

  1. AuthN 和 AuthZ 各在本站登录流程的哪一步发生?
  2. 为什么「前端隐藏按钮」不算权限控制?
  3. 应用数据库账号应该被授予哪些权限?哪些绝不能给?

小结一句话

身份与访问的铁律:服务端强制校验、最小权限、资源归属归当前用户——认证解决你是谁,授权决定你能碰什么,两者都在后端做。

延伸

  • Spring Security 官方文档
  • 下一课:应用安全——OWASP Top 10 逐条过,用本站代码演示 SQL 注入、XSS、CSRF 的真实攻防。
加载本节练习…
← 上一课密码学下一课 →应用安全