微服务安全:OpenID Connect 认证与 JWT 授权
1. 准备工作
为避免端口冲突,需先停止账户服务。在开发模式下运行的账户服务,可通过按下
CTRL - C
来停止。之后,可执行以下命令运行安全测试:
mvn test \
-Dtest=SecurityTest
2. OpenID Connect 简介
OAuth 2.0 是一种行业标准的授权协议,用于第三方应用获取对其他应用的有限访问权限。它注重客户端开发的简便性,为不同类型的应用(如 Web 应用、桌面应用、移动电话和客厅设备)提供特定的授权流程。
OpenID Connect(OIDC)在 OAuth 2.0 的基础上增加了身份验证层,支持对用户身份的认证和受控访问。在过去,服务可能需要向第三方服务提供用户的登录信息(如用户名和密码)才能访问其用户数据和功能,这会导致第三方服务掌握用户凭证并获得对用户数据的粗粒度访问权限。而 OIDC 可以避免这种情况,后续将重点介绍如何使用 OIDC 对用户进行身份验证,并提供足够的用户身份信息(特别是用户角色)以访问安全端点。
3. OIDC 与 Keycloak
OIDC 在 OAuth 2.0 上增加了身份验证流程,主要关注以下两种流程:
-
授权码流程(Authorization Code Flow)
:未认证的用户尝试访问受保护资源时,会先被重定向到 OpenID Connect 提供商进行身份验证。
-
隐式流程(Implicit Flow)
:服务直接访问 OpenID Connect 提供商以获取访问受保护资源的令牌。
在使用 OIDC 保护服务之前,需要进行一些准备工作:
-
内存准备
:使用 Keycloak 作为身份提供商,与其他服务(如 Prometheus 和 Grafana)一起运行时,至少需要 5GB 的内存。可通过以下两种方式解决内存问题:
- 启动具有更多内存的 Minikube:
minikube delete
minikube start --memory=5120
- 如果桌面内存不足,可删除之前创建的监控命名空间:
kubectl delete ns monitoring
- 安装 Keycloak :使用 Keycloak Operator(https://github.com/keycloak/keycloak-operator)安装 Keycloak,安装使用 14.0.0 版本的 Keycloak Operator。
echo "$(minikube ip) keycloak.local" | sudo tee -a /etc/hosts
scripts/install_keycloak.sh
安装 Keycloak 可能需要几分钟,具体时间取决于 RAM、处理器速度和互联网连接。安装过程中可能会出现一些 “pods keycloak - 0 not found” 消息。
获取 Keycloak 管理员密码:
kubectl get secret credential-bank-keycloak \
-n keycloak \
-o go-template='{{range $k,$v := .data}}{{printf "%s: " $k}}
➥ {{if not $v}}{{$v}}{{else}}{{$v | base64decode}}{{end}}
➥ {{"\n"}}{{end}}'
Keycloak 银行领域定义了四个用户及其分配的角色,如下表所示:
| 用户名 | 密码 | 角色 |
| ---- | ---- | ---- |
| admin | admin | bankadmin |
| duke | duke | customer |
| jwt | jwt | customer |
| quarkus | quarkus | teller |
4. 使用 OpenID Connect 访问受保护资源
OIDC 授权码流程将用户身份验证委托给身份验证服务器(这里是 Keycloak),具体流程如下:
1. 用户访问受保护资源,该资源可能受内置 HTTP 安全策略或
@RolesAllowed
注解保护。
2. 银行服务将用户重定向到
quarkus.oidc.auth - server - url
属性指定的 OIDC 提供商(即 Keycloak)。
3. OIDC 提供商向用户展示身份验证表单,要求用户输入用户名和密码。
4. 身份验证成功后,Keycloak 返回 JWT 令牌和 HTTP 重定向到最初请求的资源。
5. 浏览器被重定向到受保护资源。
6. 服务成功返回资源内容。
为了在 Quarkus 中使用 OIDC,需要向银行服务添加 Quarkus OIDC 扩展并启动服务:
cd bank_service
mvn quarkus:add-extension -Dextensions="quarkus-oidc"
添加扩展后,需要配置银行服务以与 OIDC 服务器(Keycloak)进行交互,在
application.properties
中添加以下配置:
# Security
quarkus.oidc.enabled=true
quarkus.oidc.tls.verification=none
quarkus.oidc.token.issuer=https://keycloak.local/auth/realms/bank
%dev.quarkus.oidc.auth-server-url=https://keycloak.local/auth/realms/bank
%prod.quarkus.oidc.auth-server-url=https://keycloak:8443/auth/realms/bank
quarkus.oidc.client-id=bank
quarkus.oidc.application-type=web-app
username=admin
password=secret
更新银行服务,添加
BankResource.getSecureSecrets()
方法以保护
/bank/secure/secrets
端点,只有管理员才能查看:
@RolesAllowed("bankadmin")
@GET
@Produces(MediaType.APPLICATION_JSON)
@Path("/secure/secrets")
public Map<String, String> secureGetSecrets() {
return getSecrets();
}
启动银行服务:
mvn quarkus:dev -Ddebug=5008 -Dquarkus.http.port=8008
当访问
http://localhost:8008/bank/secure/secrets
时,浏览器将被重定向到 Keycloak 以获取用户凭证。使用用户名
admin
和密码
admin
登录后,Keycloak 会将浏览器重定向回
/bank/secure/secrets
端点并显示以下内容:
{"password":"secret","db.password":"secret","db.username":"admin","username":"admin"}
需要注意的是,浏览器可能不信任 Keycloak 的自签名证书,需要选择 “同意继续” 来信任该证书以测试 Keycloak 身份验证并访问安全的 REST 端点。
5. 测试授权码流程
为了避免在单元测试中运行 Keycloak 的繁琐,Quarkus 提供了 OIDC WireMock 来替代 Keycloak 作为 OIDC 授权服务器。首先,需要在
pom.xml
中添加以下依赖:
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-junit5</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.rest-assured</groupId>
<artifactId>rest-assured</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-test-oidc-server</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>net.sourceforge.htmlunit</groupId>
<artifactId>htmlunit</artifactId>
<version>2.36.0</version>
<scope>test</scope>
</dependency>
然后,在银行服务中添加
src/test/java/io/quarkus/bank/BankTest.java
类来测试授权码流程:
import static org.junit.jupiter.api.Assertions.assertTrue;
import javax.json.bind.Jsonb;
import javax.json.bind.JsonbBuilder;
import java.io.IOException;
import java.util.HashMap;
import com.gargoylesoftware.htmlunit.HtmlForm;
import com.gargoylesoftware.htmlunit.HtmlPage;
import com.gargoylesoftware.htmlunit.SilentCssErrorHandler;
import com.gargoylesoftware.htmlunit.UnexpectedPage;
import com.gargoylesoftware.htmlunit.WebClient;
import io.quarkus.test.junit.QuarkusTest;
import io.quarkus.test.junit.QuarkusTestResource;
import io.quarkus.test.oidc.server.OidcWiremockTestResource;
@QuarkusTest
@QuarkusTestResource(OidcWiremockTestResource.class)
public class BankTest {
@Test
public void testGetSecrets() throws IOException {
try (final WebClient webClient = createWebClient()) {
webClient.getOptions().setRedirectEnabled(true);
HtmlPage page = webClient.getPage("http://localhost:8081/bank/secure/secrets");
HtmlForm loginForm = page.getForms().get(0);
loginForm.getInputByName("username").setValueAttribute("admin");
loginForm.getInputByName("password").setValueAttribute("admin");
UnexpectedPage json = loginForm.getInputByValue("login").click();
Jsonb jsonb = JsonbBuilder.create();
HashMap<String, String> credentials = jsonb.fromJson(json.getWebResponse().getContentAsString(), HashMap.class);
assertTrue(credentials.get("username").equals("admin"));
assertTrue(credentials.get("password").equals("secret"));
}
}
private WebClient createWebClient() {
WebClient webClient = new WebClient();
webClient.setCssErrorHandler(new SilentCssErrorHandler());
return webClient;
}
}
同时,需要更新测试配置以正确使用模拟的 OIDC 服务器,在
application.properties
中添加以下配置:
%test.quarkus.oidc.auth-server-url=${keycloak.url}/realms/quarkus
运行测试:
mvn test \
-Dquarkus.test.oidc.token.admin-roles="bankadmin" \
-Dquarkus.test.oidc.token.issuer=https://keycloak.local/auth/realms/bank
OidcWiremockTestResource 定义了两个用户,并且可以通过系统属性覆盖一些默认设置,如下表所示:
| 属性 | 默认值 |
| ---- | ---- |
| quarkus.test.oidc.token.user - roles | user |
| quarkus.test.oidc.token.admin - roles | user, admin |
| quarkus.test.oidc.token.issuer | https://server.example.com |
| quarkus.test.oidc.token.audience | https://server.example.com |
6. 授权码流程流程图
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
A([用户访问受保护资源]):::startend --> B(银行服务重定向到 Keycloak):::process
B --> C(用户在 Keycloak 输入用户名和密码):::process
C --> D{身份验证是否成功}:::decision
D -->|是| E(Keycloak 返回 JWT 令牌和重定向):::process
D -->|否| C
E --> F(浏览器重定向到受保护资源):::process
F --> G(服务返回资源内容):::process
7. JWT 与 MicroProfile JWT
在微服务架构中,REST API 通常需要 REST 安全,而 REST 微服务往往是无状态的,JWT 提供的无状态安全方法非常适合。JWT 是一种轻量级的 JSON Web 令牌,定义在 RFC 7519 中,它由三部分组成:头部(header)、有效负载(payload)和签名(signature),各部分用点(
.
)分隔。
例如,一个示例 JWT 令牌如下:
eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJHNjZaUWxsTmNoOWVLVVB3VGpnVWJTcTB1eTN6aFJmeFZiOUItTUxNOG9FIn0.eyJleHAiOjE2MjM1NTM5MjIsImlhdCI6MTYyMzU1MzYyMiwiYXV0aF90aW1lIjoxNjIzNTUzNjE3LCJqdGkiOiI3NWI5MmZhZi02ZTVkLTRlMjItYWFyZXIiLCJpc3MiOiJodHRwczovL2tleWNsb2FrLmxvY2FsL2F1dGgvcmVhbG1zL2JhbmsiLCJzdWIiOiJlZGJiMzlkMC1jNmZhLTQyMTEtYTc1Yy03MGQ5MzQwMzE2MjAiLCJ0eXAiOiJCZWFyZXIiLCJhenAiOiJiYW5rIiwic2Vzc2lvbl9zdGF0ZSI6IjJjYTJiNGYwLWE0NjAtN
DliMi04MTkzLWI0YzNlYTg3ZTAxYSIsImFjciI6IjEiLCJhbGxvd2VkLW9yaWdpbnMiOlsiaHR0cDovLzEyNy4wLjAuMTo4MDA4IiwiaHR0cDovL2xvY2FsaG9zdDo4MDA4IiwiaHR0cDovL2xvY2FsaG9zdDo4MDgxIiwiaHR0cDovLzEyNy4wLjAuMTo4MDgxIiwiaHR0cDovLzEyNy4wLjAuMTo4MDg4I
iwiaHR0cDovL2xvY2FsaG9zdDo4MDg4Il0sInJlYWxtX2FjY2VzcyI6eyJyb2xlcyI6WyJjdXN0b21lciJdfSwic2NvcGUiOiJvcGVuaWQgcHJvZmlsZSBtaWNyb3Byb2ZpbGUtand0IGVtYWlsIHBob
25lIiwidXBuIjoiZHVrZSIsImJpcnRoZGF0ZSI6IkZlYnJ1YXJ5IDMwLCAyMDAwIiwiZW1haWxfd
mVyaWZpZWQiOnRydWUsIm5hbWUiOiJEdWtlIEN1c3RvbWVyIiwiZ3JvdXBzIjpbImN1c3RvbWVyI
l0sInByZWZlcnJlZF91c2VybmFtZSI6ImR1a2UiLCJnaXZlbl9uYW1lIjoiRHVrZSIsImZhbWlse
V9uYW1lIjoiQ3VzdG9tZXIiLCJlbWFpbCI6ImR1a2VAYWNtZTIuY29tIn0.QrM
Su9_9VE47xih2J9t-LhSDC-JPN2ptKip0OMCE3wl_bT3-IQoaX_TPuHz9elGrUQUYNjpnUuML8D2
yQmvt5QNaXjMvmxTFyEQgob2pxzbLkrQqIHhg7eSXKPLeJZtko3uWoiWDghYHFE_QBOk6iIZFY4c
YUQgxOiFTk4M73L2lkcy94fyv6Mgr4y5UQnTJqERVTfOQCybPy-B2nuRcpAcwB0eRTMgVsXAUsEI
camVjwwe1rkaHAdJvV6Z5Y8ouafSqdDMxRElmzkwnvWOfeNthVduiqba8YK0rkmvJhj0WS7Ehq74
UTtmHe5fMPvciVCSIMPVfDGKyVc45LYC2sA
7.1 JWT 头部
JWT 头部经过 Base64 编码,可以使用任何能够解码 Base64 表示的工具查看其内容。需要注意的是,JWT 不是加密的,因此建议通过安全的传输层(如 HTTPS)传输。可以使用以下命令解码 JWT 头部:
echo "eyJhbGciOiJSUzI1NiIsInR5cCIgOiAiSldUIiwia2lkIiA6ICJHNjZaUWxsTmNoOWVLVVB3VGpnVWJTcTB1eTN6aFJmeFZiOUItTUxNOG9FIn0" | base64 -d
解码后的头部内容如下:
{"alg":"RS256",
"typ": "JWT",
"kid": "G66ZQllNch9eKUPwTjgUbSq0uy3zhRfxVb9B-MLM8oE"
}
JWT 头部的三个声明及其描述如下表所示:
| 声明 | 描述 |
| ---- | ---- |
| alg | 用于签署 JWT 的加密算法,MicroProfile JWT 要求为 RS256,它使用公钥/私钥对来验证令牌内容未被篡改。 |
| typ | 媒体类型,MicroProfile JWT 要求此头部声明定义为 JWT。 |
| kid | 提示用于保护 JWT 的密钥,当有多个密钥可供选择时或识别请求之间密钥是否更改时,此声明很有用。 |
7.2 JWT 有效负载
JWT 有效负载包含 RFC 7519 定义的一组标准化声明,MicroProfile JWT 对这些标准化声明进行了扩展,开发者也可以根据需要添加自定义声明。
为了查看从 Keycloak 返回的令牌声明,在银行服务中添加
TokenResource.java
类:
import javax.annotation.security.Authenticated;
import javax.enterprise.context.RequestScoped;
import javax.inject.Inject;
import javax.json.Json;
import javax.json.JsonObject;
import javax.json.JsonObjectBuilder;
import javax.ws.rs.GET;
import javax.ws.rs.Path;
import javax.ws.rs.Produces;
import javax.ws.rs.core.MediaType;
import java.util.HashSet;
import java.util.Set;
import org.eclipse.microprofile.jwt.JsonWebToken;
@Authenticated
@Path("/token")
@RequestScoped
public class TokenResource {
@Inject
JsonWebToken accessToken;
@GET
@Path("/tokeninfo")
@Produces(MediaType.APPLICATION_JSON)
public Set<String> token() {
HashSet<String> set = new HashSet<String>();
for (String t : accessToken.getClaimNames()) {
set.add(t + " = " + accessToken.getClaim(t));
}
return set;
}
}
在浏览器中访问
http://localhost:8008/token/tokeninfo
,使用用户名
duke
和密码
duke
登录(建议使用隐身窗口以确保不使用之前的 cookie)。登录后,将得到一个类似以下的 JSON 令牌输出:
[
"realm_access = {\"roles\":[\"customer\"]}",
"preferred_username = duke",
"jti = 75b92faf-6e5d-4e22-aaeb-65f292c356ac",
"birthdate = February 30, 2000",
"iss = https://keycloak.local/auth/realms/bank",
"scope = openid profile microprofile-jwt email phone",
"upn = duke",
"principal = duke",
"typ = Bearer",
"name = Duke Customer",
"azp = bank",
"sub = edbb39d0-c6fa-4211-a75c-70d934031620",
"email_verified = true",
"raw_token = <too long to list>",
"family_name = Customer",
"exp = 1623553922",
"session_state = 2ca2b4f0-a460-49b2-8193-b4c3ea87e01a",
"groups = [customer]",
"acr = 1",
"auth_time = 1623553617",
"iat = 1623553622",
"allowed-origins = [\"http://127.0.0.1:8008\",\"http://localhost:8008\",
\"http://localhost:8081\",\"http://127.0.0.1:8081\",\"http://127.0.0.
1:8088\",\"http://localhost:8088\"]",
"email = duke@acme2.com",
"given_name = Duke"
]
这些声明的详细解释可参考相关文档,其中
groups
声明对应用功能比较重要,因为其值决定了方法的访问权限。
通过以上步骤和技术,可以实现微服务的安全访问,利用 OIDC 进行身份验证,使用 JWT 进行授权,确保系统的安全性和可靠性。
微服务安全:OpenID Connect 认证与 JWT 授权
8. JWT 有效负载声明详解
上半部分介绍了获取 JWT 有效负载声明的方法和示例输出,下面详细解释这些声明的含义,如下表所示:
| 声明 | 描述 |
| ---- | ---- |
|
realm_access
| 包含用户在 Keycloak 领域中的角色信息,如
{"roles":["customer"]}
表示用户具有
customer
角色。 |
|
preferred_username
| 用户的首选用户名,用于显示和识别用户。 |
|
jti
| JWT 的唯一标识符,用于防止重放攻击。 |
|
birthdate
| 用户的出生日期。 |
|
iss
| 令牌的颁发者,即 Keycloak 的地址和领域信息,确保令牌来自可信源。 |
|
scope
| 令牌的权限范围,指定用户可以访问的资源和操作。 |
|
upn
| 用户主体名称,通常与用户名相同。 |
|
principal
| 代表用户的主体,一般也是用户名。 |
|
typ
| 令牌类型,这里是
Bearer
类型,表示使用令牌进行身份验证。 |
|
name
| 用户的完整姓名。 |
|
azp
| 客户端 ID,标识请求令牌的客户端应用。 |
|
sub
| 令牌的主题,通常是用户的唯一标识符。 |
|
email_verified
| 表示用户的电子邮件地址是否已验证。 |
|
raw_token
| 原始的 JWT 令牌,由于过长通常不完整显示。 |
|
family_name
| 用户的姓氏。 |
|
exp
| 令牌的过期时间,超过该时间令牌将失效。 |
|
session_state
| 会话状态信息,用于跟踪用户的会话。 |
|
groups
| 用户所属的组,与角色类似,可用于访问控制。 |
|
acr
| 身份验证上下文类引用,用于表示身份验证的强度。 |
|
auth_time
| 用户进行身份验证的时间。 |
|
iat
| 令牌的颁发时间。 |
|
allowed-origins
| 允许访问的源地址列表,用于跨域资源共享(CORS)。 |
|
email
| 用户的电子邮件地址。 |
|
given_name
| 用户的名字。 |
9. JWT 签名验证流程
JWT 的签名部分用于验证令牌的完整性和真实性,确保令牌在传输过程中没有被篡改。签名验证的流程如下:
1. 客户端接收到 JWT 令牌。
2. 客户端从令牌的头部获取签名算法(如
RS256
)和密钥 ID(
kid
)。
3. 客户端根据
kid
从可信的密钥存储中获取对应的公钥。
4. 客户端使用公钥和签名算法对令牌的头部和有效负载进行重新签名。
5. 客户端将重新签名的结果与令牌中的签名进行比较,如果相同则验证通过,否则验证失败。
graph LR
classDef startend fill:#F5EBFF,stroke:#BE8FED,stroke-width:2px
classDef process fill:#E5F6FF,stroke:#73A6FF,stroke-width:2px
classDef decision fill:#FFF6CC,stroke:#FFBC52,stroke-width:2px
A([客户端接收 JWT 令牌]):::startend --> B(解析令牌头部获取算法和 kid):::process
B --> C(根据 kid 获取公钥):::process
C --> D(使用公钥和算法重新签名头部和有效负载):::process
D --> E{重新签名结果与令牌签名是否相同}:::decision
E -->|是| F(验证通过):::process
E -->|否| G(验证失败):::process
10. 微服务安全实践总结
在微服务架构中,使用 OpenID Connect(OIDC)和 JSON Web Tokens(JWT)可以有效地实现身份验证和授权,确保服务的安全性。以下是一些实践总结:
-
OIDC 配置
:
- 确保 Keycloak 有足够的内存运行,可通过调整 Minikube 内存或删除不必要的命名空间来解决内存问题。
- 正确配置 Quarkus 应用的
application.properties
文件,包括启用 OIDC、设置授权服务器 URL、客户端 ID 等。
- 使用
@RolesAllowed
注解保护受保护的资源,限制不同角色的访问权限。
-
JWT 使用
:
- 理解 JWT 的结构(头部、有效负载、签名)和工作原理,确保令牌在安全的传输层(如 HTTPS)上传输。
- 验证 JWT 的签名和颁发者,防止令牌被篡改和伪造。
- 根据 JWT 中的声明(如
groups
、
roles
)进行授权决策,控制用户对资源的访问。
-
测试与调试
:
- 使用 Quarkus 的 OIDC WireMock 进行单元测试,避免在测试环境中运行 Keycloak 的复杂性。
- 注意浏览器的 cookie 和证书问题,使用隐身窗口进行测试,信任 Keycloak 的自签名证书。
11. 安全漏洞与防范措施
虽然 OIDC 和 JWT 提供了强大的安全机制,但仍存在一些潜在的安全漏洞,需要采取相应的防范措施:
| 安全漏洞 | 描述 | 防范措施 |
| ---- | ---- | ---- |
| 令牌泄露 | JWT 令牌可能在传输或存储过程中被泄露,导致攻击者获取用户的身份信息和权限。 | 使用 HTTPS 进行安全传输,避免在日志或不安全的存储中记录令牌。设置合理的令牌过期时间,定期刷新令牌。 |
| 重放攻击 | 攻击者可能会捕获并重新发送有效的 JWT 令牌,以获取非法访问权限。 | 使用
jti
声明为每个令牌生成唯一标识符,在服务器端记录已使用的令牌,拒绝重复使用的令牌。 |
| 密钥管理不当 | 如果 JWT 的签名密钥被泄露,攻击者可以伪造有效的令牌。 | 妥善保管签名密钥,使用安全的密钥存储和管理系统。定期更换密钥,提高安全性。 |
| 跨站脚本攻击(XSS) | 攻击者可能通过注入恶意脚本获取用户的 JWT 令牌。 | 对用户输入进行严格的验证和过滤,防止 XSS 攻击。设置
HttpOnly
和
Secure
属性,确保 cookie 只能通过 HTTP 协议访问,并且只能在 HTTPS 连接中传输。 |
12. 未来发展趋势
随着微服务架构的不断发展,安全技术也在不断演进。以下是一些可能的未来发展趋势:
-
多因素身份验证
:除了用户名和密码,结合使用其他因素(如短信验证码、指纹识别、面部识别等)进行身份验证,提高安全性。
-
零信任架构
:不再信任内部和外部网络,对每个请求都进行严格的身份验证和授权,实现细粒度的访问控制。
-
区块链技术
:利用区块链的不可篡改和去中心化特性,实现更安全的身份验证和授权机制。
-
人工智能和机器学习
:使用人工智能和机器学习算法分析用户行为和安全事件,实时检测和防范安全威胁。
通过不断关注和应用这些技术,可以进一步提升微服务的安全性,保护用户的隐私和数据安全。在实际开发中,应根据具体的业务需求和安全要求,选择合适的安全方案和技术,不断优化和完善安全体系。
超级会员免费看
909

被折叠的 条评论
为什么被折叠?



