如何使用 Envoy Gateway 在 API 网关侧基于 OIDC 实现单点登录?
本文详细介绍了如何配置 Envoy Gateway 使用 OIDC 实现单点登录。通过 Auth0 作为身份提供商,演示如何在 API 网关端实现安全、高效的单点登录,提升用户体验和系统安全性。
点击查看本文大纲
在微服务中常见的认证方式详解这篇博客中我们介绍到了 OAuth 2.0 认证,该身份认证协议有多种实现方式,其中最流行的就是 OpenID Connect(OIDC)认证。OIDC 能够为用户提供身份验证和授权。本文将介绍如何使用 Envoy Gateway 在 API 网关级别实现 OIDC 认证。
Envoy Gateway 支持的认证方式
Envoy Gateway 是一个使用 Envoy 实现的高性能的 API 网关,支持多种认证方式来保护 API 和微服务:
- JWT 认证: 使用 JSON Web Tokens(JWT)进行认证。
- mTLS(双向 TLS): 使用双向 TLS 确保客户端和服务器之间的安全通信。
- HTTP Basic 认证: 使用用户名和密码进行基本认证。
- OIDC 认证: 使用 OpenID Connect 协议进行身份验证和授权。
- 外部认证:外部认证调用外部 HTTP 或 gRPC 服务来检查传入的 HTTP 请求是否经过认证。
本文重点介绍如何在 Envoy Gateway 中配置和使用 OIDC 认证从而在网关侧实现单点登录。
什么是 OIDC?
OpenID Connect(OIDC)是一个基于 OAuth 2.0 的身份验证协议。它允许客户端通过认证服务器验证用户身份,并获取有关用户的信息。
OIDC 认证流程如下图所示:
OIDC 为 OAuth 2.0 增加了身份验证层,通过引入 ID Token 和标准化的 UserInfo Endpoint,使 OAuth 2.0 不仅能够用于授权,还可以用于安全地验证用户身份,从而实现单点登录(SSO)和用户身份信息的获取。
为什么要实现单点登录?
单点登录(Single Sign-On, SSO)是一种身份验证方式,它允许用户使用一个账户登录多个独立的应用系统,通过一次身份验证即可无缝访问所有相关应用,减少重复输入用户名和密码的麻烦,从而提升用户体验。SSO 集中管理用户身份和认证,增强了系统安全性,并简化了 IT 管理流程。
对于微服务架构,SSO 尤其重要,因为它在各个微服务之间实现统一的认证和授权,避免了每个服务单独实现身份验证逻辑的需求,减少了用户重复登录的麻烦,并提高了用户体验。集中管理的方式还可以统一应用安全策略,更有效地监控和响应安全事件,提升系统的整体安全性。同时,通过使用标准化的令牌(如 JWT),SSO 简化了微服务之间的身份验证过程,提高了开发效率,让开发人员能够专注于业务逻辑的实现。
示例:使用 Envoy Gateway 和 Auth0 的单点登录
接下来我们将使用 Auth0 作为身份供应商,演示如何使用 Envoy Gateway 在 API 网关端实现单点登录。
你可以在 Bilibili 上查看该示例演示。
基于 Auth0 实现单点登录
首先我们先说明下示例中 Envoy Gateway 基于 Auth0 实现单点登录的详细流程,如下图所示。
步骤说明:
- 用户访问网站:用户通过浏览器访问
https://www.example.com
。 - 请求转发:浏览器向 Envoy Gateway 发送 GET 请求。
- 检查 ID 令牌:Envoy Gateway 检查用户请求的 cookie 是否包含有效的 ID 令牌。
- 重定向到身份提供商:如果没有找到 ID 令牌,Envoy Gateway 将用户重定向到身份提供商(Identity Provider,IdP)的授权端点,在这里是 Auth0,关于 Auth0 如何实现 Login 的详细信息请查看 Auth0 文档。
- 用户登录:用户在身份提供商的登录页面输入用户名和密码,并提交凭证进行登录。
- 获取授权码:用户成功登录后,身份提供商将用户重定向回 Envoy Gateway,并在 URL 中包含授权码。
- 交换 ID 令牌:Envoy Gateway 使用授权码向身份提供商请求 ID 令牌。
- 设置 Cookie:身份提供商返回 ID 令牌,Envoy Gateway 将其设置为用户的 cookie。
- 重定向:Envoy Gateway 将 URL 重定向到
https://www.example.com
。 - 验证 ID 令牌:Envoy Gateway 验证用户请求中的 ID 令牌。
- 路由请求:验证通过后,Envoy Gateway 将请求路由到后端应用(App)。
通过上述流程,Envoy Gateway 实现了单点登录功能。用户的 HTTP 请求在没有得到授权的情况下都会被转发单点登录页面。除了 Auth0 以外,Envoy Gateway 还支持多个身份提供商,如 Azure AD、Keycloak、Okta、OneLogin、Salesforce、UAA 等。
下面我们将按照时序图中的流程配置 Auth0 和 Envoy Gateway。
在 Auth0 上创建应用
请参考以下步骤在 Auth0 上设置一个 Regular Web Application:
- 访问 Auth0 并注册一个免费账户。
- 创建一个新的应用,并选择常规 Web 应用程序。
- 在应用设置中,记录或设置以下字段:
- Domain :
{DOMAIN}
- Client ID:
{CLIENT_ID}
- Client Secret:
{CLIENT_SECRET}
- Allowed Callback URLs:
https://www.example.com/oauth2/myapp/callback
- Allowed Logout URLs:
https://www.example.com/myapp/logout
- Domain :
记住上面的 Auth0 字段,我们将用它们来配置 Envoy Gateway 的安全策略。
下面展示的是 Auth0 的配置页面截图,在设置好用户后并创建普通 Web 应用后,你只需要配置这两个地方。


以上就是 Auth0 的全部配置,接下来我们将安装和配置 Envoy Gateway。
安装 Envoy Gateway
参考 Envoy Gateway 快速开始在 minikube 上安装 Envoy Gateway:
minikube start --driver=docker --cpus=2 --memory=2g
helm install eg oci://docker.io/envoyproxy/gateway-helm --version v1.0.1 -n envoy-gateway-system --create-namespace
kubectl apply -f https://github.com/envoyproxy/gateway/releases/download/v1.0.1/quickstart.yaml -n default
参考安全网关,为 Envoy Gateway 配置 TLS:
# 创建根证书和私钥来签署证书
openssl req -x509 -sha256 -nodes -days 365 -newkey rsa:2048 -subj '/O=example Inc./CN=example.com' -keyout example.com.key -out example.com.crt
# 为 www.example.com 创建证书和私钥
openssl req -out www.example.com.csr -newkey rsa:2048 -nodes -keyout www.example.com.key -subj "/CN=www.example.com/O=example organization"
openssl x509 -req -days 365 -CA example.com.crt -CAkey example.com.key -set_serial 0 -in www.example.com.csr -out www.example.com.crt
# 将证书/密钥存储在 Secret 中
kubectl create secret tls example-cert --key=www.example.com.key --cert=www.example.com.crt
更新快速开始中创建的网关,使其包含 443
端口并引用 example-cert
Secret 的 HTTPS Listener:
echo '[
{
"op": "add",
"path": "/spec/listeners/-",
"value": {
"name": "https",
"protocol": "HTTPS",
"port": 443,
"tls": {
"mode": "Terminate",
"certificateRefs": [
{
"kind": "Secret",
"group": "",
"name": "example-cert"
}
]
}
}
}
]' | kubectl patch gateway eg --type=json --patch-file /dev/stdin
创建 HTTPRoute,为 /myapp
端点增加一条到backend
服务的路由:
cat <<EOF | kubectl apply -f -
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
name: myapp
spec:
parentRefs:
- name: eg
hostnames: ["www.example.com"]
rules:
- matches:
- path:
type: PathPrefix
value: /myapp
backendRefs:
- name: backend
port: 3000
EOF
配置 Envoy Gateway 的 OIDC 认证
创建一个 Kubernetes Secret,用于存储 OAuth Client 的 Client Secret:
kubectl create secret generic auth0-client-secret --from-literal=client-secret=${CLIENT_SECRET}
${CLIENT_SECRET}
替换成你的 Auth0 Client Secret。创建一个安全策略(SecurityPolicy):
cat <<EOF | kubectl apply -f -
apiVersion: gateway.envoyproxy.io/v1alpha1
kind: SecurityPolicy
metadata:
name: oidc-example
spec:
targetRef:
group: gateway.networking.k8s.io
kind: Gateway
name: eg
oidc:
provider:
issuer: "https://${DOMAIN}"
clientID: "${CLIENT_ID}"
clientSecret:
name: "auth0-client-secret"
redirectURL: "https://www.example.com/myapp/oauth2/callback"
logoutPath: "/myapp/logout"
EOF
注意事项
- 此处的
issuer
应该填写成 Auth0 Domain。 redirectURL
的值需要出现在 Auth0 配置的 Allowed Callback URLs 中。logoutPath
是必须的,即使其 URL 端点并为实现 logout 逻辑。
在这个示例中我们为 Envoy Gateway 网关设置了 OIDC,修改 targetRef
到 HTTPRoute,也可以为单个路由设置 OIDC。关于 ODIC 的具体配置,请参考 Envoy Gateway API 文档。
验证单点登录:登入
将 www.example.com
添加到本地的 /etc/hosts
文件中:
echo "127.0.0.1 www.example.com" | sudo tee -a /etc/hosts
配置应用程序的端口转发,以便你可以在本地通过域名访问示例应用:
export ENVOY_SERVICE=$(kubectl get svc -n envoy-gateway-system --selector=gateway.envoyproxy.io/owning-gateway-namespace=default,gateway.envoyproxy.io/owning-gateway-name=eg -o jsonpath='{.items[0].metadata.name}')
sudo kubectl -n envoy-gateway-system port-forward service/${ENVOY_SERVICE} 443:443
现在在浏览器中访问 https://www.example.com,跳过证书风险提示,页面将跳转到 Auth0 的登录界面,如下图所示,选择使用 Google 账户登录。

在登录完成后,浏览器将跳转会 https://www.example.com 页面,并展示 HTTP 请求结果,如下面的 JSON 代码所示。
{
"path": "/",
"host": "www.example.com",
"method": "GET",
"proto": "HTTP/1.1",
"headers": {
/*Omit*/
"Authorization": [
"Bearer eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwiaXNzIjoiaHR0cHM6Ly9kZXYtYXdoam15MzhnZzVxeng3dS51cy5hdXRoMC5jb20vIn0..IiH9LnxmnrGAVy-q.eQV_0Ssetw9mmrEaJNLlBowGJNX51awhh67WSejPrksuGU9e9-DcPJQqmR67ONFzTXWR6CFy4Rfgs4btsmEtvCtiNTCgrBHP90ddbOTg_pK31WnsQ7NThyRfGwoogSaAtK6hFrC2pxFaLj0XL7XvSPk-OaTzK1Zh1da1IM1cmWAWiBRc3nQiVWRDrExPo8-i5SawFe0jIcwytVSaRiX5Polyd3cZ7A7nlei-vDLCfj0HVzOO605nF7ED2dBSnZyev1sg14q598f3X2Vfhi2oJlnbiulGZIlpXgGbcPhzAJJxyEe6qpRpNg7Hbk8Ya-i8gUTwwNysrgm3.Zu5kD_6DzSfZPvwemttXYQ"
],
"Cookie": [
"OauthHMAC-167a6c5=RPdscXEBap0NeSIppJXoxkHt0qvMz4fNHXo2uvgDgIY=; OauthExpires-167a6c5=1716540771; BearerToken-167a6c5=eyJhbGciOiJkaXIiLCJlbmMiOiJBMjU2R0NNIiwiaXNzIjoiaHR0cHM6Ly9kZXYtYXdoam15MzhnZzVxeng3dS51cy5hdXRoMC5jb20vIn0..IiH9LnxmnrGAVy-q.eQV_0Ssetw9mmrEaJNLlBowGJNX51awhh67WSejPrksuGU9e9-DcPJQqmR67ONFzTXWR6CFy4Rfgs4btsmEtvCtiNTCgrBHP90ddbOTg_pK31WnsQ7NThyRfGwoogSaAtK6hFrC2pxFaLj0XL7XvSPk-OaTzK1Zh1da1IM1cmWAWiBRc3nQiVWRDrExPo8-i5SawFe0jIcwytVSaRiX5Polyd3cZ7A7nlei-vDLCfj0HVzOO605nF7ED2dBSnZyev1sg14q598f3X2Vfhi2oJlnbiulGZIlpXgGbcPhzAJJxyEe6qpRpNg7Hbk8Ya-i8gUTwwNysrgm3.Zu5kD_6DzSfZPvwemttXYQ; IdToken-167a6c5=eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCIsImtpZCI6ImFKWkxWbnNrai0tYmhsNlJzVm51OCJ9.eyJpc3MiOiJodHRwczovL2Rldi1hd2hqbXkzOGdnNXF6eDd1LnVzLmF1dGgwLmNvbS8iLCJhdWQiOiJUZzhlNWVoa0xKM2hka3cxTzREMTBQd21QeTcxZHZtdiIsImlhdCI6MTcxNjQ1NDM3MSwiZXhwIjoxNzE2NDkwMzcxLCJzdWIiOiJnb29nbGUtb2F1dGgyfDExMjc0NDc3OTAyMjMzMTA0ODY0MCIsInNpZCI6IjRlSjhDZnZuZjd5Mm1kaE94QXBTY0JiUEhjOS1rZUVLIn0.r9dwIy_HeiO5_I3UlohLkeRES5FGoxqQnwmcA00cA_kdc5mUxgeVopXIhBUjJnTKv7bOUVJvFw21ew4gqVRJfllDyG-s_XfhSW1-lEXmCc2bGYDtOzva6k2S_VRgyMKfG04_DWFuTgO_pLtix28aYq8cGzKJ_VglT_KgRhoktzJu4Js5iCv9JPnydRJmpvRJwX3tDv_Q3mmUSazaLkhOTdiBJFrGlS07qEzJ_iWANZgR8uDNhpXdmlcqpb3MZkkMulr5-jXIgEhBQKpw28tUiSlzh6EpAVuBH9T1w8bUmFRzCc6JPPamJRfflYW5onNgYHDfcU0RpvpsCHRHRAZbdA"
],
/*Omit*/
"User-Agent": [
"Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36"
],
"X-Envoy-Internal": [
"true"
],
"X-Forwarded-For": [
"10.244.0.51"
],
"X-Forwarded-Proto": [
"https"
],
"X-Request-Id": [
"c1e64057-c5c8-4fb2-a304-25c291eeed32"
]
},
"namespace": "default",
"ingress": "",
"service": "",
"pod": "backend-55d64d8794-4qvgd"
}
此时通过 Chrome 浏览器的 Inspector - Application - Cookies 查看到 ID Token 如下图所示:

编写代码 Python 代码,validate_id_token.py
,解析 ID Token 并验证其有效性:
import jwt
import requests
from jwt.algorithms import RSAAlgorithm
import argparse
import json
import base64
def base64url_decode(input):
rem = len(input) % 4
if rem > 0:
input += '=' * (4 - rem)
return base64.urlsafe_b64decode(input)
def get_signing_key(jwk_url, kid):
jwks = requests.get(jwk_url).json()
for jwk in jwks['keys']:
if jwk['kid'] == kid:
return RSAAlgorithm.from_jwk(jwk)
raise Exception('Public key not found.')
def validate_token(token, audience, issuer, jwk_url):
headers = jwt.get_unverified_header(token)
kid = headers['kid']
signing_key = get_signing_key(jwk_url, kid)
decoded_token = jwt.decode(
token,
signing_key,
algorithms=["RS256"],
audience=audience,
issuer=issuer
)
return decoded_token
if __name__ == "__main__":
parser = argparse.ArgumentParser(description='Validate a JWT token.')
parser.add_argument('token', type=str, help='The JWT token to validate')
args = parser.parse_args()
token = args.token
# 解析 token 的 payload 部分,提取 audience 和 issuer
header, payload, signature = token.split('.')
decoded_payload = base64url_decode(payload)
payload_json = json.loads(decoded_payload)
audience = payload_json['aud']
issuer = payload_json['iss']
jwk_url = f"{issuer}.well-known/jwks.json"
try:
decoded = validate_token(token, audience, issuer, jwk_url)
print("Token is valid. Decoded payload:")
for key, value in decoded.items():
print(f"{key}: {value}")
except Exception as e:
print(f"Token validation failed: {e}")
安装依赖的包:
pip install pyjwt requests
运行代码:
python validate_id_token.py ${ID_TOKEN}
你将看到类似下面的输出:
Token is valid. Decoded payload:
iss: https://dev-awhjmy38gg5qzx7u.us.auth0.com/
aud: Tg8e5ehkLJ3hdkw1O4D10PwmPy71dvmv
iat: 1716470905
exp: 1716506905
sub: google-oauth2|112744779022331048640
sid: 4W_hQNJJ8ftDL8S3Cozp8GEu2Au4_e9N
通过该 ID Token 的值可以得出:
- 该令牌由 Auth0 签发(
iss
字段)。 - 该令牌的受众是特定的应用或 API(
aud
字段),对于 Auth0,这个值是 Client ID。 - 令牌是在 2024 年 5 月 23 日 04:08:25 UTC 签发的(
iat
字段),并将在 2024 年 5 月 23 日 14:08:25 UTC 过期(exp
字段)。 - 令牌所代表的主体(用户)的唯一标识符是
google-oauth2|112744779022331048640
(sub
字段)。在这里,这个标识符表明使用 Google OAuth2 登录的用户 - 会话 ID 是
4W_hQNJJ8ftDL8S3Cozp8GEu2Au4_e9N
(sid
字段),用于会话管理。
验证单点登录:登出
由于我们的示例应用中没有实现 Auth0 的 Logout 逻辑,所以我们需要通过 HTTP 请求明确告知 Auth0 要 logout,在浏览器中访问该 URL:
https://${DOMAIN}/v2/logout?client_id=${CLIENT_ID}
请将 ${DOMAIN}
和 ${CLIENT_ID
修改为你的 Auth0 应用程序的配置项。关于 Auth0 如何登出 OIDC 端点的详细说明请查看 Auth0 文档。
登出后,页面将再次跳转到登录页面,在登录后,页面将重定向到 https://www.example.com。
总结
通过以上步骤,你可以在 Envoy Gateway 中实现 OIDC 认证,确保 API 的安全性。这种方法不仅能提供灵活的身份验证机制,还能简化应用程序的身份管理。通过集成 Auth0 等身份提供商,Envoy Gateway 可以轻松实现单点登录,提升用户体验和系统安全性。未来,你可以根据需求进一步配置和优化 Envoy Gateway,充分利用其强大的认证和授权功能,以满足更复杂的安全要求和业务需求。
参考
- Envoy Gateway OIDC Authentication - gateway.envoyproxy.io
- Log Users Out of Auth0 with OIDC Endpoint - auth0.com