# 登录验证

# 1、登录流程图

login

# 2、在线验签

# 2.1、接口说明

  • 本接口为游戏客户端在接收SDK客户端登录消息后,发送至游戏服务器。再由游戏服务器发送至登录服务器进行登录token有效性验证。

# 2.2、接口地址

  • https://xxxxxxx/Wbsrv/Check_Login_DH_V2.aspx

域名请联系发行获取(登录域名)

# 2.3、请求方式

DANGER

  • Method:POST
  • ContentType:application/x-www-form-urlencoded

# 2.4、请求参数说明

DANGER

  • 请求参数字段名称必须与下列参数大小写保持一致。

  • token验证不区分区服、设备、以及系统类型(Android、iOS等)

  • 参数及值来源于SDK客户端登录成功后返回给游戏客户端登录数据(sign除外)。

  • sign为签名字段,需要服务器按照签名规则生成。

参数名 类型 说明 示例
appId Int 产品ID--配置 1413829460
accountId Long 账号ID(角色唯一标识)--客户端获取 1450168626
token String 登录令牌--客户端获取 d3c40875eee54920af0efc4ff8fb8b41
timestamp long 当前时间戳-秒(unix时间戳,1970-1-1开始)--生成 1722594966
sign String 签名(加密规则见下文) --生成 a5295615da0840d4b856e8915eb9c95a
  • 请求内容示例
"accountId=1450168626&appId=1413829460&timestamp=1722594966&token=d3c40875eee54920af0efc4ff8fb8b41&sign=a5295615da0840d4b856e8915eb9c95a"

# 2.5、返回参数说明

WARNING

  • 返回参数将以JSON格式返回。当验证成功返回值resultCode10000时,datum中会返回相应账号信息。部分情况下,resultInfo会返回相应的异常信息。
参数名 类型 说明
resultCode Int 验证状态(详见下方返回result说明)
resultInfo String 验证结果内容
datum JsonObject 数据内容
> accountId Long 账号ID(角色唯一标识)
> accountView String 账号缩略(暂未使用)
> token String token验证值
> loginTimestamp Long 登录时间(unix时间戳,1970-1-1开始)
> expireTimestamp Long 登录态过期时(unix时间戳,1970-1-1开始)间
> sign String 签名(本地验签使用)
> userExtraInfo JsonObject 用户信息(登录渠道来源)
> userExtraInfo > nickName String 昵称
> userExtraInfo > avatar String 头像地址
> userExtraInfo > channelId Int 登录渠道
> userExtraInfo > channelUid String 渠道账号标识
> userExtraInfo > openId String 渠道openId

WARNING

  • 验证成功返回示例
{
    "resultCode": 10000,
    "datum": {
        "accountId": 1450168626,
        "token": "d3c40875eee54920af0efc4ff8fb8b41",
        "accountView": "",
        "loginType": 10,
        "expireTimestamp": 1723455064,
        "loginTimestamp": 1722591064,
        "sign": "d440b0c42bb491a27ec8db763d994e99",
        "userExtraInfo": {
            "nickName": "",
            "avatar": "",
            "channelId": 1707,
            "channelUid": "",
            "openId": ""
        }
    },
    "resultInfo": "sucess!",
    "memo": null,
    "rid": "pgsct.e4a6c4d65d2d43fbac33b7a1ff6f5080"
}

WARNING

  • 验证失败返回示例
  {
    "resultCode": 11042,
    "datum": null,
    "resultInfo": "Signature error!(11042)",
    "memo": null,
    "rid": "pgsct.bf09c044e3354004b451db3f678e2db5"
}

WARNING

  • resultCode 说明
resultCode 值说明
10000 验证成功
11006 token不存在
11016 参数错误
11041 timestamp超时(超过前后30分钟)
11042 签名错误
11057 游戏不存在、维护中
90002 验证失败

# 2.7、签名规则及示例

# 2.7.1、AppKey值说明

  • 加密时使用的AppKey为平台对应AppId所分配的AppKey,文档中为示例AppKey。实际AppKey请联系相应产品对接人员。在MD5签名时,需要相关AppKey参与签名,AppKey为英文字母和数字组成的32位。

# 2.7.2、签名方式

  • MD5

# 2.7.3、参数示例

accountId = 1450168626
appId = 1413829460
token = d3c40875eee54920af0efc4ff8fb8b41
timestamp=1722594966

WARNING

token验证不区分区服、设备、以及系统类型(Android、iOS等)

参数及值来源于SDK客户端登录成功后返回给游戏客户端登录数据。

# 2.7.4、签名规则

DANGER

  • 按照请求参数的key的ASTII码从小大到顺序拼接value,最后加上分配的appkey成一个字符串
  • 拼接后的字符串使用MD5加密成小写
MD5(String.format("{0}{1}{2}{3}{4}",_accountId,_appId,_timestamp,_token,appkey)),32).ToLower()

# 2.7.5、签名原文

"145016862614138294601722594966d3c40875eee54920af0efc4ff8fb8b41c62d9d95c41fc20aaf4d53245c836a"

其中c62d9d95c41fc20aaf4d53245c836a为示例AppKey

# 2.7.6、签名结果

a5295615da0840d4b856e8915eb9c95a,将此结果填入sign参数后提交验证

# 2.7.7、最终提交数据

查看请求参数说明

# 3、本地验签

# 3.1、验签说明

DANGER

  • 此验签方式仅做为在线验签的备用方案,游戏服务器可在以下两种状态下使用本地验签,无特殊情况请不要使用:

    • 使用在线验签时出现网络超时、或者HTTP状态码异常(非200)。
    • 游戏依赖于token验证机制的断线重连,或者出现大量掉线时用户密集登录。

# 3.2、验签参数

参数名 类型 说明
accountId String 账号ID(客户端登录返回)
expireTimestamp long 当前登录态有效维持到期时间(unix时间戳,1970-1-1开始)(客户端登录返回)
token String 登录令牌(客户端登录返回)
sign String (签名(客户端登录返回)

# 3.2、验签方式

MD5

# 3.3、签名规则及示例

# 3.3.1、参数示例

accountId = 1490014080
expireTimestamp = 1569057445
token = ba9939c43a1c43558a252f9b1d3453b0

WARNING

token验证不区分区服、设备、以及系统类型(Android、iOS等)

参数及值来源于SDK客户端登录成功后返回给游戏客户端登录数据。

expireTimestamp值(unix时间戳)为当前token值对应的有效维持到期时间。

# 3.3.2、签名规则

MD5(accountId+expireTimestamp+token+Appkey)

DANGER

以上参数名称替换为具体参数值,且"+"表示两个字符串的连接符,不要将"+"放入md5加密源串中

# 3.3.3、签名原文

MD5(14900140801569057445ba9939c43a1c43558a252f9b1d3453b02926cd821ee3479cbd54590ac6bdaa)

其中2926cd821ee3479cbd54590ac6bdaa为示例AppKey

# 3.3.4、签名结果

a7f44f39dcc7c5cb350da514799c0e05

# 3.3.5、数据验证

  • 将以上结果值与SDK客户端登录成功后的sign参数值进行比对,一致则表示验证通过。

# 4、常见问题

  • 登录服务器采用AppKey等加密签名机制验证安全性,请不要将AppKey配置在游戏客户端,并做好保密。

  • 登录服务器未设置任何黑白IP名单限制来源请求,如游戏服务器由负责运维,请联系相关运维同事开放防火墙中域名出口白名单,否则会导致在线验证超时。

  • 在线验证采用POST请求机制,数据body采用x-www-form-urlencoded模式。

# 5、调用代码示例

# 5.1 java调用示例

    public static void main(String[] args) {
        LoginCheckVO loginCheckVO = new LoginCheckVO();
        loginCheckVO.setAccountId(1490014080L);
        loginCheckVO.setAppid(1413829460);
        loginCheckVO.setToken("ba9939c43a1c43558a252f9b1d3453b0");
        loginCheckVO.setTimestamp(System.currentTimeMillis() / 1000);
        String appKey = "2926cd821ee3479cbd54590ac6bdaa";
        TestController testController = new TestController();
        try {
            loginCheckVO.setSign(testController.generateSign(loginCheckVO, appKey));
	        //接口域名根据实际情况替换
            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 = String.format("%d%d%d%s%s", loginCheckVO.getAccountId(), loginCheckVO.getAppid(), loginCheckVO.getTimestamp(), 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 false失败 true成功
     */
    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("timestamp", loginCheckVO.getTimestamp());
        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("resultCode") || jsonObject.getInteger("resultCode") != 10000) {
            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>
    /// 用于存储登录检查相关信息的类
    /// </summary>
    public class LoginCheckVO
    {
        public long AccountId { get; set; }
        public int appId { get; set; }
        public long Timestamp { get; set; }
        public string Token { get; set; }
        public string Sign { get; set; }
    }

    class Program
    {
        /// <summary>
        /// test入口方法
        /// </summary>
        /// <param name="context"></param>
        private async void testDemo(HttpContext context)
        {
            DHTech.Models.Common.CommonResponse responseE = new Models.Common.CommonResponse();
            responseE.ResponeCode = 0;
            responseE.ResponeInfo = "test方法返回";

            // 初始化LoginCheckVO对象以及相关信息
            LoginCheckVO loginCheckVO = new LoginCheckVO
            {
                AccountId = 1490014080,
                appId = 1413829460,
                Timestamp = (long)(DateTime.UtcNow - new DateTime(1970, 1, 1)).TotalSeconds,
                Token = "ba9939c43a1c43558a252f9b1d3453b0"
            };
            //your appKey
            string appKey = "2926cd821ee3479cbd54590ac6bdaa";
            string url = "https://sdk-login-cn.17m3.com/Wbsrv/Check_Login_DH_V2.aspx";

            // 生成签名
            loginCheckVO.Sign = GenerateSign(loginCheckVO, appKey);
            //请求校验接口校验  
            bool result = await CheckLoginAsync(url, loginCheckVO);
            if (result)
            {
                Console.WriteLine("登录成功");
            }
            else
            {
                Console.WriteLine("登录失败");
            }

            context.Response.Write(JsonConvert.SerializeObject(responseE));
        }
        
        
        /// <summary>
        /// 生成MD5签名
        /// </summary>
        /// <param name="loginCheckVO">登录信息</param>
        /// <param name="appKey">签名密钥</param>
        /// <returns>签名字符串</returns>
        public string GenerateSign(LoginCheckVO loginCheckVO, string appKey)
        {
            // 明文拼接 注意拼接顺序
			string text = String.Format("{0}{1}{2}{3}{4}", loginCheckVO.AccountId, loginCheckVO.appId,loginCheckVO.Timestamp, 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.ToString()), "accountId");
                content.Add(new StringContent(loginCheckVO.appId.ToString()), "appId");
                content.Add(new StringContent(loginCheckVO.Timestamp.ToString()), "timestamp");
                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["resultCode"] == null || (int)jsonObject["resultCode"] != 10000)
                        {
                            return false;
                        }
                        return true;
                    }
                }
                catch (Exception ex)
                {
                    // 处理超时或其他异常情况
                    Console.WriteLine("Exception: " + ex.Message);
                    return false;
                }
            }
        }
    }
	
	
		

# 5.3 PHP调用示例

<?php
class LoginCheckTest {
    // 定义LoginCheckVO类
    class LoginCheckVO {
        public $AccountId;
        public $AppId;
        public $Timestamp;
        public $Token;
        public $Sign;
    }
    // 定义testLoginCheck方法
    public function testLoginCheck()
    {
        $url = "登录校验接口地址";
        $appKey = "2926cd821ee3479cbd54590ac6bdaa";
        $loginCheckVO = new self::LoginCheckVO();
        $loginCheckVO->AccountId = 1490014080;
        $loginCheckVO->AppId = 1413829460;
        $loginCheckVO->Timestamp = time();
        $loginCheckVO->Token = "ba9939c43a1c43558a252f9b1d3453b0";

        $result = checkLogin($url, $loginCheckVO, $appKey);

        if ($result) {
            $this->assertEquals('登录验证成功', '登录验证成功');
        } else {
            $this->assertEquals('登录验证失败', '登录验证失败');
        }
    }
}


/**
 * 登录验证
 * @param $url 请求链接
 * @param $loginCheckVO 请求参数
 * @param $appKey 签名密钥
 * @return bool
 */
function checkLogin($url, $loginCheckVO, $appKey)
{
    //生成签名
    $loginCheckVO->Sign = md5($loginCheckVO->AccountId . $loginCheckVO->AppId . $loginCheckVO->Timestamp .
            $loginCheckVO->Token . $appKey);
    // 初始化cURL会话
    $ch = curl_init();

    // 设置参数
    $postData = [
        'accountId' => $loginCheckVO->AccountId,
        'appId' => $loginCheckVO->AppId,
        'timestamp' => $loginCheckVO->Timestamp,
        '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->resultCode) || $responseBody->resultCode != 10000) {
            return false;
        }
        return true;
    } catch (Exception $ex) {
        // 处理异常情况
        error_log("Exception: " . $ex->getMessage());
        return false;
    } finally {
        // 关闭cURL会话
        curl_close($ch);
    }
}

# 5.4 Golang调用示例

package main

import (
    "bytes"
    "crypto/md5"
    "encoding/hex"
    "encoding/json"
    "fmt"
    "io/ioutil"
    "net/http"
    "net/url"
    "time"
)

// LoginCheckVO 登录信息
type LoginCheckVO struct {
    AccountId int64
    AppId     int32
    Timestamp int64
    Token     string
    Sign      string
}

func main() {
    // 初始化LoginCheckVO结构,这里假设给出一些示例数值
    loginCheckVO := &LoginCheckVO{
        AccountId:  1490014080,
        AppId:      1413829460,
        Timestamp:  time.Now().Unix(),
        Token:      "ba9939c43a1c43558a252f9b1d3453b0",
    }
    //your appKey
    appKey := "2926cd821ee3479cbd54590ac6bdaa"
    sign, err := GenerateSign(loginCheckVO, appKey)
    if err!= nil {
        fmt.Printf("生成签名出错: %v\n", err)
        return
    }
    loginCheckVO.Sign = sign

    urlStr := "登录校验接口地址"
    success, err := CheckLogin(urlStr, loginCheckVO)
    if err!= nil {
        fmt.Printf("登录验证出错: %v\n", err)
        return
    }
    if success {
        fmt.Println("登录成功")
    } else {
        fmt.Println("登录失败")
    }
}

// GenerateSign 生成MD5签名
func GenerateSign(loginCheckVO *LoginCheckVO, appKey string) (string, error) {
    text := fmt.Sprintf("%d%d%d%s%s", loginCheckVO.AccountId, loginCheckVO.AppId, loginCheckVO.Timestamp, loginCheckVO.Token, appKey)
    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", fmt.Sprintf("%d", loginCheckVO.AccountId))
    data.Set("appId", fmt.Sprintf("%d", loginCheckVO.AppId))
    data.Set("timestamp", fmt.Sprintf("%d", loginCheckVO.Timestamp))
    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
    }
    var result map[string]interface{}
    err = json.Unmarshal(body, &result)
    if err!= nil {
        return false, err
    }
    code, ok := result["resultCode"]
    if!ok {
        return false, nil
    }
    num, ok := code.(float64)
    if!ok {
        return false, nil
    }
    if int(num)!= 10000 {
        return false, nil
    }
    return true, nil
}

Last Updated: 2024/12/24 17:13:30