opa
描述#
opa 插件支持与 Open Policy Agent (OPA) 集成,OPA 是一个统一的策略引擎和框架,可用于定义和执行授权策略。授权逻辑以 Rego 语言编写并存储在 OPA 中。
配置后,OPA 引擎将根据定义的策略评估客户端对受保护路由的请求,以决定是否允许其访问上游资源。
属性#
| 名称 | 类型 | 必选项 | 默认值 | 有效值 | 描述 |
|---|---|---|---|---|---|
| host | string | 是 | OPA 服务器地址。 | ||
| policy | string | 是 | 要评估的策略路径。该值会被直接拼接到 OPA 的 /v1/data/<policy>。data.result 必须是一个包含 allow 字段的对象,并可按需包含 reason、headers、status_code 等字段;因此,policy 应指向返回该对象结构的路径,而不是仅返回 true 或 false 的布尔规则。 | ||
| ssl_verify | boolean | 否 | true | 若为 true,则验证 OPA 服务器的 SSL 证书。 | |
| timeout | integer | 否 | 3000 | [1, 60000] | HTTP 调用超时时间(毫秒)。 |
| keepalive | boolean | 否 | true | 若为 true,则为多个请求保持连接活跃。 | |
| keepalive_timeout | integer | 否 | 60000 | >= 1000 | 连接空闲后关闭的等待时间(毫秒)。 |
| keepalive_pool | integer | 否 | 5 | >= 1 | 空闲连接数。 |
| with_route | boolean | 否 | 若为 true,发送当前路由的信息。 | ||
| with_service | boolean | 否 | 若为 true,发送当前 Service 的信息。 | ||
| with_consumer | boolean | 否 | 若为 true,发送当前消费者的信息。注意,Consumer 信息可能包含 API key 等敏感信息,仅在确认安全的情况下将此选项设为 true。 | ||
| send_headers_upstream | array[string] | 否 | >= 1 | 请求被允许时,需要从 OPA 响应转发到上游服务的请求头名称列表。 |
数据定义#
APISIX 向 OPA 发送信息#
下述示例展示了 APISIX 向 OPA 服务发送的数据格式:
{
"type": "http",
"request": {
"scheme": "http",
"path": "\/get",
"headers": {
"user-agent": "curl\/7.68.0",
"accept": "*\/*",
"host": "127.0.0.1:9080"
},
"query": {},
"port": 9080,
"method": "GET",
"host": "127.0.0.1"
},
"var": {
"timestamp": 1701234567,
"server_addr": "127.0.0.1",
"server_port": "9080",
"remote_port": "port",
"remote_addr": "ip address"
},
"route": {},
"service": {},
"consumer": {}
}
各字段说明如下:
type表示请求类型(http或stream)。request在type为http时使用,包含基本的请求信息(URL、请求头等)。var包含请求连接的基本信息(IP、端口、请求时间戳等)。route、service和consumer包含与 APISIX 中存储的相同数据,仅在这些对象上配置了opa插件时才会发送。
OPA 向 APISIX 返回数据#
下述示例展示了 OPA 服务对 APISIX 的响应数据格式:
{
"result": {
"allow": true,
"reason": "test",
"headers": {
"an": "header"
},
"status_code": 401
}
}
各字段说明如下:
allow是必填字段,表示请求是否允许通过 APISIX 转发。reason、headers和status_code是可选字段,仅在配置了自定义响应时返回。
使用示例#
note
你可以这样从 conf/config.yaml 中获取 admin_key 并存入环境变量:
admin_key=$(yq '.deployment.admin.admin_key[0].key' conf/config.yaml | sed 's/"//g')
在开始之前,你需要一个运行中的 OPA 服务器。可以通过 Docker 启动或部署到 Kubernetes:
- Docker
- Kubernetes
docker run -d --name opa-server -p 8181:8181 openpolicyagent/opa:1.6.0 run --server --addr :8181 --log-level debug
验证 OPA 服务器安装正常且端口已正确暴露:
curl http://127.0.0.1:8181 | grep Version
你应该看到类似如下的响应:
Version: 1.6.0
在集群中创建 OPA 的 Deployment 和 Service:
apiVersion: apps/v1
kind: Deployment
metadata:
namespace: aic
name: opa
spec:
replicas: 1
selector:
matchLabels:
app: opa
template:
metadata:
labels:
app: opa
spec:
containers:
- name: opa
image: openpolicyagent/opa:1.6.0
args:
- run
- --server
- --addr=:8181
- --log-level=debug
ports:
- containerPort: 8181
---
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: opa
spec:
selector:
app: opa
ports:
- port: 8181
targetPort: 8181
将配置应用到集群:
kubectl apply -f opa-server.yaml
等待 OPA Pod 就绪。就绪后,OPA 服务器将在集群内通过 http://opa.aic.svc.cluster.local:8181 访问。如需从集群外部推送策略,请设置端口转发:
kubectl port-forward -n aic svc/opa 8181:8181 &
实现基本策略#
以下示例在 OPA 中实现一个仅允许 GET 请求的基本授权策略。
创建一个仅允许 HTTP GET 请求的 OPA 策略:
curl "http://127.0.0.1:8181/v1/policies/getonly" -X PUT \
-H "Content-Type: text/plain" \
-d '
package getonly
default allow = false
allow if {
input.request.method == "GET"
}'
创建带有 opa 插件的路由:
- Admin API
- ADC
- Ingress Controller
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "opa-route",
"uri": "/anything",
"plugins": {
"opa": {
"host": "http://127.0.0.1:8181",
"policy": "getonly"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
将 host 替换为你的 OPA 服务器地址,policy 设置为 getonly。
services:
- name: opa-service
routes:
- name: opa-route
uris:
- /anything
plugins:
opa:
host: "http://127.0.0.1:8181"
policy: getonly
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
将 host 替换为你的 OPA 服务器地址,policy 设置为 getonly。
将配置同步到网关:
adc sync -f adc.yaml
- Gateway API
- APISIX Ingress Controller
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: opa-plugin-config
spec:
plugins:
- name: opa
config:
host: "http://opa.aic.svc.cluster.local:8181"
policy: getonly
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: opa-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: PathPrefix
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: opa-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
将配置应用到集群:
kubectl apply -f opa-ic.yaml
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: opa-route
spec:
ingressClassName: apisix
http:
- name: opa-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: opa
enable: true
config:
host: "http://opa.aic.svc.cluster.local:8181"
policy: getonly
将配置应用到集群:
kubectl apply -f opa-ic.yaml
向路由发送 GET 请求以验证策略:
curl -i "http://127.0.0.1:9080/anything"
你应该收到 HTTP/1.1 200 OK 响应。
使用 PUT 方法向路由发送请求:
curl -i "http://127.0.0.1:9080/anything" -X PUT
你应该收到 HTTP/1.1 403 Forbidden 响应。
了解数据格式#
以下示例帮助你了解 APISIX 推送给 OPA 的数据格式,以便编写授权逻辑。该示例基于上一个示例中的策略和路由。
更新之前创建的路由上的插件,使其包含路由信息:
- Admin API
- ADC
- Ingress Controller
curl "http://127.0.0.1:9180/apisix/admin/routes/opa-route" -X PATCH \
-H "X-API-KEY: ${admin_key}" \
-d '{
"plugins": {
"opa": {
"with_route": true
}
}
}'
更新 adc.yaml,添加 with_route: true:
services:
- name: opa-service
routes:
- name: opa-route
uris:
- /anything
plugins:
opa:
host: "http://127.0.0.1:8181"
policy: getonly
with_route: true
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
将配置同步到网关:
adc sync -f adc.yaml
- Gateway API
- APISIX Ingress Controller
更新 opa-ic.yaml,添加 with_route: true:
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: opa-plugin-config
spec:
plugins:
- name: opa
config:
host: "http://opa.aic.svc.cluster.local:8181"
policy: getonly
with_route: true
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: opa-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: PathPrefix
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: opa-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
将更新后的配置应用到集群:
kubectl apply -f opa-ic.yaml
更新 opa-ic.yaml,添加 with_route: true:
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: opa-route
spec:
ingressClassName: apisix
http:
- name: opa-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: opa
enable: true
config:
host: "http://opa.aic.svc.cluster.local:8181"
policy: getonly
with_route: true
将更新后的配置应用到集群:
kubectl apply -f opa-ic.yaml
向路由发送请求:
curl -i "http://127.0.0.1:9080/anything"
在 OPA 服务器日志(启用 --log-level debug)中,req_body 将在请求和变量字段之外还包含路由信息。
返回自定义响应#
以下示例演示如何在请求未授权时返回自定义响应码和消息。
创建一个仅允许 HTTP GET 请求、并在未授权时返回 302 及自定义消息的 OPA 策略:
curl "http://127.0.0.1:8181/v1/policies/customresp" -X PUT \
-H "Content-Type: text/plain" \
-d '
package customresp
default allow = false
allow if {
input.request.method == "GET"
}
reason := "The resource has temporarily moved. Please follow the new URL." if {
not allow
}
headers := {
"Location": "http://example.com/auth"
} if {
not allow
}
status_code := 302 if {
not allow
}
'
创建带有 opa 插件的路由:
- Admin API
- ADC
- Ingress Controller
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "opa-route",
"uri": "/anything",
"plugins": {
"opa": {
"host": "http://127.0.0.1:8181",
"policy": "customresp"
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
services:
- name: opa-service
routes:
- name: opa-route
uris:
- /anything
plugins:
opa:
host: "http://127.0.0.1:8181"
policy: customresp
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
将配置同步到网关:
adc sync -f adc.yaml
- Gateway API
- APISIX Ingress Controller
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: opa-customresp-plugin-config
spec:
plugins:
- name: opa
config:
host: "http://opa.aic.svc.cluster.local:8181"
policy: customresp
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: opa-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: PathPrefix
value: /anything
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: opa-customresp-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
将配置应用到集群:
kubectl apply -f opa-ic.yaml
apiVersion: apisix.apache.org/v2
kind: ApisixUpstream
metadata:
namespace: aic
name: httpbin-external-domain
spec:
ingressClassName: apisix
externalNodes:
- type: Domain
name: httpbin.org
---
apiVersion: apisix.apache.org/v2
kind: ApisixRoute
metadata:
namespace: aic
name: opa-route
spec:
ingressClassName: apisix
http:
- name: opa-route
match:
paths:
- /anything
upstreams:
- name: httpbin-external-domain
plugins:
- name: opa
enable: true
config:
host: "http://opa.aic.svc.cluster.local:8181"
policy: customresp
将配置应用到集群:
kubectl apply -f opa-ic.yaml
向路由发送 GET 请求:
curl -i "http://127.0.0.1:9080/anything"
你应该收到 HTTP/1.1 200 OK 响应。
向路由发送 POST 请求:
curl -i "http://127.0.0.1:9080/anything" -X POST
你应该收到 HTTP/1.1 302 Moved Temporarily 响应:
HTTP/1.1 302 Moved Temporarily
...
Location: http://example.com/auth
The resource has temporarily moved. Please follow the new URL.
实现 RBAC#
以下示例演示如何结合 jwt-auth 和 opa 插件实现认证和 RBAC,其中:
user角色仅能读取上游资源。admin角色可以读取和写入上游资源。
为示例消费者 john(user 角色)和 jane(admin 角色)创建 OPA RBAC 策略:
curl "http://127.0.0.1:8181/v1/policies/rbac" -X PUT \
-H "Content-Type: text/plain" \
-d '
package rbac
# Assign roles to users
user_roles := {
"john": ["user"],
"jane": ["admin"]
}
# Map permissions to HTTP methods
permission_methods := {
"read": "GET",
"write": "POST"
}
# Assign role permissions
role_permissions := {
"user": ["read"],
"admin": ["read", "write"]
}
# Get JWT authorization token
bearer_token := t if {
t := input.request.headers.authorization
}
# Decode the token to get role and permission
token := {"payload": payload} if {
[_, payload, _] := io.jwt.decode(bearer_token)
}
# Normalize permission to a list
normalized_permissions := ps if {
ps := token.payload.permission
not is_string(ps)
}
normalized_permissions := [ps] if {
ps := token.payload.permission
is_string(ps)
}
# Implement RBAC logic
default allow = false
allow if {
# Look up the list of roles for the user
roles := user_roles[input.consumer.username]
# For each role in that list
r := roles[_]
# Look up the permissions list for the role
permissions := role_permissions[r]
# For each permission
p := permissions[_]
# Check if the permission matches the request method
permission_methods[p] == input.request.method
# Check if the normalized permissions include the permission
p in normalized_permissions
}
'
在 APISIX 中创建消费者 john 和 jane,并配置 jwt-auth 凭证:
- Admin API
- ADC
- Ingress Controller
curl "http://127.0.0.1:9180/apisix/admin/consumers" \
-X PUT \
-H "X-API-KEY: ${admin_key}" \
-H "Content-Type: application/json" \
-d '{
"username": "john"
}'
curl "http://127.0.0.1:9180/apisix/admin/consumers" \
-X PUT \
-H "X-API-KEY: ${admin_key}" \
-H "Content-Type: application/json" \
-d '{
"username": "jane"
}'
为消费者配置 jwt-auth 凭证,使用默认算法 HS256:
curl "http://127.0.0.1:9180/apisix/admin/consumers/john/credentials" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "cred-john-jwt-auth",
"plugins": {
"jwt-auth": {
"key": "john-key",
"secret": "john-hs256-secret-that-is-very-long"
}
}
}'
curl "http://127.0.0.1:9180/apisix/admin/consumers/jane/credentials" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "cred-jane-jwt-auth",
"plugins": {
"jwt-auth": {
"key": "jane-key",
"secret": "jane-hs256-secret-that-is-very-long"
}
}
}'
consumers:
- username: john
credentials:
- name: cred-john-jwt-auth
type: jwt-auth
config:
key: john-key
secret: john-hs256-secret-that-is-very-long
- username: jane
credentials:
- name: cred-jane-jwt-auth
type: jwt-auth
config:
key: jane-key
secret: jane-hs256-secret-that-is-very-long
将配置同步到网关:
adc sync -f adc.yaml
- Gateway API
- APISIX Ingress Controller
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: john
spec:
gatewayRef:
name: apisix
credentials:
- type: jwt-auth
name: cred-john-jwt-auth
config:
key: john-key
secret: john-hs256-secret-that-is-very-long
---
apiVersion: apisix.apache.org/v1alpha1
kind: Consumer
metadata:
namespace: aic
name: jane
spec:
gatewayRef:
name: apisix
credentials:
- type: jwt-auth
name: cred-jane-jwt-auth
config:
key: jane-key
secret: jane-hs256-secret-that-is-very-long
将配置应用到集群:
kubectl apply -f opa-consumers-ic.yaml
使用 Ingress Controller 时,APISIX 会在消费者名称前加上 Kubernetes 命名空间前缀。例如,aic 命名空间中名为 john 的消费者会变为 aic_john。请相应更新 OPA RBAC 策略中的用户名。
ApisixConsumer CRD 存在已知问题,private_key 在配置时被错误地设为必填项。该问题将在未来版本中修复。目前无法通过 APISIX CRD 完成此示例。
创建路由并配置 jwt-auth 和 opa 插件:
- Admin API
- ADC
- Ingress Controller
curl "http://127.0.0.1:9180/apisix/admin/routes" -X PUT \
-H "X-API-KEY: ${admin_key}" \
-d '{
"id": "opa-route",
"methods": ["GET", "POST"],
"uris": ["/get","/post"],
"plugins": {
"jwt-auth": {},
"opa": {
"host": "http://127.0.0.1:8181",
"policy": "rbac",
"with_consumer": true
}
},
"upstream": {
"type": "roundrobin",
"nodes": {
"httpbin.org:80": 1
}
}
}'
更新 adc.yaml,添加带有 jwt-auth 和 opa 插件的路由:
consumers:
- username: john
credentials:
- name: cred-john-jwt-auth
type: jwt-auth
config:
key: john-key
secret: john-hs256-secret-that-is-very-long
- username: jane
credentials:
- name: cred-jane-jwt-auth
type: jwt-auth
config:
key: jane-key
secret: jane-hs256-secret-that-is-very-long
services:
- name: opa-service
routes:
- name: opa-route
uris:
- /get
- /post
methods:
- GET
- POST
plugins:
jwt-auth: {}
opa:
host: "http://127.0.0.1:8181"
policy: rbac
with_consumer: true
upstream:
type: roundrobin
nodes:
- host: httpbin.org
port: 80
weight: 1
将配置同步到网关:
adc sync -f adc.yaml
- Gateway API
- APISIX Ingress Controller
apiVersion: v1
kind: Service
metadata:
namespace: aic
name: httpbin-external-domain
spec:
type: ExternalName
externalName: httpbin.org
---
apiVersion: apisix.apache.org/v1alpha1
kind: PluginConfig
metadata:
namespace: aic
name: opa-rbac-plugin-config
spec:
plugins:
- name: jwt-auth
config:
_meta:
disable: false
- name: opa
config:
host: "http://opa.aic.svc.cluster.local:8181"
policy: rbac
with_consumer: true
---
apiVersion: gateway.networking.k8s.io/v1
kind: HTTPRoute
metadata:
namespace: aic
name: opa-rbac-route
spec:
parentRefs:
- name: apisix
rules:
- matches:
- path:
type: Exact
value: /get
method: GET
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: opa-rbac-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
- matches:
- path:
type: Exact
value: /post
method: POST
filters:
- type: ExtensionRef
extensionRef:
group: apisix.apache.org
kind: PluginConfig
name: opa-rbac-plugin-config
backendRefs:
- name: httpbin-external-domain
port: 80
将配置应用到集群:
kubectl apply -f opa-route-ic.yaml
ApisixConsumer CRD 存在已知问题,private_key 在配置时被错误地设为必填项。该问题将在未来版本中修复。目前无法通过 APISIX CRD 完成此示例。
以 john 身份验证#
要为 john 生成 JWT,可使用 JWT.io 的 JWT 编码器 或其他工具。若使用 JWT.io 的 JWT 编码器,请执行以下操作:
- 将算法填写为
HS256。 - 将 Valid secret 部分的密钥更新为
john-hs256-secret-that-is-very-long。 - 在 payload 中填入角色
user、权限read、Consumer keyjohn-key,以及 UNIX 时间戳格式的exp或nbf。
你的 payload 应类似如下:
{
"role": "user",
"permission": "read",
"key": "john-key",
"nbf": 1729132271
}
复制生成的 JWT 并存入变量:
export john_jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoidXNlciIsInBlcm1pc3Npb24iOiJyZWFkIiwia2V5Ijoiam9obi1rZXkiLCJuYmYiOjE3MjkxMzIyNzF9.rAHMTQfnnGFnKYc3am_lpE9pZ9E8EaOT_NBQ5Ss8pk4
使用 john 的 JWT 向路由发送 GET 请求:
curl -i "http://127.0.0.1:9080/get" -H "Authorization: Bearer ${john_jwt_token}"
你应该收到 HTTP/1.1 200 OK 响应。
使用相同 JWT 向路由发送 POST 请求:
curl -i "http://127.0.0.1:9080/post" -X POST -H "Authorization: Bearer ${john_jwt_token}"
你应该收到 HTTP/1.1 403 Forbidden 响应。
以 jane 身份验证#
同样地,为 jane 生成 JWT,可使用 JWT.io 的 JWT 编码器 或其他工具。若使用 JWT.io 的 JWT 编码器,请执行以下操作:
- 将算法填写为
HS256。 - 将 Valid secret 部分的密钥更新为
jane-hs256-secret-that-is-very-long。 - 在 payload 中填入角色
admin、权限["read","write"]、Consumer keyjane-key,以及 UNIX 时间戳格式的exp或nbf。
你的 payload 应类似如下:
{
"role": "admin",
"permission": ["read","write"],
"key": "jane-key",
"nbf": 1729132271
}
复制生成的 JWT 并存入变量:
export jane_jwt_token=eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJyb2xlIjoiYWRtaW4iLCJwZXJtaXNzaW9uIjpbInJlYWQiLCJ3cml0ZSJdLCJrZXkiOiJqYW5lLWtleSIsIm5iZiI6MTcyOTEzMjI3MX0.meZ-AaGHUPwN_GvVOE3IkKuAJ1wqlCguaXf3gm3Ww8s
使用 jane 的 JWT 向路由发送 GET 请求:
curl -i "http://127.0.0.1:9080/get" -H "Authorization: Bearer ${jane_jwt_token}"
你应该收到 HTTP/1.1 200 OK 响应。
使用相同 JWT 向路由发送 POST 请求:
curl -i "http://127.0.0.1:9080/post" -X POST -H "Authorization: Bearer ${jane_jwt_token}"
你也应该收到 HTTP/1.1 200 OK 响应。