广告平台的点击签名指南

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

点击签名简介

作弊分子能够凭借极低的技术成本,以广告平台的名义生成上千甚至上百万的虚假点击,并将其发送到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 点击有效性数据以每小时一次的频率进行更新并汇总。