本文为菜鸟窝作者蒋志碧的连载。“从 0 开始开发一款直播 APP ”系列来聊聊时下最火的直播 APP,如何完整的实现一个类"腾讯直播"的商业化项目
App 在开发过程中要保证账户安全,那么 Cookie,Token,加密如何保证安全呢?
Cookie和 Token 在实际使用中有什么区别?
Cookie 最初是解决 Http 连接无状态问题的产物,用于客户端和服务器共同维护一些状态数据,Cookie 会被附加到 Http 请求中,这不需要开发者做额外的支持,Cookie 存在一个的最大长度(4KB)问题,不能无限制的存储 Cookie 文件。
Token 通常作为验证后的凭证,免除在一定时间内重复验证。Token 的存储和参数传递都需要开发者来处理。
使用 Cookie 来维持登录状态,在实现过程中实际上是在 Cookie 中添加一个 Token 来维持登录状态。
客户端如何对账户密码进行加密?
客户端登录请求参数:account,md5(password + salt),加密也可使用 AES。
服务端验证后:返回 result,token。
在传输过程中,就算别人不知道密码,看到加密后的加密文,也可以请求服务器并登录成功。为抵御重放攻击,那么就需要在请求中加入时间戳,对整个请求体做签名,服务器验证签名后检查时间戳以及 account,来决定是否响应
一、Cookie
Cookie 是 web 服务器存放在用户硬盘上的一段文本,Cookie 允许一个 web 在站点在用户的机器上存放一些文本信息,并可以在以后重新获取它,这个基于文本的信息存储着一些 「键-值」对,不包含在任何可执行代码。大多数需要登录的网址在用户验证成功之后都会设置一个 Cookie,只要这个 Cookie 存在并有效,用户就可以自由浏览这个网站的任意页面,再次说明,Cookie 只包含数据,其本身而言并不有害。
Cookie 属性
Domain:域,表示当前cookie所属于哪个域或子域下面。
如果一个 Cookie不设置对应的 Domain,那么在 CookieContainer.Add(cookies) 的时候,会死掉。对于服务器返回的 Set-Cookie 中,如果没有指定 Domain 的值,那么其 Domain 的值是默认为当前所提交的 Http 的请求所对应的主域名的。比如访问 ,返回一个 Cookie,没有指名 Domain 值,那么其为值为默认的 。如 http: //live.demo.cniao5.com/Api/User ,返回一个 Cookie ,没有指定 Domain 值,默认值为 live.demo.cniao5.com。
Path:表示 Cookie 所属路径。
Expire time/Max-age:表示了 Cookie 的有效期。expire 的值,是一个时间,过了这个时间,该 Cookie 就失效了。或者是用 max-age 指定当前cookie是在多长时间之后而失效。
secure:表示该cookie只能用https传输。
一般用于包含认证信息的cookie,要求传输此cookie的时候,必须用https传输。
httponly:表示此cookie必须用于http或https传输。
这意味着,浏览器脚本,比如 javascript 中,是不允许访问操作此 cookie 的。
Cookie 持久化存储
CookieJarImpl,读者可以自己查看
//泓洋大神的 Cookie 管理类
public class CookieJarImpl implements CookieJar
{
private CookieStore cookieStore;
public CookieJarImpl(CookieStore cookieStore)
{
if (cookieStore == null) Exceptions.illegalArgument("cookieStore can not be null.");
this.cookieStore = cookieStore;
}
@Override
public synchronized void saveFromResponse(HttpUrl url, List<Cookie> cookies)
{
//添加一条符合规范的 cookie
cookieStore.add(url, cookies);
}
@Override
public synchronized List<Cookie> loadForRequest(HttpUrl url)
{
//根据 URL 读取 URL 下的所有 cookie
return cookieStore.get(url);
}
public CookieStore getCookieStore()
{
return cookieStore;
}
}
在直播中对 Cookie 实现持久化存储和获取。这样在请求网络的时候就会携带 Cookie 信息。
public class AsyncHttp {
private static AsyncHttp mInstance;
//初始化 Cookie 实现类,PersistentCookieStore 该类实现 Cookie 持久化
private CookieJarImpl mCookieJar = new CookieJarImpl(new PersistentCookieStore(LiveApplication.getInstance()));
//初始化操作,设置超时时间
private OkHttpClient okHttpClient = new OkHttpClient.Builder()
.connectTimeout(20 * 1000, TimeUnit.MILLISECONDS)
.readTimeout(20 * 1000, TimeUnit.MILLISECONDS)
.cookieJar(mCookieJar)
.build();
}
PersistentCookieStore 类对 Cookie 进行了管理
public class PersistentCookieStore implements CookieStore
{
/**
* Construct a persistent cookie store.
* @param context Context to attach cookie store to
*/
public PersistentCookieStore(Context context)
{
cookiePrefs = context.getSharedPreferences(COOKIE_PREFS, 0);
cookies = new HashMap<String, ConcurrentHashMap<String, Cookie>>();
// Load any previously stored cookies into the store
//读取cookie文件中所有的cookie数据,遍历cookie的map集合
Map<String, ?> prefsMap = cookiePrefs.getAll();
for (Map.Entry<String, ?> entry : prefsMap.entrySet())
{
if (((String) entry.getValue()) != null && !((String) entry.getValue()).startsWith(COOKIE_NAME_PREFIX))
{
String[] cookieNames = TextUtils.split((String) entry.getValue(), ",");
for (String name : cookieNames)
{
String encodedCookie = cookiePrefs.getString(COOKIE_NAME_PREFIX + name, null);
if (encodedCookie != null)
{
Cookie decodedCookie = decodeCookie(encodedCookie);
if (decodedCookie != null)
{
if (!cookies.containsKey(entry.getKey()))
cookies.put(entry.getKey(), new ConcurrentHashMap<String, Cookie>());
//在这里获取 key 和 value
cookies.get(entry.getKey()).put(name, decodedCookie);
}
}
}
}
}
}
//其他是重写的一些get,add,remove等方法,读者自己查看
//........
}
在之前的登录文章中有对用户信息进行缓存处理,没讲解用户类哪里来的,这里说明一下,请看图。之前登陆是用的测试账号,现在使用菜鸟窝账号和密码进行登录,如果没有菜鸟窝账号进行登录,是无法发起直播的,而发起直播的条件就是「购买直播课程」,否则无法发起直播。在响应体中返回了 cookie 和 token。
二、Token
基于 Token 的身份验证是无状态的,我们不将用户信息存在服务器或 Session中。NoSession 意味着你的程序可以根据需要去增减机器而不用担心用户是否登录。
Token 身份验证过程
1、客户端使用用户名和密码请求登录
2、服务端收到请求,去验证用户名和密码
3、验证成功后,服务端会签发一个 Token,再把这个 Token 发送给客户端
4、客户端收到 Token 之后将 token 存储起来,如放在 Cookie 或 缓存
5、客户端每次向服务端请求资源的时候就需要带着服务端签发的 Token
6、服务端收到请求,然后去验证客户端请求里面带着的 Token 是否一致,如果验证成功,就向客户端返回请求数据
在返回体中可以看到 token 信息和 cookie 信息。在代码中会进行验证,保证账户安全,并且对密码进行加密。
在 IRequest 网络请求基类中添加 token 验证。
public abstract class IRequest extends IDontObfuscate {
protected RequestParams mParams = new RequestParams();
public RequestParams getParams() {
String token = UserInfoCache.getToken(LiveApplication.getInstance());
if (token != null){
mParams.put("token",token);
}
return mParams;
}
}
在 UserInfoCache 用户缓存类中存储 token,并从缓存中读取 token。
public class UserInfoCache extends IDontObfuscate{
public static void saveCache(Context context, UserInfo info){
ACache.get(context).put("token",info.getToken());
}
public static String getToken(Context context){
return ACache.get(context).getAsString("token");
}
}
将 LoginRequest 中请求参数替换成利用菜鸟窝账号登录的相关参数,并对密码进行加密。
public class LoginRequest extends IRequest {
public LoginRequest(int requestId, String userName, String password) {
mRequestId = requestId;
// mParams.put("action", "login");//普通账号登录
mParams.put("action", "loginCniaow");//发起直播需要调用这个接口,使用菜鸟窝账号并且购买了直播课程
mParams.put("userName", userName);
mParams.put("password", password);
if (mParams.getUrlParams("action").equals("loginCniaow")) {
mParams.put("password", CipherUtil.getAESInfo(password));
} else {
mParams.put("password", password);
}
}
@Override
public String getUrl() {
return getHost() + "User";
}
@Override
public Type getParserType() {
return new TypeToken<Response<UserInfo>>() {
}.getType();
}
}
三、AES 加密
在 LoginRequest 中对密码进行了 AES 加密,返回的是一串字符串。AES 是一个分组密码,属于对称密码,AES 算法的模块在对称密码领域特别是分组密码领域常有使用。
详情查看 AES 算法详解。
运行程序可以看到控制台打印出来的用户信息和 token,这里只打印了头像信息和 token。
详情转至 Github
撸这个项目的一半,你就是大神 , 戳http://mp.weixin.qq.com/s/ZagocTlDfxZpC2IjUSFhHg
网友评论