为数据源插件添加身份验证
Grafana 插件可以使用数据源代理或通过自定义的 后端插件 对第三方 API 进行身份验证请求。
选择一种身份验证方法
配置您的数据源插件以通过第三方 API 进行身份验证,有以下两种方式之一
案例 | 使用 |
---|---|
您需要使用基本身份验证或 API 密钥来验证您的插件吗? | 使用数据源代理。 |
您的 API 是否支持使用客户端凭据的 OAuth 2.0? | 使用数据源代理。 |
您的 API 是否使用数据源代理不支持的自定义身份验证方法? | 使用后端插件。 |
您的 API 是否通过除 HTTP 以外的协议进行通信? | 使用后端插件。 |
您的插件是否需要警报支持? | 使用后端插件。 |
加密数据源配置
数据源插件有两种存储自定义配置的方式: jsonData
和 secureJsonData
。
具有查看器角色的用户可以访问数据源配置,例如以明文形式访问jsonData
的内容。如果您已启用匿名访问,任何可以访问浏览器中Grafana的人都可以看到jsonData
的内容。
Grafana企业版的用户可以限制对数据源的访问,仅限于特定的用户和团队。更多信息,请参阅数据源权限。
您可以通过在浏览器开发者控制台中输入window.grafanaBootData
来查看当前用户可以访问的设置。
不要使用jsonData
与敏感数据(如密码、令牌和API密钥)一起使用。如果您需要存储敏感信息,请使用secureJsonData
。
在secureJsonData
中存储配置
如果您需要存储敏感信息,请使用secureJsonData
而不是jsonData
。每当用户保存数据源配置时,secureJsonData
中的机密就会被发送到Grafana服务器,并在存储之前进行加密。
一旦加密了安全配置,就无法从浏览器中访问。在保存之后访问这些机密的唯一方法是通过使用数据源代理。
将密钥配置添加到您的数据源插件
要将密钥添加到数据源插件,您可以为配置API密钥添加支持。
-
在
types.ts
中创建一个新的接口来保存API密钥export interface MySecureJsonData {
apiKey?: string;
} -
通过更新
ConfigEditor
的props以接受该接口作为第二个类型参数,将类型信息添加到您的secureJsonData
对象中。从ConfigEditor
内部的options
prop中访问密钥的值interface Props extends DataSourcePluginOptionsEditorProps<MyDataSourceOptions, MySecureJsonData> {}
const { secureJsonData, secureJsonFields } = options;
const { apiKey } = secureJsonData;注意您可以在用户保存配置之前这样做;当用户保存配置后,Grafana将清除值。之后,您可以使用
secureJsonFields
来确定是否已配置该属性。 -
要安全地更新插件配置编辑器中的机密,请使用
onOptionsChange
prop更新secureJsonData
对象const onAPIKeyChange = (event: ChangeEvent<HTMLInputElement>) => {
onOptionsChange({
...options,
secureJsonData: {
apiKey: event.target.value,
},
});
}; -
定义一个可以接受用户输入的组件
<Input
type="password"
placeholder={secureJsonFields?.apiKey ? 'configured' : ''}
value={secureJsonData.apiKey ?? ''}
onChange={onAPIKeyChange}
/> -
可选:如果您希望用户能够重置API密钥,则需要在
secureJsonFields
对象中将属性设置为false
const onResetAPIKey = () => {
onOptionsChange({
...options,
secureJsonFields: {
...options.secureJsonFields,
apiKey: false,
},
secureJsonData: {
...options.secureJsonData,
apiKey: '',
},
});
};
一旦用户可以配置密钥,下一步就是了解我们如何将它们添加到我们的请求中。
使用数据源代理进行身份验证
一旦用户保存了数据源的配置,加密的数据源配置将不再在浏览器中可用。加密的秘密只能在服务器上访问。那么您如何在请求中添加它们呢?
Grafana服务器自带一个代理,允许您定义请求模板:代理路由。Grafana将代理路由发送到服务器,解密秘密以及其他配置,然后在发送请求之前将其添加到请求中。
请确保不要混淆数据源代理与认证代理。数据源代理用于验证数据源,而认证代理用于登录Grafana本身。
将代理路由添加到您的插件
要转发通过Grafana代理的请求,您需要配置一个或多个代理路由。代理路由是代理处理的任何传出请求的模板。您可以在plugin.json文件中配置代理路由。
-
将路由添加到
plugin.json
src/plugin.json"routes": [
{
"path": "example",
"url": "https://api.example.com"
}
]注意每次您更改
plugin.json
文件时,都需要构建您的插件并重新启动Grafana服务器。 -
在
DataSource
中,将代理URL从instanceSettings
提取到一个名为url
的类属性中export class DataSource extends DataSourceApi<MyQuery, MyDataSourceOptions> {
url?: string;
constructor(instanceSettings: DataSourceInstanceSettings<MyDataSourceOptions>) {
super(instanceSettings);
this.url = instanceSettings.url;
}
// ...
} -
在
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
中的配置。例如,如果projectId
是jsonData
对象中属性的名称src/plugin.json"routes": [
{
"path": "example",
"url": "https://api.example.com/projects/{{ .JsonData.projectId }}"
}
] -
使用
.SecureJsonData
配置存储在secureJsonData
中的敏感数据。例如,如果password
是secureJsonData
对象中属性的名称src/plugin.json"routes": [
{
"path": "example",
"url": "https://{{ .JsonData.username }}:{{ .SecureJsonData.password }}@api.example.com"
}
]
除了将URL添加到代理路由外,您还可以添加头信息、URL参数和请求正文。
将HTTP头添加到代理路由
以下是将name
和content
作为HTTP头添加的示例
"routes": [
{
"path": "example",
"url": "https://api.example.com",
"headers": [
{
"name": "Authorization",
"content": "Bearer {{ .SecureJsonData.apiToken }}"
}
]
}
]
将URL参数添加到代理路由
以下是将name
和content
作为URL参数添加的示例
"routes": [
{
"path": "example",
"url": "http://api.example.com",
"urlParams": [
{
"name": "apiKey",
"content": "{{ .SecureJsonData.apiKey }}"
}
]
}
]
请注意,urlParams
配置仅在数据源插件中受支持。它不支持在 应用插件 中。
向代理路由添加请求体
以下是将 username
和 password
添加到请求体的示例
"routes": [
{
"path": "example",
"url": "http://api.example.com",
"body": {
"username": "{{ .JsonData.username }}",
"password": "{{ .SecureJsonData.password }}"
}
}
]
将 OAuth 2.0 代理路由添加到您的插件中
由于您对每个路由的请求都通过 OAuth 2.0 身份验证在服务器端进行,因此仅支持机器到机器的请求。换句话说,如果您需要使用除客户端凭证之外的其他授权方式,您需要自行实现。
要使用 OAuth 2.0 进行身份验证,请向代理路由定义中添加一个 tokenAuth
对象。如果需要,Grafana 将在向代理路由中的 URL 发送请求之前,向 tokenAuth
中定义的 URL 发送请求以获取令牌。Grafana 会自动在令牌过期时更新令牌。
在 tokenAuth.params
中定义的任何参数都将编码为 application/x-www-form-urlencoded
并发送到令牌 URL。
{
"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
属性设置为 true
。DataSourceHttpSettings 设置提供了一个切换按钮,即 转发 OAuth 身份 选项。您也可以在数据源配置页面 UI 中构建适当的切换按钮来设置 jsonData.oauthPassThru
。
配置完成后,Grafana 可以将授权 HTTP 标头(如 Authorization
或 X-ID-Token
)转发到后端数据源。这些信息在 QueryData
、CallResource
和 CheckHealth
请求中可用。
要使 Grafana 转发标头,请使用 Grafana 插件 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)
// ...
}
处理 cookies
转发登录用户的 cookies
您的数据源插件可以将登录的 Grafana 用户的 cookies 转发到数据源。在数据源的配置页面上使用 DataSourceHttpSettings 组件。它提供了 允许的 cookies 选项,您可以在其中指定 cookie 名称。
配置完成后,与授权头信息一样,如果您使用 SDK HTTP 客户端,这些 cookies 将自动注入。
提取登录用户的 cookies
如果需要,您还可以在 QueryData
、CallResource
和 CheckHealth
请求中提取 cookies。
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
头将用户头传递给插件。您可以转发此头信息,以及授权头信息或配置的 cookies。
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")
// ...
}