认证回调

上图简要说明了用户认证的过程:

  1. 拍摄客户端调用SDK的登录API,SDK向xvs直播云发出认证请求
  2. 直播云将认证请求发送(回调)至业务接口服务器
  3. 应用接口服务器根据数据库中的用户名密码完成认证,返回认证结果
  4. 直播云将登录结果返回SDK
  5. sdk通过消息或回调方式返回登录结果给上层应用

上图中实线部分即为本文档描述的接口,SDK与直播云之间的登录交互由SDK实现,应用无需关心。 认证接口由应用开发者实现,直播云向认证接口发出HTTP请求, 注意: 回调请求是HTTP GET, 实现的应用接口应该接收并处理HTTP GET请求

客户可以登录到直播云控制台,自服务修改认证接口地址。如果客户不想自己实现认证接口,则可以使用直播云的用户管理系统,在这种情况下同样可以登录直播云控制台进行用户的管理。

认证有以下两种模式:

模式 认证模式字段值 说明
明文认证 2 传输明文密码
MD5挑战模式 3 密码经一定运算后的HASH值

字段名 说明 备注
username 用户名
password 明文密码
service_code 服务码
authen_mode 认证模式 固定为2

云平台默认的认证方式是挑战认证方式,如果需要明文认证,则需要向我们申请,同时在客户端上调用相应的API,强制成明文认证。

Android SDK强制明文认证的的API为

Manager.forceAuthenMode(mode)
//mode = Manager.HANDSHAKE_CLEAR_PASSWORD 强制明文认证
//mode = Manager.HANDSHAKE_MD5_CHALLENGE 挑战认证模式

iOS SDK强制明文认证的API为

(void)setAuthMode:(AuthMode)authMode
CHALLENGE_PASSWORD-加密,CLEAR_PASSWORD-明文

字段名 说明 备注
username 用户名
service_code 服务码
challenge 挑战字串
response 挑战应答
authen_mode 认证模式 固定为3

对于明文认证很简单,应用接口服务器在收到通知后取出用户名和密码字段,核对即可。这里不多说了。

下面详细介绍一下挑战认证的算法,

挑战认证的目的是传输时不传输原始的密码串,其核心概念是请求认证方把密码password和种子码chanllenge按一定算法进行hash运算,然后把hash的结果response和种子码一起传给认证方,认证方用同样的算法把自己这里保存的密码做同样的运算,把运算结果和认证方传来的认证结果比较,如果一致则认证通过,否则认证失败。

  1. challenge和response都是将一个16byte的二进制数组用十六进制表示的字符串。每次请求,这两个字段的值都是变化的。
  2. challenge字串是16byte的随机值生成的。
  3. response是由密码,challenge计算而得的。具体的算法: BytesToHexString( Md5(Md5(password) + HexStringToBytes(challenge) ) )

下面是三个函数的定义: md5是做hash运算的函数 byte [] Md5(byte [] param)

注意传入的参数是一个byte数组,输出也是byte数组,不是字符串

StringToBytes是指将以16进制表示的字串解码成byte数组,输入是String,输出是byte数组。

byte []HexStringToBytes(String param)

例如:输入字串 param = "1a2b3c4d5e6f7080" 则输出应该是 [0x1a, 0x2b, 0x3c, 0x4d, 0x5e, 0x6f, 0x70, 0x80]的一个数组.

BytesToHexString与StringToBytes相反,是将一个数组用十六进制字符串来表示。

String BytesToHexString(byte [] param)

例如输入para是m = [0x1a, 0x2b, 0x3c, 0x4d, 0x5e, 0x6f, 0x70, 0x80] 则返回是字符串: "1a2b3c4d5e6f7080"

以下表示两个byte数组合并: Md5(password) + HexStringToBytes(challenge)

例如: Md5(password)返回的 [0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7], HexStringToBytes返回的是[0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf] 则合并后为:[0x0, 0x1, 0x2, 0x3, 0x4, 0x5, 0x6, 0x7, 0x8, 0x9, 0xa, 0xb, 0xc, 0xd, 0xe, 0xf]

下面是一个真实的挑战认证请求,其中query parameter如下:

username=glass1&service_code=DEVEL&challenge=4d0606d422bed2376f2c22ba268a1cf2&response=99c823c2973e6418175e7a8ced39b8c0&authen_mode=3                   

假设应用数据库中保存的密码是123456, 则验证过程如下:

md5(byte array of '123456') = [0xe1, 0x0a, 0xdc, 0x39, 0x49, 0xba, 0x59, 0xab, 0xbe, 0x56, 0xe0, 0x57, 0xf2, 0x0f, 0x88, 0x3e]
HexStringToBytes(challenge) = [0x4d, 0x06, 0x06, 0xd4, 0x22, 0xbe, 0xd2, 0x37, 0x6f, 0x2c, 0x22, 0xba, 0x26, 0x8a, 0x1c, 0xf2]
md5(byte array of '123456') + HexStringToBytes(challenge) = [0xe1, 0x0a, 0xdc, 0x39, 0x49, 0xba, 0x59, 0xab, 0xbe, 0x56, 0xe0, 0x57, 0xf2, 0x0f, 0x88, 0x3e, 0x4d, 0x06, 0x06, 0xd4, 0x22, 0xbe, 0xd2, 0x37, 0x6f, 0x2c, 0x22, 0xba, 0x26, 0x8a, 0x1c, 0xf2]
md5(md5(byte array of '123456') + HexStringToBytes(challenge))  = [0x99, 0xc8, 0x23, 0xc2, 0x97, 0x3e, 0x64, 0x18, 0x17, 0x5e, 0x7a, 0x8c, 0xed, 0x39, 0xb8, 0xc0]
BytesToHexString(Md5(Md5(password)+HexStringToBytes(challenge))) = "99c823c2973e6418175e7a8ced39b8c0"

将计算出来的结果与response字段的值比较,发现两者一致,则认为用户密码合法,认证通过。

有的时候应用出于安全的考虑数据库中保存的已经是经过md5运算密码的hash值,则md5(password)这一步跳过,直接将保存的hash过的字串做计算: 上面的公式就改为了: BytesToHexString( Md5(HexStringToBytes(hashed_password) + HexStringToBytes(challenge) ) )

function calculate_challenge_response(challenge, password) {
  var md5_signer = crypto.createHash('md5');
  md5_signer.update(password);
  var hashed_pass = md5_signer.digest();
  md5_signer = crypto.createHash('md5');
  md5_signer.update(hashed_pass);
  md5_signer.update(new Buffer(challenge, 'hex'));
  return md5_signer.digest('hex');
}

返回值即为计算出的response字串。

如果a和b都是数组 md5_signer.update(a) md5_signer.update(b) 的结果和 md5_signer.update(a.concat(b)) 的结果是一样的。

/*
* 将两个ASCII字符合成一个字节; 
* 如:"EF"--> 0xEF 
* @param src0 byte 
* @param src1 byte 
* @return byte 
*/ 
public static byte uniteBytes(byte src0, byte src1) { 
    byte _b0 = Byte.decode("0x" + new String(new byte[]{src0})).byteValue(); 
    _b0 = (byte)(_b0 << 4); 
    byte _b1 = Byte.decode("0x" + new String(new byte[]{src1})).byteValue(); 
    byte ret = (byte)(_b0 ^ _b1); 
    return ret; 
} 

/** 
* 将指定字符串src,以每两个字符分割转换为16进制形式 
* 如:"2B44EFD9" --> byte[]{0x2B, 0x44, 0xEF, 0xD9} 
* @param src String 
* @return byte[] 
*/ 
public static byte[] HexString2Bytes(String src){ 
    byte[] ret = new byte[src.length()/2]; 
    byte[] tmp = src.getBytes(); 
    for(int i=0; i<src.length()/2; i++){ 
        ret[i] = uniteBytes(tmp[i*2], tmp[i*2+1]); 
    } 
    return ret; 
}

byte[] newpwd = Md5Util.HexString2Bytes(password)
byte[] newchallenge = Md5Util.HexString2Bytes(challenge)
//将密码和挑战串转成16进制表示的字节数据
byte[] result =new byte[newpwd.length+newchallenge.length]
System.arraycopy(newpwd, 0, result, 0, newpwd.length);
System.arraycopy(newchallenge, 0, result, newpwd.length, newchallenge.length)
//合并数据
String response = Md5Util.byte2MD5(result)
//md5加密计算response

byte[] bytepwd = HexStringToBytes(md5(pwd));
byte[] bytechallenge = HexStringToBytes(challenge);
byte[] newbyte = copybyte(bytepwd, bytechallenge);
string response =MD5(newbyte);

//16进制字符串转换为字节数组
public static byte[] HexStringToBytes(string mHex)
{
    {
        mHex = mHex.Replace(" ", "");
        if (mHex.Length % 2 != 0) { mHex = ""; };
        byte[] vBytes = new byte[mHex.Length / 2];
        string TempStr = "";
        for (int i = 0; i < mHex.Length; i += 2)
        {
            TempStr = mHex.Substring(i, 2);
             vBytes[i / 2] = Convert.ToByte(TempStr, 16);
         }
         return vBytes;
    }
}

 $cal_response = md5(hex2bin( md5($password) . $challenge))

其中$password为密码字串, $challenge是回调请求的challenge字串。

相关参考: Java中bytes []Hex String互相转换的工具类参见: "Hex":http://commons.apache.org/proper/commons-codec/apidocs/org/apache/commons/codec/binary/Hex.html 其中上述工式中提到的: BytesToHexString相当于 String encodeHexString(byte[] data) HexStringToBytes相当于:static byte[] decodeHex(char[] data)

应用认证接口收到云平台的认证请求后,应该返回json字串来表示认证的结果

{"ret": 0}

返回参数释义:

  • ret 此参数必须有,直播云根据应用认证有接口的返回json串的ret参数值判断登录用户是否合法,ret = 0表示认证成功,其他值均表示认证失败。
  • output_formats 用户定制化的输出描述字串,这里可以返回给当前认证用户定制化的输出描述,通过此字段,则可以实现同一服务码下不同用户输出不同格式,或不同地址的业务功能。比如某个应用需要通过直播云向其他平台推出rtmp流,并且需要每个用户推到不同的地址,则可以通过此字段来定义每个用户的直播输出行为。

    这里举个例子,某个应用需要将用户1的直播流推送到 rtmp://xyz.the3rd.com:1935/user1这个rtmp地址,而用户2要推到 rtmp://the3rd.com:1935/user2, 则用户user1认证成功后返回的output_formats输出格式串为

    <output tag="rtmp_push">
      <extension>rtmp</extension>
      <format>flv</format>
      <codec-v>h264</codec-v>
      <codec-a>aac</codec-a>
      <try-copy-video/>
      <output-url>xyz.the3rd.com:1935/user1</output-url>
    </output> 

    而用户2认证成功后返回的output_formats可以为

    <output tag="rtmp_push">
      <extension>rtmp</extension>
      <format>flv</format>
      <codec-v>h264</codec-v>
      <codec-a>aac</codec-a>
      <try-copy-video/>
      <output-url>xyz.the3rd.com:1935/user2</output-url>
    </output> 

    则当用户 user1 发起一个直播,直播云给他分配的task_id为 ae12f87a2时,其直播流实际被推到了这样一个地址 rtmp://xyz.the3rd.com:1935/user1/ae12f87a2。通过此种方法,可以针对不同用户输出不同格式、不同路径、不同的向外推流地址等。