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