当前位置: 首页 > article >正文

Android Cookie读写

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

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

 1 设置cookie  

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()

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

3 WebViewFactory

4 CookieManagerClassic

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

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();
}
    
}

WebCookieJar.cpp

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

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_

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方法。

// 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

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。

// 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


http://www.kler.cn/a/413136.html

相关文章:

  • PySide6 QSS(Qt Style Sheets) Reference: PySide6 QSS参考指南
  • BUUCTF—Reverse—不一样的flag(7)
  • spring boot框架漏洞复现
  • 概率论中交并集的公式
  • Java后端如何进行文件上传和下载 —— 本地版
  • Springboot启动报错’javax.management.MBeanServer’ that could not be found.
  • nuxt3项目搭建相关
  • 26届JAVA 学习日记——Day16
  • Python图像处理:打造平滑液化效果动画
  • Vue: computed 计算属性
  • 11.27 深度学习-损失函数+BP算法
  • 高级 SQL 技巧:提升数据库操作效率与灵活性
  • xiaolin coding 图解网络笔记——IP 篇
  • 【算法】快速求出 n 最低位的 1
  • 【大数据学习 | Spark-SQL】Spark-SQL编程
  • 如何做好一份技术文档?
  • 新型大语言模型的预训练与后训练范式,阿里Qwen
  • 网络安全审计机制与实现技术
  • Unity3D Lua如何支持面向对象详解
  • 使用 pycharm 新建不使用 python 虚拟环境( venv、conda )的工程
  • 摄像头原始数据读取——gstreamer(gst_parse_launch)
  • UI设计-色彩、层级、字体、边距(一)
  • java脚手架系列16-AI大模型集成
  • 使用Hutool读取大Excel
  • C++学习日记---第14天(蓝桥杯备赛)
  • 前端实现把整个页面转成PDF保存到本地(DOM转PDF)