登录验证
1、登录流程图

2、在线验签
2.1、接口说明
本接口为游戏客户端在接收SDK客户端登录消息后,发送至游戏服务器。再由游戏服务器发送至登录服务器进行登录token有效性验证。
2.2、接口地址 
【正式环境】:https://xxxxxxx/Wbsrv/Check_Login_DH.aspx
【测试环境】:http://xxxxxxx/Wbsrv/Check_Login_DH.aspx
域名请联系发行获取(登录域名)
2.3、环境说明
为方便游戏在接入阶段进行调试,及SDK的不定期更新,特启用正式及测试两套环境。
环境的使用需注意,验证接口的正式或测试表示SDK客户端生成token时对应的登录服务器环境,该环境取决于客户端的配置选择。
获取当前客户端处于何种环境有:
1、游戏客户端手动配置(通常应用于游戏母包的配置),可按照以下方法进行切换。
- Android
 - IOS
 
<!--SDK环境配置(true:测试环境,false:正式环境)-->
<meta-data android:name="dh_eng" android:value="false" />
    在AppDelegate.m文件中,导入DHUnionSDK.h,DHUnionDelegate.h,DHUnonConfig.h头文件
    (如果是Unity工程,则在UnityAppController.m文件中)
    #import "DHUnionSDK.h"
    #import "DHUnionDelegate.h"
    #import "DHUnionConfig.h"
    然后在以下回调中进行配置(必须配置,否则会导致登录支付等出现问题)
    - (BOOL)application:(UIApplication *)application didFinishLaunchingWithOptions:(NSDictionary *)launchOptions {
        // Override point for customization after application launch.
        //初始化SDK,必须
        [DHUnionSDK initUnionSDK];
        //配置SDK
        DHUnionConfig *config = [[DHUnionConfig alloc] init];
        //请在正式打包上线时设为NO,否则会调用测试的登录地址
        config.isDebug = NO;
        [DHUnionSDK configureUnionSDK:config];
        //其它配置
        return YES;
    }
2、开放平台在线出包配置(在线出包时需特别注意,不论母包中如何配置dh_eng值,实际出包时都将按照在线配置修改dh_eng值进行输出)。

3、游戏服务器根据验证token后返回值(result)判定当前客户端处于何种环境。
如将正式环境生成的token发送至测试环境验证,则result会返回-6,此时该将验证地址修改为测试环境地址再次验证。同理,如果测试环境token发送至正式环境验证,则result会返回-7。
由于SDK客户端的特殊性,目前暂无法在登录成功返回给游戏客户端消息数据中包含环境标识,需游戏客户端自行获取,或根据服务器验证token返回值进行程序自动切换。
测试环境仅允许出现于项目的接入及调试阶段,发布正式版本前,Android包必须通过开放平台进行出包,且关闭测试环境选项。IOS也必须在出包前在本地关闭相关环境配置。
2.4、请求方式
POST

2.5、请求参数说明 
请求参数字段名称必须与下列参数大小写保持一致。
token验证不区分区服、设备、以及系统类型(Android、iOS等)
参数及值来源于SDK客户端登录成功后返回给游戏客户端登录数据。
| 参数名 | 类型 | 说明 | 示例 | 
|---|---|---|---|
| accountid | String | 账号ID | 1490014080 | 
| appid | String | 产品ID | 1413829460 | 
| logintype | String | 登录类型 | LoginType_Quick_Visitor | 
| token | String | token值(登录令牌) | ba9939c43a1c43558a252f9b1d3453b0 | 
| sign | String | 签名(加密规则) | 4b06a255ab468d231624c078c001aba7 | 
2.6、返回参数说明
返回参数将以JSON格式返回。当验证成功返回值result为1时,data中会返回相应账号信息。部分情况下,resultInfo会返回相应的异常信息。
| 参数名 | 类型 | 说明 | 
|---|---|---|
| result | Int | 验证状态(详见下方返回result说明) | 
| resultInfo | String | 验证结果内容 | 
| data | JsonObject | 数据内容 | 
| > accountid | Int | 账号ID(角色唯一标识) | 
| > account | String | 账号(非唯一) | 
| > accountview | String | 账号缩略(暂未使用) | 
| > token | String | token验证值 | 
| > timestamp | String | 服务器时间戳 | 
| > logintype | String(海外)、Int(国内) | 验证返回时该字段无效具体请参考SDK客户端回调值为准,建议不要做业务关联 | 
| > region | int | 特殊处理字段,新接入游戏无需处理。 | 
| > isRealNameAuth | int | 是否实名(0:未实名,1:已实名)(海外请忽略) | 
| > isAdult | int | 是否成年(0:未成年,1:已成年)(海外请忽略) | 
| > age | int | 年龄 (海外请忽略) | 
- 验证成功返回示例
 - 验证失败加密错误返回示例
 
{
"data":{
    "account": "12470082",
    "accountid": 2087249778,
    "accountview": "124***82",
    "age": 18,
    "bindphone": 0,
    "chanOpenID": "12470082",
    "isAdult": 1,
    "isRealNameAuth": 1,
    "logintype": 6,
    "region": 1,
    "sign": "83c4ece87bc76bdda4d6f3232a7b5a00",
    "timestamp": "1662107094",
    "token": "63ab96995cfb4e3d9631229dc87dbcc8",
    "userinfo": {}
},
"result": 1,
"resultInfo": "成功"
}
  {
    "result": -2,
    "resultInfo": "签名错误",
    "data": {
        "accountid": 0,
        "account": null,
        "accountview": null,
        "token": null,
        "timestamp": null,
        "sign": null,
        "logintype": null,
        "region": 0,
        "isRealNameAuth":0,
        "isAdult":0,
        "age":0
    }
    
}
- 返回result说明
 
| result值 | 值说明 | 
|---|---|
| 0 | 验证失败 | 
| 1 | 验证成功 | 
| -1 | 参数错误 | 
| -2 | 签名错误 | 
| -3 | token过期(测试开发阶段出现请确认客户端与服务端appid是否一致) | 
| -4 | token已使用 | 
| -5 | token错误 | 
| -6 | 当前token为正式环境生成,却被发送至测试环境验证。(请前往正式环境验证) | 
| -7 | 当前token为测试环境生成,却被发送至正式环境验证。(请前往测试环境验证) | 
| -11 | 系统错误 | 
2.7、签名规则及示例
2.7.1、AppKey值说明
加密时使用的AppKey为平台对应AppId所分配的AppKey,文档中为示例AppKey。实际AppKey请联系相应产品对接人员。在MD5签名时,需要相关AppKey参与签名,AppKey为英文字母和数字组成的32位。
2.7.2、签名方式
MD5
2.7.3、参数示例
accountid = 1490014080
appid = 1413829460
logintype = LoginType_Quick_Visitor
token = ba9939c43a1c43558a252f9b1d3453b0
token验证不区分区服、设备、以及系统类型(Android、iOS等)
参数及值来源于SDK客户端登录成功后返回给游戏客户端登录数据。
2.7.4、签名规则
MD5(accountid+appid+logintype+token+Appkey)
以上参数名称替换为具体参数值,且"+"表示两个字符串的连接符,不要将"+"放入md5加密源串中
2.7.5、签名原文
MD5(14900140801413829460LoginType_Quick_Visitorba9939c43a1c43558a252f9b1d3453b02926cd821ee3479cbd54590ac6bdaa)
其中2926cd821ee3479cbd54590ac6bdaa为示例AppKey
2.7.6、签名结果
4b06a255ab468d231624c078c001aba7(转换为小写),将此结果填入sign参数后提交验证
2.7.7、最终提交数据
查看请求参数说明
3、本地验签
3.1、验签说明
此验签方式仅做为在线验签的备用方案,游戏服务器可在以下两种状态下使用本地验签,无特殊情况请不要使用:
1、使用在线验签时出现网络超时、或者HTTP状态码异常(非200)。
2、游戏依赖于token验证机制的断线重连,或者出现大量掉线时用户密集登录。
3.2、验签参数
| 参数名 | 类型 | 说明 | 
|---|---|---|
| accountid | String | 账号ID(角色唯一标识) | 
| timestamp | long | 当前登录态有效维持到期时间(unix时间戳,1970-1-1开始) | 
| token | String | 登录令牌 | 
| sign | String | MD5(accountid+timestamp+token+appkey) | 
3.2、验签方式
MD5
3.3、签名规则及示例
3.3.1、参数示例
accountid = 1490014080
timestamp = 1569057445
token = ba9939c43a1c43558a252f9b1d3453b0
token验证不区分区服、设备、以及系统类型(Android、iOS等)
参数及值来源于SDK客户端登录成功后返回给游戏客户端登录数据。
timestamp值(unix时间戳)为当前token值对应的有效维持到期时间。
3.3.2、签名规则
MD5(accountid+timestamp+token+Appkey)
以上参数名称替换为具体参数值,且"+"表示两个字符串的连接符,不要将"+"放入md5加密源串中
3.3.3、签名原文
MD5(14900140801569057445ba9939c43a1c43558a252f9b1d3453b02926cd821ee3479cbd54590ac6bdaa)
其中2926cd821ee3479cbd54590ac6bdaa为示例AppKey
3.3.4、签名结果
a7f44f39dcc7c5cb350da514799c0e05(转换为小写)
3.3.5、数据验证
将以上结果值与SDK客户端登录成功后的sign参数值进行比对,一致则表示验证通过。
4、常见问题
1、登录服务器采用AppKey等加密签名机制验证安全性,请不要将AppKey配置在游戏客户端,并做好保密。
2、登录服务器未设置任何黑白IP名单限制来源请求,如游戏服务器由负责运维,请联系相关运维同事开放防火墙中域名出口白名单,否则会导致在线验证超时。
3、在线验证采用POST请求机制,数据body采用form-data模式。
4、在开发调试阶段,在线验证result返回值为-3(token过期),常见于接口地址环境使用错误,目前登录服务器会以(-6,-7)状态返回,提示应选择正确的接口地址环境。
5、调用代码示例
5.1 java调用示例
    public static void main(String[] args) {
        LoginCheckVO loginCheckVO = new LoginCheckVO();
        loginCheckVO.setAccountId("1490014080");
        loginCheckVO.setAppid("1413829460");
        loginCheckVO.setLoginType("LoginType_Quick_Visitor");
        loginCheckVO.setToken("ba9939c43a1c43558a252f9b1d3453b0");
        String appKey = "2926cd821ee3479cbd54590ac6bdaa";
        TestController testController = new TestController();
        try {
            String sign = testController.generateSign(loginCheckVO, appKey);
            loginCheckVO.setSign(sign);
        //接口域名根据实际情况替换
            String url = "https://xxxxxxxxxx/Wbsrv/Check_Login_DH.aspx";
            //验证结果
            Boolean checkFlag = testController.checkLogin(url, loginCheckVO);
            System.out.println(checkFlag);
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }
    /**
     * 生成MD5签名
     * @param loginCheckVO 登录信息
     * @param appKey 签名密钥
     * @return string 签名字符串
     * @throws NoSuchAlgorithmException
     */
    public String generateSign(LoginCheckVO loginCheckVO, String appKey) throws NoSuchAlgorithmException {
        //明文拼接 注意拼接顺序
        String text = loginCheckVO.getAccountId() + loginCheckVO.getAppid() + loginCheckVO.getLoginType() +
                loginCheckVO.getToken() + appKey;
        //获取MD5消息摘要实例
        MessageDigest md = MessageDigest.getInstance("MD5");
        //将输入字符串转换为utf8编码的字节数组并更新消息摘要
        byte[] hash = md.digest(text.getBytes(StandardCharsets.UTF_8));
        //使用DatatypeConverter将字节数组转换为十六进制字符串
        return DatatypeConverter.printHexBinary(hash).toLowerCase();
    }
    /**
     * 登录验证
     * @param url 请求链接
     * @param loginCheckVO 请求参数
     * @return
     */
    public Boolean checkLogin(String url, LoginCheckVO loginCheckVO) {
        RestTemplate restTemplate = new RestTemplate();
        //超时时间可以根据项目自定义
        SimpleClientHttpRequestFactory httpRequestFactory = new SimpleClientHttpRequestFactory();
        httpRequestFactory.setConnectTimeout(5000);
        httpRequestFactory.setReadTimeout(30000);
        restTemplate.setRequestFactory(httpRequestFactory);
        //设置参数
        MultiValueMap<String, Object> multiValueMap = new LinkedMultiValueMap<>(5);
        multiValueMap.set("accountid", loginCheckVO.getAccountId());
        multiValueMap.set("appid", loginCheckVO.getAppid());
        multiValueMap.set("logintype", loginCheckVO.getLoginType());
        multiValueMap.set("token", loginCheckVO.getToken());
        multiValueMap.set("sign", loginCheckVO.getSign());
        //设置请求头
        HttpHeaders headers = new HttpHeaders();
        headers.setContentType(MediaType.MULTIPART_FORM_DATA);
        HttpEntity<MultiValueMap<String, Object>> httpEntity = new HttpEntity<>(multiValueMap, headers);
        ResponseEntity<String> response = restTemplate.postForEntity(url, httpEntity, String.class);
        //超时等异常情况
        if (ObjectUtil.isNull(response) || !response.getStatusCode().equals(HttpStatus.OK) || 
            StrUtil.isBlank(response.getBody())) {
            return false;
        }
        //项目中已经引入fastjson2来实现json反序列化,也可用其他方式
        JSONObject jsonObject = JSONObject.parseObject(response.getBody());
        if (ObjectUtil.isNull(jsonObject) || !jsonObject.containsKey("result") || jsonObject.getInteger("result") != 1) {
            return false;
        }
        return true;
    }
5.2 c#调用示例
    //引用部分
    using System;
    using System.Collections.Generic;
    using System.Linq;
    using System.Web;
    using System.Threading.Tasks;
    using System.Net.Http;
    using System.Net.Http.Headers;
    using Newtonsoft.Json.Linq;
    
    /// <summary>
        /// 生成MD5签名
        /// </summary>
        /// <param name="loginCheckVO">登录信息</param>
        /// <param name="appKey">签名密钥</param>
        /// <returns>签名字符串</returns>
        public string GenerateSign(LoginCheckVO loginCheckVO, string appKey)
        {
            // 明文拼接 注意拼接顺序
            string text = loginCheckVO.AccountId + loginCheckVO.Appid + loginCheckVO.LoginType +
                          loginCheckVO.Token + appKey;
            // 获取MD5消息摘要实例
            using (var md5 = System.Security.Cryptography.MD5.Create())
            {
                // 将输入字符串转换为utf8编码的字节数组并更新消息摘要
                byte[] hash = md5.ComputeHash(System.Text.Encoding.UTF8.GetBytes(text));
                // 将字节数组转换为十六进制字符串
                System.Text.StringBuilder sb = new System.Text.StringBuilder();
                foreach (byte b in hash)
                {
                    sb.Append(b.ToString("x2"));
                }
                return sb.ToString();
            }
        }
        /// <summary>
        /// 登录验证
        /// </summary>
        /// <param name="url">请求链接</param>
        /// <param name="loginCheckVO">请求参数</param>
        /// <returns>是否登录成功</returns>
        public async Task<bool> CheckLoginAsync(string url, LoginCheckVO loginCheckVO)
        {
            using (var httpClient = new HttpClient { Timeout = TimeSpan.FromMilliseconds(30000) })
            using (var content = new MultipartFormDataContent())
            {
                // 设置参数
                content.Add(new StringContent(loginCheckVO.AccountId), "accountid");
                content.Add(new StringContent(loginCheckVO.Appid), "appid");
                content.Add(new StringContent(loginCheckVO.LoginType), "logintype");
                content.Add(new StringContent(loginCheckVO.Token), "token");
                content.Add(new StringContent(loginCheckVO.Sign), "sign");
                try
                {
                    // 使用CancellationTokenSource来设置5秒连接超时
                    using (var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromMilliseconds(5000)))
                    {
                        HttpResponseMessage response = await httpClient.PostAsync(url, content, cts.Token);
                        // 异常情况处理
                        if (!response.IsSuccessStatusCode)
                        {
                            return false;
                        }
                        string responseBody = await response.Content.ReadAsStringAsync();
                        if (string.IsNullOrWhiteSpace(responseBody))
                        {
                            return false;
                        }
                        // 使用Json.NET来反序列化JSON响应
                        JObject jsonObject = JObject.Parse(responseBody);
                        if (jsonObject == null || jsonObject["result"] == null || (int)jsonObject["result"] != 1)
                        {
                            return false;
                        }
                        return true;
                    }
                }
                catch (Exception ex)
                {
                    // 处理超时或其他异常情况
                    Console.WriteLine("Exception: " + ex.Message);
                    return false;
                }
            }
        }
5.3 PHP调用示例
/**
 * 登录验证
 * @param $url 请求链接
 * @param $loginCheckVO 请求参数
 * @param $appKey 签名密钥
 * @return bool
 */
function checkLogin($url, $loginCheckVO, $appKey)
{
    //生成签名
    $loginCheckVO->Sign = md5($loginCheckVO->AccountId . $loginCheckVO->Appid . $loginCheckVO->LoginType .
            $loginCheckVO->Token . $appKey);
    // 初始化cURL会话
    $ch = curl_init();
    // 设置参数
    $postData = [
        'accountid' => $loginCheckVO->AccountId,
        'appid' => $loginCheckVO->Appid,
        'logintype' => $loginCheckVO->LoginType,
        'token' => $loginCheckVO->Token,
        'sign' => $loginCheckVO->Sign
    ];
    // 配置cURL选项
    curl_setopt($ch, CURLOPT_URL, $url);
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
    curl_setopt($ch, CURLOPT_TIMEOUT_MS, 5000); // 设置5秒连接超时
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_POSTFIELDS, http_build_query($postData));
    // 配置HTTPS选项
    curl_setopt($ch, CURLOPT_SSL_VERIFYPEER, true); // 是否验证对等证书
    curl_setopt($ch, CURLOPT_SSL_VERIFYHOST, 2);    // 认证是否来自可信主机
    //curl_setopt($ch, CURLOPT_CAINFO, '/path/to/cacert.pem'); // CA证书文件路径(如果需要)
    try {
        // 执行POST请求
        $response = curl_exec($ch);
        // 检查cURL错误
        if (curl_errno($ch)) {
            throw new Exception(curl_error($ch));
        }
        // 检查HTTP状态码
        $httpStatusCode = curl_getinfo($ch, CURLINFO_HTTP_CODE);
        if ($httpStatusCode != 200) {
            return false;
        }
        // 解析响应体
        $responseBody = json_decode($response);
        if (empty($responseBody)) {
            return false;
        }
        // 验证响应结果
        if (!isset($responseBody->result) || $responseBody->result != 1) {
            return false;
        }
        return true;
    } catch (Exception $ex) {
        // 处理异常情况
        error_log("Exception: " . $ex->getMessage());
        return false;
    } finally {
        // 关闭cURL会话
        curl_close($ch);
    }
}
5.4 Golang调用示例
import (
    "bytes"
    "crypto/md5"
    "encoding/hex"
    "fmt"
    "io/ioutil"
    "net/http"
    "net/url"
    "time"
)
// LoginCheckVO 登录信息
type LoginCheckVO struct {
    AccountId string
    Appid     string
    LoginType string
    Token     string
    Sign      string
}
// GenerateSign 生成MD5签名
func GenerateSign(loginCheckVO *LoginCheckVO, appKey string) (string, error) {
    // 明文拼接 注意拼接顺序
    text := loginCheckVO.AccountId + loginCheckVO.Appid + loginCheckVO.LoginType + loginCheckVO.Token + appKey
    // 获取MD5消息摘要实例
    hash := md5.New()
    _, err := hash.Write([]byte(text))
    if err != nil {
        return "", err
    }
    // 将字节数组转换为十六进制字符串
    md5Sum := hex.EncodeToString(hash.Sum(nil))
    return md5Sum, nil
}
// CheckLogin 登录验证
func CheckLogin(urlStr string, loginCheckVO *LoginCheckVO) (bool, error) {
    client := &http.Client{
        Timeout: 30 * time.Second,
    }
    // 设置参数
    data := url.Values{}
    data.Set("accountid", loginCheckVO.AccountId)
    data.Set("appid", loginCheckVO.Appid)
    data.Set("logintype", loginCheckVO.LoginType)
    data.Set("token", loginCheckVO.Token)
    data.Set("sign", loginCheckVO.Sign)
    req, err := http.NewRequest(http.MethodPost, urlStr, bytes.NewBufferString(data.Encode()))
    if err != nil {
        return false, err
    }
    // 设置请求头
    req.Header.Set("Content-Type", "application/x-www-form-urlencoded")
    resp, err := client.Do(req)
    if err != nil {
        return false, err
    }
    defer resp.Body.Close()
    // 超时等异常情况
    if resp.StatusCode != http.StatusOK {
        return false, nil
    }
    body, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        return false, err
    }
    responseBody := string(body)
    if responseBody == "" {
        return false, nil
    }
    // 假设你已经引入了某种JSON库来解析响应体
    var result map[string]interface{}
    err = json.Unmarshal(body, &result)
    if err != nil {
        return false, err
    }
    if result["result"] == nil || int(result["result"].(float64)) != 1 {
        return false, nil
    }
    return true, nil
}