概要:广告平台可以在点击中添加签名验证,这样作弊点击就不会被归因到该渠道,避免承担作弊风险。
点击签名简介
作弊分子能够凭借极低的技术成本,以广告平台的名义生成上千甚至上百万的虚假点击,并将其发送到AppsFlyer。有些情况下,广告平台本身也不知情。
为了确保AppsFlyer归因到某个平台的点击确实来自该平台,而非作弊平台生成的虚假点击,建议广告平台在其点击中使用HMAC-SHA256签名。
点击签名还能帮助广告平台避免点击撞库导致的流量截断(capping)。这个机制会在广告平台出现极端水平的点击撞库并到达capping阈值时,停止AppsFlyer当天对其点击的记录和归因。
AppsFlyer可以通过这类签名来验证广告平台的点击,并确保点击信息不被作弊分子篡改。
- 经过验证的点击会被记录到AppsFlyer,并归因到相关的广告平台。
-
未通过验证的点击会被拦截,并且:
- 相关数据会通过Protect360报告提供给广告平台(而非广告主)。了解详情
- 不会影响该广告平台的转化率或点击截断(capping)的阈值。
点击签名的接入方式
链路
下图简要说明了点击签名从初始开发和基础测试到生产环境测试,最后正式上线的整条链路。
流程
前期准备:需要让您的账户管理员提供API V2.0 token,用于点击签名API的授权。
点击签名的部署方式如下:
-
使用Generate secret key API生成一个密钥(secret key)。
推荐方式:每24小时生成一个新的secret key并投入使用,每个key的有效期为36小时。 -
在您的服务器中编写相关代码,用于调用Generate secret key API,从而获取secret key并生成HMAC-SHA256签名。详情请参见代码示例。
此外,您还可以使用下文表格中列出的其他API。 - 该代码会在您的点击链接中添加以下内容:
- 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 | link_domain | 是 |
点击链接中的域名,例如
|
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"]]
- 各参数都以键值对的格式呈现,即["key", "value"]。
参数值转义(escape)
- JSON中的值必须按照JSON格式标准以小写字符串编写。
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, ¶ms)
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 |
配置点击签名的模式:
|
Configure circuit breaker |
配置熔断机制可以保护广告平台,避免产生过多被拦截的点击:
|
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 |
|
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>
|
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 |
可用值包括:
|
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 |
|
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 |
可能出现的值:
|
circuit-breaker-config |
一个包含status(状态)参数的JSON对象,可能出现的值为:
|
active-key-ids |
JSON格式的数组,其中包含现行密钥的以下信息:
|
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方法关闭熔断机制。
-
如果您发现点击拦截有误伤,请完成以下操作:
- 使用get configuration方法,检查点击签名的配置,确保您使用的secret key是一个有效密钥。
- 使用点击签名报告进一步掌握被拦截点击的信息,考察无效点击的来源(如代理/应用ID)及成因。
- 如果您发现问题来自某个未进行标准化对接的应用,请使用exclude app API该应用从点击签名验证机制中排除。
-
如果您发现配置有问题:
- 继续使用report-only(仅报告)模式。
- 修复您的点击签名流程。
- 查看点击签名报告中的数据,看到点击可正常得到验证后,再重新启用点击签名验证。
常见问题解答
问:如何在不影响生产环境的前提下测试点击签名? 答:测试点击签名的方式有以下两种:
|
问:API token和secret key有什么区别? 答: API token用于授权并执行点击签名API,每个广告平台只有一个API token。AppsFlyer API V2.0 token必须由账户管理员获取。 Secret key用于生成签名,可通过Generate secret key方法创建。广告平台需要负责生成新的secret key,详见特点说明部分。 |
问:能否仅对特定的广告系列使用点击签名验证? 答:点击签名会对一个广告平台所产生的所有点击生效。您可以将特定的应用排除在验证机制之外,但无法按广告系列进行排除。 |
特点
特点 |
说明 |
---|---|
点击签名 | 签名必须在广告平台侧的服务器中完成。 |
Secret Key |
|
Report API | 点击有效性数据以每小时一次的频率进行更新并汇总。 |