PHP微服务架构中的认证与授权分离:使用OpenID Connect或JWT的最佳实践
大家好!今天我们来深入探讨一个在微服务架构中至关重要的话题:认证与授权的分离。在单体应用中,我们通常将用户认证和权限控制紧密耦合,但在微服务环境下,这种做法会带来诸多问题,例如代码重复、维护困难、以及服务间的依赖性增强。因此,我们需要一个更优雅、更具扩展性的解决方案。
我们将重点讨论两种在PHP微服务架构中常用的技术:OpenID Connect (OIDC) 和 JSON Web Token (JWT),以及如何在实践中应用它们来实现认证与授权的分离。
1. 认证与授权分离的必要性
在深入技术细节之前,我们首先要理解为什么要在微服务架构中分离认证与授权。
| 问题 | 单体应用 | 微服务架构 |
|---|---|---|
| 代码重复 | 用户认证和授权逻辑在多个模块中重复。 | 每个微服务都需要独立的认证和授权逻辑,导致大量重复代码。 |
| 维护困难 | 认证和授权逻辑修改需要重新部署整个应用。 | 修改任何一个微服务的认证和授权逻辑都需要重新部署,影响范围扩大。 |
| 服务间依赖性增强 | 微服务之间直接调用用户数据库进行认证和授权。 | 微服务之间紧密耦合,某个微服务故障会影响其他服务。 |
| 安全风险 | 单点故障风险,一旦认证模块被攻破,整个应用沦陷。 | 每个微服务都需要独立保护,任何一个微服务的漏洞都可能被利用。 |
| 扩展性受限 | 难以针对特定模块进行独立扩展。 | 认证和授权逻辑的瓶颈会限制整个微服务架构的扩展性。 |
通过分离认证与授权,我们可以获得以下好处:
- 代码复用性提升: 将认证逻辑提取到单独的服务中,多个微服务可以共享同一个认证服务。
- 维护成本降低: 认证和授权逻辑的修改只需要更新认证服务,而不需要重新部署所有微服务。
- 服务间解耦: 微服务不再直接依赖用户数据库,而是通过标准的API或协议与认证服务进行交互。
- 安全性增强: 集中管理认证和授权逻辑,可以更好地实施安全策略,并减少安全漏洞。
- 扩展性提升: 认证服务可以独立扩展,以满足不断增长的用户需求。
2. OpenID Connect (OIDC) 概述
OpenID Connect (OIDC) 是一个建立在 OAuth 2.0 协议之上的身份认证层。它允许客户端验证用户的身份,并获取关于用户的基本信息。OIDC的核心思想是:Authentication as a Service (AaaS)。
OIDC引入了以下关键概念:
- Client (客户端): 需要认证用户的应用程序或微服务。
- Resource Server (资源服务器): 保护用户资源的微服务,需要验证访问令牌。
- Authorization Server (授权服务器): 负责认证用户并颁发访问令牌和ID令牌。
- ID Token (ID令牌): 一个包含用户身份信息的 JWT,由授权服务器签名。
- Access Token (访问令牌): 用于访问受保护资源的令牌。
- Userinfo Endpoint (用户信息端点): 一个受保护的API端点,客户端可以使用访问令牌来获取用户的更多信息。
OIDC 流程:
- 客户端发起认证请求: 客户端重定向用户到授权服务器的认证端点。
- 用户认证: 用户在授权服务器上进行身份验证 (例如,输入用户名和密码)。
- 授权服务器颁发授权码: 如果用户认证成功,授权服务器将颁发一个授权码给客户端。
- 客户端交换授权码: 客户端使用授权码向授权服务器的令牌端点请求访问令牌和ID令牌。
- 授权服务器颁发令牌: 授权服务器验证授权码,并颁发访问令牌和ID令牌给客户端。
- 客户端访问资源: 客户端使用访问令牌访问资源服务器上的受保护资源。
- 资源服务器验证令牌: 资源服务器验证访问令牌,如果有效,则允许客户端访问资源。
PHP中使用OIDC:
在PHP中,可以使用各种OIDC客户端库来简化与OIDC授权服务器的交互。 其中一个流行的库是league/oauth2-client,结合 OIDC 中间件可以方便的实现OIDC的客户端。
<?php
require 'vendor/autoload.php';
use LeagueOAuth2ClientProviderGenericProvider;
// 配置 OIDC 客户端
$provider = new GenericProvider([
'clientId' => 'YOUR_CLIENT_ID', // 客户端 ID
'clientSecret' => 'YOUR_CLIENT_SECRET', // 客户端密钥
'redirectUri' => 'YOUR_REDIRECT_URI', // 回调 URI
'urlAuthorize' => 'OIDC_AUTHORIZATION_ENDPOINT', // 授权端点
'urlAccessToken' => 'OIDC_TOKEN_ENDPOINT', // 令牌端点
'urlResourceOwnerDetails' => 'OIDC_USERINFO_ENDPOINT', // 用户信息端点
'scopes' => ['openid', 'profile', 'email'], // 请求的 scope
]);
// 1. 发起认证请求
if (!isset($_GET['code'])) {
// 获取授权 URL
$authorizationUrl = $provider->getAuthorizationUrl();
// 将 state 存储在 session 中,以防止 CSRF 攻击
$_SESSION['oauth2state'] = $provider->getState();
// 重定向到授权 URL
header('Location: ' . $authorizationUrl);
exit;
// 2. 处理回调
} else {
// 检查 state 是否匹配
if (empty($_GET['state']) || ($_GET['state'] !== $_SESSION['oauth2state'])) {
unset($_SESSION['oauth2state']);
exit('Invalid state');
}
try {
// 尝试获取访问令牌
$accessToken = $provider->getAccessToken('authorization_code', [
'code' => $_GET['code']
]);
// 使用访问令牌获取用户信息
$resourceOwner = $provider->getResourceOwner($accessToken);
// 输出用户信息
var_dump($resourceOwner->toArray());
// 或者,直接访问 ID Token
$idToken = $accessToken->getValues()['id_token'];
// 可以使用 JWT 库解码 ID Token,并验证签名
} catch (LeagueOAuth2ClientProviderExceptionIdentityProviderException $e) {
// 获取访问令牌失败
exit($e->getMessage());
}
}
3. JSON Web Token (JWT) 概述
JSON Web Token (JWT) 是一种紧凑的、自包含的方式,用于安全地传输信息作为 JSON 对象。 JWT 可以被签名 (使用 secret 或者 public/private key pair)。 JWT 包含声明 (claims),这些声明可以用于验证用户的身份和授权。
JWT 的结构:
- Header (头部): 包含令牌类型 (typ) 和签名算法 (alg)。
- Payload (载荷): 包含声明 (claims),例如用户ID、角色、权限等。
- Signature (签名): 使用 header 中指定的算法和密钥对 header 和 payload 进行签名,用于验证令牌的完整性。
JWT 的优点:
- 自包含性: JWT 包含了所有需要的信息,无需查询数据库。
- 可扩展性: 可以在 payload 中添加自定义声明。
- 跨域性: JWT 可以跨域使用,非常适合微服务架构。
- 易于验证: 可以使用公钥或密钥轻松验证 JWT 的签名。
PHP中使用JWT:
在PHP中,可以使用各种JWT库来生成和验证JWT。 一个流行的库是 firebase/php-jwt。
<?php
require 'vendor/autoload.php';
use FirebaseJWTJWT;
use FirebaseJWTKey;
// 密钥 (在生产环境中应该使用更安全的存储方式)
$key = "YOUR_SECRET_KEY";
// 1. 生成 JWT
$payload = array(
"iss" => "your-app.com", // 发行者
"aud" => "your-app.com", // 接收者
"iat" => time(), // 签发时间
"nbf" => time() + 10, // 生效时间
"exp" => time() + 3600, // 过期时间 (1 小时)
"data" => array(
"userId" => 123,
"username" => "john.doe",
"roles" => ["admin", "editor"]
)
);
$jwt = JWT::encode($payload, $key, 'HS256');
echo "JWT: " . $jwt . "n";
// 2. 验证 JWT
try {
$decoded = JWT::decode($jwt, new Key($key, 'HS256'));
print_r($decoded);
} catch (Exception $e) {
echo "JWT 验证失败: " . $e->getMessage() . "n";
}
4. 微服务架构中的认证与授权实践
现在,我们将讨论如何在微服务架构中应用 OIDC 和 JWT 来实现认证与授权分离。
场景:
假设我们有一个电商平台,包含以下微服务:
- 用户服务 (User Service): 负责用户注册、登录和信息管理。
- 商品服务 (Product Service): 负责商品信息的管理。
- 订单服务 (Order Service): 负责订单的创建和管理。
方案一:使用 OIDC 进行认证,使用 JWT 进行授权
- 用户服务作为 OIDC 授权服务器: 用户服务负责认证用户,并颁发 JWT 访问令牌。
- 其他微服务作为 OIDC 客户端和资源服务器: 商品服务和订单服务作为 OIDC 客户端,向用户服务请求访问令牌。 同时,它们也是资源服务器,需要验证访问令牌才能访问受保护的资源。
- API Gateway (可选): 可以使用 API Gateway 来统一处理认证和授权,减轻微服务的负担。
流程:
- 用户登录: 用户通过客户端 (例如,Web 浏览器或移动应用) 向用户服务发起登录请求。
- 用户服务认证: 用户服务验证用户的用户名和密码。
- 颁发 JWT: 用户服务生成一个 JWT 访问令牌,并将用户ID、角色和权限等信息添加到 payload 中,然后使用私钥对 JWT 进行签名。
- 客户端存储 JWT: 客户端将 JWT 存储在本地 (例如,localStorage 或 cookie)。
- 客户端访问微服务: 客户端在 HTTP 请求头中添加 JWT (通常使用
Authorization: Bearer <JWT>格式) 来访问商品服务或订单服务。 - 微服务验证 JWT: 商品服务和订单服务接收到请求后,从 HTTP 请求头中提取 JWT,并使用用户服务的公钥验证 JWT 的签名。
- 授权: 验证 JWT 成功后,微服务从 JWT 的 payload 中获取用户的角色和权限信息,并根据这些信息来判断用户是否有权访问请求的资源。
- 访问资源: 如果用户有权访问,微服务则返回请求的资源。
代码示例 (商品服务):
<?php
require 'vendor/autoload.php';
use FirebaseJWTJWT;
use FirebaseJWTKey;
// 公钥 (从用户服务获取)
$publicKey = 'YOUR_USER_SERVICE_PUBLIC_KEY';
// 从 HTTP 请求头中获取 JWT
$authHeader = $_SERVER['HTTP_AUTHORIZATION'];
$jwt = str_replace('Bearer ', '', $authHeader);
if (!$jwt) {
http_response_code(401);
echo json_encode(['message' => 'Missing JWT']);
exit;
}
try {
// 验证 JWT
$decoded = JWT::decode($jwt, new Key($publicKey, 'RS256'));
// 从 JWT 中获取用户信息
$userId = $decoded->data->userId;
$roles = $decoded->data->roles;
// 授权 (例如,只有管理员才能创建商品)
if (!in_array('admin', $roles)) {
http_response_code(403);
echo json_encode(['message' => 'Unauthorized']);
exit;
}
// 处理请求 (创建商品)
// ...
echo json_encode(['message' => 'Product created successfully']);
} catch (Exception $e) {
http_response_code(401);
echo json_encode(['message' => 'Invalid JWT: ' . $e->getMessage()]);
exit;
}
方案二:使用 OIDC 统一认证和授权
- OIDC 授权服务器: 可以使用现有的 OIDC 授权服务器 (例如,Keycloak, Auth0, Okta) 或自建 OIDC 授权服务器。
- 所有微服务作为 OIDC 客户端和资源服务器: 所有微服务都向 OIDC 授权服务器注册为客户端,并使用访问令牌访问受保护的资源。
- 细粒度授权: 可以在 OIDC 授权服务器中配置细粒度的授权策略,例如,基于角色的访问控制 (RBAC) 或基于属性的访问控制 (ABAC)。
流程:
- 用户登录: 用户通过客户端向 OIDC 授权服务器发起登录请求。
- OIDC 授权服务器认证: OIDC 授权服务器验证用户的身份。
- 颁发访问令牌: OIDC 授权服务器颁发一个包含用户身份和授权信息的访问令牌。
- 客户端存储访问令牌: 客户端将访问令牌存储在本地。
- 客户端访问微服务: 客户端在 HTTP 请求头中添加访问令牌来访问微服务。
- 微服务验证访问令牌: 微服务向 OIDC 授权服务器验证访问令牌的有效性。
- 授权: 微服务根据访问令牌中的授权信息来判断用户是否有权访问请求的资源。
- 访问资源: 如果用户有权访问,微服务则返回请求的资源。
选择哪种方案?
| 特性 | 方案一 (OIDC + JWT) | 方案二 (OIDC 统一认证和授权) |
|---|---|---|
| 复杂度 | 中等,需要实现 JWT 的生成和验证。 | 较低,主要依赖 OIDC 授权服务器。 |
| 灵活性 | 较高,可以自定义 JWT 的 payload。 | 较低,受 OIDC 授权服务器的限制。 |
| 安全性 | 取决于 JWT 密钥的安全性。 | 取决于 OIDC 授权服务器的安全性。 |
| 适用场景 | 需要自定义授权逻辑,且对性能有较高要求的场景。 | 希望简化认证和授权流程,且对灵活性要求不高的场景。 |
5. 安全考量
无论选择哪种方案,都需要注意以下安全问题:
- 保护密钥: JWT 的密钥必须安全存储,避免泄露。
- 防止重放攻击: 可以使用 JWT 的
jti(JWT ID) 声明来防止重放攻击。 - 令牌过期时间: 设置合理的令牌过期时间,避免令牌被滥用。
- HTTPS: 始终使用 HTTPS 来保护令牌在网络传输过程中的安全。
- 验证算法: 选择安全的签名算法 (例如,RS256) 来生成 JWT。
6. 关于Refresh Token
在实际应用中,为了避免用户频繁登录,通常会使用 Refresh Token 机制。 Refresh Token 是一种长期有效的令牌,用于在 Access Token 过期后,向授权服务器请求新的 Access Token,而无需用户重新进行身份验证。
使用 Refresh Token 的流程如下:
- 颁发 Refresh Token: 当用户成功完成身份验证后,授权服务器除了颁发 Access Token 之外,还会颁发一个 Refresh Token。
- 客户端存储 Refresh Token: 客户端将 Refresh Token 安全地存储在本地。
- Access Token 过期: 当 Access Token 过期时,客户端向授权服务器发送 Refresh Token。
- 授权服务器验证 Refresh Token: 授权服务器验证 Refresh Token 的有效性。
- 颁发新的 Access Token: 如果 Refresh Token 有效,授权服务器会颁发一个新的 Access Token 和一个新的 Refresh Token (可选)。
- 客户端更新 Access Token: 客户端使用新的 Access Token 来访问受保护的资源。
7. 总结
认证与授权分离是构建安全、可扩展的微服务架构的关键。 OpenID Connect 和 JWT 是实现这一目标的两大利器。 选择哪种方案取决于具体的业务需求和安全考量。
最后的一些思考:选择最适合的方案
在微服务架构中,认证授权方案的选择需要结合实际情况,权衡复杂度、灵活性和安全性。没有银弹,只有最适合的方案。