限制错误次数,数据查询接口协议

图片 1

 

1.概述

image

登录 保存密码 安全 加密
最近发现群内大伙对用Hbuilder做的APP怎么做登录功能以及维护登录状态非常困惑,而我前一段时间正好稍微研究了一下,所以把我知道的告诉大家,节约大家查找资料的时间。

1.1.用途

向集成客户端提供查询数据和登录服务。
如果资源需要授权才能查看和使用,请首先阅读“5.登录验证”小节。

在院子里面看到了一个没人用的路由器(ws860s),看起来像个黑科技的玩意儿,就想着进去看看,到底有什么好玩的。看到后面的标签上有web界面的地址,然后登陆进去看看,发现有密码,然后我想,路由器的密码应该都是可以reset的,然后我就用笔戳那个reset键,奇迹没有发生,原来这个reset键坏了。

你是否真的需要登录功能?
把这个问题放在最前面并不是灌水,而是真的见过很多并不需要登录的APP去做了登录功能,或者是并不需要强制登录的APP把登录作为启动页。
用户对你的APP一无所知,你就要求对方注册并登录,除非APP本身已经很有名气或者是用户有强需求,否则正常人应该会直接把它删掉。
比较温和的方式是将一些并不需要登录,但可以给用户带来帮助的东西,第一时间展现给他们,让他们产生兴趣,再在合适的时机引导他们注册(比如使用需要使用更高级的功能,或用户需要收藏某个喜欢的信息时)。

1.2.通信协议

客户端和服务器通过HTTP协议通信,客户端使用HTTP
Get向服务器发送请求,服务器返回json格式的业务数据或操作结果给客户端。

图片 2

登录和注册要足够简单
这是小小的手机端,用再好的输入法,打字也是不方便的,所以别把登录页设计得需要填很多东西。如果有可能的话,只填手机号,让用户收到短信验证码就完成注册是最好不过的了。想获得更多信息?想想大公司的APP是怎么做的,他们会告诉用户,现在的个人资料完善程度是30%,如果想获得更多积分,你需要填完。
tips:如果你想发布在Appstore并且同时包含注册功能,那么注册页面必须做一个用户许可协议的链接,否则有可能通不过审核。

1.3.接口请求

接口地址是一个HTTP协议的url地址,具体格式是:

ip替换成实际服务器的ip或域名,如果端口不是默认端口,需要把端口加上。
token是认证字符串,在登录接口中获取,如果没有登录则省略。
其他内容参见接口的定义。
当URL请求参数值中包含URL地址保留字符时,应对参数值进行URL编码。
具体参见“RFC2396: Uniform Resource Identifiers (URI): Generic
Syntax”。
当请求参数包含中文字符时,应对中文字符采用UTF-8编码。

image

实现登录后的session有几种方式?
APP当浏览器用,直接载入远程页面

1.4.返回消息结构

返回的json消息数据结构具有严格的一致性,客户端可以采用一致的接收和解析方式处理返回消息。
简单消息
简单的返回消息包含对请求的处理结果,结构如下:

{
    "code":0,
    "err_desc":""
}

其中:
code 为0表示处理成功,其它值表示处理失败。
err_desc是对错误的描述,在code为0时err_desc会被省略。
特殊情况,在用户认证的login1和login2接口中,err_desc具有特殊用途用法,具体参见接口描述。除这两个接口之外,err_desc都表示错误描述。
带业务数据的消息
有的返回消息除了包含处理结果信息,还包含业务数据记录集,结构如下:

 {
        "code":0,
        "data":{
            "count":1,
            "items":[...]
        }
    }

其中:
data 业务数据的根节点:
count 业务数据的条数,可能的值为0 ~ n
items
业务数据,是一个数组,数据条数由count属性定义。当count为0时,items属性可能为null或者不存在。
本文档后续章节中,在描述items元素的属性时,会省略一些属性的描述,即实际调用接口返回的属性在本文档中可能会没有描述,这种情况下请直接忽略被忽略描述的属性值。本文档中描述的属性是实际返回内容的一个子集,没有描述到的内容对集成本系统没有影响。
带分页数据的消息
如果返回数据较多,服务器会对返回的数据进行分页,客户端可以按照页码请求指定范围的数据。带分页信息的返回数据结构如下:

    {
    "code":0,
    "data":{
        "page":1,
        "page_size":"20",
        "pages":"1",
        "total":"2",
        "count":2,
        "items":[...]
                }
}

分页数据信息在data元素下,意义如下:
page 当前页码
page_size 每页数据记录条数
pages 总共的页数
total 总数据条数
count 当前返回页的数据条数
如果返回的数据带有分页信息,则可以在调用接口时使用page参数来请求指定页码的数据。

图片 3

这种情况是很多偷懒的程序员或者傻X的老板选择的方式,因为做起来实在太快。如果本身网站是响应式布局,那么很有可能不需要做什么更改,就只要在开发时打开首页就好了,这样Hybird的APP外壳就纯粹成为了一个浏览器。
但比起这样做带来的无数缺点来,开发速度快的优点几乎可以忽略不计。
首先,在网络环境不佳时,纯大白页,用户体验0;
然后,CSS和JS等资源不在本地,需要远程载入,如果使用了bootstrap之类的框架,那用户为了开一下APP而耗费的流量真是令人感动;
再然后,网页里常用的jquery,在手机的webview里速度并不理想,而如果是非ajax的网页那就更糟心了,每次操作都要跳转和页面渲染,要让人把它当成APP那实在是笑话。
再再然后,这样的所谓APP,要通过Appstore的审查,那是做梦的(除非审核员当天闹肚子严重,拿着纸巾奔向厕所前误点了通过……),苹果的要求是,这得是APP,而不能是某个网站做成APP的样子,那样的情况适合做Web
APP。而据我所知,国内几个较大的Android市场,这样的APP也是无法通过审核的。

1.5.参考

[1] RFC 2616, Hypertext Transfer Protocol — HTTP/1.1[S].
[2] RFC 3986, Uniform Resource Identifier (URI): Generic
Syntax[S].
[3] Introducing JSON

image

调用后端接口

2.查询分类

分析过程

这是个很好的时代,因为无论后端你是用Java、PHP,还是node.js,都可以通过xml、json来和APP通讯。遥想当年写服务端要自己写包结构,然后为了解决并发问题还折腾了半年IOCP模型,真心觉得现在太幸福了。
把刚才那个用APP当浏览器使的案例的所有缺点反过来看,就是这样做的优点,在优化完善的情况下体验接近原生,而且通讯流量极少,通过各种审核也是妥妥的。
tips:通过plus对象中的XMLHttpRequest来Get、Post远程的后端接口,或者使用Mui中封装好的AJAX相关函数。

2.1.查询分类

  • 用途
    查询CMS上的分类信息。
    请求

    parent 上级分类编号。如果忽略,会返回一级分类列表。
    如果要查询一级分类,请去掉parent参数。

  • 响应

{
    "code": 0,
    "data": {
        "count": 2,
        "items": [
            {
                "id": 1,
                "name": "公共栏目",
                "comment": "",
                "upper_catalog_id": 0
            },
            {
                "id": 2,
                "name": "私有栏目",
                "comment": "",
                "upper_catalog_id": 0
            }
        ]
    }
}

id 编号
name 名称
comment 备注
“upper_catalog_id 上级分类编号, 0 表示当前分类是一级分类。

抓包

插一段代码,我把mui的ajax又做了进一步的封装,对超时进行了自动重试,而对invalid_token等情况也做相应处理:

2.2.查询分类树

  • 用途
    查询所有分类及其下级分类。
    请求

  • 响应

{
    "code": 0,
    "data": {
        "count": 2,
        "items": [
            {
                "id": 1,
                "name": "公共栏目",
                "comment": "",
                "upper_catalog_id": 0,
                "sub_items": [
                                {
                                    "id": 5,
                                    "name": "二级分类1",
                                    "comment": "",
                                    "upper_catalog_id": 1
                                },
                                ...
                    ]
            },
            ...
        ]
    }
}

id 编号
name 名称
comment 备注
“upper_catalog_id 上级分类编号, 0 表示当前分类是一级分类。
sub_items 下级分类数组,包含 0 或多个下级分类。

1、打开路由的web页面:192.168.3.1,路由器返回

mui.web_query = function(func_url, params, onSuccess, onError,
retry){
    var onSuccess = arguments[2]?arguments[2]:function(){};
    var onError = arguments[3]?arguments[3]:function(){};
    var retry = arguments[4]?arguments[4]:3;
    func_url = ‘’ + func_url;
    mui.ajax(func_url, {
        data:params,
        dataType:’json’,
        type:’post’,
        timeout:3000,
        success:function(data){
            if(data.err === ‘ok’){
                onSuccess(data);
            }
            else{
                onError(data.code);
            }
        },
        error:function(xhr,type,errorThrown){
            retry–;
            if(retry > 0) return mui.web_query(func_url, params,
onSuccess, onError, retry);
            onError(‘FAILED_NETWORK’);
        }
    })
};
var onError = function(errcode){
    switch(errcode){
    case ‘FAILED_NETWORK’:
        mui.toast(‘网络不佳’);
        break;
    case ‘INVALID_TOKEN’:
        wv_login.show();
        break;
    default:
        console.log(errcode);
    }
};
var params = {per:10, pageno:coms_current_pageno};
mui.web_query(‘get_com_list’, params, onSuccess, onError, 3);
调用后端接口怎么样才安全?
在APP中保存登录数据,每次调用接口时传输

3.查询媒体资源

图片 4

程序员总能给自己找到偷懒的方法,有的程序为了省事,会在用户登录后,直接把用户名和密码保存在本地,然后每次调用后端接口时作为参数传递。真省事儿啊!可这种方法简单就像拿着一袋子钱在路上边走边喊“快来抢我呀!快来抢我呀!”,一个小小的嗅探器就能把用户的密码拿到手,如果用户习惯在所有地方用一个密码,那么你闯大祸了,黑客通过撞库的方法能把用户的所有信息一锅端。

3.1.查询媒体资源

  • 用途
    查询媒体资源。
    可以查询某个编号的资源的信息,也可以查询某个分类下的所有资源信息。
    请求

    parent
    分类编号,如果要查询某个分类下的所有资源,请忽略下一个参数。
    media_id
    资源编号,如果给出该参数,则只查询编号为media_id的一个资源的信息,并忽略parent参数。

  • 响应

{
    "code": 0,
    "data": {
        "page": 1,
        "page_size": "20",
        "pages": 9,
        "total": "18",
        "count": 2,
        "items": [
            {
                "id": 79,
                "catalog_id": 2,
                "title": "vod - 8898",
                "sub_title": "G3视频",
                "abstract": null,
                "text": null,
                "resource_type": "vod",
                "cover": "/mserver/cms/covers/res_cover_79.jpg?1515729601",
                "duration": 98,
                "add_time": "2018-01-08 19:19:26",
                "view_times": 0,
                "open_status": 0
            },
            ...
        ]
    }
}

返回0个或多个资源信息。
id 资源编号
catalog_id 所属分类编号
title 标题
sub_title 小标题
abstract 摘要描述
text 描述
resource_type vod或live
cover 封面地址
duration 播放时长
add_time 添加时间
view_times 观看次数
open_status 开放状态

image

登录时请求一次token,之后用token调用接口

4.查询播放地址

图片 5

这是比较安全的方式,用户在登录时,APP调用获取token的接口(比如),用post将用户名和密码的摘要传递给服务器,然后服务器比对数据库中的用户信息,匹配则返回绑定该用户的token(这一般翻译为令牌,很直观的名字,一看就知道是有了这玩意,就会对你放行),而数据库中,在用户的token表中也同时插入了这个token相关的数据:这个token属于谁?这个token的有效期是多久?这个token当前登录的ip地址是?这个token对应的deviceid是?……
这样即便token被有心人截获,也不会造成太大的安全风险。因为没有用户名和密码,然后如果黑客通过这个token伪造用户请求,我们在服务器端接口被调用时就可以对发起请求的ip地址、user-agent之类的信息作比对,以防止伪造。再然后,如果token的有效期设得小,过一会儿它就过期了,除非黑客可以持续截获你的token,否则他只能干瞪眼。(插一句题外话:看到这里,是不是明白为什么不推荐在外面随便接入来历不明的wifi热点了?)
tips:token如何生成?
可以根据用户的信息及一些随机信息(比如时间戳)再通过hash编码(比如md5、sha1等)生成唯一的编码。
tips:token的安全级别,取决于你的实际需求,所以如果不是涉及财产安全的领域,并不建议太严格(比如用户走着走着,3G换了个基站,闪断了一下IP地址变了,尼玛token过期了,这就属于为了不必要的安全丢了用户体验,当然如果变换的IP地址跨省的话还是应该验证一下的,想想QQ有时候会让填验证码就明白了)。
tips:接口在返回信息时,可以包含本次请求的状态,比如成功调用,那么result[‘status’]可能就是’success’,而反之则是’error’,而如果是’error’,则result[‘errcode’]中就可以包含错误的原因,比如errcode中是’invalid_token’就可以告诉APP这个token过期或无效,这时APP应弹出登录框或者用本地存储的用户名或密码再次请求token(用户选择“记住密码”,就应该在本地保存用户名和密码的摘要,方法见plus.storage的文档)。

4.1.查询播放地址

  • 用途
    查询某个媒体资源的播放地址。
    请求

    media_id 资源编号。
    protocol
    播出协议,点播资源可以是hls,http-flv或http-mp4;直播资源可以是
    rtmp或hls。如果省略,返回所有协议的地址。
    客户端请根据终端类型选用合适的播出协议:
    android、ios或其他支持H5的浏览器,可以选用:hls, http-mp4协议
    PC选用http-flv或rtmp协议

  • 响应

{
    "code": 0,
    "data": {
        "count": 1,
        "items": [
            {
                "id": 104,
                "resource_id": 39,
                "web_url": "/mp4/vod/yellowstone/yellowstone.mp4",
                "web_io_protocol": "http-mp4",
                "add_time": "2017-08-25 16:35:16"
            }
        ]
    }
}

返回0个或多个播放地址,一个资源可能有多个不同协议的播放地址。
resource_id 资源编号
web_url 播出地址
“web_io_protocol 播出协议

image

再插点代码,基于plus.storage的用户信息类,注意:需要在plusReady之后再使用。

5.登录验证

会得到csrf和cookie和所需要的值,这些值都要保留下来,后面会用。

;function UserInfo(){
};

概述

  • 1)登录的要求和意义
    客户端应当首选判断服务器是否要求必须登录。如果要求,则应首先调用登录接口登录,然后再请求其他接口。
    是否需要强制登录,跟运营需求有关,网站运营者可以通过管理平台设置这个选项。
    如果没有强制要求,客户端可以登录,也可以不登陆。
    用户正确登录后,会获得一个token值,在后续的接口中,应当将该token值带入。例如:

  • 2)登录的流程
    服务器和客户端通过“挑战->应答”方式(challenge-response)进行身份认证交互,在这个过程中,客户端需要调用两次接口向服务器证明身份。认证过程中不需要传递密码,密码用于签名验证。
    身份认证的过如下:
    1)客户端使用“用户名”作为参数调用“login1”接口,向服务器发出身份认证请求
    1.1)服务器确认用户是否是有效的用户:
    1.2)若不是,则不做进一步处理,返回错误信息
    1.3)若是,服务器产生一个“随机数(挑战字符串)”发送给客户端
    2)客户端使用“用户密码”和“随机数(挑战字符串)”作为输入,按约定的算法生成一个hash值,用该hash值作为
    调用“login2”接口的参数,请求login2接口。
    2.1)服务器用收到的hash值与自己的计算结果比较,若二者相同,则通过认证;否则,认证失败
    2.2)若认证通过,服务器返回“token”给客户端,否者返回错误信息。

2、输入用户名密码后:

//清除登录信息
UserInfo.clear = function(){
    plus.storage.removeItem(‘username’);
    plus.storage.removeItem(‘password’);
    plus.storage.removeItem(‘token’);
}

5.1.判断是否必须登录

  • 用途
    判断是否要求必须登录。
    如果要求必须登录,则需要先登录,否者查询数据的接口会返回没有权限的错误。

请求

  • 响应

{
    "code": 0,
    "err_desc": "no"
}

err_desc 属性描述了对登录的要求。no 表示不强制要求, yes
表示必须要求登录。

图片 6

//检查是否包含自动登录的信息
UserInfo.auto_login = function(){
    var username = UserInfo.username();
    var pwd = UserInfo.password();
    if(!username || !pwd){
        return false;
    }
    return true;
}

5.2.login1

  • 用途
    提交认证申请,接口返回挑战字符串。
    请求

    username 登录用户名。

  • 响应

{
    "code": 0,
    "data": {
        "count": 1,
        "items": [
            {
                "id": 37,
                "chcode": "9luqgrnj5vvszmjw"
            }
        ]
    }
}

id session识别号,用于login2接口,原样传递给login2即可。
chcode
挑战字符串,客户端按约定规则使用该字符串生成一个hash值,然后调用login2接口。

image

//检查是否已登录
UserInfo.has_login = function(){
    var username = UserInfo.username();
    var pwd = UserInfo.password();
    var token = UserInfo.token();
    if(!username || !pwd || !token){
        return false;
    }
    return true;
};

5.3.login2

  • 用途
    使用login1返回的chcode计算出一个hash值,提交给本接口申请到一个token。该token值用于其他接口的认证。
    请求

    id login1接口返回的id值,原样带入。
    hash 根据约定计算出的hash值。算法:
    hash=md5(md5(password)+chcode)
    描述:首先计算出密码的hash值,然后在生成的密码hash值尾部拼接上挑战字符串形成新的字符串,最后计算这个新字符串的hash值。
    hash算法采用md5算法,生成的摘要采用16进制编码,编码生成的字符采用小写字母。
    例如,字符串111111的hash值是 96e79218965eb72c92a549dd5a330112

  • 响应

{
    "code": 0,
    "data": {
        "count": 6,
        "items": [{
            "id": 2,
            "name": "王工",
            "sex": 1,
            "logo": null,
            "token": "c9xpghlmgxn58kdq",
            "group_id": 1
        }]
    }
}

name 用户名
sex 性别,1男 0女
logo 用户头像,null或者头像url
token
认证令牌,在无法保持session的情况下,在请求其他接口中应当将token参数带入
group_id 用户所属的用户组

图片 7

UserInfo.username = function(){
    if(arguments.length == 0){
        return plus.storage.getItem(‘username’);
    }
    if(arguments[0] === ”){
        plus.storage.removeItem(‘username’);
        return;
    }
    plus.storage.setItem(‘username’, arguments[0]);
};

5.4.logout

  • 用途
    退出登录,退出应用前请尽量调用该接口。
    请求

    =abcdefg
    token 登录接口中获得的token

  • 响应

{
    "code": 0,
}

image

UserInfo.password = function(){
    if(arguments.length == 0){
        return plus.storage.getItem(‘password’);
    }
    if(arguments[0] === ”){
        plus.storage.removeItem(‘password’);
        return;
    }
    plus.storage.setItem(‘password’, arguments[0]);
};

图片 8

UserInfo.token = function(){
    if(arguments.length == 0){
        return plus.storage.getItem(‘token’);
    }
    if(arguments[0] === ”){
        plus.storage.removeItem(‘token’);
        return;
    }
    plus.storage.setItem(‘token’, arguments[0]);
};
这样当用户启动APP或使用了需要登录才能使用的功能时,就可以使用UserInfo.has_login()来判断是否已经登录,如果已登录,则使用UserInfo.token()来获取到token数据,作为参数调用远程的后端接口。

image

if(UserInfo.has_login()){
    //打开需要展示给用户的页面,或者是调用远端接口
}
else{
    wv_login.show(‘slide-in-up’);   //从底部向上滑出登录页面
}
在登录页面中,用户输入了用户名和密码后,并点击了”登录“按钮,我们下一步做什么?再插段代码(注意:此处使用的是我刚才代码中扩展的web_query函数,你也可以直接使用mui的ajax):

图片 9

function get_pwd_hash(pwd){
    var salt = ‘hbuilder’; 
//此处的salt是为了避免黑客撞库,而在md5之前对原文做一定的变形,可以设为自己喜欢的,只要和服务器验证时的salt一致即可。
    return md5(salt + pwd);
//此处假设你已经引用了md5相关的库,比如github上的JavaScript-MD5
}

image

//这里假设你已经通过DOM操作获取到了用户名和密码,分别保存在username和password变量中。
var username = xxx;
var password = xxx;
var pwd_hash = get_pwd_hash(password);

3、路由器返回数据

var onSuccess = function(data){
    UserInfo.username(username);
    UserInfo.password(pwd_hash);
    UserInfo.token(data.token); //把获取到的token保存到storage中
    var wc = plus.webview.currentWebview();
    wc.hide(‘slide-out-bottom’);   
//此处假设是隐藏登录页回到之前的页面,实际你也可以干点儿别的
}

图片 10

var onError = function(errcode){
    switch(errcode){
    case ‘INCORRECT_PASSWORD’:
        mui.toast(‘密码不正确’);
        break;
    case ‘USER_NOT_EXISTS’:
        mui.toast(‘用户尚未注册’);
        break;
    }
}

image

mui.web_query(‘get_token’, {username:username,password:pwd_hash},
onSuccess, onError, 3);
更安全一点,获取token通过SSL

密码的生成方法

刚才的方法,机智一点儿的读者大概会心存疑虑:那获取token时不还是得明文传输一次密码吗?
是的,你可以将这个获取token的地址,用SSL来保护(比如),这样黑客即使截了包,一时半会儿也解不出什么信息。
SSL证书的获取渠道很多,我相信你总有办法查到,所以不废话了。不过话说namecheap上的SSL证书比godaddy的要便宜得多……(这是吐槽)
tips:前段时间OpenSSL漏洞让很多服务器遭殃,所以如果自己搭服务器,一定记得装补丁。
tips:可以把所有接口都弄成SSL的吗?可以。但会拖慢服务器,如果是配置并不自信的VPS,建议不折腾。

从上面抓包的结果来看,Password字段是经过加密的,所以如果我们要Python暴力破解,需要把这个password的生成算法找出来。

还要更更安全(这标题真省事)

打开web登录页面,查看源代码,找找算法

还记得刚才APP向服务器请求token时,可以加入的用户信息吗?比如用户的设备deviceid。
如果我们在调用接口时,还附带一个当前时间戳参数timestamp,同时,用deviceid和这个时间戳再生成一个参数sign,比如
md5(deviceid timestamp
token)这样的形式。而服务端首先验证一下参数中的时间戳与当前服务器时间是否一致(误差保持在合理范围内即可,比如5分钟),然后根据用户保存在服务器中的deviceid来对参数中的时间戳进行相同的变形,验证是否匹配,那便自然“更更安全”了。
tips:如果对整个调用请求中的参数进行排序,再以deviceid和timestamp加上排序后的参数来对整个调用生成1个sign,黑客即使截获sign,不同的时间点、参数请求所使用的sign也是不同的,难以伪造,自然会更安全。当然,写起来也更费事。
tips:明白了原理,整个验证过程是可以根据自己的需求改造的。

图片 11

image

图片 12

image

图片 13

image

图片 14

image

图片 15

image

在linux上修改ip地址,很简单一条命令就可以解决:

图片 16

image

[password:bbbbbbbb];{“errorCategory”:”user_pass_err”,”csrf_param”:”FcnG919l8J7XhQsOYQEMS3WhsC2liSX”,”count”:2,”csrf_token”:”IQ/LfSZSx7gTp6VflYnZelobNSpoMy2″}

ip地址被限制,需要等待1分钟的提示:

[password:aaaaaaaa];{“errorCategory”:”Three_time_err”,”csrf_param”:”VKGTylVILQA9SFsTyYdpkHv8qfJPIIw”,”count”:3,”csrf_token”:”MTQLBcWQN+1DJjAP+A6xC4AUSXciBod”}

登录成功的提示:

****[password:xxxxxxxx];{“csrf_param”:”H/DyWxogz7+2y4UfzhqddowkjH1uL04″,”csrf_token”:”MorgBb0+PNpoE8KhwBwq4OoioD2NcCs”,”errorCategory”:”ok”,”level”:2,”IsWizard”:true,”IsFirst”:true}

流程

图片 17

image

所有的数据都准备好了,下一步,就是开始利用Python写程序了。

核心Python代码:

图片 18

image

图片 19

image

图片 20

image

图片 21

image

图片 22

image

图片 23

image

图片 24

image

图片 25

image

图片 26

image

图片 27

image

图片 28

image

图片 29

image

图片 30

image

图片 31

image

图片 32

image

怎么防止暴力破解?

图片 33

image

图片 34