# 登录验证
# 1、登录流程图
# 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×tamp=1722594966&token=d3c40875eee54920af0efc4ff8fb8b41&sign=a5295615da0840d4b856e8915eb9c95a"
# 2.5、返回参数说明
WARNING
- 返回参数将以
JSON
格式返回。当验证成功返回值resultCode
为10000
时,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
}