在微服务中常见的认证方式详解这篇博客中我们介绍到了 OAuth 2.0 认证,该身份认证协议有多种实现方式,其中最流行的就是 OpenID Connect(OIDC)认证。OIDC 能够为用户提供身份验证和授权。本文将介绍如何使用 Envoy Gateway 在 API 网关级别实现 OIDC 认证。
Envoy Gateway 是一个使用 Envoy 实现的高性能的 API 网关,支持多种认证方式来保护 API 和微服务:
本文重点介绍如何在 Envoy Gateway 中配置和使用 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 简化了微服务之间的身份验证过程,提高了开发效率,让开发人员能够专注于业务逻辑的实现。
接下来我们将使用 Auth0 作为身份供应商,演示如何使用 Envoy Gateway 在 API 网关端实现单点登录。
你可以在 Bilibili 上查看该示例演示。
首先我们先说明下示例中 Envoy Gateway 基于 Auth0 实现单点登录的详细流程,如下图所示。
步骤说明:
https://www.example.com
。https://www.example.com
。通过上述流程,Envoy Gateway 实现了单点登录功能。用户的 HTTP 请求在没有得到授权的情况下都会被转发单点登录页面。除了 Auth0 以外,Envoy Gateway 还支持多个身份提供商,如 Azure AD、Keycloak、Okta、OneLogin、Salesforce、UAA 等。
下面我们将按照时序图中的流程配置 Auth0 和 Envoy Gateway。
请参考以下步骤在 Auth0 上设置一个 Regular Web Application:
{DOMAIN}
{CLIENT_ID}
{CLIENT_SECRET}
https://www.example.com/oauth2/myapp/callback
https://www.example.com/myapp/logout
记住上面的 Auth0 字段,我们将用它们来配置 Envoy Gateway 的安全策略。
下面展示的是 Auth0 的配置页面截图,在设置好用户后并创建普通 Web 应用后,你只需要配置这两个地方。
以上就是 Auth0 的全部配置,接下来我们将安装和配置 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
创建一个 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 的值可以得出:
iss
字段)。aud
字段),对于 Auth0,这个值是 Client ID。iat
字段),并将在 2024 年 5 月 23 日 14:08:25 UTC 过期(exp
字段)。google-oauth2|112744779022331048640
(sub
字段)。在这里,这个标识符表明使用 Google OAuth2 登录的用户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,充分利用其强大的认证和授权功能,以满足更复杂的安全要求和业务需求。
最后更新于 2024/11/22