跳到主内容

为数据源插件添加认证

Grafana 插件可以使用*数据源代理*或通过自定义*后端插件*对第三方 API 执行认证请求。

选择认证方法

通过以下两种方式之一配置您的数据源插件以对第三方 API 进行认证

情况使用
您是否需要使用基本认证(Basic Auth)或 API 密钥对插件进行认证?使用数据源代理。
您的 API 是否支持使用客户端凭据的 OAuth 2.0?使用数据源代理。
您的 API 是否使用数据源代理不支持的自定义认证方法?使用后端插件。
您的 API 是否使用除 HTTP 之外的其他协议进行通信?使用后端插件。
您的插件是否需要告警支持?使用后端插件。

加密数据源配置

数据源插件有两种方式存储自定义配置:jsonDatasecureJsonData

具有查看者(Viewer)角色的用户可以明文访问数据源配置,例如 jsonData 的内容。如果您启用了匿名访问,任何可以在浏览器中访问 Grafana 的人都可以看到 jsonData 的内容。

Grafana Enterprise 的用户可以将数据源访问权限限制给特定用户和团队。有关更多信息,请参阅数据源权限

您可以通过在浏览器开发者控制台中输入 window.grafanaBootData 来查看当前用户有权访问的设置。

警告

不要在 jsonData 中使用敏感数据,例如密码、令牌和 API 密钥。如果您需要存储敏感信息,请改用 secureJsonData

将配置存储在 secureJsonData 中

如果您需要存储敏感信息,请改用 secureJsonData 而不是 jsonData。每当用户保存数据源配置时,secureJsonData 中的秘密信息都会发送到 Grafana 服务器并在存储前进行加密。

加密安全配置后,将无法从浏览器访问。保存后访问秘密信息的唯一方法是使用*数据源代理*

为数据源插件添加秘密配置

要为数据源插件添加秘密信息,您可以添加对配置 API 密钥的支持。

  1. types.ts 中创建一个新接口来存储 API 密钥

    export interface MySecureJsonData {
    apiKey?: string;
    }
  2. 通过更新 ConfigEditor 的 props 以将该接口作为第二个类型参数接受,为 secureJsonData 对象添加类型信息。从 ConfigEditor 内部的 options prop 访问秘密值

    interface Props extends DataSourcePluginOptionsEditorProps<MyDataSourceOptions, MySecureJsonData> {}
    const { secureJsonData, secureJsonFields } = options;
    const { apiKey } = secureJsonData;
    注意

    您可以在用户保存配置之前这样做;当用户保存配置后,Grafana 会清除该值。之后,您可以使用 secureJsonFields 来确定属性是否已配置。

  3. 要在插件的配置编辑器中安全地更新秘密信息,请使用 onOptionsChange prop 更新 secureJsonData 对象

    const onAPIKeyChange = (event: ChangeEvent<HTMLInputElement>) => {
    onOptionsChange({
    ...options,
    secureJsonData: {
    apiKey: event.target.value,
    },
    });
    };
  4. 定义一个可以接受用户输入的组件

    <Input
    type="password"
    placeholder={secureJsonFields?.apiKey ? 'configured' : ''}
    value={secureJsonData.apiKey ?? ''}
    onChange={onAPIKeyChange}
    />
  5. 可选:如果您希望用户能够重置 API 密钥,则需要在 secureJsonFields 对象中将该属性设置为 false

    const onResetAPIKey = () => {
    onOptionsChange({
    ...options,
    secureJsonFields: {
    ...options.secureJsonFields,
    apiKey: false,
    },
    secureJsonData: {
    ...options.secureJsonData,
    apiKey: '',
    },
    });
    };

一旦用户可以配置秘密信息,下一步就是了解如何将它们添加到我们的请求中。

使用数据源代理进行认证

用户保存数据源配置后,秘密数据源配置将不再在浏览器中可用。加密的秘密信息只能在服务器上访问。那么如何将它们添加到请求中呢?

Grafana 服务器带有一个代理,允许您定义请求模板:*代理路由*。Grafana 将代理路由发送到服务器,解密秘密信息以及其他配置,并在发送请求之前将其添加到请求中。

注意

请勿混淆数据源代理与认证代理(auth proxy)。数据源代理用于认证数据源,而认证代理用于登录 Grafana 本身。

为插件添加代理路由

要通过 Grafana 代理转发请求,您需要配置一个或多个*代理路由*。代理路由是代理处理的任何出站请求的模板。您可以在 plugin.json 文件中配置代理路由。

  1. 将路由添加到 plugin.json

    src/plugin.json
    "routes": [
    {
    "path": "example",
    "url": "https://api.example.com"
    }
    ]
    注意

    每次更改 plugin.json 文件时,都需要构建插件并重启 Grafana 服务器。

  2. DataSource 中,将 instanceSettings 中的代理 URL 提取到名为 url 的类属性中

    export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
    url?: string;

    constructor(instanceSettings: DataSourceInstanceSettings<MyDataSourceOptions>) {
    super(instanceSettings);

    this.url = instanceSettings.url;
    }

    // ...
    }
  3. query 方法中,使用 BackendSrv 发出请求。URL 路径的第一部分需要与您的代理路由的 path 匹配。数据源代理会将 this.url + routePath 替换为路由的 url。根据我们的示例,请求的 URL 将是 https://api.example.com/v1/users

    import { getBackendSrv } from '@grafana/runtime';
    const routePath = '/example';

    getBackendSrv().datasourceRequest({
    url: this.url + routePath + '/v1/users',
    method: 'GET',
    });

为插件添加动态代理路由

Grafana 将代理路由发送到服务器,数据源代理在该处解密任何敏感数据,并在发出请求之前用解密的数据填充模板变量。

要将用户定义的配置添加到您的路由中

  • 对于存储在 jsonData 中的配置,使用 `.JsonData`。例如,其中 projectIdjsonData 对象中的属性名称

    src/plugin.json
    "routes": [
    {
    "path": "example",
    "url": "https://api.example.com/projects/{{ .JsonData.projectId }}"
    }
    ]
  • 对于存储在 secureJsonData 中的敏感数据,使用 `.SecureJsonData`。例如,其中 passwordsecureJsonData 对象中的属性名称

    src/plugin.json
    "routes": [
    {
    "path": "example",
    "url": "https://{{ .JsonData.username }}:{{ .SecureJsonData.password }}@api.example.com"
    }
    ]

除了将 URL 添加到代理路由外,您还可以添加请求头、URL 参数和请求体。

为代理路由添加 HTTP 请求头

这是一个将 namecontent 作为 HTTP 请求头的示例

src/plugin.json
"routes": [
{
"path": "example",
"url": "https://api.example.com",
"headers": [
{
"name": "Authorization",
"content": "Bearer {{ .SecureJsonData.apiToken }}"
}
]
}
]

为代理路由添加 URL 参数

这是一个将 namecontent 作为 URL 参数的示例

src/plugin.json
"routes": [
{
"path": "example",
"url": "http://api.example.com",
"urlParams": [
{
"name": "apiKey",
"content": "{{ .SecureJsonData.apiKey }}"
}
]
}
]
注意

请注意,urlParams 配置仅在数据源插件中受支持。它在应用插件中*不受*支持。

为代理路由添加请求体

这是一个将 usernamepassword 添加到请求体的示例

src/plugin.json
"routes": [
{
"path": "example",
"url": "http://api.example.com",
"body": {
"username": "{{ .JsonData.username }}",
"password": "{{ .SecureJsonData.password }}"
}
}
]

为插件添加 OAuth 2.0 代理路由

由于您对每个路由的请求都在服务器端进行,并使用 OAuth 2.0 认证,因此仅支持机器到机器的请求。换句话说,如果您需要使用客户端凭据以外的其他授权类型(grant),则需要自己实现。

要使用 OAuth 2.0 进行认证,请在代理路由定义中添加一个 tokenAuth 对象。如有必要,Grafana 会向 tokenAuth 中定义的 URL 发出请求以检索令牌,然后再向您的代理路由中的 URL 发出请求。Grafana 会在令牌过期时自动续订。

tokenAuth.params 中定义的任何参数都将编码为 application/x-www-form-urlencoded 并发送到令牌 URL。

src/plugin.json
{
"routes": [
{
"path": "api",
"url": "https://api.example.com/v1",
"tokenAuth": {
"url": "https://api.example.com/v1/oauth/token",
"params": {
"grant_type": "client_credentials",
"client_id": "{{ .SecureJsonData.clientId }}",
"client_secret": "{{ .SecureJsonData.clientSecret }}"
}
}
}
]
}
注意

请注意,tokenAuth 配置仅在数据源插件中受支持。它在应用插件中*不受*支持。

使用后端插件进行认证

虽然数据源代理支持 HTTP API 最常见的认证方法,但使用代理路由有一些限制

  • 代理路由仅支持 HTTP 或 HTTPS。
  • 代理路由不支持自定义令牌认证。

如果您的插件存在这些限制中的任何一项,您需要添加一个后端插件。由于后端插件在服务器上运行,它们可以访问解密的秘密信息,这使得实现自定义认证方法更加容易。

解密的秘密信息可以从实例设置的 DecryptedSecureJSONData 字段中获取。

func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
instanceSettings := req.PluginContext.DataSourceInstanceSettings

if apiKey, exists := instanceSettings.DecryptedSecureJSONData["apiKey"]; exists {
// Use the decrypted API key.
}

// ...
}

转发已登录用户的 OAuth 身份

如果您的数据源使用与 Grafana 本身相同的 OAuth 提供者,例如使用通用 OAuth 认证,那么您的数据源插件可以复用已登录 Grafana 用户的访问令牌。

要允许 Grafana 将访问令牌传递给插件,请更新数据源配置并将 jsonData.oauthPassThru 属性设置为 trueDataSourceHttpSettings 设置为此提供了一个开关,即**转发 OAuth 身份**选项。您也可以在数据源配置页面的 UI 中构建一个适当的开关来设置 jsonData.oauthPassThru

配置后,Grafana 可以将诸如 `Authorization` 或 `X-ID-Token` 等授权 HTTP 请求头转发到后端数据源。此信息在 `QueryData`、`CallResource` 和 `CheckHealth` 请求中都可用。

要让 Grafana 转发这些请求头,请使用Grafana plugin SDK for Go 创建一个 HTTP 客户端,并将 `ForwardHTTPHeaders` 选项设置为 `true`(默认情况下为 `false`)。此包公开了请求信息,这些信息随后可以向下游转发或直接在插件内部使用,或两者皆可。

func NewDatasource(ctx context.Context, settings backend.DataSourceInstanceSettings) (instancemgmt.Instance, error) {
opts, err := settings.HTTPClientOptions(ctx)
if err != nil {
return nil, fmt.Errorf("http client options: %w", err)
}

// Important: Reuse the same client for each query to avoid using all available connections on a host.

opts.ForwardHTTPHeaders = true

cl, err := httpclient.New(opts)
if err != nil {
return nil, fmt.Errorf("httpclient new: %w", err)
}
return &Datasource{
httpClient: cl,
}, nil
}

func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
// Necessary to keep the Context, since the injected middleware is configured there
req, err := http.NewRequestWithContext(ctx, http.MethodGet, "https://some-url", nil)
if err != nil {
return nil, fmt.Errorf("new request with context: %w", err)
}
// Authorization header is automatically injected if oauthPassThru is configured
resp, err := ds.httpClient.Do(req)
// ...
}

您可以在这里看到一个完整的插件工作示例:datasource-http-backend

从 HTTP 请求中提取请求头

如果您需要直接访问 HTTP 请求头信息,您也可以从请求中提取该信息

func (ds *dataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
token := strings.Fields(req.GetHTTPHeader(backend.OAuthIdentityTokenHeaderName))
var (
tokenType = token[0]
accessToken = token[1]
)
idToken := req.GetHTTPHeader(backend.OAuthIdentityIDTokenHeaderName) // present if user's token includes an ID token

// ...
return &backend.CheckHealthResult{Status: backend.HealthStatusOk}, nil
}

func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
token := strings.Fields(req.GetHTTPHeader(backend.OAuthIdentityTokenHeaderName))
var (
tokenType = token[0]
accessToken = token[1]
)
idToken := req.GetHTTPHeader(backend.OAuthIdentityIDTokenHeaderName)

for _, q := range req.Queries {
// ...
}
}

func (ds *dataSource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
token := req.GetHTTPHeader(backend.OAuthIdentityTokenHeaderName)
idToken := req.GetHTTPHeader(backend.OAuthIdentityIDTokenHeaderName)

// ...
}

处理 Cookie

转发已登录用户的 Cookie

您的数据源插件可以将已登录 Grafana 用户的 Cookie 转发到数据源。在数据源的配置页面上使用DataSourceHttpSettings 组件。它提供了**允许的 Cookie** 选项,您可以在其中指定 Cookie 名称。

配置后,与授权请求头类似,如果您使用 SDK HTTP 客户端,这些 Cookie 会自动注入。

提取已登录用户的 Cookie

如果需要,您还可以在 `QueryData`、`CallResource` 和 `CheckHealth` 请求中提取 Cookie。

QueryData

func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
cookies:= req.GetHTTPHeader(backend.CookiesHeaderName)

// ...
}

CallResource

func (ds *dataSource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
cookies:= req.GetHTTPHeader(backend.CookiesHeaderName)

// ...
}

CheckHealth

func (ds *dataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
cookies:= req.GetHTTPHeader(backend.CookiesHeaderName)

// ...
}

转发已登录用户的用户请求头

启用send_user_header后,Grafana 会使用 `X-Grafana-User` 请求头将用户请求头传递给插件。您可以转发此请求头,以及授权请求头已配置的 Cookie

QueryData

像这样转发 `QueryData` 请求头

func (ds *dataSource) QueryData(ctx context.Context, req *backend.QueryDataRequest) (*backend.QueryDataResponse, error) {
u := req.GetHTTPHeader("X-Grafana-User")

// ...
}

CallResource

像这样转发 `CallResource` 请求头

func (ds *dataSource) CallResource(ctx context.Context, req *backend.CallResourceRequest, sender backend.CallResourceResponseSender) error {
u := req.GetHTTPHeader("X-Grafana-User")

// ...
}

CheckHealth

像这样转发 `CheckHealth` 请求头

func (ds *dataSource) CheckHealth(ctx context.Context, req *backend.CheckHealthRequest) (*backend.CheckHealthResult, error) {
u := req.GetHTTPHeader("X-Grafana-User")

// ...
}