istio安装

命令行下载Istio

1
curl -L https://istio.io/downloadIstio | sh -

安装目录包含:

  • samples/ 目录下的示例应用程序
  • bin/ 目录下的 istioctl 客户端二进制文件。

把bin目录下的istioctl添加到PATH,方便后续使用istioctl命令操作。

1
export PATH=$PWD/bin:$PATH

安装istio:

1
istioctl install --set profile=demo -y

卸载

1
istioctl uninstall --purge

注入代理

给命名空间添加标签,指示 Istio 在部署应用的时候,自动注入 Envoy 边车代理:

1
kubectl label namespace default istio-injection=enabled

实践

  • 部署
    1
    2
    kubectl apply -f samples/bookinfo/platform/kube/bookinfo.yaml
    kubectl apply -f samples/bookinfo/networking/bookinfo-gateway.yaml
  • 将LB修改为NodePort
    1
    kubectl patch service istio-ingressgateway -n istio-system -p '{"spec":{"type":"NodePort"}}'
    查看80绑定的端口为30984
    1
    kubectl -n istio-system get service istio-ingressgateway
  • 本地
    1
    2
    export INGRESS_HOST=127.0.0.1
    export INGRESS_PORT=30984
  • 最终网关地址
    1
    export GATEWAY_URL=$INGRESS_HOST:$INGRESS_PORT

背景

我们前期有使用过
Ory Hydra之OAuth 2.0 Authorize Code Flow
Ory Hydra之Oauth 2.0 Client Credentials flow
当时采用的并非2.0,本次完整的使用2.0完整的走一遍,并完整的讲解,如何在授权认证流程对接自己的用户系统。

部署

./docker-compose.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
version: "3.7"
services:
hydra:
image: oryd/hydra:v2.0.2
ports:
- "4444:4444" # Public port
- "4445:4445" # Admin port
- "5555:5555" # Port for hydra token user
command: serve -c /etc/config/hydra/hydra.yml all --dev
volumes:
- type: bind
source: ./config
target: /etc/config/hydra
environment:
- DSN=postgres://hydra:secret@postgresd:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4
restart: unless-stopped
depends_on:
- hydra-migrate
networks:
- intranet
hydra-migrate:
image: oryd/hydra:v2.0.2
environment:
- DSN=postgres://hydra:secret@postgresd:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4
command: migrate -c /etc/config/hydra/hydra.yml sql -e --yes
volumes:
- type: bind
source: ./config
target: /etc/config/hydra
restart: on-failure
networks:
- intranet
consent:
environment:
- HYDRA_ADMIN_URL=http://hydra:4445
image: oryd/hydra-login-consent-node:v2.0.2
ports:
- "3000:3000"
restart: unless-stopped
networks:
- intranet
postgresd:
image: postgres:11.8
ports:
- "5432:5432"
environment:
- POSTGRES_USER=hydra
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=hydra
networks:
- intranet
networks:
intranet:

./config/hydra.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
serve:
cookies:
same_site_mode: Lax

urls:
self:
issuer: http://127.0.0.1:4444
consent: http://127.0.0.1:3000/consent
login: http://127.0.0.1:3000/login
logout: http://127.0.0.1:3000/logout

secrets:
system:
- youReallyNeedToChangeThis

oidc:
subject_identifiers:
supported_types:
- pairwise
- public
pairwise:
salt: youReallyNeedToChangeThis

演示

Authorization Code Grant && client credentials Grant

创建客户端

2.0开始,不需要client_id,自动生成一个uuid,client_secret不填写,会自动生成。

  • POST请求 http://localhost:4445/admin/clients
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    {
    "client_name": "crm",
    "token_endpoint_auth_method": "client_secret_basic",
    "redirect_uris": [
    "http://127.0.0.1:5555/callback"
    ],
    "scope": "openid offline",
    "grant_types": [
    "authorization_code",
    "refresh_token",
    "implicit",
    "client_credentials"
    ],
    "response_types": [
    "code",
    "id_token",
    "token"
    ]
    }
  • 响应
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    {
    "client_id": "a9ea2e4c-5c9e-4edd-8a53-09124b870477",
    "client_name": "crm",
    "client_secret": "A2pnWQdJPYokBG9SvN3zKbnlKL",
    "redirect_uris": [
    "http://127.0.0.1:5555/callback"
    ],
    "grant_types": [
    "authorization_code",
    "refresh_token",
    "implicit",
    "client_credentials"
    ],
    "response_types": [
    "code",
    "id_token",
    "token"
    ],
    "scope": "openid offline",
    "audience": [],
    "owner": "",
    "policy_uri": "",
    "allowed_cors_origins": [],
    "tos_uri": "",
    "client_uri": "",
    "logo_uri": "",
    "contacts": null,
    "client_secret_expires_at": 0,
    "subject_type": "public",
    "jwks": {},
    "token_endpoint_auth_method": "client_secret_basic",
    "userinfo_signed_response_alg": "none",
    "created_at": "2022-11-07T07:14:24Z",
    "updated_at": "2022-11-07T07:14:23.930344Z",
    "metadata": {},
    "registration_access_token": "ory_at_E71s0oXkgZJfLeVn4r7dYsvyanvauuPn6AiQ0uGoh2M.a4MwTXBT6z7rRGVdLK_Cmi-rNF_EH09MymOwpBB6QaE",
    "registration_client_uri": "http://127.0.0.1:4444/oauth2/register/a9ea2e4c-5c9e-4edd-8a53-09124b870477",
    "authorization_code_grant_access_token_lifespan": null,
    "authorization_code_grant_id_token_lifespan": null,
    "authorization_code_grant_refresh_token_lifespan": null,
    "client_credentials_grant_access_token_lifespan": null,
    "implicit_grant_access_token_lifespan": null,
    "implicit_grant_id_token_lifespan": null,
    "jwt_bearer_grant_access_token_lifespan": null,
    "refresh_token_grant_id_token_lifespan": null,
    "refresh_token_grant_access_token_lifespan": null,
    "refresh_token_grant_refresh_token_lifespan": null
    }

请求与响应

普遍的流程为以下三个步骤

  • 1、授权请求 Authorization Request 浏览器打开

    1
    2
    3
    4
    5
    6
    7
    8
    GET {认证终点}
    ?response_type=code // 必选项
    &client_id={客户端的ID} // 必选项
    &redirect_uri={重定向URI} // 可选项
    &scope={申请的权限范围} // 可选项
    &state={任意值} // 推荐
    HTTP/1.1
    HOST: {认证服务器}
  • 2、授权响应 Authorization Response 获取code

    1
    2
    3
    4
    HTTP/1.1 302 Found
    Location: {重定向URI}
    ?code={授权码} // 必填
    &state={任意文字} // 如果授权请求中包含 state的话那就是必填
  • 3、令牌请求 Access Token Request code换token

    1
    2
    3
    4
    5
    6
    7
    8
    POST {令牌终点} HTTP/1.1
    Host: {认证服务器}
    Content-Type: application/x-www-form-urlencoded

    grant_type=authorization_code // 必填
    &code={授权码} // 必填 必须是认证服务器响应给的授权码
    &redirect_uri={重定向URI} // 如果授权请求中包含 redirect_uri 那就是必填
    &code_verifier={验证码} // 如果授权请求中包含 code_challenge 那就是必填

我们按照上面这三个步骤来讲解一下Hydra是怎么做的。

  • 1、Hydra 授权请求 Authorization Request 浏览器打开
    1
    2
    3
    4
    5
    GET http://127.0.0.1:4444/oauth2/auth
    ?response_type=code
    &client_id=a9ea2e4c-5c9e-4edd-8a53-09124b870477
    &scope=openid offline
    &state=nqvresaazswwbofkeztgnvfs
    http://127.0.0.1:4444/oauth2/auth?response_type=code&client_id=a9ea2e4c-5c9e-4edd-8a53-09124b870477&scope=openid offline&state=nqvresaazswwbofkeztgnvfs

打开后我们发现,我们被重定向到了http://127.0.0.1:3000/login?login_challenge=9ba37003126244608ab2d4501f9b32f5

Hydra通过步骤1链接步骤2获取code,抽象为两个流程:Login和Consent,这两个流程便于我们对接我们自己系统的用户授权认证.Login流程主要为 登录认证,Consent流程主要为 授权

我来看看./config/hydra.yml中的配置

1
2
3
consent: http://127.0.0.1:3000/consent   // 授权(前端)
login: http://127.0.0.1:3000/login // 登录认证(前端)
logout: http://127.0.0.1:3000/logout // 登出

Login流程

我们发现重定向的位置就是配置中的login,Login流程是一个登录认证服务(前后端),需要我们在自己业务中实现,链接中还携带了login_challenge,

  • 前端
    此时,我们把账号密码以及login_challenge通过接口发往我们后端
    7Kg0sn

  • 后端
    后端拿到用户名密码和login_challenge,做如下2件事

1、自己业务系统的用户名密码校验
2、携带用户信息和login_challenge调用acceptLoginRequest登录请求

登录请求

请求地址:
http://127.0.0.1:4445/admin/oauth2/auth/requests/login/accept?login_challenge=66cc8259bf0c4a3880e26c189968bbd6
请求方式:PUT
请求类型:application/json
请求参数:

1
2
3
4
5
6
7
8
{
"subject": "foo@bar.com",
"acr": "1",
"context": {},
"force_subject_identifier": "2",
"remember": false,
"remember_for": -4068005
}

请求成功返回:

1
2
3
{
"redirect_to": "http://127.0.0.1:4444/oauth2/auth?client_id=624b45d4-ef0f-4bec-a6be-9c18e7103c3e&login_verifier=b458cfc4152a4d9389fc52413087c020&response_type=code&scope=openid+offline&state=nqvresaazswwbofkeztgnvfs"
}

打开这个重定向,就进入了Consent流程

Consent流程

  • 前端
    此时,我们把用户授权以及consent_challenge通过接口发往我们后端
    miv0HD

  • 后端
    后端拿到用户授权以及consent_challenge,做如下2件事
    1、自己业务系统的授权
    2、consent_challenge调用acceptLoginRequest认证请求

认证请求

请求地址:http://127.0.0.1:4445/admin/oauth2/auth/requests/consent/accept?consent_challenge=xxxxxx
请求方式:PUT
请求类型:application/json
请求参数:

说明下,session是可以写你想要放进id_token里面的东西,但是但是!请不要有中文,比如说:”name”:”小白”,这样Hydra也无法识别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
{
"grant_access_token_audience": [],
"grant_scope": [
"openid",
"offline",
],
"handled_at": "2019-04-16T04:45:05.685Z",
"remember": false,
"remember_for": -72766940,
"session": {
"access_token": {},
"id_token": {
"userId": "111"
}
}
}

请求成功返回:

1
2
3
{
"redirect_to": "http://127.0.0.1:4444/oauth2/auth?client_id=624b45d4-ef0f-4bec-a6be-9c18e7103c3e&consent_verifier=2f77bd26c6504ccb8a5e88d65a5818b7&response_type=code&scope=openid+offline&state=nqvresaazswwbofkeztgnvfs"
}

重定向打开后,完成Consent流程,获取到code,此时会将code携带到我们创建应用时候的redirect_uris=http://127.0.0.1:5555/callback

1
http://127.0.0.1:5555/callback?code=ory_ac_0T0UehFyo-BVCDcdiu2qUuxLw4jNLpwFDjqkC157-ms.eUZpm0ZokBUBdxgEI5y5w8BTjf1URAzMwwXddW3gf4Q&scope=openid+offline&state=nqvresaazswwbofkeztgnvfs

token获取

获取令牌、刷新令牌

请求地址:
http://127.0.0.1:5444/oauth2/token
请求方式:POST
请求类型:application/x-www-form-urlencoded

请求参数 参数类型 参数说明
grant_type 字符串 授予类型,必填项
code 字符串 授权码
refresh_token 字符串 刷新令牌
client_id 字符串 客户端id,必填项
client-secret 字符串 客户端秘钥,必填项
redirect_uri 字符串 重定向uri

Authorization使用 Basic Auth 将client_id和client_secret写入

1
2
3
4
5
6
{
"grant_type": "authorization_code",
"client_id": "facebook-photo-backup",
"redirect_uri": "http://localhost:9020/login"
"code": "Qk4jf3dZ_DSkAAtlbS9pTilVFTRCeAYHdPpUN"
}

nN5AYX

Yt3jVA

返回

1
2
3
4
5
6
7
8
{
"access_token": "ory_at_D0wqbtwGY_rtFdy_wEfqhyQGmD7V358y8XWw_94AvGM.cEbzrl27tVxIQO6fJMDBJgCO72OAenBuZgXv1VPUrDc",
"expires_in": 3600,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6IjhmYjRlMjlhLTZlZmItNGIxMy04ODM2LTM5M2ZjM2I1NWUyOSIsInR5cCI6IkpXVCJ9.eyJhY3IiOiJsYWJvIiwiYXRfaGFzaCI6InRiMjcza0kwWWhkYk52by0zQ0FKYmciLCJhdWQiOlsiYTllYTJlNGMtNWM5ZS00ZWRkLThhNTMtMDkxMjRiODcwNDc3Il0sImF1dGhfdGltZSI6MTY2NzgwODIxNSwiZXhwIjoxNjY3ODExODYyLCJpYXQiOjE2Njc4MDgyNjIsImlzcyI6Imh0dHA6Ly8xMjcuMC4wLjE6NDQ0NCIsImp0aSI6ImNlN2UwNGI4LTI1ODUtNGQzYi1hMjBhLTA1OThmMDdjZDUyOCIsInJhdCI6MTY2NzgwODIwMiwic2lkIjoiMDE4ZDc3MWEtMjQyNi00YzhhLWFmZjQtYjQ3MGJlYWM5NGI0Iiwic3ViIjoiZm9vQGJhci5jb20iLCJ1c2VySWQiOiIxMTEifQ.FuUDY0w94H9SPFr8iakHvEo63w9RTVqjHgjzi7gngHgL6sRV3yP9-hZrc4HBZys_PFT5KP_bQra3IKqM-OhF9UZZnfXM4je6HSAW8XdX0PbMZQGut1_5jh8rZjqXPJNY_YL2CNnm4YhID7CO-sEIqcBrVu1O30l44cC93NJJbU9N8wrlHf4H2ROoUhkpPl8WSoRDviUX0NB6dg3Y87q8MDLUTjvQpLNK7SejSI9c6AzNyQneGYBVAVksxItluulWcLgjM98gmZ_35jge5KeOel8q0kpdjbKIOfDCva8PibXoSWZtIvCi4EHYE2aSvu5TL1NlaDhkzE-tuuxjmQJJdIeLOy-kcDFd63t-l3k9dy859UM7B6BNKKFcHmc5bkg2BRf7iZxc7Q6BEvi2F7mrsThJYFtpTNjQCCOsO-E3d2WXi7uFwSI_qQpE5eAcBa0-qivv8RHUqiFIhNDNp1WYk2yDCgqeQx3NokZ03N4oM_CWCnyt2M0WKnofPL0YpnZXiIzxM_KvnqTfZy9ckoVj7gf1H9yZkhQunQVx2oIFIcEqshA1cbvtJ-XN5mZgLAnwSYN3_vRUsW3GQIP4GCT8zf8CVIW-7H5JkWSSLs2DrnexwYEuMX-6TttytflF1FNru4TfF539z9HkBp35-aa8xvh1j-GFSXwlaUr2KKjeTDU",
"refresh_token": "ory_rt_PNaBzXflt2ICDwbh7j68eerGO-8HEtC8S6WzUlTNknQ.6ih8eHalnW5zNwHM2RQktv1WDjSOw3S7mwzUutMMLk8",
"scope": "openid offline",
"token_type": "bearer"
}

最后我们拿着idToken去JWT解析看看效果可以看到,idToken解析出来你需要的信息。
至此我们就获取到了访问令牌access_token,前端可以将令牌缓存在cookie或session中,相应的后台也会缓存,后面前端调用其他服务时携带令牌调用接口,后台校验根据token来判断是否放行。

vXRiND

相关

5min-tutorial
jq

cli相关操作

cli 创建客户端

1
2
3
4
5
6
7
8
9
10
11
code_client=$(docker-compose -f docker-compose.yml exec hydra \
hydra create client \
--endpoint http://127.0.0.1:4445 \
--grant-type authorization_code,refresh_token \
--response-type code,id_token \
--format json \
--scope openid --scope offline \
--redirect-uri http://127.0.0.1:5555/callback)

code_client_id=$(echo $code_client | jq -r '.client_id')
code_client_secret=$(echo $code_client | jq -r '.client_secret')

使用hydra示例授权(Hydra 提供快速验证oauth授权流程)

1
2
3
4
5
6
7
docker-compose -f docker-compose.yml exec hydra \
hydra perform authorization-code \
--client-id $code_client_id \
--client-secret $code_client_secret \
--endpoint http://127.0.0.1:4444/ \
--port 5555 \
--scope openid --scope offline

我们在sidecar构架之dapr授权中间件使用的dapr自身的中间件进行授权校验,但一般情况,我们会使用网关,在网关层进行授权认证,本次我们来使用apisix网关实践一下。
这里我们搭建了oauth2 server,并完成了oidc的搭建OIDC搭建之Ory Hydar 2.0实践

部署

1
2
3
4
5
6
#将 Apache APISIX 的 Docker 镜像下载到本地
git clone https://github.com/apache/apisix-docker.git
# 将当前的目录切换到 apisix-docker/example 路径下
cd apisix-docker/example
# 运行 docker-compose 命令,安装 Apache APISIX
docker-compose -p docker-apisix up -d

Q771x0

使用

apisix-dashboard

apisix-dashboard访问地址:
http://localhost:9000
账号密码:admin / admin

新建Upstream

H1jF7K

新建Route

首先进入Route列表页,点击创建Create如下图:
slFQSW

PMDsIt

路由测试

可以直接通过浏览器访问:http://localhost:9080/web
会发现已经成功路由到Web1和Web2服务,并在二者间切换。

1
2
hello web1
hello web2

插件

jwt-auth

增加消费者

此步骤主要确定 key 和 secret
2f0xCT

1
2
3
4
5
6
7
8
9
10
11
{
"username": "jwt",
"plugins": {
"jwt-auth": {
"disable": false,
"exp": 86400,
"key": "user",
"secret": "user"
}
}
}

开启路由jwt-auth

对应上 key secret
bCVinE

测试

开启一个 public-api 用于 apisix 提供的token生成及校验

1
2
3
4
 {
"disable": false,
"uri": "/apisix/plugin/jwt/sign"
}

生成一个token

1
2
3
curl https://api.XXX.com/apisix/plugin/jwt/sign?key=user

eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJrZXkiOiJ1c2VyIiwiZXhwIjoxNjcyMjI3OTMxfQ.IK0rx-ScZM5c70FSql1VnRmTM1dfs1KoOpCgfqwS_YQ
  • 有token
    1
    2
    curl --location --request GET 'https://api.XXX.com/test/v1' \
    --header 'Authorization: Bearer eyJ0eXAiOiJKV1QiLCJhbGciOiJIUzI1NiJ9.eyJrZXkiOiJ1c2VyIiwiZXhwIjoxNjcyMjI3OTMxfQ.IK0rx-ScZM5c70FSql1VnRmTM1dfs1KoOpCgfqwS_YQ'
    1
    2

    * 没token
    curl –location –request GET ‘https://api.XXX.com/test/v1
    1
    2
    3
    4
    ```
    {
    "message": "Missing JWT token in request"
    }

ZHeWif

我们搭建和走了一下oauth2的流程
Ory Hydra之OAuth 2.0 Authorize Code Flow
Ory Hydra之Oauth 2.0 Client Credentials flow

接下来我们配置一个 OAuth 中间件来说明下 Dapr 中间件授权的使用方法。

OAuth 2.0 Authorize Code 中间件示例

我们分别使用github和自建的oauth2.0进行尝试

github

我们尝试以github来走一遍OAuth 2.0 Authorize Code

创建github oauth app

Q4SKZz

4w6KLf

注意这里的Authorization callback URL,就是你授权通过,github通过这个填写地址(www.example.com)回调一个code,然后你可以通过code去请求token

~/.dapr/components/oauth2.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: oauth2
spec:
type: middleware.http.oauth2
version: v1
metadata:
- name: clientId
value: "d9d90c604c41aea4a0ac"
- name: clientSecret
value: "b1e75686aafd69253aac0e532432bace331f5be7"
- name: scopes
value: "https://www.googleapis.com/auth/userinfo.email"
- name: authURL
value: "https://github.com/login/oauth/authorize"
- name: tokenURL
value: "https://github.com/login/oauth/access_token"
- name: redirectURL
value: "http://www.example.com"
- name: authHeaderName
value: "authorization"
- name: forceHTTPS
value: "false"

~/.dapr/config.yaml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: daprConfig
spec:
httpPipeline:
handlers:
- name: oauth2
type: middleware.http.oauth2
- name: uppercase
type: middleware.http.uppercase
nameResolution:
component: "consul"
configuration:
client:
address: "10.8.99.45:8500"
selfRegister: false

z2cVKK

当这一步成功授权,github会授权回调并携带code到 www.example.com?code=XXX 我们拿到code,再向githu去申请token即可

自建 TODO:

Ory Hydra之OAuth 2.0 Authorize Code Flow

Oauth2.0搭建之Ory Hydar 2.0实践

OAuth2 client credentials中间件示例

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: oauth2clientcredentials
spec:
type: middleware.http.oauth2clientcredentials
version: v1
metadata:
- name: clientId
value: "<your client ID>"
- name: clientSecret
value: "<your client secret>"
- name: scopes
value: "https://www.googleapis.com/auth/userinfo.email"
- name: tokenURL
value: "https://accounts.google.com/o/oauth2/token"
- name: headerName
value: "authorization"

Dapr配置

1
2
3
4
5
6
7
8
9
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: appconfig
spec:
httpPipeline:
handlers:
- name: oauth2clientcredentials
type: middleware.http.oauth2clientcredentials

自建

Ory Hydra之Oauth 2.0 Client Credentials flow

背景

我们使用Hydra v1.11.10版本,并使用docker-compose来部署Hydra,本次将验证一下Client Credentials flow

部署

这次我们使用docker-compose来部署,参考自quickstart.yml

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
version: "3.7"
services:
hydra:
image: oryd/hydra:v1.11.10
ports:
- "4444:4444" # Public port
- "4445:4445" # Admin port
- "5555:5555" # Port for hydra token user
command: serve all --dangerous-force-http
environment:
- DSN=postgres://hydra:secret@postgresd:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4
restart: unless-stopped
depends_on:
- hydra-migrate
networks:
- hydranet
hydra-migrate:
image: oryd/hydra:v1.11.10
environment:
- DSN=postgres://hydra:secret@postgresd:5432/hydra?sslmode=disable&max_conns=20&max_idle_conns=4
command: migrate sql -e --yes
restart: on-failure
networks:
- hydranet
postgresd:
image: postgres:9.6
ports:
- "5432:5432"
environment:
- POSTGRES_USER=hydra
- POSTGRES_PASSWORD=secret
- POSTGRES_DB=hydra
networks:
- hydranet
networks:
hydranet:
1
docker-compose -f quickstart.yml up --build

postgresd 数据库
hydra-migrate 初始化数据库
hydra 创建认证服务

创建应用并验证Client Credentials flow

通过Hydra CLI

创建应用

1
2
3
4
5
6
7
8
9
docker-compose -f quickstart.yml exec hydra hydra clients create \
--endpoint http://localhost:4445/ \
--id my-client \
--secret my-secret \
--grant-types client_credentials \
--scope api

You should not provide secrets using command line flags, the secret might leak to bash history and similar systems
OAuth 2.0 Client ID: my-client

生成 token

1
2
3
4
5
6
docker-compose -f quickstart.yml exec hydra hydra token client \
--endpoint http://localhost:4444/ \
--client-id my-client \
--client-secret my-secret

vRm9SR63-7vuhdMhZs72PT9Uhj4HQXCL3QrKVRja_yI.jpXIW0ichJFr4ANUSMVvXwL7CFEuNmCQNdUU6FgkGHc

验证token

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
docker-compose -f quickstart.yml exec hydra hydra token introspect \
--endpoint http://localhost:4445/ \
vRm9SR63-7vuhdMhZs72PT9Uhj4HQXCL3QrKVRja_yI.jpXIW0ichJFr4ANUSMVvXwL7CFEuNmCQNdUU6FgkGHc

{
"active": true,
"aud": [],
"client_id": "my-client",
"exp": 1661071059,
"iat": 1661067458,
"iss": "http://localhost:4444/",
"nbf": 1661067458,
"sub": "my-client",
"token_type": "Bearer",
"token_use": "access_token"
}

通过rest api

创建应用

1
curl -X POST 'http://localhost:5445/clients' -H 'Content-Type: application/json' --data-raw '{ "client_id": "my-client", "client_name": "MyClientApp", "client_secret": "my-secret", "grant_types": ["client_credentials"], "scope": "api" }'

ydc36b

生成 token

1
curl -u 'my-client:my-secret' -X POST 'http://localhost:5444/oauth2/token' -H 'Content-Type: application/x-www-form-urlencoded' --data-raw 'grant_type=client_credentials&scope=api'
1
2
3
4
5
6
{
"access_token": "fKPlJDSuZ2fJ0gGhDzf3GIfIv2RKxp03wN9FRhAyub4.3PAALlq2b1TU6i8n-IbSPEhM3GNXwANV9S3Tw4A4DrQ",
"expires_in": 3599,
"scope": "api",
"token_type": "bearer"
}

0HHVyk
jM7mrU

验证token

1
curl -X POST 'http://localhost:4445/oauth2/introspect' -H 'Content-Type: application/x-www-form-urlencoded' --data-raw 'token=fKPlJDSuZ2fJ0gGhDzf3GIfIv2RKxp03wN9FRhAyub4.3PAALlq2b1TU6i8n-IbSPEhM3GNXwANV9S3Tw4A4DrQ'

dmpnF4

背景

单独把身份认证服务部署起来,在调用其他服务的时候,先通过身份认证。

因为Hydra版本出现了大的变动,即将2.0,改动巨大,本篇暂时以v1.10.6进行

本次我们来走一遍 OAuth 2.0 Authorize Code Flow

部署

启动Hydra服务

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
# 创建一个独立的网段
docker network create hydraguide

# 拉取pg,当然也可使用mysql等其他数据库
docker pull postgres:9.6

# 拉取hydra
docker pull oryd/hydra:v1.10.6

# 运行数据库(帐号:hydra 密码:secret)
docker run \
--network hydraguide \
--name ory-hydra-example--postgres \
-e POSTGRES_USER=hydra \
-e POSTGRES_PASSWORD=secret \
-e POSTGRES_DB=hydra \
-d postgres:9.6

# 设置加密
export SECRETS_SYSTEM=$(export LC_CTYPE=C; cat /dev/urandom | tr -dc 'a-zA-Z0-9' | fold -w 32 | head -n 1)
# 当然你也可以写死
export SECRETS_SYSTEM=SHARINGTOMMY123456789

# 创建临时的环境变量 DSN
export DSN=postgres://hydra:secret@ory-hydra-example--postgres:5432/hydra?sslmode=disable

# 初始化数据库
docker run -it --rm \
--network hydraguide \
oryd/hydra:v1.10.6 \
migrate sql --yes $DSN

# 启动一个Hydra服务
docker run -d \
--name ory-hydra-example--hydra \
--network hydraguide \
-p 5444:4444 \
-p 5445:4445 \
-e SECRETS_SYSTEM=$SECRETS_SYSTEM \
-e DSN=$DSN \
-e URLS_SELF_ISSUER=http://localhost:5444/ \
-e URLS_CONSENT=http://localhost:9020/consent \
-e URLS_LOGIN=http://localhost:9020/login \
oryd/hydra:v1.10.6 serve all --dangerous-force-http

# 验证(能看到正常启动日志)
docker logs ory-hydra-example--hydra

说明:
--network hydraguide 网络
-p 5444:4444 public API http://localhost:5444/
-p 5445:4445 Hydra’s administrative API http://localhost:5445/
-e SECRETS_SYSTEM=$SECRETS_SYSTEM 加密变量
-e DSN=$DSN 数据库变量
-e URLS_SELF_ISSUER=http://localhost:5444/ 是你的服务器地址
-e URLS_CONSENT=http://localhost:9020/consent 是你前端用户同意授权地址
-e URLS_LOGIN=http://localhost:9020/login 是前端用户登录地址
-e URLS_LOGOUT 是你退出登录地址
-e URLS_POST_LOGOUT_REDIRECT 是你退出登录成功后跳转到的地址
-e TTL_ID_TOKEN id_token 过期时间的设置单位 h m s,默认为1小时
-e TTL_ACCESS_TOKEN 配置刷新令牌有效的时间。默认值为720h。设置为-1可使刷新令牌永不过期。
-e TTL_REFRESH_TOKEN配置标识令牌有效的时间。默认为1小时。

–dangerous-force-http 加了这句话就是不需要 https
如果你不加的话,URLS_SELF_ISSUER=https://localhost:4444/ 这里就要加s
加了https,https会有证书等问题。

登录/授权样例网站启动

该部分一般就是我们的前端登录授权页面,只是hydra提供了一个示例

1
2
3
4
5
6
7
8
docker pull oryd/hydra-login-consent-node:v1.10.6
docker run -d \
--name ory-hydra-example--consent \
-p 9020:3000 \
--network hydraguide \
-e HYDRA_ADMIN_URL=http://ory-hydra-example--hydra:4445 \
-e NODE_TLS_REJECT_UNAUTHORIZED=0 \
oryd/hydra-login-consent-node:v1.10.6

-p 9020:3000暴露9020端口,这个端口就是URLS_CONSENTURLS_LOGIN(URLS_CONSENT=http://localhost:9020/consent, URLS_LOGIN=http://localhost:9020/login).
HYDRA_ADMIN_URL=http://ory-hydra-example--hydra:4445 Hydra后台管理接口
NODE_TLS_REJECT_UNAUTHORIZED=0 取消TLS校验

示例页面:
fGU37K

OexXJh

演示OAuth2.0流程

图解

3YxYQv
xCH2Wd
TvxaSN
iFMa3c
CKoZI4
v6NTz1
icA7m3
0N432n
ITxNk0
qJn2BQ
GAmN24
8P0SbL

请求与响应

  • 授权请求 Authorization Request 浏览器打开

    1
    2
    3
    4
    5
    6
    7
    8
    GET {认证终点}
    ?response_type=code // 必选项
    &client_id={客户端的ID} // 必选项
    &redirect_uri={重定向URI} // 可选项
    &scope={申请的权限范围} // 可选项
    &state={任意值} // 推荐
    HTTP/1.1
    HOST: {认证服务器}
  • 授权响应 Authorization Response 获取code

    1
    2
    3
    4
    HTTP/1.1 302 Found
    Location: {重定向URI}
    ?code={授权码} // 必填
    &state={任意文字} // 如果授权请求中包含 state的话那就是必填
  • 令牌请求 Access Token Request code换token

    1
    2
    3
    4
    5
    6
    7
    8
    POST {令牌终点} HTTP/1.1
    Host: {认证服务器}
    Content-Type: application/x-www-form-urlencoded

    grant_type=authorization_code // 必填
    &code={授权码} // 必填 必须是认证服务器响应给的授权码
    &redirect_uri={重定向URI} // 如果授权请求中包含 redirect_uri 那就是必填
    &code_verifier={验证码} // 如果授权请求中包含 code_challenge 那就是必填

根据具体情况有可能是向客户端服务器进行请求,这时候请加上 Basic 认证(Authorization 头部)或者是 参数 client_id & client_secret

  • 令牌响应 Access Token Response
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    HTTP/1.1 200 OK
    Content-Type: application/json;charset=UTF-8
    Cache-Control: no-store
    Pragma: no-cache

    {
    "access_token":"{访问令牌}", // 必填
    "token_type":"{令牌类型}", // 必填
    "expires_in":{过期时间}, // 任意
    "refresh_token":"{刷新令牌}", // 任意
    "scope":"{授权范围}" // 如果请求和响应的授权范围不一致就必填
    }

Hydra演示

创建一个facebook-photo-backup应用并获得id和secret

通过 Hydra CLI 命令创建
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
docker run --rm -it \
-e HYDRA_ADMIN_URL=http://ory-hydra-example--hydra:4445 \
--network hydraguide \
oryd/hydra:v1.10.6 \
clients create --skip-tls-verify \
--id facebook-photo-backup \
--secret some-secret \
--grant-types authorization_code,refresh_token,client_credentials,implicit \
--response-types token,code,id_token \
--scope openid,offline,photos.read \
--callbacks http://127.0.0.1:9010/callback


You should not provide secrets using command line flags, the secret might leak to bash history and similar systems
OAuth 2.0 Client ID: facebook-photo-backup

测试环境会有提示You should not provide secrets using command line flags, the secret might leak to bash history and similar systems,忽略即可。
此时我们得到

1
2
Client ID: facebook-photo-backup
Client Secret: some-secret
通过 rest api 创建

创建应用客户端
P3Hu1k

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
POST http://localhost:5445/clients
{
"client_id": "facebook-photo-backup1",
"client_secret": "some-secret",
"token_endpoint_auth_method": "client_secret_basic",
"redirect_uris": [
"http://127.0.0.1:9010/callback"
],
"scope": "openid offline photos.read",
"grant_types": [
"authorization_code",
"refresh_token",
"implicit",
"client_credentials"
],
"response_types": [
"code",
"id_token",
"token"
]
}

执行一个 OAuth 2.0 授权流程

使用CLI简易

以下示例将执行一个 OAuth 2.0 授权流程。为简化此操作,Hydra CLI 提供了一个名为 hydra token user 的辅助命令。

1
2
3
4
5
6
7
8
9
10
11
docker run --rm -it \
--network hydraguide \
-p 9010:9010 \
oryd/hydra:v1.10.6 \
token user --skip-tls-verify \
--port 9010 \
--auth-url http://localhost:5444/oauth2/auth \
--token-url http://ory-hydra-example--hydra:4444/oauth2/token \
--client-id facebook-photo-backup \
--client-secret some-secret \
--scope openid,offline,photos.read

上面这个服务的过程请参考 下面的 使用Hydra api,其实就是简化接口参数过程。

打开 http://127.0.0.1:9010/

ggvyYz

fGU37K

OexXJh

SCz661

这个时候我们使用Access Token去调用userinfo API,即可正常获取到用户信息.

1
2
3
4
5
6
7
curl -X GET \
http://localhost:5444/userinfo \
-H 'authorization: Bearer 6BboGKvjsyXG_ZFqX8NQboVi4v4JFqiLoKRZ2ex3QRI.fowTdOf3QqCojmmKTdm5MveKulsv-vcEkp2KdiROAMI' \
-H 'cache-control: no-cache' \
-H 'content-type: application/json' \
-H 'postman-token: fecb7032-db0a-bb1b-a61b-de22add7e5bc'

得到如下信息,sub就是用户的信息,为什么这里用户信息只有一个sub呢?因为他们实现ory-hydra-example–consent的时候什么都没加进去,根据自己需要的信息加入sub就可以了。

1
2
3
4
5
6
7
8
{
"subject": "foo@bar.com",
"acr": "labo",
"context": "<object>",
"force_subject_identifier": "ex fugiat aliquip amet dolore",
"remember": false,
"remember_for": -4068005
}
使用Hydra api

YljCLk
HTSmvp

整个过程最终目的就是要获取授权码,然后通过授权码去拿token,通过图可以看出获取授权码一共需要两个流程:LoginConsent

下面我们就开始完整的演示一遍获取授权码的接口流程:

浏览器打开:
http://localhost:5444/oauth2/auth?&client_id=facebook-photo-backup&response_type=code&scope=openid&state=nqvresaazswwbofkeztgnvfs

Hydra服务器会302重定向到你在创建的时候设定的前端登录地址:http://localhost:9020/login?login_challenge=xxxxxxxxxx,带着login_challenge回来,这个东西就是下面接口需要的东西

7Kg0sn
这时候,我们前端应该进行身份验证的提交,并携带login_challenge到后端服务,后端接收到,通过对用户名和密码的校验后,请求acceptLoginRequest

登录请求

请求地址:
http://localhost:5445/oauth2/auth/requests/login/accept?login_challenge=66cc8259bf0c4a3880e26c189968bbd6
请求方式:PUT
请求类型:application/json
请求参数:

1
2
3
4
5
6
7
8
{
"subject": "foo@bar.com",
"acr": "labo",
"context": "<object>",
"force_subject_identifier": "ex fugiat aliquip amet dolore",
"remember": false,
"remember_for": -4068005
}

请求成功返回:

1
2
3
{
"redirect_to": "http://localhost:5444/oauth2/auth?client_id=facebook-photo-backup&login_verifier=289c0dbac2eb49e28eebc4b8f208b8c8&response_type=code&scope=openid&state=nqvresaazswwbofkeztgnvfs"
}

lwBuzu

MpbDZl

我们再通过浏览器打开

Login认证流程就结束了,把请求成功返回的结果,继续丢到浏览器中,他会302重定向到你创建hydra的时候设定的consent地址,并携带consent_challenge,,我们再通过前端提交把consent_challenge 的值传递给后端,让后端拿到下面去继续请求

认证请求

请求地址:
http://localhost:5445/oauth2/auth/requests/consent/accept?consent_challenge=xxxxxx
请求方式:PUT
请求类型:application/json
请求参数:
说明下,session是可以写你想要放进id_token里面的东西,但是但是!请不要有中文,比如说:”name”:”小白”,这样Hydra也无法识别

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
{
"grant_access_token_audience": [],
"grant_scope": [
"openid"
],
"handled_at": "2019-04-16T04:45:05.685Z",
"remember": true,
"remember_for": -72766940,
"session": {
"access_token": {},
"id_token": {
"userId": "111"
}
}
}

请求成功返回:

1
2
3
{
"redirect_to": "http://localhost:4444/oauth2/auth?client_id=tommy&consent_verifier=373e5b86d4444fe2a78390df64efc9b1&prompt=&response_type=code&scope=openid&state=nqvresaazswwbofkeztgnvfs"
}

z5hXVM
把结果的验证接口地址继续放到浏览器中回车,hydra服务器会重定向到你创建应用时候设置的callback地址,并且后面带着code,如:http://127.0.0.1:9010/callback?code=xxxxxxxxx,拿到这个code到下面的接口,就可以请求获取到Token了。

Jd7Vsk

获取令牌、刷新令牌

请求地址:
http://localhost:5444/oauth2/token
请求方式:POST
请求类型:application/x-www-form-urlencoded

请求参数 参数类型 参数说明
grant_type 字符串 授予类型,必填项
code 字符串 授权码
refresh_token 字符串 刷新令牌
client_id 字符串 客户端id,必填项
client-secret 字符串 客户端秘钥,必填项
redirect_uri 字符串 重定向uri
1
2
3
4
5
6
{
"grant_type": "authorization_code",
"client_id": "facebook-photo-backup",
"redirect_uri": "http://localhost:9020/login"
"code": "Qk4jf3dZ_DSkAAtlbS9pTilVFTRCeAYHdPpUN"
}

返回

1
2
3
4
5
6
7
8
{
"access_token": "nRNAO1g8LNMI2i_FJXQDoLxvHL7aLz4sILhWoL_de4w.EcGjSbAlCmsBSPlE_KtNk99AcFVaE_eqye0Sh41dXSk",
"expires_in": 299,
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6InB1YmxpYzpjNDk2N2ZkYi0yYTg0LTRhNjAtODRlZi02M2Q3MGFmNTRmNzIifQ.eyJhdF9oYXNoIjoia05wVkVkWGhTa2U4MmdvSk81SUFqUSIsImF1ZCI6WyJ6c3ctY2xvdWQtZGV2Il0sImF1dGhfdGltZSI6MTY1ODYyOTIxOSwiZXhwIjoxNjU4NjI5NTI2LCJpYXQiOjE2NTg2MjkyMjYsImlzcyI6Imh0dHBzOi8venN3LWh5ZHJhLmNvbS8iLCJqdGkiOiI1ZmIwMWM4NC1mNTZmLTQ0MGUtYjFmZC1iOTBjODNhY2ZkODIiLCJyYXQiOjE2NTg2MjkyMTIsInNpZCI6IjU2OWY2ZDczLTgzZDEtNDBlOS04YmMxLWFlMjFhY2QzOGYyZCIsInN1YiI6InpzdyJ9.OaAlvwFY84BU2_fF8RxsXK_ueoURmvIMl_Xa7xZ566laeZdJ8GyONzrlGDSLwNNhdKV8Mcl3U8aNoGZDb5w3DRca9C0rqaedo-r4zMrsAZ-YNUAXvuv_Ga-n_MDPA2FxLF0vz1Til48jkbWhQ0QmJnT_m6DvUo4veVjtbU6Ggbz2-rYO7adW2rp1gf4I_AwwUOjfBtQmqZRPNvQIkX-Md-bQfhqnGikMEkeoZdYuZP3ags6H1cm3E8eMLyJk4kGXGkMosSKLE8LFh1HrXYQfCDwCVpL1dy_-b0ZKyj20RVVdusBzdb97MV4QFeKleuyGIRBXHI0etW9EELOVjPWcz59tuE29uToSopiEArFpeCotsh4nllFxqtvqRM4zh5ZMjf6MIHpm74IW8nVlXdCVjBjzZp3Lg3th7iWEDrZm_9tZ1o0SmYYwf9IbjjttrIaBbph-iTm5aijN6WHrKM0HNOcrERrcK4REcSFKueL46-yHRKmOhwXNROJHZQu3mTpZRO8BnR3eWBsRuFmVGLt8BKi8s_fAR7AI__WN1y8rek2_34LnAVrh8CJQnzBAIB-9y6AeGH8a9t_tqxkJWeLa8ohXVH8VTceKkCMNm_7x9vvhhqlb8lyVau9ktvkIgoalyGRmBf66FZQkxpDFht0XiC7ZGq9IusI-fDSIcGRuJa8",
"refresh_token": "emWnXUsZ43Sb9_eR0HyxirTltitFX0rvv2ouwRHdZ6U.4zKlg3iCdoHco02jryMHj432xzz0yQrh41zaQejp52M",
"scope": "openid offline",
"token_type": "bearer"
}

最后我们拿着idToken去JWT解析看看效果可以看到,idToken解析出来你需要的信息。
至此我们就获取到了访问令牌access_token,前端可以将令牌缓存在cookie或session中,相应的后台也会缓存,后面前端调用其他服务时携带令牌调用接口,后台校验根据token来判断是否放行。

以上就是获取token的整个流程。

当然token也有过期的时候,下面说一下刷新令牌,接口和上面获取token是同一个http://localhost:5444/oauth2/token,只是传参不同

1
2
3
4
5
6
7
{
"grant_type": "refresh_token",
"client_id": "facebook-photo-backup",
"redirect_uri": "http://localhost:9020/login"
// refresh_token是获取token时的refresh_token,不是access_token
"refresh_token": "emWnXUsZ43Sb9_eR0HyxirTltitFX0rvv2ouwRHdZ6U.4zKlg3iCdoHco02jryMHj432xzz0yQrh41zaQejp52M"
}

可用于用户登出

请求地址:
http://localhost:5444/oauth2/revoke
请求方式:POST
请求类型:application/x-www-form-urlencoded

1
2
3
{
"token":""
}

可用于获取用户标识或校验token是否存活

请求地址:
http://localhost:5444/oauth2/introspect
请求方式:POST
请求类型:application/x-www-form-urlencoded

1
2
3
{
"token":""
}

相关链接

Run Ory Hydra in Docker
deprecate –dangerous-force-http flag
[简易图解]『 OAuth2.0』 猴子都能懂的图解
[简易图解]『 OAuth2.0』 『进阶』 授权模式总结
微服务Token方案之ORY Hydra授权中心_Java实现

背景

Dapr 允许通过链接一系列中间件组件来定义自定义处理管道。 请求在路由到用户代码之前经过所有已定义的中间件组件,然后在返回到客户机之前,按相反顺序经过已定义的中间件,如下图中所示。
DdyQpe

中间件示例

uppercase中间件示例

我们拿 sidecar构架之dapr的跨物理机负载均衡的部署来继续实践一下将http请求的body内容转换为大写。

配置dapr3虚拟机

因为我们是通过dapr3(service2) 去 调用dapr1和dapr2的(service1)服务,那么我们在入口dapr3机器上配置中间件

~/.dapr/components/uppercase.yaml

1
2
3
4
5
6
7
apiVersion: dapr.io/v1alpha1
kind: Component
metadata:
name: uppercase
spec:
type: middleware.http.uppercase
version: v1

~/.dapr/config.yaml

1
2
3
4
5
6
7
8
9
10
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: pipeline
namespace: default
spec:
httpPipeline:
handlers:
- name: uppercase
type: middleware.http.uppercase

增加接口

我们增加一个/word的post接口,接口返回目标机器的ip和我们body参数的英文字母

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
const express = require('express')
const os = require('os');
const app = express()
const bodyParser = require('body-parser')
const port = 3000
app.use(bodyParser.json());// 添加json解析
app.use(bodyParser.urlencoded({extended: false}));

function getIpAddress() {
var interfaces=os.networkInterfaces()

for (var dev in interfaces) {
let iface = interfaces[dev]

for (let i = 0; i < iface.length; i++) {
let {family, address, internal} = iface[i]

if (family === 'IPv4' && address !== '127.0.0.1' && !internal) {
return address
}
}
}
}

function randomCoding(){
//创建26个字母数组
var arr = ['a','b','c','d','e','f','g','h','i','j','k','l','m','n','o','p','q','r','s','t','u','v','w','x','y','z'];
var idvalue ='';
const n = 4;
for(var i=0;i<n;i++){
idvalue+=arr[Math.floor(Math.random()*26)];
}
return idvalue;
}

app.post('/word', (req, res) => {
const ipAddress = getIpAddress()
const body = req.body
body.ip = ipAddress
res.send(body)
})

app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})

调用结果

我们来调用/word接口,发现我们的body请求参数abc,通过middleware.http.uppercase大小写中间件,转换为大写,并向下游的service1发送请求,下游2台负载均衡的service1服务,均接受到中间件改变的大写参数。
1JMjA6

lg1r3L

相关链接

OAuth2 client credentials

服务调用示意

dapr调用示意

ZidVEq

sidecar 之间的通信都是 gRPC (3、6)
application与sidecar之间的通信是http/grpc (1、7、4、5)

  1. 服务 A 对服务 B 发起HTTP/gRPC的调用。
  2. Dapr使用服务注册发现平台的名称解析组件发现服务B的位置。(例如:mDNS、consul等)
  3. Dapr 将消息转发至服务 B的 Dapr 边车
    注: Dapr 边车之间的所有调用考虑到性能都优先使用 gRPC。 仅服务与 Dapr 边车之间的调用可以是 HTTP 或 gRPC
  4. 服务 B的 Dapr 边车将请求转发至服务 B 上的特定端点 (或方法) 。 服务 B 随后运行其业务逻辑代码。
  5. 服务 B 发送响应给服务 A。 响应将转至服务 B 的边车。
  6. Dapr 将消息转发至服务 A 的 Dapr 边车。
  7. 服务 A 接收响应。

python和node服务示例

ipdCVT

  1. Node.js应用程序有一个app IDnodeapp的Dapr应用程序。 当python应用程序通过 POST http://localhost:3500/v1.0/invoke/nodeapp/method/neworder 调用 Node.js 应用程序的 neworder方法时, 首先会到达python app的本地dapr sidecar。
  2. Dapr 使用本地机器运行的名称解析组件(在这种情况下自动运行的 mDNS),发现 Node.js 应用的位置。
  3. Dapr 使用刚刚收到的位置将请求转发到 Node.js 应用的 sidecar。
  4. Node.js 应用的 sidecar 将请求转发到 Node.js 应用程序。 Node.js 应用执行其业务逻辑,记录收到的消息,然后将订单 ID 存储到 Redis (未在图表中显示)中
  5. Node.js应 用程序通过 Node.js sidecar 向 Python 应用程序发送一个响应。
  6. Dapr 转发响应到 Python 的 Dapr sidecar
  7. Python 应用程序收到响应。

API 和端口

Dapr runtime 对外提供两个 API,分别是 Dapr HTTP API 和 Dapr gRPC API。另外两个 dapr runtime 之间的通讯 (Dapr internal API) 固定用 gRPC 协议。

两个 Dapr API 对外暴露的端口

  • 3500: HTTP 端口,可以通过命令行参数 dapr-http-port 设置
  • 50001: gRPC 端口,可以通过命令行参数 dapr-grpc-port 设置
1
dapr run --app-id nodeapp --app-port 3000 --dapr-http-port 3500 --dapr-grpc-port 50001 node app.js

Dapr internal API 是内部端口,比较特殊,没有固定的默认值,而是取任意随机可用端口。也可以通过命令行参数 dapr-internal-grpc-port 设置。

为了向服务器端的应用发送请求,dapr 需要获知应用在哪个端口监听并处理请求,这个信息通过命令行参数 app-port 设置。Dapr 的示例中一般喜欢用 3000 端口。

实践

我们使用官方的教程来实验一下

启动服务

我们先开启node的订单服务

1
2
3
cd ./hello-world/node
npm install
dapr run --app-id nodeapp --app-port 3000 --dapr-http-port 3500 node app.js
1
ℹ️  Starting Dapr with id nodeapp. HTTP Port: 3500. gRPC Port: 54626

服务解析

启动后,我们先看看订单服务有什么操作
创建订单:/neworder

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
app.post('/neworder', async (req, res) => {
const data = req.body.data;
const orderId = data.orderId;
console.log("Got a new order! Order ID: " + orderId);

const state = [{
key: "order",
value: data
}];

try {
const response = await fetch(stateUrl, {
method: "POST",
body: JSON.stringify(state),
headers: {
"Content-Type": "application/json"
}
})
if (!response.ok) {
throw "Failed to persist state.";
}
console.log("Successfully persisted state.");
res.status(200).send();
} catch (error) {
console.log(error);
res.status(500).send({message: error});
}
});

获取订单:/order

1
2
3
4
5
6
7
8
9
10
11
12
13
14
app.get('/order', async (_req, res) => {
try {
const response = await fetch(`${stateUrl}/order`)
if (!response.ok) {
throw "Could not get state.";
}
const orders = await response.text();
res.send(orders);
}
catch (error) {
console.log(error);
res.status(500).send({message: error});
}
});

服务调用

http调用方式

yMot68

使用 Dapr cli 调用
1
2
3
dapr invoke --app-id nodeapp --method neworder --data-file sample.json
// sample.json
{"data":{"orderId":"42"}}
1
2
3
4
// 创建订单
dapr invoke --app-id nodeapp --method neworder --data '{"data": { "orderId": "42" } }'
// 获取订单
dapr invoke --app-id nodeapp --method order --verb GET
使用curl调用

通过dapr的Endpoint

1
2
3
4
// 创建订单
curl -XPOST -d @sample.json -H Content-Type:application/json http://localhost:3500/v1.0/invoke/nodeapp/method/neworder
// 获取订单
curl http://localhost:3500/v1.0/invoke/nodeapp/method/order

通过Node程序自己的Endpoint,这样不通过Dapr Sidecar。

1
curl -XPOST -d @sample.json -H "Content-Type:application/json" http://localhost:3000/neworder
使用Postman调用

6iLdya
0cTGFL

使用SDK

默认HTTP

1
2
3
4
5
6
7
8
9
10
11
12
13
import { DaprClient } from "@dapr/dapr";
const client = new DaprClient(daprHost, daprPort);

const serviceAppId = "nodeapp";
const serviceMethod = "neworder";

// POST Request
const response = await client.invoker.invoke(serviceAppId , serviceMethod , HttpMethod.POST, { data: {"orderId":"42"} });

const serviceMethod2 = "order";
// GET Request
const response = await client.invoker.invoke(serviceAppId , serviceMethod2 , HttpMethod.GET);

另一个程序语言的服务调用(python)

接下来部署Python的程序。Python也得先装运行环境:

1
2
3
cd ./hello-world/python
sudo apt install python3-pip
dapr run --app-id pythonapp --dapr-http-port 3501 python3 app.py

启动成功了。因为Python自己不提供服务,所以–app-port不用指定。–dapr-http-port是3501,这是自己的Sidecar用的端口,不能跟别人的重了。再看看刚才Node的窗口,不停的有新的Request过来,就是Python程序来的每隔一秒的Request。
最后看一下dapr list的结果:

1
2
3
APP ID     HTTP PORT  GRPC PORT  APP PORT  COMMAND         AGE  CREATED              PID
nodeapp 3500 35485 3000 node app.js 41m 2020-12-27 00:54.54 18395
pythonapp 40175 33349 0 python3 app.py 1m 2020-12-27 01:36.27 31185

GRPC调用方式

ryUFBq

GRPC调用方式,在application得起一个GRPC服务,然后通过dapr调用application的GRPC服务

使用SDK
1
2
3
4
5
6
7
import { DaprClient, CommunicationProtocol } from "@dapr/dapr";
const client = new DaprClient(daprHost, daprPort, CommunicationProtocol.GRPC);

// Create a Proxy that allows us to use our gRPC code

const clientProxy = await client.proxy.create<GreeterClient>(GreeterClient);

前提

本次我们将实践dapr的mDns和consul的负载均衡

服务示例

  1. service1是一个获取ip的nodejs项目,分别使用dapr部署在dapr1dapr2的虚拟机上
  2. service2是另外一个服务,调用server1的getIp服务,部署在dapr3的虚拟机上

我们的目标是通过postman访问service2,调用部署在dapr1、dapr2的getIp服务,看看是否能达到负载均衡

service1核心代码:

index.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
const express = require('express')
const os = require('os');
const app = express()
const port = 3000

function getIpAddress() {
var interfaces=os.networkInterfaces()

for (var dev in interfaces) {
let iface = interfaces[dev]

for (let i = 0; i < iface.length; i++) {
let {family, address, internal} = iface[i]

if (family === 'IPv4' && address !== '127.0.0.1' && !internal) {
return address
}
}
}
}

app.get('/getIp', (req, res) => {
const ipAddress = getIpAddress()
res.send(ipAddress)
})

app.listen(port, () => {
console.log(`Example app listening on port ${port}`)
})

mDns负载均衡(局域网内)

1
2
3
4
5
6
7
8
9
10
11
12

dapr1 192.168.10.201: dapr部署service1
dapr run --dapr-http-port 3600 --app-port 3000 --app-id service1 node index.js


dapr2 192.168.10.202: dapr部署service1
dapr run --dapr-http-port 3600 --app-port 3000 --app-id service1 node index.js


dapr3: 192.168.10.203: dapr部署service2
dapr run --dapr-http-port 3500 --app-id service2

多次访问,可看到达到了负载的效果

1
curl http://192.168.10.203:3500/v1.0/invoke/service1/method/getIp
1
2
3
4
192.168.10.202
192.168.10.201
192.168.10.202
192.168.10.201

consul的负载均衡(夸机房,不需要在同一局域网)

前面的负载我们使用了mDNS,官方还为我们提供了Consul名字解析组件,我们新建一个虚拟机Consul(192.168.10.204)来安装Consul
安装步骤可参考:Consul集群-服务自动发现实践

Q7hLRH

下面修改Dapr1和Dapr2机器上的~/.dapr/config.yaml配置,让其使用Consul来解析名字

1
2
3
4
5
6
7
8
9
10
11
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: daprConfig
spec:
nameResolution:
component: "consul"
configuration:
client:
address: "192.168.10.204:8500"
selfRegister: true

修改Dapr3机器上的~/.dapr/config.yaml配置

1
2
3
4
5
6
7
8
9
10
11
apiVersion: dapr.io/v1alpha1
kind: Configuration
metadata:
name: daprConfig
spec:
nameResolution:
component: "consul"
configuration:
client:
address: "10.8.99.45:8500"
selfRegister: false

当我们重新启动dapr1和dapr2的service1后,我们看到,已经注册到consul上去了。
fmRPJh

问题

待解决
consul如何自动剔除失效服务

Consul移除失效服务

1
curl -X PUT http://127.0.0.1:8500/v1/agent/service/deregister/{service_id}

Consul移除正常关机nodes节点

1
curl -X PUT http://127.0.0.1:8500/v1//agent/force-leave/{node}

目标

consul集群服务+consul-template+nginx实现nginx反向代理地址的自动更新

  • consul:自动发现、自动更新,为容器提供服务(添加、删除、生命周期)
  • registrator(自动发现+注册到consul-server端)
  • consul-template模板(更新)

核心机制: 后端每更新一个容器,会向registrator进行注册,控制consul完成更新操作,consul会触发consul-template模板进行热更新(reload)

启动Consul集群

启动4个consul,其中consul1 是主节点,consul2、consul3 是子节点。consul4是提供ui服务的。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
version: '3.5'
services:
consul1:
image: consul:latest
container_name: consul1
restart: always
command: agent -server -client=0.0.0.0 -bootstrap-expect=3 -node=consul1
volumes:
- ./consul1/data:/consul/data
- ./consul1/config:/consul/config
consul2:
image: consul:latest
container_name: consul2
restart: always
command: agent -server -client=0.0.0.0 -retry-join=consul1 -node=consul2
volumes:
- ./consul2/data:/consul/data
- ./consul2/config:/consul/config
consul3:
image: consul:latest
container_name: consul3
restart: always
command: agent -server -client=0.0.0.0 -retry-join=consul1 -node=consul3
volumes:
- ./consul3/data:/consul/data
- ./consul3/config:/consul/config
consul4:
image: consul:latest
container_name: consul4
restart: always
ports:
- 8500:8500
command: agent -client=0.0.0.0 -retry-join=consul1 -ui -node=client1
volumes:
- ./consul4/data:/consul/data
- ./consul4/config:/consul/config

EVl7kb

  • -server:表示当前使用的server模式;如果没有指定,则表示是client模式。
  • -node:指定当前节点在集群中的名称。
  • -config-dir:指定配置文件路径,定义服务的;路径下面的所有.json结尾的文件都被访问;缺省值为:/consul/config。
  • -data-dir: consul存储数据的目录;缺省值为:/consul/data。
  • -datacenter:数据中心名称,缺省值为dc1。
  • -ui:使用consul自带的web UI界面 。
  • -join:加入到已有的集群中。
  • -enable-script-checks: 检查服务是否处于活动状态,类似开启心跳。
  • -bind: 绑定服务器的ip地址。
  • -client: 客户端可访问ip,缺省值为:“127.0.0.1”,即仅允许环回连接。
  • -bootstrap-expect:在一个datacenter中期望的server节点数目,consul启动时会一直等待直到达到这个数目的server才会引导整个集群。这个参数的值在同一个datacenter的所有server节点上必须保持一致。

consul 端口

端口 说明
TCP/8300 8300端口用于服务器节点。客户端通过该端口RPC协议调用服务端节点
TCP/UDP/8301 8301端口用于单个数据中心所有节点之间的互相通信, 即对LAN池信息的同步。她使得整个数据中心能够自动发现服务器地址,分布式检测节点故障,事件广播(如领导选举事件)
TCP/UDP/8302 8302端口用于单个或多个数据中心之间的服务器节点的信息同步,即对WAN池信息的同步。它针对互联网的高延迟进行了优化,能够实现跨数据中心请求
8500 8500端口基于HTTP协议,用于API接口或者WEB UI访问
8600 8600端口作为DNS服务器,它使得我们可以通过节点名查询节点信息

通过http api 获取集群信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
# 查看集群server成员
[root@localhost consul]# curl 127.0.0.1:8500/v1/status/peers
[
"172.23.0.5:8300",
"172.23.0.3:8300",
"172.23.0.4:8300"
]

# 查看集群Raf leader
[root@localhost consul]# curl 127.0.0.1:8500/v1/status/leader
"172.23.0.4:8300"

# 查看注册的所有服务
[root@localhost consul]# curl 127.0.0.1:8500/v1/catalog/services
{"consul":[]}

# 查看nginx服务的信息
[root@localhost consul]# curl 127.0.0.1:8500/v1/catalog/nginx

# 集群节点详细信息
[root@localhost consul]# curl 127.0.0.1:8500/v1/catalog/nodes
[{"ID":"dc6703d1-2324-c388-d5bc-226d7d79e733","Node":"client1","Address":"172.23.0.2","Datacenter":"dc1","TaggedAddresses":{"lan":"172.23.0.2","lan_ipv4":"172.23.0.2","wan":"172.23.0.2","wan_ipv4":"172.23.0.2"},"Meta":{"consul-network-segment":""},"CreateIndex":8,"ModifyIndex":11},{"ID":"f328a0ce-ba8b-b270-f2f0-850f2f762334","Node":"consul1","Address":"172.23.0.4","Datacenter":"dc1","TaggedAddresses":{"lan":"172.23.0.4","lan_ipv4":"172.23.0.4","wan":"172.23.0.4","wan_ipv4":"172.23.0.4"},"Meta":{"consul-network-segment":""},"CreateIndex":5,"ModifyIndex":10},{"ID":"324bef0c-fa7e-ae47-3bcc-6f06e45f8e4b","Node":"consul2","Address":"172.23.0.5","Datacenter":"dc1","TaggedAddresses":{"lan":"172.23.0.5","lan_ipv4":"172.23.0.5","wan":"172.23.0.5","wan_ipv4":"172.23.0.5"},"Meta":{"consul-network-segment":""},"CreateIndex":7,"ModifyIndex":13},{"ID":"d59cbbc9-d1b9-da27-5c9b-3e35eabae824","Node":"consul3","Address":"172.23.0.3","Datacenter":"dc1","TaggedAddresses":{"lan":"172.23.0.3","lan_ipv4":"172.23.0.3","wan":"172.23.0.3","wan_ipv4":"172.23.0.3"},"Meta":{"consul-network-segment":""},"CreateIndex":9,"ModifyIndex":12}]

部署registrator

1
2
3
4
5
6
7
8
9
10
version: "3.5"
services:
registrator:
image: gliderlabs/registrator:latest
container_name: registrator
restart: always
network_mode: host
volumes:
- "/var/run/docker.sock:/tmp/docker.sock"
command: consul://10.8.99.45:8500

consul-template自动加入nginx集群

1
2
3
4
5
cd /opt/
wget https://releases.hashicorp.com/consul-template/0.26.0/consul-template_0.26.0_linux_amd64.zip

# 解压到指定目录,推荐/usr/bin/目录下,可以直接使用
tar -xf consul-template_0.26.0_linux_amd64.zip -C /usr/bin/

配置模板文件

vim /data/template/nginx.ctmpl

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
upstream http_backend {         
{{range service "nginx"}}
server {{.Address}}:{{.Port}};
{{end}}
}

server {
listen 1216; ##监听consul的端口,这是代理端口,consul是作为一个代理服务,访问后端的容器服务
server_name localhost 10.8.99.45; ##监听本地地址,监听代理端的地址
access_log /var/log/nginx/access.log; ##nginx日志目录,如果是编译安装的nginx需要自行创建
index index.html index.php;
location / { ##反向代理的信息,代理的头部信息
proxy_set_header HOST $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Client-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://http_backend; ##跳转到服务器池的地址和端口
}
}

安装nginx

1
yum install -y nignx
1
2
3
4
cat /etc/nginx/nginx.conf
...
include /etc/nginx/conf.d/*.conf;
...

启动consul-template,指定template模板文件及生成路径

1
2
3
# 进入监控状态
[root@localhost ~]# consul-template -consul-addr 10.8.99.45:8500 -template \ "/data/template/nginx.ctmpl:/etc/nginx/conf.d/app.conf:nginx -s reload" \
--log-level=info
  • /data/template/nginx.ctmpl 模板文件
  • /etc/nginx/conf.d/app.conf 生成的配置文件
  • nginx -s reload,重载服务
  • -log-level=info,指定日志级别

查看生成后的文件

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
cat /etc/nginx/conf.d/app.conf

upstream http_backend {

server 10.8.99.45:81;

server 10.8.99.45:82;

server 10.8.99.45:83;

server 10.8.99.45:84;

}
server {
listen 1216;
server_name localhost 10.0.0.14;
access_log /var/log/nginx/access.log;
index index.html index.php;
location / {
proxy_set_header HOST $host;
proxy_set_header X-Real-IP $remote_addr;
proxy_set_header Client-IP $remote_addr;
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
proxy_pass http://http_backend;
}
}

由此完成通过consul的实时新增删除服务,然后compose-template可以实现动态增加服务节点到nginx代理的配置文件,这样就形成了服务的自动化增减

0%