ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

API Key、JWT与OAuth 2.0:后端接口认证方案深度解析与实践指南

API Key、JWT与OAuth 2.0:后端接口认证方案深度解析与实践指南 在实际后端开发和系统集成中接口认证是保障服务安全的第一道防线。无论是微服务间的内部调用还是向第三方开放 API开发者都需要在 API Key、JWT 和 OAuth 等方案中做出选择。很多项目在初期为了快速上线可能随手选了一种但随着业务复杂度和安全要求的提升不恰当的认证机制会带来巨大的维护成本和安全风险。本文旨在彻底厘清这三种主流认证方式的本质区别、适用场景和实现细节帮助开发者在设计系统时做出正确的技术选型并提供一个可落地的、包含完整错误处理的实践示例。1. 核心概念理解认证、授权与凭证的本质在深入具体技术之前必须明确几个基础概念这是后续所有讨论的基石。混淆这些概念是导致方案选型错误和实现漏洞的常见原因。1.1 认证、授权与凭证认证解决的是“你是谁”的问题。系统需要确认请求发起方的身份是否真实可信。例如用户输入用户名和密码登录就是一次认证过程。授权解决的是“你能做什么”的问题。在确认身份后系统需要判断该身份是否拥有执行某项操作的权限。例如普通用户不能访问管理员后台这就是授权控制。凭证是认证和授权过程中用于传递身份和权限信息的载体。密码、API Key、Token 都是凭证的不同形式。凭证本身不产生价值其价值在于它背后所代表的身份和权限以及系统对它的验证逻辑。1.2 三种凭证的定位与核心差异API Key、JWT 和 OAuth 虽然都常被称作“认证方式”但它们的定位和解决的问题层面有显著不同。API Key是一种简单的、长期有效的静态密钥。它本质上是一个“共享密钥”客户端持有它在每次请求时出示通常在 HTTP 头中服务端通过比对预先存储的 Key 来验证请求是否来自合法的调用方。它的核心是身份验证通常不直接包含细粒度的授权信息。API Key 一旦泄露就相当于把“家门钥匙”给了别人风险较高。JWT是一种令牌的格式标准。它定义了一种紧凑的、自包含的、用于在各方之间安全传输信息的 JSON 对象。JWT 的核心价值在于无状态和自包含。服务端签发一个包含身份和声明的 Token 后无需在服务端存储会话信息。后续请求只需验证 Token 的签名是否有效、内容是否被篡改、是否过期即可。JWT 本身是一种优秀的凭证载体常用于实现基于 Token 的认证和授权。OAuth 2.0是一个授权框架它解决的核心问题是“在用户不向第三方提供密码的前提下授权第三方应用访问用户在某服务中的特定资源”。例如用微信登录一个新网站网站并不需要知道你的微信密码。OAuth 定义了角色资源所有者、客户端、授权服务器、资源服务器和一套标准的授权流程授权码模式、隐式模式等。在 OAuth 流程中最终客户端获取到的访问令牌其格式可以是 JWT也可以是其他不透明的令牌。为了更直观地理解可以参考下表特性API KeyJWTOAuth 2.0核心定位静态身份凭证令牌格式标准授权框架主要目的验证调用方身份安全传输声明信息委托授权状态管理服务端需存储/验证 Key无状态自验证授权服务器需管理令牌生命周期典型生命周期长期有效手动轮换短期有效分钟/小时级短期有效可刷新信息承载通常只标识客户端ID可包含身份、权限、自定义声明通过令牌访问用户资源令牌本身可能不透明适用场景服务器到服务器的简单调用、内部服务间通信前后端分离的 Web/App 登录、微服务间身份传递第三方应用集成、单点登录、开放平台理解这个表格是正确选型的第一步。接下来我们将深入每种方案的具体实现和细节。2. API Key简单直接的身份验证方案API Key 是最古老也最直接的认证方式。它的设计哲学是“你知道这个秘密你就是我信任的人”。实现简单但安全性完全依赖于 Key 本身的保密性。2.1 工作原理与典型流程生成与分发服务端为每个客户端如一个合作方、一个内部服务生成一个唯一的、高熵值的字符串作为 API Key。这个 Key 和客户端的元信息如名称、权限、限流额度被存储在服务端的数据库或缓存中。客户端使用客户端在调用 API 时必须将这个 API Key 包含在请求中。最常见的做法是放在 HTTP 请求头里例如X-API-Key: your_api_key_here或遵循更通用的Authorization: Bearer your_api_key_here格式。服务端验证服务端接收到请求后从指定请求头中提取 API Key然后在自己的存储中查找。验证通常包括检查 Key 是否存在。检查 Key 是否已启用/未过期。可选检查 Key 关联的权限是否允许当前请求的操作。可选进行限流检查。2.2 实现示例Spring Boot 拦截器验证 API Key以下是一个使用 Spring Boot 实现 API Key 验证的简单示例。我们通过一个自定义拦截器来集中处理认证逻辑。首先定义 API Key 的存储实体和 Repository这里使用 Spring Data JPA 示例// 实体类 Entity public class ApiClient { Id GeneratedValue(strategy GenerationType.UUID) private String id; private String clientName; Column(unique true) private String apiKey; // 存储哈希值而非明文 private boolean enabled true; private LocalDateTime expiresAt; private String permissions; // 简单用逗号分隔如 read:user,write:order // getters and setters } // 仓库接口 public interface ApiClientRepository extends JpaRepositoryApiClient, String { OptionalApiClient findByApiKey(String apiKeyHash); }接着创建一个拦截器来验证请求头中的 API KeyComponent public class ApiKeyAuthInterceptor implements HandlerInterceptor { Autowired private ApiClientRepository apiClientRepository; Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { // 1. 从请求头获取 API Key String apiKey request.getHeader(X-API-Key); if (apiKey null || apiKey.isBlank()) { sendError(response, HttpStatus.UNAUTHORIZED, Missing API Key); return false; } // 2. 计算哈希值存储的应是哈希值此处演示直接比对生产环境必须哈希比对 // String hashedKey hashFunction(apiKey); // OptionalApiClient clientOpt apiClientRepository.findByApiKey(hashedKey); // 为简化示例假设存储的是明文实际严禁 OptionalApiClient clientOpt apiClientRepository.findByApiKey(apiKey); // 3. 验证客户端 if (clientOpt.isEmpty()) { sendError(response, HttpStatus.UNAUTHORIZED, Invalid API Key); return false; } ApiClient client clientOpt.get(); if (!client.isEnabled()) { sendError(response, HttpStatus.FORBIDDEN, API Key disabled); return false; } if (client.getExpiresAt() ! null client.getExpiresAt().isBefore(LocalDateTime.now())) { sendError(response, HttpStatus.FORBIDDEN, API Key expired); return false; } // 4. 将客户端信息存入请求上下文供后续授权使用 request.setAttribute(apiClient, client); return true; } private void sendError(HttpServletResponse response, HttpStatus status, String message) throws IOException { response.setStatus(status.value()); response.setContentType(application/json); response.getWriter().write(String.format({\error\: \%s\, \message\: \%s\}, status.getReasonPhrase(), message)); } }然后注册这个拦截器到 Spring MVC 配置中Configuration public class WebConfig implements WebMvcConfigurer { Autowired private ApiKeyAuthInterceptor apiKeyAuthInterceptor; Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(apiKeyAuthInterceptor) .addPathPatterns(/api/**) // 保护所有 /api 开头的路径 .excludePathPatterns(/api/public/**); // 排除公开接口 } }最后在控制器中你可以从请求属性中获取客户端信息并进行授权判断RestController RequestMapping(/api/data) public class DataController { GetMapping public ResponseEntityString getData(HttpServletRequest request) { ApiClient client (ApiClient) request.getAttribute(apiClient); // 检查权限例如 client.getPermissions() 是否包含 read:data if (!hasPermission(client, read:data)) { return ResponseEntity.status(HttpStatus.FORBIDDEN).body(Insufficient permissions); } return ResponseEntity.ok(Sensitive data for client: client.getClientName()); } private boolean hasPermission(ApiClient client, String requiredPermission) { // 简单的权限检查逻辑 return Arrays.asList(client.getPermissions().split(,)).contains(requiredPermission); } }2.3 API Key 的常见陷阱与最佳实践陷阱1明文存储和传输现象API Key 以明文形式存储在数据库或代码中或在网络上明文传输。风险数据库泄露或网络抓包直接导致 Key 泄露。解决存储在数据库存储 Key 的加盐哈希值如 bcrypt验证时比对哈希。上述示例为了清晰直接存储明文生产环境绝对禁止。传输必须使用 HTTPS。陷阱2单一 Key 权限过大现象一个 API Key 拥有所有接口的访问权限。风险该 Key 泄露意味着攻击者拥有全部权限。解决遵循最小权限原则。为不同用途创建不同的 Key并绑定细粒度的权限范围Scopes。陷阱3缺乏生命周期管理现象Key 生成后永久有效没有过期、禁用、轮换机制。风险长期有效的 Key 增加了泄露和被滥用的时间窗口。解决为 Key 设置合理的过期时间。提供 Key 轮换接口允许客户端生成新 Key 后旧 Key 在一段缓冲期后失效。在管理后台提供一键禁用 Key 的功能。最佳实践清单[ ] 使用高熵值、随机生成的 Key如 UUID v4。[ ] 在服务端存储 Key 的加盐哈希值。[ ] 强制所有 API 调用使用 HTTPS。[ ] 为每个 Key 绑定具体的权限范围Scopes。[ ] 实现 Key 的过期、禁用和轮换机制。[ ] 记录所有 API 调用日志包含 Key 标识用于审计和异常检测。[ ] 对 Key 的使用进行速率限制。3. JWT无状态且自包含的令牌JWT 的出现是为了解决服务端会话存储的压力和跨域、跨服务认证的问题。它是一串经过编码和签名的字符串由三部分组成Header头部、Payload负载、Signature签名。3.1 JWT 的结构与验证机制一个典型的 JWT 看起来像这样eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiaWF0IjoxNTE2MjM5MDIyfQ.SflKxwRJSMeKKF2QT4fwpMeJf36POk6yJV_adQssw5c1. Header描述令牌类型和签名算法。{ alg: HS256, typ: JWT }2. Payload包含声明Claims即要传递的信息。有三种类型的声明注册声明预定义的标准字段如iss签发者、exp过期时间、sub主题等。公共声明可以自定义但为避免冲突应使用 IANA 注册的命名或包含命名空间的 URI。私有声明供通信双方约定使用的自定义字段。{ sub: 1234567890, name: John Doe, iat: 1516239022, exp: 1516242622, scope: read:user write:post }3. Signature对编码后的 Header 和 Payload使用 Header 中指定的算法和一个密钥进行签名确保令牌未被篡改。例如使用 HMAC SHA256 算法HMACSHA256(base64UrlEncode(header) “.” base64UrlEncode(payload), secret)。验证流程服务端收到 JWT 后1) 检查格式三段式2) 用相同的密钥和算法重新计算签名并与令牌中的签名比对3) 验证标准声明如exp是否过期、iss签发者是否正确等。3.2 实现示例Spring Security JJWT 实现登录与 Token 签发我们使用io.jsonwebtoken:jjwt-api及其实现库来创建和验证 JWT。首先添加 Maven 依赖dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-api/artifactId version0.11.5/version /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-impl/artifactId version0.11.5/version scoperuntime/scope /dependency dependency groupIdio.jsonwebtoken/groupId artifactIdjjwt-jackson/artifactId version0.11.5/version scoperuntime/scope /dependency创建一个 JWT 工具类负责生成和解析 TokenComponent public class JwtTokenProvider { Value(${jwt.secret}) private String jwtSecret; // 从配置读取必须足够复杂且保密 Value(${jwt.expiration.ms}) private long jwtExpirationMs; private Key getSigningKey() { byte[] keyBytes Decoders.BASE64.decode(jwtSecret); return Keys.hmacShaKeyFor(keyBytes); } public String generateToken(String username, ListString roles) { Date now new Date(); Date expiryDate new Date(now.getTime() jwtExpirationMs); return Jwts.builder() .setSubject(username) .claim(roles, roles) // 自定义声明角色 .setIssuedAt(now) .setExpiration(expiryDate) .signWith(getSigningKey(), SignatureAlgorithm.HS256) .compact(); } public String getUsernameFromToken(String token) { Claims claims Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); return claims.getSubject(); } public ListString getRolesFromToken(String token) { Claims claims Jwts.parserBuilder() .setSigningKey(getSigningKey()) .build() .parseClaimsJws(token) .getBody(); return claims.get(roles, List.class); } public boolean validateToken(String token) { try { Jwts.parserBuilder().setSigningKey(getSigningKey()).build().parseClaimsJws(token); return true; } catch (JwtException | IllegalArgumentException e) { // 日志记录异常 return false; } } }配置 Spring Security使用 JWT 过滤器Configuration EnableWebSecurity public class SecurityConfig extends WebSecurityConfigurerAdapter { Autowired private JwtTokenProvider jwtTokenProvider; Override protected void configure(HttpSecurity http) throws Exception { http .csrf().disable() // 对于纯 API通常禁用 CSRF .sessionManagement().sessionCreationPolicy(SessionCreationPolicy.STATELESS) // 无状态 .and() .authorizeRequests() .antMatchers(/api/auth/**).permitAll() // 认证接口公开 .anyRequest().authenticated() // 其他所有接口需要认证 .and() .addFilterBefore(new JwtAuthenticationFilter(jwtTokenProvider), UsernamePasswordAuthenticationFilter.class); } Bean public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } }创建 JWT 认证过滤器public class JwtAuthenticationFilter extends OncePerRequestFilter { private final JwtTokenProvider jwtTokenProvider; public JwtAuthenticationFilter(JwtTokenProvider jwtTokenProvider) { this.jwtTokenProvider jwtTokenProvider; } Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String token resolveToken(request); if (token ! null jwtTokenProvider.validateToken(token)) { String username jwtTokenProvider.getUsernameFromToken(token); ListString roles jwtTokenProvider.getRolesFromToken(token); // 构建 Authentication 对象 ListGrantedAuthority authorities roles.stream() .map(SimpleGrantedAuthority::new) .collect(Collectors.toList()); UsernamePasswordAuthenticationToken auth new UsernamePasswordAuthenticationToken( username, null, authorities); auth.setDetails(new WebAuthenticationDetailsSource().buildDetails(request)); SecurityContextHolder.getContext().setAuthentication(auth); } filterChain.doFilter(request, response); } private String resolveToken(HttpServletRequest request) { String bearerToken request.getHeader(Authorization); if (StringUtils.hasText(bearerToken) bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } }最后创建认证控制器RestController RequestMapping(/api/auth) public class AuthController { Autowired private AuthenticationManager authenticationManager; Autowired private JwtTokenProvider jwtTokenProvider; Autowired private UserDetailsService userDetailsService; PostMapping(/login) public ResponseEntity? login(RequestBody LoginRequest loginRequest) { // 1. 认证用户名密码 Authentication authentication authenticationManager.authenticate( new UsernamePasswordAuthenticationToken(loginRequest.getUsername(), loginRequest.getPassword())); SecurityContextHolder.getContext().setAuthentication(authentication); // 2. 获取用户详情和角色 UserDetails userDetails (UserDetails) authentication.getPrincipal(); ListString roles userDetails.getAuthorities().stream() .map(GrantedAuthority::getAuthority) .collect(Collectors.toList()); // 3. 生成 JWT String jwt jwtTokenProvider.generateToken(userDetails.getUsername(), roles); // 4. 返回 Token return ResponseEntity.ok(new JwtResponse(jwt)); } }3.3 JWT 的常见陷阱与最佳实践陷阱1将敏感信息放入 Payload现象在 JWT Payload 中存储密码、手机号等敏感信息。风险JWT 默认仅签名不加密JWE 可加密但复杂。Payload 是 Base64 解码即可读的一旦泄露导致敏感信息暴露。解决Payload 只存放必要的、非敏感的身份标识如 userId和授权信息如 roles, scopes。陷阱2Token 无法失效现象依赖exp过期时间但在用户登出或密码修改后已签发的 Token 在过期前依然有效。风险Token 被盗后在有效期内可一直被滥用。解决短期 Token设置较短的过期时间如 15-30 分钟。结合 Refresh Token使用长生命周期的 Refresh Token 来获取新的 Access Token。将 Refresh Token 存入服务端如数据库可随时使其失效。维护令牌黑名单用户登出时将 Token 标识加入黑名单缓存验证时检查。这会引入状态部分牺牲无状态性。陷阱3签名密钥管理不当现象使用弱密钥或将密钥硬编码在客户端、版本库中。风险攻击者破解或获取密钥后可以伪造任意有效的 JWT。解决使用强随机密钥如openssl rand -base64 32生成。密钥作为机密配置通过环境变量或配置中心注入。定期轮换密钥需处理新旧 Token 同时有效的问题。最佳实践清单[ ] 使用强签名算法如 HS256, RS256。[ ] Payload 中仅存放非敏感的必要声明。[ ] 设置合理的短期过期时间exp。[ ] 验证 Token 时必须检查签名、exp、iss签发者等关键声明。[ ] 考虑使用 HTTPS 传输防止 Token 被截获。[ ] 为关键操作如修改密码、支付设计更严格的二次认证不要仅依赖 JWT。4. OAuth 2.0标准的授权框架OAuth 2.0 是一个授权框架而非简单的认证协议。它定义了四个角色和多种授权流程用于解决第三方应用在用户授权下访问受保护资源的问题。4.1 核心角色与授权码模式流程四个核心角色资源所有者拥有受保护资源所有权的实体通常是最终用户。客户端请求访问受保护资源的应用程序第三方应用。授权服务器在成功认证资源所有者并获得授权后向客户端颁发访问令牌的服务器。资源服务器托管受保护资源的服务器它接受并验证访问令牌然后提供资源。授权码模式是最安全、最常用的流程适用于有后端的 Web 应用。其流程如下sequenceDiagram participant User as 用户 (资源所有者) participant Client as 客户端应用 participant Auth as 授权服务器 participant Resource as 资源服务器 User-Client: 1. 访问客户端点击“用XX登录” Client-User: 2. 重定向到授权服务器 User-Auth: 3. 认证并授权 Auth-User: 4. 重定向回客户端附带授权码 User-Client: 5. 传递授权码 Client-Auth: 6. 用授权码客户端密钥交换访问令牌 Auth-Client: 7. 返回访问令牌 (和刷新令牌) Client-Resource: 8. 用访问令牌访问资源 Resource-Client: 9. 返回受保护资源授权请求客户端将用户重定向到授权服务器的授权端点携带client_id、redirect_uri、scope请求的权限范围、state防 CSRF 随机数等参数。用户认证与授权用户在授权服务器上登录如果未登录并确认是否授权客户端请求的权限。颁发授权码用户同意后授权服务器将用户重定向回客户端事先注册的redirect_uri并在 URL 查询参数中附带一个短期有效的授权码。交换访问令牌客户端后端使用这个授权码连同自己的client_id和client_secret向授权服务器的令牌端点发起 POST 请求换取访问令牌。访问资源客户端使用获取到的访问令牌通常放在Authorization: Bearer头中去资源服务器请求受保护资源。4.2 实现示例使用 Spring Authorization Server 搭建简易 OAuth 2.0 服务Spring Security 5.2 之后官方提供了 Spring Authorization Server 项目来构建 OAuth 2.0 授权服务器。以下是一个极简配置示例。首先添加依赖dependency groupIdorg.springframework.security/groupId artifactIdspring-security-oauth2-authorization-server/artifactId version0.3.1/version !-- 请使用最新版本 -- /dependency配置授权服务器Configuration EnableWebSecurity public class AuthServerConfig { Bean Order(Ordered.HIGHEST_PRECEDENCE) public SecurityFilterChain authorizationServerSecurityFilterChain(HttpSecurity http) throws Exception { OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http); return http.build(); } Bean public RegisteredClientRepository registeredClientRepository() { RegisteredClient registeredClient RegisteredClient.withId(UUID.randomUUID().toString()) .clientId(my-client) // 客户端ID .clientSecret({noop}my-client-secret) // 客户端密钥{noop}表示明文生产环境用加密 .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_BASIC) .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE) // 授权码模式 .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN) // 支持刷新令牌 .redirectUri(http://localhost:8080/login/oauth2/code/my-client) // 回调地址 .scope(read) // 授权范围 .scope(write) .clientSettings(ClientSettings.builder().requireAuthorizationConsent(true).build()) // 要求用户确认授权 .build(); return new InMemoryRegisteredClientRepository(registeredClient); } Bean public ProviderSettings providerSettings() { return ProviderSettings.builder() .issuer(http://auth-server:9000) // 签发者标识 .build(); } }配置资源服务器另一个服务或模块Configuration EnableWebSecurity EnableResourceServer // 旧版注解新版Spring Security OAuth2资源服务器配置方式不同 public class ResourceServerConfig extends ResourceServerConfigurerAdapter { Override public void configure(HttpSecurity http) throws Exception { http .authorizeRequests() .antMatchers(/api/public/**).permitAll() .antMatchers(/api/**).authenticated(); // 保护 /api/** 路径 } }在资源服务器的控制器中可以通过AuthenticationPrincipal注入认证信息RestController RequestMapping(/api/user) public class UserController { GetMapping(/me) public MapString, Object getCurrentUser(AuthenticationPrincipal Jwt jwt) { // Jwt 对象包含了令牌中的所有声明 return Map.of( username, jwt.getSubject(), scopes, jwt.getClaimAsStringList(scope), issuedAt, jwt.getIssuedAt() ); } }4.3 OAuth 2.0 的常见陷阱与安全考量陷阱1错误使用隐式模式现象在传统 Web 应用或移动端 App 中使用隐式模式。风险隐式模式直接将访问令牌通过 URL 片段返回给浏览器容易通过 Referer 头、浏览器历史记录泄露。OAuth 2.1 已废弃隐式模式。解决对于有后端的 Web 应用始终使用授权码模式。对于单页应用SPA使用授权码模式 PKCE。陷阱2redirect_uri 校验不严现象授权服务器未严格校验客户端注册的重定向 URI或允许任意重定向。风险攻击者构造恶意链接将授权码或令牌重定向到其控制的服务器导致令牌泄露。解决授权服务器必须精确匹配预先注册的redirect_uri包括协议、主机、端口和路径。陷阱3client_secret 保管不当现象在移动端 App 或浏览器端 JavaScript 中存储client_secret。风险client_secret可能被反编译或调试工具获取失去保密性。解决对于原生 App 或 SPA使用PKCE扩展它可以避免在客户端存储静态密钥。对于机密客户端如后端服务必须妥善保管client_secret并使用安全的传输方式。安全配置清单[ ] 为机密客户端使用强client_secret并定期轮换。[ ] 严格校验redirect_uri防止开放重定向攻击。[ ] 使用state参数防止 CSRF 攻击。[ ] 为访问令牌设置较短的过期时间如 1 小时。[ ] 使用刷新令牌来获取新的访问令牌并安全地存储刷新令牌。[ ] 对 SPA 和移动 App 使用授权码模式 PKCE。[ ] 监控授权日志及时发现异常授权请求。5. 综合对比与选型指南理解了三种方案的具体实现后我们需要一个清晰的决策框架来指导选型。下表从多个维度进行了对比维度API KeyJWTOAuth 2.0 (授权码模式)核心目标验证客户端身份安全传递身份/权限声明安全的第三方委托授权状态性服务端需存储 Key 状态无状态授权服务器需管理令牌状态凭证类型静态密钥自包含的签名令牌访问令牌 (可能为 JWT) 刷新令牌典型生命周期数月或数年手动轮换分钟到小时级访问令牌短小时刷新令牌长天/月信息承载少通常仅客户端ID丰富可包含身份、权限、自定义声明依赖令牌格式Opaque Token 无信息JWT 同左客户端类型服务器、脚本、IoT设备任何能安全存储令牌的客户端第三方 Web 应用、移动 App、SPA主要风险密钥泄露、权限过大Token 泄露、无法立即失效、密钥泄露重定向攻击、CSRF、客户端密钥泄露实现复杂度低中高运维成本低密钥管理低无状态高需维护授权服务器、令牌生命周期适用场景内部服务间通信、机器对机器(M2M)、简单第三方集成前后端分离应用、微服务间身份传递、单点登录(SSO)开放平台、第三方登录、需要精细权限控制的第三方集成选型决策路径场景是纯机器对机器M2M没有用户参与是- 优先考虑API Key。例如公司内部的数据同步服务、服务器监控 agent 上报数据。确保做好密钥的存储、传输和权限隔离。否- 进入第 2 步。场景是用户访问自己的资源且所有服务都在你的掌控之下是- 优先考虑JWT。例如你的前端 React/Vue 应用访问你的后端 Spring Boot API。JWT 的无状态特性非常适合微服务架构。否- 进入第 3 步。场景是用户需要授权第三方应用访问其在你平台上的资源是- 必须使用OAuth 2.0。例如开发微信小程序需要获取用户头像、开发 GitHub App 需要访问用户仓库。这是 OAuth 的标准场景。否- 可能是内部员工访问多个内部系统考虑使用OAuth 2.0或SAML实现单点登录。混合使用模式在实际复杂系统中这三种技术常常结合使用OAuth 2.0 JWT授权服务器颁发 JWT 格式的访问令牌。兼具了 OAuth 的授权流程和 JWT 的自包含、无状态验证优点。API Gateway JWTAPI 网关统一验证 JWT然后将用户身份信息如 userId通过 HTTP 头传递给下游微服务。内部服务间使用 API Key对外用户接口使用 JWT/OAuth根据信任边界划分认证方式。6. 实战构建一个混合认证的微服务网关为了将理论付诸实践我们设计一个简单的场景一个微服务系统对外提供用户 API使用 JWT同时内部有一个数据导出服务供合作伙伴调用使用 API Key。我们将使用 Spring Cloud Gateway 作为网关来统一处理这两种认证。架构概览客户端 (浏览器/App) --(携带 JWT)-- API Gateway --(转发请求)-- 用户服务 合作伙伴 (脚本) --(携带 API Key)-- API Gateway --(转发请求)-- 数据导出服务1. 网关依赖与配置# application.yml spring: cloud: gateway: routes: - id: user-service-route uri: lb://USER-SERVICE predicates: - Path/api/user/** filters: - name: JwtAuthFilter # 自定义 JWT 认证过滤器 - id: export-service-route uri: lb://EXPORT-SERVICE predicates: - Path/api/export/** filters: - name: ApiKeyAuthFilter # 自定义 API Key 认证过滤器2. 实现全局 JWT 认证过滤器Component public class JwtAuthFilter implements GlobalFilter, Ordered { Autowired private JwtTokenProvider jwtTokenProvider; // 复用之前的组件 Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path exchange.getRequest().getURI().getPath(); // 仅对 /api/user/** 路径进行 JWT 认证 if (!path.startsWith(/api/user/)) { return chain.filter(exchange); } String token resolveToken(exchange.getRequest()); if (token null || !jwtTokenProvider.validateToken(token)) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().setComplete(); } // 验证通过将用户名添加到请求头传递给下游服务 String username jwtTokenProvider.getUsernameFromToken(token); ServerHttpRequest mutatedRequest exchange.getRequest().mutate() .header(X-Authenticated-User, username) .build(); return chain.filter(exchange.mutate().request(mutatedRequest).build()); } private String resolveToken(ServerHttpRequest request) { String bearerToken request.getHeaders().getFirst(Authorization); if (StringUtils.hasText(bearerToken) bearerToken.startsWith(Bearer )) { return bearerToken.substring(7); } return null; } Override public int getOrder() { return -1; // 高优先级 } }3. 实现全局 API Key 认证过滤器Component public class ApiKeyAuthFilter implements GlobalFilter, Ordered { Autowired private ApiClientRepository apiClientRepository; // 假设已注入 Override public MonoVoid filter(ServerWebExchange exchange, GatewayFilterChain chain) { String path exchange.getRequest().getURI().getPath(); // 仅对 /api/export/** 路径进行 API Key 认证 if (!path.startsWith(/api/export/)) { return chain.filter(exchange); } String apiKey exchange.getRequest().getHeaders().getFirst(X-API-Key); if (apiKey null) { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().writeWith(Mono.just(exchange.getResponse() .bufferFactory().wrap(Missing API Key.getBytes()))); } // 验证 API Key (此处简化生产环境需查库并校验状态) MonoBoolean isValidKey Mono.fromCallable(() - { OptionalApiClient client apiClientRepository.findByApiKey(apiKey); return client.isPresent() client.get().isEnabled(); }).subscribeOn(Schedulers.boundedElastic()); return isValidKey.flatMap(valid - { if (valid) { return chain.filter(exchange); } else { exchange.getResponse().setStatusCode(HttpStatus.UNAUTHORIZED); return exchange.getResponse().writeWith(Mono.just(exchange.getResponse() .bufferFactory().wrap(Invalid API Key.getBytes()))); } }); } Override public int getOrder() { return -1; // 与 JWT 过滤器同优先级实际可根据路径精确匹配避免冲突 } }通过这个网关我们统一了入口并根据路由规则将不同的认证逻辑解耦。下游的user-service只需信任网关传来的X-Authenticated-User头而export-service则信任来自网关的请求因为网关已经完成了 API Key 验证。这种模式简化了下游服务的认证逻辑并集中了安全策略管理。选择认证与授权方案是一个需要权衡安全性、复杂度、用户体验和运维成本的过程。对于内部可信环境下的服务间调用API Key 简单有效对于需要无状态、可扩展的用户会话管理JWT 是优秀选择而当需要与第三方安全地共享用户资源时OAuth 2.0 是行业标准。理解它们各自的工作原理、安全边界和陷阱是构建健壮 API 安全体系的起点。在实际项目中往往需要根据具体的信任边界、客户端类型和合规要求灵活组合或分层使用这些技术。
返回列表