【从 0 开始开发一款直播 APP】16 利用 Cookie、Token、加密保证用户安全

1,378 阅读6分钟
原文链接: www.cniao5.com
本文为菜鸟窝作者蒋志碧的连载。“从 0 开始开发一款直播 APP ”系列来聊聊时下最火的直播 APP,如何完整的实现一个类”腾讯直播”的商业化项目

App 在开发过程中要保证账户安全,那么 Cookie,Token,加密如何保证安全呢?

Cookie 最初是解决 Http 连接无状态问题的产物,用于客户端和服务器共同维护一些状态数据,Cookie 会被附加到 Http 请求中,这不需要开发者做额外的支持,Cookie 存在一个的最大长度(4KB)问题,不能无限制的存储 Cookie 文件。

Token 通常作为验证后的凭证,免除在一定时间内重复验证。Token 的存储和参数传递都需要开发者来处理。

使用 Cookie 来维持登录状态,在实现过程中实际上是在 Cookie 中添加一个 Token 来维持登录状态。

客户端如何对账户密码进行加密?

客户端登录请求参数:account,md5(password + salt),加密也可使用 AES。

服务端验证后:返回 result,token。

在传输过程中,就算别人不知道密码,看到加密后的加密文,也可以请求服务器并登录成功。为抵御重放攻击,那么就需要在请求中加入时间戳,对整个请求体做签名,服务器验证签名后检查时间戳以及 account,来决定是否响应

Cookie 是 web 服务器存放在用户硬盘上的一段文本,Cookie 允许一个 web 在站点在用户的机器上存放一些文本信息,并可以在以后重新获取它,这个基于文本的信息存储着一些 「键-值」对,不包含在任何可执行代码。大多数需要登录的网址在用户验证成功之后都会设置一个 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 的。

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

更多内容,请关注菜鸟窝(微信公众号ID: cniao5),程序猿的在线学习平台。 如需转载,请注明出处(菜鸟窝 , 原文链接: http://www.cniao5.com/forum/thread/746785dc32d811e7882200163e0230fa

关注公众号免费领取" N套客户端实战项目教程"