Android Cookie读写

1 在负责登录SDK的过程中,其他会遇到cookie丢失的问题,除了一些代码上的bug,也发现了登录成功后,成功写了CookieManger,并且调用了sync/flush接口,此时读cookie是成功的,但是重启后cookie却丢失了。

我们知道android底层使用了chrome浏览器,最终存储cookie是内存+sqlite数据库存储。那上面的现象大概就是内存已更新,sqlite未更新,因此借机也梳理了一把Android Cookie的底层源码。参考文档Android中CookieManager的底层实现中已经将getCookie的逻辑梳理的比较清楚了,因此我们主要针对setCookie进行源码梳理。

1 设置cookie

java 复制代码
CookieManager.getInstance().setCookie(url, cookieStr);

private void flushCookie() {
        try {
            if (Build.VERSION.SDK_INT >= 21) {
                CookieManager.getInstance().flush();
            } else {
                CookieSyncManager cookieSyncManager = CookieSyncManager.createInstance(mContext);
                cookieSyncManager.sync();
            }
        } catch (Throwable e) {
            e.printStackTrace();
        }
}

2 getInstance()

java 复制代码
    /**
     * Gets the singleton CookieManager instance.
     *
     * @return the singleton CookieManager instance
     */
    public static CookieManager getInstance() {
        return WebViewFactory.getProvider().getCookieManager();
    }

3 WebViewFactory

4 CookieManagerClassic

java 复制代码
class CookieManagerClassic extends CookieManager {

    /**
     * See {@link #setCookie(String, String)}
     * @param url The URL for which the cookie is set
     * @param value The value of the cookie, as a string, using the format of
     *              the 'Set-Cookie' HTTP response header
     * @param privateBrowsing Whether to use the private browsing cookie jar
     */
    void setCookie(String url, String value, boolean privateBrowsing) {
        WebAddress uri;
        try {
            uri = new WebAddress(url);
        } catch (ParseException ex) {
            Log.e(LOGTAG, "Bad address: " + url);
            return;
        }
        nativeSetCookie(uri.toString(), value, privateBrowsing);
    }



    @Override
    public String getCookie(String url, boolean privateBrowsing) {
        WebAddress uri;
        try {
            uri = new WebAddress(url);
        } catch (ParseException ex) {
            Log.e(LOGTAG, "Bad address: " + url);
            return null;
        }
        return nativeGetCookie(uri.toString(), privateBrowsing);
    }


}

5 CookieManager.cpp

java 复制代码
static jstring getCookie(JNIEnv* env, jobject, jstring url, jboolean privateBrowsing)
{
    GURL gurl(jstringToStdString(env, url));
    CookieOptions options;
    options.set_include_httponly();
    std::string cookies = WebCookieJar::get(privateBrowsing)->cookieStore()->GetCookieMonster()->GetCookiesWithOptions(gurl, options);
    return stdStringToJstring(env, cookies);
}

static void setCookie(JNIEnv* env, jobject, jstring url, jstring value, jboolean privateBrowsing)
{
    GURL gurl(jstringToStdString(env, url));
    std::string line(jstringToStdString(env, value));
    CookieOptions options;
    options.set_include_httponly();
    WebCookieJar::get(privateBrowsing)->cookieStore()->GetCookieMonster()->SetCookieWithOptions(gurl, line, options);
}

static void flushCookieStore(JNIEnv*, jobject)
{
    WebCookieJar::flush();
}
    
}

6 WebCookieJar.cpp

关键代码:WebCookieJar创建了CookieMonster 和 SQLitePersistentCookieStore

java 复制代码
namespace android {
static const std::string& databaseDirectory()
{
    // This method may be called on any thread, as the Java method is
    // synchronized.
    static WTF::Mutex databaseDirectoryMutex;
    MutexLocker lock(databaseDirectoryMutex);
    static std::string databaseDirectory;
    if (databaseDirectory.empty()) {
        JNIEnv* env = JSC::Bindings::getJNIEnv();
        jclass bridgeClass = env->FindClass("android/webkit/JniUtil");
        jmethodID method = env->GetStaticMethodID(bridgeClass, "getDatabaseDirectory", "()Ljava/lang/String;");
        databaseDirectory = jstringToStdString(env, static_cast<jstring>(env->CallStaticObjectMethod(bridgeClass, method)));
        env->DeleteLocalRef(bridgeClass);
    }
    return databaseDirectory;
}

static std::string databaseDirectory(bool isPrivateBrowsing)
{
    static const char* const kDatabaseFilename = "/webviewCookiesChromium.db";
    static const char* const kDatabaseFilenamePrivateBrowsing = "/webviewCookiesChromiumPrivate.db";
    std::string databaseFilePath = databaseDirectory();
    databaseFilePath.append(isPrivateBrowsing ? kDatabaseFilenamePrivateBrowsing : kDatabaseFilename);
    return databaseFilePath;
}


WebCookieJar* WebCookieJar::get(bool isPrivateBrowsing)
{
    MutexLocker lock(instanceMutex);
    if (!isFirstInstanceCreated && fileSchemeCookiesEnabled)
        net::CookieMonster::EnableFileScheme();
    isFirstInstanceCreated = true;
    scoped_refptr<WebCookieJar>* instancePtr = instance(isPrivateBrowsing);
    if (!instancePtr->get())
        *instancePtr = new WebCookieJar(databaseDirectory(isPrivateBrowsing));
    return instancePtr->get();
}


void WebCookieJar::initCookieStore() {
    MutexLocker lock(m_cookieStoreInitializeMutex);
    if (m_cookieStoreInitialized)
        return;
    // Setup the permissions for the file
    const char* cDatabasePath = m_databaseFilePath.c_str();
    mode_t mode = S_IRUSR | S_IWUSR | S_IRGRP | S_IWGRP;
    if (access(cDatabasePath, F_OK) == 0)
        chmod(cDatabasePath, mode);
    else {
        int fd = open(cDatabasePath, O_CREAT, mode);
        if (fd >= 0)
            close(fd);
    }
    FilePath cookiePath(cDatabasePath);
    m_cookieDb = new SQLitePersistentCookieStore(cookiePath);
    m_cookieStore = new net::CookieMonster(m_cookieDb.get(), 0);
    m_cookieStoreInitialized = true;
}

net::CookieStore* WebCookieJar::cookieStore()
{
    initCookieStore();
    return m_cookieStore.get();
}

void WebCookieJar::flush()
{
    // Flush both cookie stores (private and non-private), wait for 2 callbacks.
    static scoped_refptr<FlushSemaphore> semaphore(new FlushSemaphore());
    semaphore->SendFlushRequest(get(false)->cookieStore()->GetCookieMonster());
    semaphore->SendFlushRequest(get(true)->cookieStore()->GetCookieMonster());
    semaphore->Wait(2);
}

}

7 CookieMonster

cookie_monster.h中申明了一个内存map:cookies_

java 复制代码
class NET_EXPORT CookieMonster : public CookieStore {
 public:
  class PersistentCookieStore;

    using CookieMap =
      std::multimap<std::string, std::unique_ptr<CanonicalCookie>>;

    
    CookieMap cookies_;
    scoped_refptr<PersistentCookieStore> store_;

}

cookie_monster.cc中写单条cookie的方法为InternalInsertCookie,该方法中调用了store_的AddCookie方法,并且调用了cookies_的insert方法。

java 复制代码
// In steady state, most cookie requests can be satisfied by the in memory
// cookie monster store. If the cookie request cannot be satisfied by the in
// memory store, the relevant cookies must be fetched from the persistent
// store. The task is queued in CookieMonster::tasks_pending_ if it requires
// all cookies to be loaded from the backend, or tasks_pending_for_key_ if it
// only requires all cookies associated with an eTLD+1.
//
// On the browser critical paths (e.g. for loading initial web pages in a
// session restore) it may take too long to wait for the full load. If a cookie
// request is for a specific URL, DoCookieTaskForURL is called, which triggers a
// priority load if the key is not loaded yet by calling PersistentCookieStore
// :: LoadCookiesForKey. The request is queued in
// CookieMonster::tasks_pending_for_key_ and executed upon receiving
// notification of key load completion via CookieMonster::OnKeyLoaded(). If
// multiple requests for the same eTLD+1 are received before key load
// completion, only the first request calls
// PersistentCookieStore::LoadCookiesForKey, all subsequent requests are queued
// in CookieMonster::tasks_pending_for_key_ and executed upon receiving
// notification of key load completion triggered by the first request for the
// same eTLD+1.


bool CookieMonster::SetCookieWithOptions(const GURL& url,
                                         const std::string& cookie_line,
                                         const CookieOptions& options) {
  DCHECK(thread_checker_.CalledOnValidThread());
  if (!HasCookieableScheme(url)) {
    return false;
  }
  return SetCookieWithCreationTimeAndOptions(url, cookie_line, Time(), options);
}

bool CookieMonster::SetCookieWithCreationTimeAndOptions(
    const GURL& url,
    const std::string& cookie_line,
    const Time& creation_time_or_null,
    const CookieOptions& options) {
  DCHECK(thread_checker_.CalledOnValidThread());
  VLOG(kVlogSetCookies) << "SetCookie() line: " << cookie_line;
  Time creation_time = creation_time_or_null;
  if (creation_time.is_null()) {
    creation_time = CurrentTime();
    last_time_seen_ = creation_time;
  }
  std::unique_ptr<CanonicalCookie> cc(
      CanonicalCookie::Create(url, cookie_line, creation_time, options));
  if (!cc.get()) {
    VLOG(kVlogSetCookies) << "WARNING: Failed to allocate CanonicalCookie";
    return false;
  }
  return SetCanonicalCookie(std::move(cc), url, options);
}

bool CookieMonster::SetCanonicalCookie(std::unique_ptr<CanonicalCookie> cc,
                                       const GURL& source_url,
                                       const CookieOptions& options) {
  DCHECK(thread_checker_.CalledOnValidThread());
  Time creation_time = cc->CreationDate();
  const std::string key(GetKey(cc->Domain()));
  bool already_expired = cc->IsExpired(creation_time);
  if (DeleteAnyEquivalentCookie(key, *cc, source_url,
                                options.exclude_httponly(), already_expired,
                                options.enforce_strict_secure())) {
    std::string error;
    if (options.enforce_strict_secure()) {
      error =
          "SetCookie() not clobbering httponly cookie or secure cookie for "
          "insecure scheme";
    } else {
      error = "SetCookie() not clobbering httponly cookie";
    }
    VLOG(kVlogSetCookies) << error;
    return false;
  }
  VLOG(kVlogSetCookies) << "SetCookie() key: " << key
                        << " cc: " << cc->DebugString();
  // Realize that we might be setting an expired cookie, and the only point
  // was to delete the cookie which we've already done.
  if (!already_expired) {
    // See InitializeHistograms() for details.
    if (cc->IsPersistent()) {
      histogram_expiration_duration_minutes_->Add(
          (cc->ExpiryDate() - creation_time).InMinutes());
    }
    InternalInsertCookie(key, std::move(cc), source_url, true);
  } else {
    VLOG(kVlogSetCookies) << "SetCookie() not storing already expired cookie.";
  }
  // We assume that hopefully setting a cookie will be less common than
  // querying a cookie.  Since setting a cookie can put us over our limits,
  // make sure that we garbage collect...  We can also make the assumption that
  // if a cookie was set, in the common case it will be used soon after,
  // and we will purge the expired cookies in GetCookies().
  GarbageCollect(creation_time, key, options.enforce_strict_secure());
  return true;
}

CookieMonster::CookieMap::iterator CookieMonster::InternalInsertCookie(
    const std::string& key,
    std::unique_ptr<CanonicalCookie> cc,
    const GURL& source_url,
    bool sync_to_store) {
  DCHECK(thread_checker_.CalledOnValidThread());
  CanonicalCookie* cc_ptr = cc.get();
  if ((cc_ptr->IsPersistent() || persist_session_cookies_) && store_.get() &&
      sync_to_store)
    store_->AddCookie(*cc_ptr);
  CookieMap::iterator inserted =
      cookies_.insert(CookieMap::value_type(key, std::move(cc)));
  if (delegate_.get()) {
    delegate_->OnCookieChanged(*cc_ptr, false,
                               CookieStore::ChangeCause::INSERTED);
  }
  // See InitializeHistograms() for details.
  int32_t type_sample = cc_ptr->SameSite() != CookieSameSite::NO_RESTRICTION
                            ? 1 << COOKIE_TYPE_SAME_SITE
                            : 0;
  type_sample |= cc_ptr->IsHttpOnly() ? 1 << COOKIE_TYPE_HTTPONLY : 0;
  type_sample |= cc_ptr->IsSecure() ? 1 << COOKIE_TYPE_SECURE : 0;
  histogram_cookie_type_->Add(type_sample);
  // Histogram the type of scheme used on URLs that set cookies. This
  // intentionally includes cookies that are set or overwritten by
  // http:// URLs, but not cookies that are cleared by http:// URLs, to
  // understand if the former behavior can be deprecated for Secure
  // cookies.
  if (!source_url.is_empty()) {
    CookieSource cookie_source_sample;
    if (source_url.SchemeIsCryptographic()) {
      cookie_source_sample =
          cc_ptr->IsSecure()
              ? COOKIE_SOURCE_SECURE_COOKIE_CRYPTOGRAPHIC_SCHEME
              : COOKIE_SOURCE_NONSECURE_COOKIE_CRYPTOGRAPHIC_SCHEME;
    } else {
      cookie_source_sample =
          cc_ptr->IsSecure()
              ? COOKIE_SOURCE_SECURE_COOKIE_NONCRYPTOGRAPHIC_SCHEME
              : COOKIE_SOURCE_NONSECURE_COOKIE_NONCRYPTOGRAPHIC_SCHEME;
    }
    histogram_cookie_source_scheme_->Add(cookie_source_sample);
  }
  RunCookieChangedCallbacks(*cc_ptr, CookieStore::ChangeCause::INSERTED);
  return inserted;
}

8 SQLitePersistentCookieStore

持久化存储,最终调用sqlite写db

sqlite_persistent_cookie_store.h

java 复制代码
class COMPONENT_EXPORT(NET_EXTRAS) SQLitePersistentCookieStore
    : public CookieMonster::PersistentCookieStore {

    class Backend;
    const scoped_refptr<Backend> backend_;

}  

sqlite_persistent_cookie_store.cc

代码中可以看到AddCookie最终调用了BatchOperation进行批处理。类定义前面的一段说明,也讲述了db的更新频次:每30s,或者每512个操作 ,或者调用Flush方法, 会触发一次写db。

java 复制代码
// This class is designed to be shared between any client thread and the
// background task runner. It batches operations and commits them on a timer.
//
// SQLitePersistentCookieStore::Load is called to load all cookies.  It
// delegates to Backend::Load, which posts a Backend::LoadAndNotifyOnDBThread
// task to the background runner.  This task calls Backend::ChainLoadCookies(),
// which repeatedly posts itself to the BG runner to load each eTLD+1's cookies
// in separate tasks.  When this is complete, Backend::CompleteLoadOnIOThread is
// posted to the client runner, which notifies the caller of
// SQLitePersistentCookieStore::Load that the load is complete.
//
// If a priority load request is invoked via SQLitePersistentCookieStore::
// LoadCookiesForKey, it is delegated to Backend::LoadCookiesForKey, which posts
// Backend::LoadKeyAndNotifyOnDBThread to the BG runner. That routine loads just
// that single domain key (eTLD+1)'s cookies, and posts a Backend::
// CompleteLoadForKeyOnIOThread to the client runner to notify the caller of
// SQLitePersistentCookieStore::LoadCookiesForKey that that load is complete.
//
// Subsequent to loading, mutations may be queued by any thread using
// AddCookie, UpdateCookieAccessTime, and DeleteCookie. These are flushed to
// disk on the BG runner every 30 seconds, 512 operations, or call to Flush(),
// whichever occurs first.

class SQLitePersistentCookieStore::Backend
    : public SQLitePersistentStoreBackendBase {


    void SQLitePersistentCookieStore::AddCookie(const CanonicalCookie& cc) {
      backend_->AddCookie(cc);
    }

    
void SQLitePersistentCookieStore::Backend::AddCookie(
    const CanonicalCookie& cc) {
  BatchOperation(PendingOperation::COOKIE_ADD, cc);
}


void SQLitePersistentCookieStore::Backend::BatchOperation(
    PendingOperation::OperationType op,
    const CanonicalCookie& cc) {
  // Commit every 30 seconds.
  constexpr base::TimeDelta kCommitInterval = base::Seconds(30);
  // Commit right away if we have more than 512 outstanding operations.
  constexpr size_t kCommitAfterBatchSize = 512;
  DCHECK(!background_task_runner()->RunsTasksInCurrentSequence());
  // We do a full copy of the cookie here, and hopefully just here.
  auto po = std::make_unique<PendingOperation>(op, cc);
  PendingOperationsMap::size_type num_pending;
  {
    base::AutoLock locked(lock_);
    // When queueing the operation, see if it overwrites any already pending
    // ones for the same row.
    auto key = cc.StrictlyUniqueKey();
    auto iter_and_result = pending_.emplace(key, PendingOperationsForKey());
    PendingOperationsForKey& ops_for_key = iter_and_result.first->second;
    if (!iter_and_result.second) {
      // Insert failed -> already have ops.
      if (po->op() == PendingOperation::COOKIE_DELETE) {
        // A delete op makes all the previous ones irrelevant.
        ops_for_key.clear();
      } else if (po->op() == PendingOperation::COOKIE_UPDATEACCESS) {
        if (!ops_for_key.empty() &&
            ops_for_key.back()->op() == PendingOperation::COOKIE_UPDATEACCESS) {
          // If access timestamp is updated twice in a row, can dump the earlier
          // one.
          ops_for_key.pop_back();
        }
        // At most delete + add before (and no access time updates after above
        // conditional).
        DCHECK_LE(ops_for_key.size(), 2u);
      } else {
        // Nothing special is done for adds, since if they're overwriting,
        // they'll be preceded by deletes anyway.
        DCHECK_LE(ops_for_key.size(), 1u);
      }
    }
    ops_for_key.push_back(std::move(po));
    // Note that num_pending_ counts number of calls to BatchOperation(), not
    // the current length of the queue; this is intentional to guarantee
    // progress, as the length of the queue may decrease in some cases.
    num_pending = ++num_pending_;
  }
  if (num_pending == 1) {
    // We've gotten our first entry for this batch, fire off the timer.
    if (!background_task_runner()->PostDelayedTask(
            FROM_HERE, base::BindOnce(&Backend::Commit, this),
            kCommitInterval)) {
      DUMP_WILL_BE_NOTREACHED() << "background_task_runner() is not running.";
    }
  } else if (num_pending == kCommitAfterBatchSize) {
    // We've reached a big enough batch, fire off a commit now.
    PostBackgroundTask(FROM_HERE, base::BindOnce(&Backend::Commit, this));
  }
}


}    

从源码上看,理论上用户登录成功,SDK调用flush方法后,就会触发写db。除非用户很快的离开了App,db来不及完成写入的操作。否则不应该出现该情况。但上报打点中,确实有此类case。因此我们在app启动,初始化SDK时,进行了cookie校验,以及cookie恢复操作。

参考文章:

1 Android中CookieManager的底层实现-CSDN博客

2 cookieMonster设计文档

3 chome源码学习

源码:

sqlite_persistent_cookie_store.cc

cookie_monster.h

cookie_monster.cc

sqlite_persistent_cookie_store.h

相关推荐
openinstall全渠道统计3 小时前
免填邀请码工具:赋能六大核心场景,重构App增长新模型
android·ios·harmonyos
双鱼大猫3 小时前
一句话说透Android里面的ServiceManager的注册服务
android
双鱼大猫4 小时前
一句话说透Android里面的查找服务
android
双鱼大猫4 小时前
一句话说透Android里面的SystemServer进程的作用
android
双鱼大猫4 小时前
一句话说透Android里面的View的绘制流程和实现原理
android
双鱼大猫4 小时前
一句话说透Android里面的Window的内部机制
android
双鱼大猫5 小时前
一句话说透Android里面的为什么要设计Window?
android
双鱼大猫5 小时前
一句话说透Android里面的主线程创建时机,frameworks层面分析
android
苏金标5 小时前
android 快速定位当前页面
android
雾里看山9 小时前
【MySQL】内置函数
android·数据库·mysql