鴻蒙OpenHarmony 安全子系統(tǒng)-ATM(AccessToken)

庫(kù)代碼地址:https://gitee.com/openharmony/security_access_token

以下引用官方介紹:


FireShot Capture 715 - security_access_token_ ATM(AccessTokenManager)是OpenHarmony上基于AccessTo_ - gitee.com.png

以下是代碼及主要流程分析:

創(chuàng)建AccessTokenId
通過(guò)隨機(jī)數(shù)創(chuàng)建后保存在
#define TOKEN_ID_CFG_FILE_PATH "/data/service/el0/access_token/nativetoken.json"
https://gitee.com/openharmony/security_access_token/blob/master/interfaces/innerkits/nativetoken/src/nativetoken.c

NativeAtId CreateNativeTokenId(void)
{
    uint32_t rand;
    NativeAtId tokenId;
    AtInnerInfo *innerId = (AtInnerInfo *)(&tokenId);

    int ret = GetRandomTokenId(&rand);
    if (ret != ATRET_SUCCESS) {
        return 0;
    }

    innerId->reserved = 0;
    innerId->tokenUniqueId = rand & (0xFFFFFF);
    innerId->type = TOKEN_NATIVE_TYPE;
    innerId->version = 1;
    return tokenId;
}

使用者:

native進(jìn)程
在native進(jìn)程拉起前,需要調(diào)用GetAccessTokenId函數(shù),獲取該native進(jìn)程的TokenID茄厘;再調(diào)用SetSelfTokenID將進(jìn)程TokenID設(shè)置到內(nèi)核中深浮。
在native進(jìn)程運(yùn)行過(guò)程中,可以通過(guò)調(diào)用GetNativeTokenInfo、CheckNativeDCap來(lái)查驗(yàn)對(duì)應(yīng)進(jìn)程所具備的token信息溅话,包括分布式能力、APL等級(jí)等信息歌焦。

https://gitee.com/openharmony/startup_init_lite/blob/master/services/init/standard/init_service.c

int SetAccessToken(const Service *service)
{
    INIT_ERROR_CHECK(service != NULL, return SERVICE_FAILURE, "%s failed", service->name);
    int ret = SetSelfTokenID(service->tokenId);
    INIT_LOGI("%s: token id %lld, set token id result %d", service->name, service->tokenId, ret);
    return ret == 0 ? SERVICE_SUCCESS : SERVICE_FAILURE;
}

void GetAccessToken(void)
{
    InitGroupNode *node = GetNextGroupNode(NODE_TYPE_SERVICES, NULL);
    while (node != NULL) {
        Service *service = node->data.service;
        if (service != NULL) {
            if (service->capsArgs.count == 0) {
                service->capsArgs.argv = NULL;
            }
            if (strlen(service->apl) == 0) {
                (void)strncpy_s(service->apl, sizeof(service->apl), "system_core", sizeof(service->apl) - 1);
            }
            uint64_t tokenId = GetAccessTokenId(service->name, (const char **)service->capsArgs.argv,
                service->capsArgs.count, service->apl);
            if (tokenId  == 0) {
                INIT_LOGE("Get totken id %lld of service \' %s \' failed", tokenId, service->name);
            }
            service->tokenId = tokenId;
        }
        node = GetNextGroupNode(NODE_TYPE_SERVICES, node);
    }
}

系統(tǒng)調(diào)用入口并寫入內(nèi)核
https://gitee.com/openharmony/security_access_token/blob/master/interfaces/innerkits/token_setproc/src/token_setproc.c

設(shè)備驅(qū)動(dòng)
#define TOKENID_DEVNODE "/dev/access_token_id"

set接口:

int SetSelfTokenID(uint64_t tokenID)
{
    int fd = open(TOKENID_DEVNODE, O_RDWR);
    if (fd < 0) {
        return ACCESS_TOKEN_ERROR;
    }
    int ret = ioctl(fd, ACCESS_TOKENID_SET_TOKENID, &tokenID);
    if (ret) {
        close(fd);
        return ACCESS_TOKEN_ERROR;
    }

    close(fd);
    return ACCESS_TOKEN_OK;
}

get接口:

uint64_t GetSelfTokenID()
{
    uint64_t token = INVAL_TOKEN_ID;
    int fd = open(TOKENID_DEVNODE, O_RDWR);
    if (fd < 0) {
        return INVAL_TOKEN_ID;
    }
    int ret = ioctl(fd, ACCESS_TOKENID_GET_TOKENID, &token);
    if (ret) {
        close(fd);
        return INVAL_TOKEN_ID;
    }

    close(fd);
    return token;
}
內(nèi)核驅(qū)動(dòng)

../kernel/linux/linux-5.10/drivers/accesstokenid/access_tokenid.c

dev設(shè)備驅(qū)動(dòng)入內(nèi)核層調(diào)用入口

static long access_tokenid_ioctl(struct file *file, unsigned int cmd,
                 unsigned long arg)
{
    void __user *uarg = (void __user *)arg;
    unsigned int func_cmd = _IOC_NR(cmd);

    if (uarg == NULL) {
        pr_err("%s: invalid user uarg\n", __func__);
        return -EINVAL;
    }

    if (_IOC_TYPE(cmd) != ACCESS_TOKEN_ID_IOCTL_BASE) {
        pr_err("%s: access tokenid magic fail, TYPE=%d\n",
               __func__, _IOC_TYPE(cmd));
        return -EINVAL;
    }

    if (func_cmd >= ACCESS_TOKENID_MAX_NR) {
        pr_err("%s: access tokenid cmd error, cmd:%d\n",
            __func__, func_cmd);
        return -EINVAL;
    }

    if (g_func_array[func_cmd])
        return (*g_func_array[func_cmd])(file, uarg);

    return -EINVAL;
}

設(shè)置tokenid至進(jìn)程上下文的token字段并保存:
current即為task_struct數(shù)據(jù)結(jié)構(gòu)

int access_tokenid_set_tokenid(struct file *file, void __user *uarg)
{
    unsigned long long tmp = 0;

    if (!check_permission_for_set_tokenid(file))
        return -EPERM;

    if (copy_from_user(&tmp, uarg, sizeof(tmp)))
        return -EFAULT;

    current->token = tmp;
    return 0;
}

在設(shè)置前做了一個(gè)簡(jiǎn)單的安全校驗(yàn)check_permission_for_set_tokenid飞几,只有root權(quán)限進(jìn)程才可以設(shè)置

access_token在內(nèi)核的使用:
鴻蒙對(duì)內(nèi)核新增加CONFIG_ACCESS_TOKENID宏控制此feature的使能
可以看到當(dāng)前版本主要是在binder,文件系統(tǒng)和創(chuàng)建進(jìn)程中調(diào)用独撇,目前版本邏輯不多屑墨,
只要是賦值和保存

./include/linux/sched.h:1479:#ifdef CONFIG_ACCESS_TOKENID
./fs/proc/base.c:3311:#ifdef CONFIG_ACCESS_TOKENID
./fs/proc/base.c:3318:#endif /* CONFIG_ACCESS_TOKENID */
./fs/proc/base.c:3436:#ifdef CONFIG_ACCESS_TOKENID
./fs/proc/base.c:3771:#ifdef CONFIG_ACCESS_TOKENID
./drivers/Makefile:195:obj-$(CONFIG_ACCESS_TOKENID) += accesstokenid/
./drivers/android/binder.c:98:#ifdef CONFIG_ACCESS_TOKENID
./drivers/android/binder.c:102:#endif /* CONFIG_ACCESS_TOKENID */
./drivers/android/binder.c:558:#ifdef CONFIG_ACCESS_TOKENID
./drivers/android/binder.c:560:#endif /* CONFIG_ACCESS_TOKENID */
./drivers/android/binder.c:606:#ifdef CONFIG_ACCESS_TOKENID
./drivers/android/binder.c:609:#endif /* CONFIG_ACCESS_TOKENID */
./drivers/android/binder.c:3107:#ifdef CONFIG_ACCESS_TOKENID
./drivers/android/binder.c:3110:#endif /* CONFIG_ACCESS_TOKENID */
./drivers/android/binder.c:4560:#ifdef CONFIG_ACCESS_TOKENID
./drivers/android/binder.c:4565:#endif /* CONFIG_ACCESS_TOKENID */
./drivers/android/binder.c:5167:#ifdef CONFIG_ACCESS_TOKENID
./drivers/android/binder.c:5190:#endif /* CONFIG_ACCESS_TOKENID */
./drivers/accesstokenid/Makefile:2:obj-$(CONFIG_ACCESS_TOKENID)     += access_tokenid.o
./kernel/fork.c:877:#ifdef CONFIG_ACCESS_TOKENID
上層使用及相關(guān)邏輯

應(yīng)用hap
在應(yīng)用安裝時(shí),需要調(diào)用AllocHapToken創(chuàng)建獲取該應(yīng)用的TokenID纷铣。
在應(yīng)用運(yùn)行過(guò)程中卵史,需要進(jìn)行鑒權(quán)等操作時(shí),可調(diào)用VerifyAccessToken搜立、GetReqPermissions等函數(shù)查詢校驗(yàn)應(yīng)用權(quán)限以躯、APL等信息。
在應(yīng)用卸載時(shí)啄踊,需要調(diào)用DeleteToken函數(shù)刪除系統(tǒng)中管理的對(duì)應(yīng)Accesstoken信息忧设。

應(yīng)用安裝時(shí)調(diào)用ATM服務(wù)生成AccessTokenId

AccessToken::AccessTokenID BundlePermissionMgr::CreateAccessTokenId(
    const InnerBundleInfo &innerBundleInfo, const std::string bundleName, const int32_t userId)
{
    APP_LOGD("BundlePermissionMgr::CreateAccessTokenId bundleName = %{public}s, userId = %{public}d",
        bundleName.c_str(), userId);
    AccessToken::HapInfoParams hapInfo;
    hapInfo.userID = userId;
    hapInfo.bundleName = bundleName;
    hapInfo.instIndex = 0;
    hapInfo.appIDDesc = innerBundleInfo.GetProvisionId();
    AccessToken::HapPolicyParams hapPolicy = CreateHapPolicyParam(innerBundleInfo);
    AccessToken::AccessTokenIDEx accessToken = AccessToken::AccessTokenKit::AllocHapToken(hapInfo, hapPolicy);
    APP_LOGD("BundlePermissionMgr::CreateAccessTokenId accessTokenId = %{public}u",
             accessToken.tokenIdExStruct.tokenID);
    return accessToken.tokenIdExStruct.tokenID;
}

調(diào)用ATM服務(wù)接口

AccessTokenIDEx AccessTokenManagerService::AllocHapToken(const HapInfoParcel& info, const HapPolicyParcel& policy)
{
    ACCESSTOKEN_LOG_INFO(LABEL, "%{public}s called", __func__);
    AccessTokenIDEx tokenIdEx;
    tokenIdEx.tokenIDEx = 0LL;

    int ret = AccessTokenInfoManager::GetInstance().CreateHapTokenInfo(
        info.hapInfoParameter, policy.hapPolicyParameter, tokenIdEx);
    if (ret != RET_SUCCESS) {
        ACCESSTOKEN_LOG_INFO(LABEL, "hap token info create failed");
    }
    return tokenIdEx;
}

針對(duì)Hap包應(yīng)用信息生成AccessToken主要接口

int AccessTokenInfoManager::CreateHapTokenInfo(
    const HapInfoParams& info, const HapPolicyParams& policy, AccessTokenIDEx& tokenIdEx)
{
    if (!DataValidator::IsUserIdValid(info.userID) || !DataValidator::IsBundleNameValid(info.bundleName)
        || !DataValidator::IsAppIDDescValid(info.appIDDesc) || !DataValidator::IsDomainValid(policy.domain)) {
        ACCESSTOKEN_LOG_ERROR(LABEL, "hap token param failed");
        return RET_FAILED;
    }

    AccessTokenID tokenId = AccessTokenIDManager::GetInstance().CreateAndRegisterTokenId(TOKEN_HAP);
    if (tokenId == 0) {
        ACCESSTOKEN_LOG_INFO(LABEL, "token Id create failed");
        return RET_FAILED;
    }

    std::shared_ptr<HapTokenInfoInner> tokenInfo = std::make_shared<HapTokenInfoInner>(tokenId, info, policy);
    if (tokenInfo == nullptr) {
        AccessTokenIDManager::GetInstance().ReleaseTokenId(tokenId);
        ACCESSTOKEN_LOG_INFO(LABEL, "alloc token info failed");
        return RET_FAILED;
    }

    int ret = AddHapTokenInfo(tokenInfo);
    if (ret != RET_SUCCESS) {
        ACCESSTOKEN_LOG_WARN(LABEL, "%{public}s add token info failed", info.bundleName.c_str());
        AccessTokenIDManager::GetInstance().ReleaseTokenId(tokenId);
        return RET_FAILED;
    }
    ACCESSTOKEN_LOG_INFO(LABEL,
        "create hap token 0x%{public}x bundle name %{public}s user %{public}d inst %{public}d ok!",
        tokenId, tokenInfo->GetBundleName().c_str(), tokenInfo->GetUserID(), tokenInfo->GetInstIndex());

    tokenIdEx.tokenIdExStruct.tokenID = tokenId;
    tokenIdEx.tokenIdExStruct.tokenAttr = 0;
    RefreshTokenInfoIfNeeded();
    return RET_SUCCESS;
}

AccessTokenID AccessTokenIDManager::CreateAndRegisterTokenId(ATokenTypeEnum type)
{
    AccessTokenID tokenId = 0;
    // random maybe repeat, retry twice.
    for (int i = 0; i < MAX_CREATE_TOKEN_ID_RETRY; i++) {
        tokenId = CreateTokenId(type);
        if (tokenId == 0) {
            ACCESSTOKEN_LOG_WARN(LABEL, "create tokenId failed");
            return 0;
        }

        int ret = RegisterTokenId(tokenId, type);
        if (ret == RET_SUCCESS) {
            break;
        } else if (i == MAX_CREATE_TOKEN_ID_RETRY - 1) {
            ACCESSTOKEN_LOG_INFO(LABEL, "reigster tokenId failed, maybe repeat, retry");
        } else {
            ACCESSTOKEN_LOG_WARN(LABEL, "reigster tokenId finally failed");
        }
    }
    return tokenId;
}

創(chuàng)建進(jìn)程時(shí)寫入內(nèi)核

void AppSpawnServer::SetAppAccessToken(const ClientSocket::AppProperty *appProperty)
{
    int32_t ret = SetSelfTokenID(appProperty->accessTokenId);
    if (ret != 0) {
        HiLog::Error(LABEL, "AppSpawnServer::Failed to set access token id, errno = %{public}d", errno);
    }
#ifdef WITH_SELINUX
    HapContext hapContext;
    ret = hapContext.HapDomainSetcontext(appProperty->apl, appProperty->processName);
    if (ret != 0) {
        HiLog::Error(LABEL, "AppSpawnServer::Failed to hap domain set context, errno = %{public}d", errno);
    }
#endif
}

調(diào)用及檢查權(quán)限:

如包管理中檢查權(quán)限
foundation/appexecfwk/standard/services/bundlemgr/src/bundle_permission_mgr.cpp:554:    int32_t ret = AccessToken::AccessTokenKit::VerifyAccessToken(callerToken, permissionName);
foundation/appexecfwk/standard/services/bundlemgr/src/bundle_permission_mgr.cpp:743:    int32_t ret = AccessToken::AccessTokenKit::VerifyAccessToken(tokenId, permissionName);

bool BundlePermissionMgr::VerifyCallingPermission(const std::string &permissionName)
{
    APP_LOGD("VerifyCallingPermission permission %{public}s", permissionName.c_str());
    AccessToken::AccessTokenID callerToken = IPCSkeleton::GetCallingTokenID();
    APP_LOGD("callerToken : %{public}u", callerToken);
    AccessToken::ATokenTypeEnum tokenType = AccessToken::AccessTokenKit::GetTokenTypeFlag(callerToken);
    if (tokenType == AccessToken::ATokenTypeEnum::TOKEN_NATIVE) {
        APP_LOGD("caller tokenType is native, verify success");
        return true;
    }
    int32_t ret = AccessToken::AccessTokenKit::VerifyAccessToken(callerToken, permissionName);
    if (ret == AccessToken::PermissionState::PERMISSION_DENIED) {
        APP_LOGE("permission %{public}s: PERMISSION_DENIED", permissionName.c_str());
        return false;
    }
    APP_LOGD("verify AccessToken success");
    return true;
}

根據(jù)tokenID和permissionName,最終調(diào)用PermissionManager權(quán)限模塊進(jìn)行驗(yàn)證權(quán)限是否定義存在等颠通,進(jìn)行權(quán)限檢查址晕,最后回到AccessTokenInfoManager中根據(jù)tokenid找到某個(gè)應(yīng)用應(yīng)用權(quán)限管理策略中集合,并判斷某個(gè)權(quán)限的情況并返回

int AccessTokenManagerService::VerifyAccessToken(AccessTokenID tokenID, const std::string& permissionName)
{
    ACCESSTOKEN_LOG_INFO(LABEL,
        "%{public}s called, tokenID: 0x%{public}x, permissionName: %{public}s", __func__,
        tokenID, permissionName.c_str());
    return PermissionManager::GetInstance().VerifyAccessToken(tokenID, permissionName);
}

int PermissionManager::VerifyAccessToken(AccessTokenID tokenID, const std::string& permissionName)
{
    ACCESSTOKEN_LOG_INFO(LABEL, "%{public}s called, tokenID: 0x%{public}x, permissionName: %{public}s", __func__,
        tokenID, permissionName.c_str());
    if (!PermissionValidator::IsPermissionNameValid(permissionName)) {
        ACCESSTOKEN_LOG_ERROR(LABEL, "invalid params!");
        return PERMISSION_DENIED;
    }
    std::shared_ptr<HapTokenInfoInner> tokenInfoPtr =
        AccessTokenInfoManager::GetInstance().GetHapTokenInfoInner(tokenID);
    if (tokenInfoPtr == nullptr) {
        ACCESSTOKEN_LOG_ERROR(LABEL, "can not find tokenInfo!");
        return PERMISSION_DENIED;
    }

    if (!tokenInfoPtr->IsRemote() && !PermissionDefinitionCache::GetInstance().HasDefinition(permissionName)) {
        ACCESSTOKEN_LOG_ERROR(
            LABEL, "no definition for permission: %{public}s!", permissionName.c_str());
        return PERMISSION_DENIED;
    }
    std::shared_ptr<PermissionPolicySet> permPolicySet =
        AccessTokenInfoManager::GetInstance().GetHapPermissionPolicySet(tokenID);
    if (permPolicySet == nullptr) {
        ACCESSTOKEN_LOG_ERROR(LABEL, "invalid params!");
        return PERMISSION_DENIED;
    }

    return permPolicySet->VerifyPermissStatus(permissionName);
}

看到這里可能有些疑問(wèn)
tokenID和應(yīng)用程序Hap中權(quán)限管理的關(guān)系是什么顿锰?

通過(guò)以下代碼看出谨垃,應(yīng)用的HapTokenInfo中擁有字段permPolicySet_,而permPolicySet_是通過(guò)tokenID和應(yīng)用的權(quán)限集合等參數(shù)生成的對(duì)象

HapTokenInfoInner::HapTokenInfoInner(AccessTokenID id,
    const HapInfoParams &info, const HapPolicyParams &policy) : isRemote_(false)
{
    tokenInfoBasic_.tokenID = id;
    tokenInfoBasic_.userID = info.userID;
    tokenInfoBasic_.ver = DEFAULT_TOKEN_VERSION;
    tokenInfoBasic_.tokenAttr = 0;
    tokenInfoBasic_.bundleName = info.bundleName;
    tokenInfoBasic_.instIndex = info.instIndex;
    tokenInfoBasic_.appID = info.appIDDesc;
    tokenInfoBasic_.deviceID = "0";
    tokenInfoBasic_.apl = policy.apl;
    permPolicySet_ = PermissionPolicySet::BuildPermissionPolicySet(id, policy.permList, policy.permStateList);
}

所以可以根據(jù)應(yīng)用的tokenID,可以判斷應(yīng)用的某個(gè)權(quán)限狀態(tài)是允許還是拒絕

回想一下Android中的權(quán)限模型設(shè)計(jì)撵儿,frameworks層是以App的UID為單位進(jìn)行權(quán)限判斷
鴻蒙為什么這樣設(shè)計(jì)呢乘客?

int CheckNativeDCap(AccessTokenID tokenID, const std::string& dcap); 檢測(cè)指定tokenID對(duì)應(yīng)的native進(jìn)程是否具有指定的分布式能力

(AccessToken可以跨設(shè)備狐血,可能與分布式權(quán)限管理能力有關(guān))

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末淀歇,一起剝皮案震驚了整個(gè)濱河市,隨后出現(xiàn)的幾起案子匈织,更是在濱河造成了極大的恐慌浪默,老刑警劉巖,帶你破解...
    沈念sama閱讀 223,126評(píng)論 6 520
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件缀匕,死亡現(xiàn)場(chǎng)離奇詭異纳决,居然都是意外死亡,警方通過(guò)查閱死者的電腦和手機(jī)乡小,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 95,421評(píng)論 3 400
  • 文/潘曉璐 我一進(jìn)店門阔加,熙熙樓的掌柜王于貴愁眉苦臉地迎上來(lái),“玉大人满钟,你說(shuō)我怎么就攤上這事胜榔「炫纾” “怎么了?”我有些...
    開封第一講書人閱讀 169,941評(píng)論 0 366
  • 文/不壞的土叔 我叫張陵夭织,是天一觀的道長(zhǎng)吭露。 經(jīng)常有香客問(wèn)我,道長(zhǎng)尊惰,這世上最難降的妖魔是什么讲竿? 我笑而不...
    開封第一講書人閱讀 60,294評(píng)論 1 300
  • 正文 為了忘掉前任,我火速辦了婚禮弄屡,結(jié)果婚禮上题禀,老公的妹妹穿的比我還像新娘。我一直安慰自己琢岩,他們只是感情好投剥,可當(dāng)我...
    茶點(diǎn)故事閱讀 69,295評(píng)論 6 398
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著担孔,像睡著了一般江锨。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上糕篇,一...
    開封第一講書人閱讀 52,874評(píng)論 1 314
  • 那天啄育,我揣著相機(jī)與錄音,去河邊找鬼拌消。 笑死挑豌,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的墩崩。 我是一名探鬼主播氓英,決...
    沈念sama閱讀 41,285評(píng)論 3 424
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來(lái)是場(chǎng)噩夢(mèng)啊……” “哼鹦筹!你這毒婦竟也來(lái)了铝阐?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 40,249評(píng)論 0 277
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤铐拐,失蹤者是張志新(化名)和其女友劉穎徘键,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體遍蟋,經(jīng)...
    沈念sama閱讀 46,760評(píng)論 1 321
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡吹害,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 38,840評(píng)論 3 343
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了虚青。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片它呀。...
    茶點(diǎn)故事閱讀 40,973評(píng)論 1 354
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出纵穿,到底是詐尸還是另有隱情烟号,我是刑警寧澤,帶...
    沈念sama閱讀 36,631評(píng)論 5 351
  • 正文 年R本政府宣布政恍,位于F島的核電站汪拥,受9級(jí)特大地震影響,放射性物質(zhì)發(fā)生泄漏篙耗。R本人自食惡果不足惜迫筑,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 42,315評(píng)論 3 336
  • 文/蒙蒙 一、第九天 我趴在偏房一處隱蔽的房頂上張望宗弯。 院中可真熱鬧脯燃,春花似錦、人聲如沸蒙保。這莊子的主人今日做“春日...
    開封第一講書人閱讀 32,797評(píng)論 0 25
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)邓厕。三九已至逝嚎,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間详恼,已是汗流浹背补君。 一陣腳步聲響...
    開封第一講書人閱讀 33,926評(píng)論 1 275
  • 我被黑心中介騙來(lái)泰國(guó)打工, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留昧互,地道東北人挽铁。 一個(gè)月前我還...
    沈念sama閱讀 49,431評(píng)論 3 379
  • 正文 我出身青樓,卻偏偏與公主長(zhǎng)得像敞掘,于是被迫代替她去往敵國(guó)和親叽掘。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,982評(píng)論 2 361

推薦閱讀更多精彩內(nèi)容

  • 1.zabbix的簡(jiǎn)介 zabbix是一個(gè)高度集成的監(jiān)控解決方案玖雁;可以實(shí)現(xiàn)企業(yè)級(jí)的開源分布式監(jiān)控更扁;zabbix通過(guò)...
    學(xué)有境閱讀 530評(píng)論 0 5
  • 目錄 1 概念 2 Springboot 3 Spring Cloud Eureka 4 Spring C...
    小小千千閱讀 669評(píng)論 0 0
  • Spring Cloud為開發(fā)人員提供了快速構(gòu)建分布式系統(tǒng)中一些常見模式的工具(例如配置管理,服務(wù)發(fā)現(xiàn)茄菊,斷路器疯潭,智...
    卡卡羅2017閱讀 134,722評(píng)論 18 139
  • 一赊堪、Web端 https://www.nowcoder.com/discuss/588372 1.float如何清...
    陳一季閱讀 4,246評(píng)論 2 18
  • 這篇博文主要是介紹Android操作系統(tǒng)中面殖,個(gè)人匯總的比較重要的一些知識(shí)點(diǎn),在Android的系統(tǒng)四層結(jié)構(gòu)中...
    Android開發(fā)_Hua閱讀 4,142評(píng)論 0 4