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

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

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

学习

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

更多

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

© 2026 CodeRoadMap

津ICP备2026012044号-1·津公网安备12011002026155号·coderoadmap@126.com
API 设计实战入门/课时/认证
RawBlame
课时目录12 节

API 设计实战入门

  1. 01API 基础
  2. 02HTTP 协议层
  3. 03REST 资源设计
  4. 04错误处理
  5. 05认证
  6. 06授权模型
  7. 07API 安全
  8. 08API 风格
  9. 09架构与集成
  10. 10测试
  11. 11可观测与合规
  12. 12文档与工具

认证

JWT、OAuth2 与 API Key。

30 分钟

认证

这一课你将能做到

  1. 实现基于 JWT 的无状态认证,理解 Token 的生成、校验、刷新全流程
  2. 说清楚 OAuth2 的四种授权模式,知道什么场景用哪种
  3. 设计 API Key 机制,支持第三方系统安全调用

为什么要学

认证是 API 的大门。没有认证,任何人都能调你的接口;认证做得差,用户会被盗号、数据会泄露。面试必考:JWT 结构?OAuth2 流程?怎么防止 Token 泄露?

先建立图像

认证方式对比:

┌─────────────┐     ┌─────────────┐     ┌─────────────┐
│   Session   │     │     JWT     │     │   API Key   │
│   + Cookie  │     │   (无状态)   │     │  (长期有效)  │
└──────┬──────┘     └──────┬──────┘     └──────┬──────┘
       │                   │                   │
  服务器存会话          自包含,不存          简单字符串
  水平扩展需共享        可直接校验          需定期轮换
  适合浏览器            适合 App/SPA        适合机器调用

JWT 结构:
xxxxx.yyyyy.zzzzz
│     │     │
│     │     └── Signature(签名,防篡改)
│     └──────── Payload(载荷,用户ID、过期时间等)
└────────────── Header(算法、类型)

跟着做

步骤 1:引入 JWT 依赖

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-api</artifactId>
    <version>0.11.5</version>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-impl</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>
<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt-jackson</artifactId>
    <version>0.11.5</version>
    <scope>runtime</scope>
</dependency>

步骤 2:JWT 工具类

@Component
public class JwtTokenProvider {

    private final String secret;
    private final long accessTokenValidity;
    private final long refreshTokenValidity;

    public JwtTokenProvider(@Value("${jwt.secret}") String secret,
                           @Value("${jwt.access-token-validity}") long accessTokenValidity,
                           @Value("${jwt.refresh-token-validity}") long refreshTokenValidity) {
        this.secret = Base64.getEncoder().encodeToString(secret.getBytes());
        this.accessTokenValidity = accessTokenValidity;
        this.refreshTokenValidity = refreshTokenValidity;
    }

    public String createAccessToken(Long userId, String username, List<String> roles) {
        Date now = new Date();
        Date validity = new Date(now.getTime() + accessTokenValidity);

        return Jwts.builder()
                .setSubject(userId.toString())
                .claim("username", username)
                .claim("roles", roles)
                .setIssuedAt(now)
                .setExpiration(validity)
                .signWith(Keys.hmacShaKeyFor(secret.getBytes()))
                .compact();
    }

    public String createRefreshToken(Long userId) {
        Date now = new Date();
        Date validity = new Date(now.getTime() + refreshTokenValidity);

        return Jwts.builder()
                .setSubject(userId.toString())
                .setIssuedAt(now)
                .setExpiration(validity)
                .signWith(Keys.hmacShaKeyFor(secret.getBytes()))
                .compact();
    }

    public boolean validateToken(String token) {
        try {
            Jwts.parserBuilder()
                    .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
                    .build()
                    .parseClaimsJws(token);
            return true;
        } catch (JwtException | IllegalArgumentException e) {
            return false;
        }
    }

    public Long getUserId(String token) {
        Claims claims = Jwts.parserBuilder()
                .setSigningKey(Keys.hmacShaKeyFor(secret.getBytes()))
                .build()
                .parseClaimsJws(token)
                .getBody();
        return Long.parseLong(claims.getSubject());
    }
}

步骤 3:登录接口颁发 Token

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final JwtTokenProvider tokenProvider;
    private final UserService userService;

    @PostMapping("/login")
    public ApiResponse<TokenResponse> login(@RequestBody @Valid LoginRequest request) {
        User user = userService.authenticate(request.getUsername(), request.getPassword());
        if (user == null) {
            throw new BusinessException(401, "用户名或密码错误");
        }

        String accessToken = tokenProvider.createAccessToken(
                user.getId(), user.getUsername(), user.getRoles());
        String refreshToken = tokenProvider.createRefreshToken(user.getId());

        return ApiResponse.success(new TokenResponse(accessToken, refreshToken, "Bearer"));
    }

    @PostMapping("/refresh")
    public ApiResponse<TokenResponse> refresh(@RequestBody @Valid RefreshRequest request) {
        if (!tokenProvider.validateToken(request.getRefreshToken())) {
            throw new BusinessException(401, "Refresh Token 无效或已过期");
        }

        Long userId = tokenProvider.getUserId(request.getRefreshToken());
        User user = userService.findById(userId);

        String newAccessToken = tokenProvider.createAccessToken(
                user.getId(), user.getUsername(), user.getRoles());
        String newRefreshToken = tokenProvider.createRefreshToken(user.getId());

        return ApiResponse.success(new TokenResponse(newAccessToken, newRefreshToken, "Bearer"));
    }
}

步骤 4:JWT 认证过滤器

@Component
public class JwtAuthenticationFilter extends OncePerRequestFilter {

    private final JwtTokenProvider tokenProvider;

    @Override
    protected void doFilterInternal(HttpServletRequest request,
                                   HttpServletResponse response,
                                   FilterChain filterChain) throws ServletException, IOException {
        String token = resolveToken(request);
        if (token != null && tokenProvider.validateToken(token)) {
            Long userId = tokenProvider.getUserId(token);
            // 加载用户详情,构建 Authentication
            UserDetails userDetails = userDetailsService.loadUserById(userId);
            UsernamePasswordAuthenticationToken auth =
                    new UsernamePasswordAuthenticationToken(userDetails, null, userDetails.getAuthorities());
            SecurityContextHolder.getContext().setAuthentication(auth);
        }
        filterChain.doFilter(request, response);
    }

    private String resolveToken(HttpServletRequest request) {
        String bearerToken = request.getHeader("Authorization");
        if (bearerToken != null && bearerToken.startsWith("Bearer ")) {
            return bearerToken.substring(7);
        }
        return null;
    }
}

步骤 5:API Key 机制

@RestController
@RequestMapping("/api/v1")
public class ApiKeyController {

    @GetMapping("/data")
    public ApiResponse<Data> getData(
            @RequestHeader("X-API-Key") String apiKey) {
        ApiClient client = apiClientService.validate(apiKey);
        if (client == null) {
            throw new BusinessException(401, "无效的 API Key");
        }
        if (!client.hasPermission("read:data")) {
            throw new BusinessException(403, "API Key 无此权限");
        }
        return ApiResponse.success(dataService.getData());
    }
}

API Key 设计要点:

  • 格式:prefix_randomstring,如 cf_live_abc123...(便于识别环境)
  • 存储:哈希存储(如 SHA-256),不存明文
  • 权限:每个 Key 绑定权限范围(scope)
  • 过期:支持设置有效期,强制轮换
  • 限流:按 Key 限流,防止滥用

讲清楚要点

  1. JWT 是自包含的:服务器不存会话,Token 里包含用户信息和过期时间,验签即可。优点是水平扩展无压力,缺点是无法主动失效(除非加黑名单或缩短有效期)。
  2. Access Token + Refresh Token 是标配:Access Token 短期(15-30 分钟),用于接口调用;Refresh Token 长期(7-30 天),用于换新 Access Token。这样即使 Access Token 泄露,攻击窗口也很小。
  3. JWT 不加密,只签名:Payload 是 Base64 编码,任何人都能解码看内容。敏感信息(如密码)绝对不能放 JWT 里。签名只防篡改,不防窥探。
  4. OAuth2 是授权框架,不是认证协议:OAuth2 解决「第三方应用怎么安全地访问用户数据」。常见场景:微信登录、GitHub 登录、企业微信授权。
  5. API Key 适合机器对机器:没有用户交互的场景(如定时任务同步数据、第三方 SaaS 调用)。API Key 要配合 HTTPS、IP 白名单、限流使用。

你可能会懵的地方

  • JWT 和 Session 怎么选? JWT:无状态、水平扩展好、适合 App/SPA/微服务;Session:可主动失效、适合传统 Web 应用、需要共享 Session 存储。现代架构推荐 JWT。

  • JWT 过期了怎么办? Access Token 过期:用 Refresh Token 调 /api/auth/refresh 换新。Refresh Token 过期:用户重新登录。

  • 怎么防止 JWT 被盗用? HTTPS 传输、短有效期、HttpOnly Cookie 存储(防 XSS)、绑定 IP 或设备指纹(增加盗用难度)、异常检测(如同一账号多地登录)。

  • OAuth2 四种模式? 授权码模式(Authorization Code):最安全,适合有后端的 Web 应用。简化模式(Implicit):已废弃,被授权码 + PKCE 取代。密码模式(Resource Owner Password):用户把账号密码给第三方,仅限可信应用。客户端模式(Client Credentials):机器对机器,无用户参与。

  • API Key 和 JWT 能一起用吗? 可以。API Key 标识「哪个应用在调用」,JWT 标识「哪个用户在操作」。比如开放平台:先用 API Key 认证应用,再用 JWT 认证用户。

错法对照

错法问题正解
JWT 里存用户密码Base64 可解码,密码泄露只存非敏感信息(id、username、roles)
Access Token 有效期设 7 天泄露后长期可用Access Token 15-30 分钟,Refresh Token 7-30 天
API Key 明文存数据库数据库泄露 = 所有 Key 泄露哈希存储(SHA-256 + Salt)
登录接口不限流暴力破解密码登录接口限流(如 5 次/分钟/账号)
Token 放 URL 参数会被日志、浏览器历史记录放 Authorization Header

卡点急救

  1. Token 解析失败:检查 secret 是否一致(编码问题)、Token 是否过期、时钟是否同步(分布式系统)。
  2. 跨域时 Authorization Header 丢失:CORS 配置没允许 Authorization header,或预检请求没通过。
  3. Refresh Token 被重复使用:实现 Refresh Token 轮换(每次使用都换新,旧 Token 立即失效),检测到旧 Token 复用则全部作废(可能被盗)。

面试常问

  • JWT 的结构和原理? Header(算法)、Payload(声明)、Signature(签名)。服务器用密钥签名,客户端带 Token 请求,服务器验签确认身份。能讲出优缺点:无状态、可扩展 vs 无法主动失效、体积较大。

  • OAuth2 授权码模式流程?

    1. 用户点击「微信登录」→ 跳转微信授权页;2. 用户同意 → 微信回调带 code;3. 后端用 code + secret 换 access_token;4. 用 access_token 拉取用户信息;5. 生成本系统 JWT 返回。能画时序图。
  • 怎么设计一个安全的 API Key 系统? 生成:随机字符串 + 前缀;存储:哈希;权限:scope 控制;使用:HTTPS + Header;轮换:定期更换;监控:异常调用告警。

自测三问

  1. 如果 Access Token 被 XSS 攻击窃取,怎么把损失降到最低?
  2. 设计一个第三方天气 API 的认证方案,要求支持不同权限等级(免费/付费)。
  3. 为什么 OAuth2 的授权码模式比简化模式更安全?

小结一句话

认证的核心是「证明你是谁」:JWT 适合无状态分布式系统,OAuth2 解决第三方授权,API Key 服务机器调用;Access Token 短期 + Refresh Token 长期是行业标准。

延伸

  • JWT 官方介绍
  • OAuth2 RFC 6749
  • 下一课:授权模型——RBAC、ABAC 与权限设计。
加载本节练习…
← 上一课错误处理下一课 →授权模型