How can we help?

广告平台的点击签名指南

  • 更新

概要:广告平台可以在点击中添加签名验证,这样作弊点击就不会被归因到该渠道,避免承担作弊风险。 

点击签名简介

作弊分子能够凭借极低的技术成本,以广告平台的名义生成上千甚至上百万的虚假点击,并将其发送到AppsFlyer。有些情况下,广告平台本身也不知情。

为了确保AppsFlyer归因到某个平台的点击确实来自该平台,而非作弊平台生成的虚假点击,建议广告平台在其点击中使用HMAC-SHA256签名

点击签名还能帮助广告平台避免点击撞库导致的流量截断(capping)。这个机制会在广告平台出现极端水平的点击撞库并到达capping阈值时,停止AppsFlyer当天对其点击的记录和归因。

AppsFlyer可以通过这类签名来验证广告平台的点击,并确保点击信息不被作弊分子篡改。

  • 经过验证的点击会被记录到AppsFlyer,并归因到相关的广告平台。
  • 未通过验证的点击会被拦截,并且:
    • 相关数据会通过Protect360报告提供给广告平台(而非广告主)。了解详情
    • 不会影响该广告平台的转化率或点击截断(capping)的阈值。

点击签名的接入方式

链路

下图简要说明了点击签名从初始开发和基础测试到生产环境测试,最后正式上线的整条链路。

Click_signing_integration_flow.png

流程

前期准备:需要让您的账户管理员提供API V2.0 token,用于点击签名API的授权。

点击签名的部署方式如下

  1. 使用Generate secret key API生成一个密钥(secret key)。
    推荐方式:每24小时生成一个新的secret key并投入使用,每个key的有效期为36小时
  2. 在您的服务器中编写相关代码,用于调用Generate secret key API,从而获取secret key并生成HMAC-SHA256签名。详情请参见代码示例。
    此外,您还可以使用下文表格中列出的其他API
  3. 该代码会在您的点击链接中添加以下内容: 
    • expires参数,用于表达secret key的过期时间,其中包含一个Unix时间戳(UTC时区),过期后相关平台就不再认领该点击。
    • HMAC-SHA256 签名。
      示例:
      https://app.appsflyer.com/com.app.id?pid=adnetwork_int&c=my_campaign&clickid=sdkfjasksjskdfj9845weh&af_siteid=12345&expires=1597657118&signature_v2=8fnDVzZP_WRZnv3KNJaREOEfvB5p9oRc_XlKEvUo8gk

 注意

如果您的链接中存在特殊字符或空格,请务必在点击签名生成之前完成所有的URL编码, 否则会导致验证失败。

点击签名的创建方式

如需创建点击签名,必须完成以下操作:

  1. 使用下文的参数列表JSON格式创建一个JSON字符串。
  2. 使用HMAC56为这个JSON创建签名

参数列表

点击签名以及广告交互签名支持下列参数。

顺序 参数 是否必须配置 说明
1 link_domain

点击链接中的域名,例如

  • app.appsflyer.com
  • myapp.onelink.me
  • click.mycustomdomain.com
2 link_path 点击链接的路径,不带前导反斜杠
在单一平台链接中即为app-id;在OneLink链接中即为template-id
3 pid  
4 af_prt  
5 af_siteid  
6 clickid 相关点击的一次性唯一标识符
7 expires 相关点击的过期时间
8 af_engagement_type  
9 af_click_lookback  
10 af_viewthrough_lookback  
11 af_reengagement_window  
12 is_retargeting  
13 af_ip  
14 advertising_id  
15 oaid  
16 fire_advertising_id  
17 idfa  
18 idfv  

参数及JSON格式说明

参数的顺序和形式

  • 广告交互链接中适用参数及其值必须以JSON格式配置。
  • JSON格式中的参数必须以上表所列的顺序排列。

空值参数

  • JSON中所列参数的值不能为空,也不能仅包含空格。

JSON的数据结构

  • JSON数据必须由一个参数数组构成。
    • 各参数都以键值对的格式呈现,即["key", "value"]。
      示例: 
      [["key-1","value-1"],["key-2","value-2"]...["key-n","value-n"]]

参数值转义(escape)

JSON中的空格

  • 必须对JSON进行轻量化处理,即参数值之间不能出现任何空格、tab字符或换行字符。

签名算法 

  • 使用HmacSHA256和secret key为该JSON创建签名
  • 使用Base64对该签名进行编码,不带尾部填充(padding)
  • 以signature_v2参数的形式将该签名添加到点击链接中

//generate a signature from the click url and encode it with base64 without padding String generatedSignature = Base64.getUrlEncoder().withoutPadding().encodeToString(mac.doFinal(jsonString.getBytes()));

签名示例

签名创建环节的示例

多平台的OneLink点击链接示例:

https://yourbrand.onelink.me/qsWL?pid=mediasource_int&advertising_id=12345678-1234-1234-1234-123456789012
&af_ad_type=video&af_adset=MMP&clickid=sdkfjasksjskdfj9845weh&af_siteid=my_site&af_viewthrough_lookback=2h&c=my_campaign
&expires=1689695615

用于签名的json对象示例(未删除空格):


[
	["link_domain","yourbrand.onelink.me"],
	["link_path","qswl"],
	["pid","mediasource_int"],
	["af_siteid","my_site"],
	["clickid","12345"],
	["expires","1689695615"]
]

用于签名的json对象示例(删除空格后):


[["link_domain","yourbrand.onelink.me"],["link_path","qswl"],["pid","mediasource_int"],["template-id","qswl"],["af_siteid","my_site"],["clickid","12345"],["expires","1689695615"]]

最后可得到以下带有签名的点击链接:

https://yourbrand.onelink.me/qsWL?pid=mediasource_int&advertising_id=12345678-1234-1234-1234-123456789012
&af_ad_type=video&af_adset=MMP&af_siteid=my_site&af_viewthrough_lookback=2h&c=my_campaign
&expires=1689695615
&signature_v2=WIfCmfLAPSsVrBTqCqfihMeLCnbE4dIAlhHF84WsiWA

代码示例

signing-supported-params.json


[
  {"index":1, "name":"pid", "mandatory":true},
  {"index":2, "name":"af_prt", "mandatory":false},
  {"index":3, "name":"af_siteid", "mandatory":true},
  {"index":4, "name":"clickid", "mandatory":true},
  {"index":5, "name":"expires", "mandatory":true},
  {"index":6, "name":"af_engagement_type", "mandatory":false},
  {"index":7, "name":"af_click_lookback", "mandatory":false},
  {"index":8, "name":"af_viewthrough_lookback", "mandatory":false},
  {"index":9, "name":"af_reengagement_window", "mandatory":false},
  {"index":10, "name":"is_retargeting", "mandatory":false},
  {"index":11, "name":"af_ip", "mandatory":false},
  {"index":12, "name":"advertising_id", "mandatory":false},
  {"index":13, "name":"oaid", "mandatory":false},
  {"index":14, "name":"fire_advertising_id", "mandatory":false},
  {"index":15, "name":"idfa", "mandatory":false},
  {"index":16, "name":"idfv", "mandatory":false}
]

signature-example.go


package main

import (
	"crypto/hmac"
	"crypto/sha256"
	"encoding/base64"
	"encoding/json"
	"errors"
	"fmt"
	"io"
	"net/url"
	"os"
	"strings"
	"time"
)

type Param struct {
	Name      string
	Mandatory bool
}

func LoadSupportedParams() ([]Param, error) {
	path := "./signing-supported-params.json"
	jsonFile, _ := os.Open(path)
	defer jsonFile.Close()
	byteValue, err := io.ReadAll(jsonFile)
	if err != nil {
		return nil, errors.New("failed loading " + path)
	}
	var params []Param
	_ = json.Unmarshal(byteValue, &params)
	return params, nil
}

func computeHmac256(message, secret string) (res string, errResult error) {
	defer func() {
		if r := recover(); r != nil {
			errResult = r.(error)
			fmt.Println("failed to invoke ComputeHmac256. err: %+v", errResult)
			res = ""
			return
		}
	}()

	key := []byte(secret)
	h := hmac.New(sha256.New, key)
	h.Write([]byte(message))
	return base64.URLEncoding.WithPadding(base64.NoPadding).EncodeToString(h.Sum(nil)), nil
}

// this function accepts a click url, a ttl for the click in seconds, and the list of supported params
// the function:
// 1. adds the expiration to the click based on the provided ttl
// 2. builds a json for the signature
// 3. create an HMAC256 signature
// 4. adds the signature to the click url
func signURLV2(originalURL string, secret string, clickTtlSeconds int64, supportedParams []Param) (signedURL string, err error) {

	//add an expiration to the click
	expires := time.Now().UTC().Unix() + clickTtlSeconds
	urlWithExpired := fmt.Sprintf("%s%s%d", originalURL, "&expires=", expires)

	//parse the url
	parsedURL, err := url.Parse(urlWithExpired)
	if err != nil {
		return "", errors.New("failed parsing URL")
	}

	//build a json from the url
	jsonStr, err := buildJSONFromURLV2(parsedURL, supportedParams)
	if err != nil {
		return "", err
	}

	//create a signature
	signatureV2, err := computeHmac256(jsonStr, secret)
	if err != nil {
		fmt.Println("Failed to computeHMAC256 for url %s. err: %+v", jsonStr, err)
		return "", errors.New("fail to computeHMAC256 for url")
	}

	//add the signature to the url
	signedURL = fmt.Sprintf("%s%s%s", urlWithExpired, "&signature_v2=", signatureV2)

	return signedURL, nil

}

// this function builds a json from a given click url.
// It returns a string representation of the json ready to be signed.
func buildJSONFromURLV2(parsedURL *url.URL, supportedParams []Param) (string, error) {
	//initiate an empty json in the structure [[key-1,val-1],[key-2,val-2]...[key-n,val-n]]
	var jsonData [][2]string

	//add the url host domain to the json
	domain := parsedURL.Host
	param := [2]string{"link_domain", domain}
	jsonData = append(jsonData, param)

	//add the path (app-id or template-id) to the json
	path := parsedURL.Path
	if len(path) > 1 {
		param := [2]string{"link_path", path[1:]}
		jsonData = append(jsonData, param)
	}

	//loop over the ordered list of supported parameters and add them to the json
	for i := 0; i <len(supportedParams); i++ {name := supportedParams[i].Name val := parsedURL.Query().Get(name) if len(val) > 0 {
			param := [2]string{name, val}
			jsonData = append(jsonData, param)
		} else if supportedParams[i].Mandatory {
			return "", errors.New("missing mandatory param: " + name)
		}
	}

	//generate string representation of the json object
	jsonObj, _ := json.Marshal(jsonData)
	return strings.ToLower(string(jsonObj)), nil
}

func main() {

	supportedParams, err := LoadSupportedParams()
	if err != nil {
		fmt.Println("Error: ", err)
		os.Exit(1)
	}

	var secretKey = "tqJU4Qd/eFTEWfqW7KCG9asDO0bmZoFzv8GY3VPSPAM="
	var clickTtlSeconds int64 = 60
	var originalUrl = "https://yourbrand.onelink.me/qsWL?pid=mediasource_int&advertising_id=12345678-1234-1234-1234-123456789012&clickid=1234&af_ad_type=video&af_adset=MMP&af_siteid=my_site&af_viewthrough_lookback=2h&c=my_campaign"

	fmt.Println("Original URL: ", originalUrl)

	signedUrl, err := signURLV2(originalUrl, secretKey, clickTtlSeconds, supportedParams)
	if err != nil {
		fmt.Println("Error: ", err)
		os.Exit(1)
	} else {
		fmt.Println("Signed URL: ", signedUrl)
	}
}

用于点击签名的API

AppsFlyer提供多种API,帮助广告平台管理并测试其点击签名流程。如需详细了解这些API的使用方式,请参考下文的API列表以及后续章节的说明。

用于点击签名的API

API方法 说明
Generate secret key 生成秘钥(secret key),后续在签名中使用。
Revoke secret key 撤销泄露的秘钥。
Test 发送单次点击,用于测试签名。
Configure mode

配置点击签名的模式:

  • disabled(默认禁用):AppsFlyer不验证点击签名。
  • report-only(仅报告,用于测试模式):AppsFlyer会验证点击签名,但不会拦截签名无效的点击。广告平台可以使用Report API获取验证成功/失败的点击数据,以便在不影响生产环境和真实流量的情况下测试点击签名。
  • enabled(启用):AppsFlyer会拦截签名无效或缺失的点击。
Configure circuit breaker

配置熔断机制可以保护广告平台,避免产生过多被拦截的点击:

  • enabled(默认启用):如果Protect360系统发现过多点击被标记为无效,则为了保护相关平台,避免发生点击拦截误伤:
    • AF会将Configure mode API更改为report-only(仅报告)。
    • 并向广告平台发送邮件,提醒其检查点击签名API的设置是否正确。
  • disabled(禁用):即使拦截比率超过正常水平,Protect360仍会拦截所有被标记为无效的点击。
Get configuration

获取现行secret key的模式和ID信息。

Report

用于在系统处于report-only(仅报告)或enabled(启用)模式时获取验证成本/失败的点击数据,以便在不影响生产环境和真实流量的情况下测试点击签名。

Exclude app 配置应用ID,在点击签名验证机制中排除相关应用。
Remove excluded app 配置应用ID,将被排除的应用其重新添加到点击签名验证机制中。

历史版本

下列信息仅供参考,请勿使用这些历史版本。

V1(历史版本)

Generate secret key方法

Generate secret key方法的基础信息

类别 注意事项

说明

请求 HTTP方法 POST
路径

https://hq1.appsflyer.com/api/p360-click-signing/secret?ttlHours=<ttlHours>

授权请求头(Authorization header) 
响应 结果 以JSON格式返回Secret key
  请求限制 一次最多请求2个现行secret key。

API请求

方法

POST https://hq1.appsflyer.com/api/p360-click-signing/secret?ttlHours=<ttlHours>

参数

参数

说明
ttlHours
  • 相关secret key的有效期,以小时为单位,可用范围为1-1440小时。
  • 默认为36小时。

JSON响应

说明

secret-key-id

相关secret key的ID

secret key

相关点击签名的secret key

expiration

以毫秒为单位的Epoch时间

Generate secret key的curl示例及响应

Curl请求


curl --location --request POST 'https://hq1.appsflyer.com/api/p360-click-signing/secret?ttlHours=36' \
-H 'Authorization: Bearer {API V2.0 token available to the admin in the dashboard.}'

JSON响应

{
	"secret-key-id": "59ad6547-affc-45eb-a6c9-9805f88ee755",
	"secret-key": "zGW6Rhrmb8+vuhHtL/Kp6rW5Ci9PNsjH1J5MGO9SIeg=",
	"expiration": 1610533263
}

HTTP响应代码

响应代码

代码 

响应消息

说明

200 OK

 

401 Not authorized(拒绝授权)

授权请求头(authorization header)无效或缺失

Revoke secret key方法

Revoke secret key方法的基础信息

类别 注意事项

说明

请求 HTTP方法 DELETE
路径

https://hq1.appsflyer.com/api/p360-click-signing/secret/<secret-id>

授权请求头(Authorization header) 
响应 结果

API请求

方法

DELETE https://hq1.appsflyer.com/api/p360-click-signing/secret/<secret-id>

参数

参数

说明
secret-id

用于撤销相关secret key的ID

Generate secret key的curl示例及响应

Curl请求


curl --location --request DELETE 'https://hq1.appsflyer.com/api/p360-click-signing/secret/59ad6547-affc-45eb-a6c9-9805f88ee755' \
-H 'Authorization: Bearer {API V2.0 token available to the admin in the dashboard.}'

HTTP响应代码

响应代码

代码 

响应消息

说明

200 OK

 

401 Not authorized(拒绝授权)

授权请求头(authorization header)无效或缺失

Test方法

Test方法的基础信息

类别 注意事项

说明

请求 HTTP方法 POST
路径

https://hq1.appsflyer.com/api/p360-click-signing/test

授权请求头(Authorization header) 
响应 结果 以JSON格式返回数据

API请求

方法

POST https://hq1.appsflyer.com/api/p360-click-signing/test

参数

参数

说明
url

要测试的点击链接(含签名)

JSON响应

说明

test-status

Passed(通过)或Failed(未通过)

message

测试失败的原因。比如:/span>

  • Missing signature(签名缺失)
  • Invalid signature(签名无效)
  • Expired (已过期)

Test方法的curl示例及响应

Curl请求


curl --location --request POST 'https://hq1.appsflyer.com/api/p360-click-signing/test' \
-H 'Authorization: Bearer {API V2.0 token available to the admin in the dashboard.}' \
--header 'Content-Type: application/json' \
--data-raw '{
   "url": "https://app.appsflyer.com/com.app.id?pid=adnetwork_int&c=my_campaign&clickid=sdkfjasksjskdfj9845weh&af_site_id=12345&expires=1597657118&signature=8fnDVzZP_WRZnv3KNJaREOEfvB5p9oRc_XlKEvUo8gk"
}'

JSON响应

{
	"test-status":"Passed / Failed",
	"message": "Invalid signature"
}

HTTP响应代码

响应代码

代码 

响应消息

说明

200 OK

 

401 Not authorized(拒绝授权)

授权请求头(authorization header)无效或缺失

Configure mode方法

Configure mode方法的基础信息

类别 注意事项

说明

请求 HTTP方法 POST
路径

https://hq1.appsflyer.com/api/p360-click-signing/config/mode/<mode>

授权请求头(Authorization header) 
响应 结果 以JSON格式返回数据

API请求

方法

POST https://hq1.appsflyer.com/api/p360-click-signing/config/mode/<mode>

参数

参数

说明
mode

可用值包括:

  • enabled(启用)
    请注意:请务必先在“report-only”(仅报告)模式下运行数小时,并检查您的报告,确保相关配置正确无误且所有点击均已通过签名验证,然后再切换到“enabled”。
  • disabled(禁用)
  • report-only(仅报告)

Configure mode方法的curl示例

Curl请求


curl --location --request POST 'https://hq1.appsflyer.com/api/p360-click-signing/config/mode/report-only' \
-H 'Authorization: Bearer {API V2.0 token available to the admin in the dashboard.}'

HTTP响应代码

响应代码

代码 

响应消息

说明

200 OK

 

400 Bad request(请求无效)

模式无效

401 Not authorized(拒绝授权)

授权请求头(authorization header)无效或缺失

Configure circuit breaker方法

Configure circuit breaker方法的基础信息

类别 注意事项

说明

请求 HTTP方法 POST
路径

https://hq1.appsflyer.com/p360-click-signing/config/circuit-breaker

授权请求头(Authorization header) 
响应 结果 HTTP状态

API请求

方法

POST https://hq1.appsflyer.com/p360-click-signing/config/circuit-breaker

请求体中的JSON格式参数

参数

说明
status
  • enabled(启用)
  • disabled(禁用)

Configure circuit breaker方法的curl示例和响应

Curl请求


curl --location --request POST 'https://hq1.appsflyer.com/api/p360-click-signing/config/circuit-breaker' \
-H 'Authorization: Bearer {API V2.0 token available to the admin in the dashboard.}'
--data-raw '{
"status":"enabled"
}'

HTTP响应代码

响应代码

代码 

响应消息

说明

200 OK

 

400 Bad request(请求无效)

状态无效

401 Not authorized(拒绝授权)

授权请求头(authorization header)无效或缺失

Get configuration方法

Get configuration方法的基础信息

类别 注意事项

说明

请求 HTTP方法 GET
路径

https://hq1.appsflyer.com/api/p360-click-signing/config

授权请求头(Authorization header) 
响应 结果 以JSON格式返回数据

API请求

方法

GET https://hq1.appsflyer.com/api/p360-click-signing/config

JSON响应

说明

mode

可能出现的值:

  • enabled(启用)
  • disabled(禁用)
  • report-only(仅报告)
circuit-breaker-config

一个包含status(状态)参数的JSON对象,可能出现的值为:

  • enabled(启用)
  • disabled(禁用)

active-key-ids

JSON格式的数组,其中包含现行密钥的以下信息:

  • secret-key-id:随机生成的密钥ID
  • expiration:密钥效期的Epoch时间,以毫秒为单位

excluded-app-ids

JSON格式的数组,其中包含被排除应用的app-ids

Get configuration方法的curl示例及响应

Curl请求


curl --location --request GET 'https://hq1.appsflyer.com/api/p360-click-signing/config' \
-H 'Authorization: Bearer {API V2.0 token available to the admin in the dashboard.}'

JSON响应

{
	"mode": "report-only",
	"active-key-ids": [
		{
			"secret-key-id": "59ad6547-affc-45eb-a6c9-9805f88ee755",
			"expiration": 1610533263
		}
	],
	"excluded-app-ids": [
		"app-id-1", 	"app-id-2"
	]

}

HTTP响应代码

响应代码

代码 

响应消息

说明

200 OK

 

401 Not authorized(拒绝授权)

授权请求头(authorization header)无效或缺失

Report方法

Report方法的基础信息

类别 注意事项

说明

请求 HTTP方法 GET
路径

https://hq1.appsflyer.com/api/p360-click-signing/report

授权请求头(Authorization header) 
响应 结果 以CSV格式返回数据

API请求

方法

GET https://hq1.appsflyer.com/api/p360-click-signing/report

参数

参数

说明
start-date

报告的起始日期和时间。格式:yyyy-mm-ddThh

end-date

报告的截止日期和时间。格式:yyyy-mm-ddThh

API配置要求:同时配置start-date和end-date,或者两者都不配置。若不配置起始/截止日期,则报告会显示过去24小时的数据。

CSV响应

栏位

说明

time

指点击的日期和时间。格式为:yyyy-mm-ddThh

total_clicks

报告周期内的点击总数

valid_clicks

报告周期内的有效点击数量

missing_signature

报告周期内出现签名缺失的点击数量

expired_clicks

报告周期内过期的点击数量

invalid_signature

报告周期内签名无效的点击数量

no_active_secrets

由于系统中没有现行的secret key,因而被拦截的点击数量(通常是因为系统处于report-only模式)

Test方法的curl示例及响应

Curl请求


curl --location --request GET 'https://hq1.appsflyer.com/api/p360-click-signing/report?start-date=2021-01-07T07&end-date=2021-01-17T12' \
-H 'Authorization: Bearer {API V2.0 token available to the admin in the dashboard.}' \

CSV响应

time

total_clicks

valid_clicks

missing_signature

expired_clicks

invalid_signature

no_active_signatures

2021-01-17T07

928082156

928082156

 0

 0

 0

 0

2021-01-17T08

923796132

923796132

 0

 0

 0

 0

2021-01-17T09

917541373

917541373

 0

 0

 0

 0

2021-01-17T10

909977064

909977064

 0

 0

 0

 0

2021-01-17T11

965104299

965104299

 0

 0

 0

 0

2021-01-17T12

975134824

975134824

 0

 0

 0

 0

HTTP响应代码

响应代码

代码 

响应消息

说明

200 OK

 

401 Not authorized(拒绝授权)

授权请求头(authorization header)无效或缺失

Exclude app方法

Exclude app方法的基础信息

类别 注意事项

说明

请求 HTTP方法 POST
路径

https://hq1.appsflyer.com/api/p360-click-signing/config/excluded-app/<app-id>

授权请求头(Authorization header) 
响应 结果

API请求

方法

POST https://hq1.appsflyer.com/api/p360-click-signing/config/excluded-app/<app-id>

参数

参数

说明
app-id

需要从点击签名验证机制中排除的应用ID

Exclude app方法的curl示例

Curl请求


curl --location --request POST 'https://hq1.appsflyer.com/api/p360-click-signing/config/excluded-app/appname.com' \
-H 'Authorization: Bearer {API V2.0 token available to the admin in the dashboard.}'

HTTP响应代码

响应代码

代码 

响应消息

说明

200 OK

 

401 Not authorized(拒绝授权)

授权请求头(authorization header)无效或缺失

Remove excluded app方法

Remove excluded app方法的基础信息

类别 注意事项

说明

请求 HTTP方法 DELETE
路径

https://hq1.appsflyer.com/api/p360-click-signing/config/excluded-app/<app-id>

授权请求头(Authorization header) 
响应 结果

API请求

方法

DELETE https://hq1.appsflyer.com/api/p360-click-signing/config/excluded-app/<app-id>

参数

参数

说明
app-id

需要从点击签名验证机制的排除列表中移除的应用ID(将这些应用重新加入点击签名验证机制)

Remove excluded app方法的curl示例

Curl请求


curl --location --request DELETE 'https://hq1.appsflyer.com/api/p360-click-signing/config/excluded-app/appname.com' \
-H 'Authorization: Bearer {API V2.0 token available to the admin in the dashboard.}'

HTTP响应代码

响应代码

代码 

响应消息

说明

200 OK

 

401 Not authorized(拒绝授权)

授权请求头(authorization header)无效或缺失

代码示例

package sign;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.util.Base64;

.
.
.

String clickUrl = "https://app.appsflyer.com/com.app.id?pid=adnetwork_int&c=my_campaign&clickid=sdkfjasksjskdfj9845weh&af_site_id=12345";
String secretKey = "secret_key"; 
int ttlMinutes = 5;


//add expiration to the click URL
long expiration = System.currentTimeMillis() + (60000L * ttlMinutes);
clickUrl += "&expires="+expiration;

//create a SecretKey object from the given key string
SecretKeySpec signingKey = new SecretKeySpec(secretKey.getBytes(), "HmacSHA256");
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(signingKey);

//generate a signature from the click url and encode it with base64 without padding
String generatedSignature =     
Base64.getUrlEncoder().withoutPadding().encodeToString(mac.doFinal(clickUrl.getBytes()));

//add the signature to the click URL
String signedClickUrl = clickUrl + "&signature=" + generatedSignature;

其他相关信息

问题排查

如果一小时内超过90%的点击都未通过签名验证,AppsFlyer会停止点击签名验证,并切换到report-only(仅报告)模式。

这是为了保护贵司免受技术问题影响,并帮助您排查异常现象的根因:

  • 如果您发现签名正常生效,且点击拦截正确无误,请使用configure circuit breaker方法关闭熔断机制。
  • 如果您发现点击拦截有误伤,请完成以下操作:
    1. 使用get configuration方法,检查点击签名的配置,确保您使用的secret key是一个有效密钥。
    2. 使用点击签名报告进一步掌握被拦截点击的信息,考察无效点击的来源(如代理/应用ID)及成因。
  • 如果您发现问题来自某个未进行标准化对接的应用,请使用exclude app API该应用从点击签名验证机制中排除。
  • 如果您发现配置有问题:
    1. 继续使用report-only(仅报告)模式。
    2. 修复您的点击签名流程。
    3. 查看点击签名报告中的数据,看到点击可正常得到验证后,再重新启用点击签名验证。

常见问题解答

问:如何在不影响生产环境的前提下测试点击签名?

测试点击签名的方式有以下两种:

  1. 使用test API。这一方法适用于开发阶段,可测试单次点击的签名。
  2. 使用report-only(仅报告)模式。Report-only模式下,AppsFlyer会验证生产环境中的点击签名,并为您呈现拦截结果,但是不会对无效点击进行实际拦截。这样,您就可以在不影响真实流量的情况下测试点击签名。

问:API token和secret key有什么区别?

: API token用于授权并执行点击签名API,每个广告平台只有一个API token。AppsFlyer API V2.0 token必须由账户管理员获取。

Secret key用于生成签名,可通过Generate secret key方法创建。广告平台需要负责生成新的secret key,详见特点说明部分。

问:能否仅对特定的广告系列使用点击签名验证?

点击签名会对一个广告平台所产生的所有点击生效。您可以将特定的应用排除在验证机制之外,但无法按广告系列进行排除。

特点

特点

说明

点击签名 签名必须在广告平台侧的服务器中完成。
Secret Key
  • 一个广告平台最多同时可以有两个现行的secret key。
  • 每个secret key都有一个有效期。
  • 使用过期的secret key签名的点击会被拦截。
Report API 点击有效性数据以每小时一次的频率进行更新并汇总。

This article was translated using AI and may contain errors. For the most accurate information, please refer to the English version using the language selector.


Share article: