Apache Shiro 授權

fmms 12年前發布 | 70K 次閱讀 Shiro 安全相關 Apache Shiro

Apache Shiro 授權

Apache Shiro 授權

授權,亦即訪問控制,是管理資源訪問的過程,換言之,也就是控制在一個程序中“誰”有權利訪問“什么”。

授權的例子有:是否允許這個用戶查看這個頁面,編輯數據,看到按鈕,或者從這臺打印機打印?這些決定一個用戶可以訪問什么的決斷。

 

授權組件

授權有三個核心組件,在Shiro中我們經常要用到它們:權限(permissions)、角色(roles)和用戶(users)。

 

權限(Permissions

權限是Apache Shiro中安全策略最基本的組件,它們是一組關于行為的基本指令,以明確表示在一個程序中什么可以做。一個很好定義的權限指令必須描述資源以及當一個Subject與這些資源交互時什么動作可以執行。

 

下面是一些權限指令的例子:

打開一個文件;

查看“/user/list”頁面;

打印文檔;

刪除“Jsmith”用戶。

 

大部分資源都支持基本的CRUDcreate,read,update,delete)操作,而任何能識別特定資源類型的動作也是支持的的。最基礎的思想是在資源和動作的基礎上設置的最小量的權限指令。

 

當看到權限時,也許最重要的事情是意識到一個權限指令沒有描述“誰”可以執行這個動作,它們只是一些“什么”可以做的指令。

 

權限只描述行為

權限指令只描述行為(和資源相關的動作),并不關心“誰”有能力執行這個動作。

 

定義“誰”(用戶)被允許做“什么”(權限)需要用一些方法將權限賦給用戶,這通常取決于程序的數據模型而且經常在程序中發生改變。

 

例如,一組權限可以歸于一個角色而角色與一個或多個用戶對象關聯,或者一些程序可以有一組用戶而一個組可以指定一個角色,在這里關系將被傳遞也就是說組內用戶隱含被賦予角色的權限。

 

有很多方式可以將權限賦予用戶--程序根據需求決定如何設計。

 

我們稍后討論shiro如何判斷一個Subject是否被允許。

 

權限粒度

上面的權限示例都是對資源(門、文件、客戶等)指定的動作(打開、讀、刪除等),在一些場景中,他們也會指定非常細粒度的“實例級別”行為--例如,“刪除”(delete)名為“Jsmith”(實例標識)的“用戶”(resource type),在Shiro中,你可以精確定義指令到所能細化到的程度。

 

我們在ShiroPermissions文檔中詳細討論權限粒度和權限指令的“級別”。

 

角色

角色是一個實體名,代表一組行為或職責,這些行為在程序中轉換為你可以或者不能做的事情。角色通常賦給用戶帳號,關聯后,用戶就可以“做”屬于不同角色的事情。

 

有兩種有效的角色,Shiro都支持。

 

隱含角色:大部分用戶隱含創建角色:程序只是在一個角色名稱上隱含了一組行為(也就是權限),使用隱含角色時,在軟件級別不會說“某角色允許執行行為ABC”,行為隱含于一個單獨的名字中。

 

潛在的安全隱患

雖然這是一個非常簡單和常用的方法,但隱含的角色可能會增加軟件的維護成本和管理問題。

例如,如果你想增加或刪除一個角色,或者重定義角色的行為怎么辦?你不得不重新打開代碼修改所有已更改角色的檢測,每次都需要這樣做,這還沒提到其引起的執行代價(重測試,通過質量驗證,關閉程序,升級軟件,重啟程序等)。

對于簡單程序這種方法可能適用(比如只有一個'admin'角色和'everyone else'角色),但復雜的程序中,這會成為你程序生命周期中一個主要的問題,會給你的軟件帶來很大的維護代價。

 

明確角色:明確角色本質上是集合了實際權限指令的一個名字,在這種形式下,程序(以及shiro)準確知道是否擁有特定的角色意味著什么,因為它確切知道某行為是否可以執行,不用猜測或隱喻一個特定的角色可以或不可以做什么。

 

shiro團隊提倡使用權限和明確角色替代原始的隱含方法,你可以對程序安全提供更強的控制。

 

基于資源的訪問控制

讀一下Les Hazlewood的文章:The New RBAC: Resource-Based Access Control,這篇文章深入討論了使用權限和明確角色(以及對源代碼的影響)代替舊的隱含角色方法的好處。

 

用戶

一個用戶本質上是程序中的“誰”,如同我們前面提到的,Subject實際上是shiro的“用戶”概念。

 

用戶(Subjects)通過與角色或權限關聯被允許執行程序內特定的動作,程序數據模型確切定義了Subject是否允許做什么事情。

 

例如,在你的數據模型中,你定義了一個普通的用戶類并且直接為其設置了權限,或者你只是直接給角色設置了權限,然后將用戶與該角色關聯,通過這種關聯,用戶就“有”了角色所具備的權限,或者你也可以通過“組”的概念完成這件事,這取決于你程序的設計。

 

數據模型定義了如何進行授權,Shiro依賴一個Realm實現將你的數據模型關聯轉換成Shiro可以理解的內容,我們將稍后討論Realms

 

最終,Realm實現是與你的數據源(RDBMS,LDAP等)所做的交流,Realm用來告知Shiro是否角色或權限存在,你可以完全控制你的授權模型如何創建和定義。

 

授權對象

Shiro中執行授權可以有三種途徑:

程序中授權--你可以在你的JAVA代碼中執行用類似于ifelse的結構來執行法令檢查。

JDK 注解--你可以在你的JAVA方法上附加授權注解

JSP/GSP標簽--你可以基于角色和權限控制JSPGSP頁面輸出。

 

程序中授權

直接在程序中為當前Subject實例授權可能是最簡單也最常用的方法。

 

基于角色的授權

如果你要基于簡單/傳統的隱含角色名進行訪問掏,你可以執行角色檢查:

 

角色檢查

如果你想簡單地檢查一下當前Subject是否擁有一個角色,你可以在一個實例上調用hasRole*方法的變形。

例如,查看一個Subject是否有特定(單獨)的角色,你可以調用subject.hasRole(roleName)方法,做出相應的反饋。

Subject currentUser = SecurityUtils.getSubject();

 

if (currentUser.hasRole("administrator")) {

    //show the admin button

} else {

    //don't show the button?  Grey it out?

}

 

下面是你可以根據需要調用的函數:

hasRole(String roleName)

如果Subject指定了特定的角色返回真,否則返回假;

hasRoles(List roleNames)

返回一個與參數順序相對應的hasRole結果數組,當一次有多個角色需要檢測時非常有用(如定制一個復雜的視圖)。

hasAllRoles(Collection roleNames)

如果Subject指定了所有角色返回真,否則返回假。

 

角色判斷

還有另一個方法檢測Subjet是否是指定為某個角色,你可以在的代碼執行之前簡單判斷他們是否是所要求的角色,如果Subject不是所要求的角色, AuthorizationException異常將被拋出,如果是所要求的角色,判斷將安靜地執行并按期望順序執行下面的邏輯。

 

例如:

Subject currentUser = SecurityUtils.getSubject();

 

//guarantee that the current user is a bank teller and

//therefore allowed to open the account:

currentUser.checkRole("bankTeller");

openBankAccount();

 

hasRole*方法相比,這種方法的好處在于代碼更為清晰,如果當前Subject不滿足所需條件你不需要建立你自己的AuthorizationExceptions

isPermitted* 方法相比較,這種方法的優勢是代碼更為清晰,如果當前Subject不符合條件,你不必創建你自己的AuthorizationExceptions異常(如果你不想那么做)。。

下面是你可以根據需要調用的函數:

checkRole(String roleName)

如果Subject被指定為特定角色則安靜地返回否則拋出AuthorizationException異常;

checkRoles(Collection roleNames)

如果Subject被指定了所胡特定的角色則安靜地返回否則拋出AuthorizationException異常;

checkRoles(String... roleNames)

和上面的checkRoles具有相同的效果,但允許Java5的變參形式。

 

基于權限的授權

就像我們上面在角色概述中提到的,通過基于權限的授權執行訪問控制是更好的方法。基于權限的授權,因為其與程序功能(以及程序核心資源上的行為)緊 密聯系,基于權限授權的源代碼在程序功能改變時才需要改變,而與安全策略無關。這意味著與同樣基于角色的授權相比,對代碼的影響更少。

 

權限檢查

如果你希望檢查一個Subject是否允許做某件事情,你可以調用isPermitted*方法的變形,有兩種主要方式檢查授權--基于對象的權限實例或者代表權限的字符串。

 

基于對象的權限檢查

執行權限檢查的一種方法是實例化一個Shiroorg.apache.shiro.authz.Permission接口并且將它傳遞給接收權限實例的*isPermitted方法。

例如,假設以下情景:辦公室里有一臺唯一標識為laserjet4400n的打印機,在我們向用戶顯示打印按鈕之前,軟件需要檢查當前用戶是否允許用這臺打印機打印文檔,檢查權限的方式會是這樣:

Permission printPermission = new PrinterPermission("laserjet4400n", "print");

 

Subject currentUser = SecurityUtils.getSubject();

 

if (currentUser.isPermitted(printPermission)) {

    //show the Print button

} else {

    //don't show the button?  Grey it out?

}

 

在這個例子中,我們同樣看到了一個非常強大的實例級別的訪問控制檢查--在單獨數據實例上限制行為的能力。

 

基于對象的權限對下列情況非常有用:

希望編譯期類型安全;

希望確保正確地引用和使用的權限;

希望對權限判斷邏輯(稱作權限隱含邏輯,基于權限接口的implies方法)執行進行明確控制;

希望確保權限正確地反映程序資源(例如,在一個對象域模型上創建一個對象時,權限類可能自動產生)。

下面是你可以根據需要調用的函數:

isPermitted(Permission p)

如果Subject允許執行特定權限實例綜合的動作或資源訪問返回真,否則返回假;

isPermitted(List perms)

按參數順序返回isPermitted的結果數組,如果許多權限需要檢查時非常有用(如定制一個復雜的視圖);

isPermittedAll(Collection perms)

如果Subject擁有指定的所有權限返回真,否則返回假。

 

基于字符串的權限檢查

雖然基于對象的權限檢查很有用(編譯期類型安全,對行為擔保,定制隱含邏輯等),但在許多程序里有時候感覺有點笨重,另一種選擇是用普通的字符串來代表權限實例。

例如,對于上面打印權限的例子,我們可以使用字符串權限檢查達到同樣的結果:

Subject currentUser = SecurityUtils.getSubject();

 

if (currentUser.isPermitted("printer:print:laserjet4400n")) {

    //show the Print button

} else {

    //don't show the button?  Grey it out?

}

 

這個例子同樣實現了實例級別的權限檢查,但是所有主要權限部件--printer(資源類型)、print(動作)、laserjet4400n(實例ID)都表現為一個字符串。

 

上面的例子展示了一種以冒號分割的特殊形式的字符串,定義于Shiro org.apache.shiro.authz.permission.WildcardPermission實現中,它適合大多數用戶的需求。

 

上面的代碼塊基本上是下面這段代碼的縮寫:

 

Permission p = new WildcardPermission("printer:print:laserjet4400n");

 

if (currentUser.isPermitted(p) {

    //show the Print button

} else {

    //don't show the button?  Grey it out?

}

 

WildcardPermission令牌形式和構成選項將在ShiroPermission文檔中深入討論。

 

上面的字符串使用默認的WildcardPermission格式,實際上你可以創造并使用你自己的字符串格式,我們將在下面Realm授權章節討論如何這樣做。

 

基于字符串的權限有利的一面在于你不需要實現一個接口而且簡單的字符串也非常易讀,而不利的一面在于不保證類型安全,而且當你需要定義超出字符串表現能力之外的更復雜的行為時,你仍舊希望基于權限接口實現你自己的權限對象。實際上,大部分Shiro的終端用戶回為其簡單而選擇基于字符串的方式,但最終你的程序需求決定了哪一種方法會更好。

 

和基于對象的權限檢查方法一樣,下面是字符串權限檢查的函數:

isPermitted(String perm)

如果Subject被允許執行字符串表達的動作或資源訪問權限,返回真,否則返回假;

isPermitted(String... perms)

按照參數順序返回isPermitted的結果數組,當許多字符串權限需要檢查時非常有用(如定制一個復雜的視圖時);

isPermittedAll(String... perms)

Subject具備所有字符串定義的權限時返回真,否則返回假。

 

權限判斷

作為檢查Subject是否被允許做某件事之后的一個選擇,你可以在邏輯執行之前簡單判斷他們是否具備所需的權限,如果Subject不被允許,AuthorizationException異常被拋出,如果是允許的,判斷將安靜地執行并按期望順序執行下面的邏輯。

 

例如:

Subject currentUser = SecurityUtils.getSubject();

 

//guarantee that the current user is permitted

//to open a bank account:

Permission p = new AccountPermission("open");

currentUser.checkPermission(p);

openBankAccount();

 

或者,同樣的判斷,可以用字符串形式:

Subject currentUser = SecurityUtils.getSubject();

 

//guarantee that the current user is permitted

//to open a bank account:

currentUser.checkPermission("account:open");

openBankAccount();

 

isPermitted* 方法相比較,這種方法的優勢是代碼更為清晰,如果當前Subject不符合條件,你不必創建你自己的AuthorizationExceptions異常(如果你不想那么做)。

 

下面是你可以根據需要調用的函數:

checkPermission(Permission p)

如果Subject被允許執行特定權限實例指定的動作或資源訪問,安靜地返回,否則拋出AuthorizationException異常。

checkPermission(String perm)

如果Subject被允許執行權限字符串指定的動作或資源訪問,安靜地返回,否則拋出AuthorizationException異常。

checkPermissions(Collection perms)

如果Subject被允許執行所有權限實例指定的動作或資源訪問,安靜地返回,否則拋出AuthorizationException異常。

checkPermissions(String... perms) 和上面的checkPermissions效果一樣,只是使用字符串權限類型。

 

基于注解的授權

如果你更喜歡基于注解的授權控制,除了SubjectAPI之外,Shiro提供了一個Java5的注解集。

 

配置

在你使用JAVA的注解之前,你需要在程序中啟動AOP支持,因為有許多AOP框架,所以很不幸,在這里并沒有標準的在程序中啟用AOP的方法。

 

關于AspectJ,你可以查看我們的AspectJ sample applicationhttp://svn.apache.org/repos/asf/shiro/trunk/samples/aspectj/);

關于Spring,你可以查看Spring Integration文檔;

關于Guice,你可以查看我們的 Guice Integration文檔;

 

RequiresAuthentication注解

RequiresAuthentication注解要求在訪問或調用被注解的類/實例/方法時,Subject在當前的session中已經被驗證。

 

例如:

@RequiresAuthentication

public void updateAccount(Account userAccount) {

    //this method will only be invoked by a

    //Subject that is guaranteed authenticated

    ...

}

 

這基本上與下面的基于對象的邏輯效果相同:

public void updateAccount(Account userAccount) {

    if (!SecurityUtils.getSubject().isAuthenticated()) {

        throw new AuthorizationException(...);

    }

   

    //Subject is guaranteed authenticated here

    ...

}

 

 

RequiresGuest注解

RequiresGuest注解要求當前Subject是一個“訪客”,也就是,在訪問或調用被注解的類/實例/方法時,他們沒有被認證或者在被前一個Session記住。

例如:

@RequiresGuest

public void signUp(User newUser) {

    //this method will only be invoked by a

    //Subject that is unknown/anonymous

    ...

}

 

這基本上與下面的基于對象的邏輯效果相同:

public void signUp(User newUser) {

    Subject currentUser = SecurityUtils.getSubject();

    PrincipalCollection principals = currentUser.getPrincipals();

    if (principals != null && !principals.isEmpty()) {

        //known identity - not a guest:

        throw new AuthorizationException(...);

    }

   

    //Subject is guaranteed to be a 'guest' here

    ...

}

 

RequiresPermissions 注解

RequiresPermissions 注解要求當前Subject在執行被注解的方法時具備一個或多個對應的權限。

例如:

@RequiresPermissions("account:create")

public void createAccount(Account account) {

    //this method will only be invoked by a Subject

    //that is permitted to create an account

    ...

}

 

這基本上與下面的基于對象的邏輯效果相同:

public void createAcc

/this method will only be invoked by an administrator

    ...

}

 

RequiresRoles 注解

RequiresPermissions 注解要求當前Subject在執行被注解的方法時具備所有的角色,否則將拋出AuthorizationException異常。

例如:

@RequiresRoles("administrator")

public void deleteUser(User user) {

    //this method will only be invoked by an administrator

    ...

}

 

這基本上與下面的基于對象的邏輯效果相同:

public void deleteUser(User user) {

    Subject currentUser = SecurityUtils.getSubject();

    if (!subject.hasRole("administrator")) {

        throw new AuthorizationException(...);

    }

   

    //Subject is guaranteed to be an 'administrator' here

    ...

}

 

RequiresUser 注解

RequiresUser*注解要求在訪問或調用被注解的類/實例/方法時,當前Subject是一個程序用戶,“程序用戶”是一個已知身份的Subject,或者在當前Session中被驗證過或者在以前的Session中被記住過。

例如:

@RequiresUser

public void updateAccount(Account account) {

    //this method will only be invoked by a 'user'

    //i.e. a Subject with a known identity

    ...

}

 

這基本上與下面的基于對象的邏輯效果相同:

public void updateAccount(Account account) {

    Subject currentUser = SecurityUtils.getSubject();

    PrincipalCollection principals = currentUser.getPrincipals();

    if (principals == null || principals.isEmpty()) {

        //no identity - they're anonymous, not allowed:

        throw new AuthorizationException(...);

    }

   

    //Subject is guaranteed to have a known identity here

    ...

}

 

JSP標簽庫授權

Shiro提供了一個標簽庫來控制JSP/GSP頁面輸出,這將在Web文檔中的JSP/GSP標簽庫中討論。

 

授權序列

現在我們已經看到如何對當前Subject執行授權,讓我們了解一下當一個授權命令調用時Shiro內部發生了什么事情。

我們仍使用前面Architecture章節里的架構圖,在左側僅僅與授權相關的組件是高亮的,每一個數字代表授權操作中的一個步驟:

Apache Shiro 授權

1:程序或框架代碼調用一個SubjecthasRole*checkRole* isPermitted*或者 checkPermission*方法,傳遞所需的權限或角色。

2Subject實例,通常是一個DelegatingSubject(或子類),通過調用securityManager與各hasRole*checkRole* isPermitted*checkPermission*基本一致的方法傳遞給程序的SecurityManager

3SecurityManager,基本的安全組件,通過調用authorizer的各hasRole*checkRole* isPermitted*checkPermission*方法轉交/委托給其內部的org.apache.shiro.authz.Authorizer實例,它負責在授權操作中協調一個或多個Realm實例。

4,每一個被配置的Realm被檢查是否實現相同的Authorizer接口,如果是,Realm自己的各hasRole*checkRole* isPermitted*checkPermission*方法被調用。

 

ModularRealmAuthorizer

前面提到過,Shiro SecurityManager默認使用ModularRealmAuthorizer實例,ModularRealmAuthorizer實例對用一個Realm的程序和用多個Realm的程序同樣支持。

 

對于任何授權操作,ModularRealmAuthorizer將與其內部的Realm集迭代,按迭代順序同每一個Realm交互,與每一個Realm交互的方法如下:

1.如果Realm自己實現Authorizer接口,調用它各自的授權方法(hasRole* checkRole*isPermitted* checkPermission*)。

   1.如果Realm函數的結果是一個exception,該exception衍生自一個Subject調用者的AuthorizationException,這切斷了授權過程,剩余的授權Realm將不在執行。

   2.如果Realm的方法是一個hasRole*isPermitted*,并且返回真,則真值立即被返回而且剩余的Realm被短路,這種做法作為一種性能增強,在一個Realm判斷允許后,隱含認為這個Subject被允許。它支持最安全的安全策略:默認情況下所有都被禁止,明確指定允許的事情。

2.如果Realm沒有實現Authorizer接口,將被忽略。

 

Realm授權順序

需要指出非常重要的一點,就如同驗證一樣ModularRealmAuthorizer按迭代順序與Realm交互。

 

 ModularRealmAuthorizer擁有SecurityManager配置的Realm實例的入口,當執行一個授權操作時,它將在整個集合中進行迭代,對于每一個實現Authorizer接口的Realm,調用Realm各自的Authorizer方法(如 hasRole* checkRole* isPermitted* checkPermission*)。

 

配置全局的PermissionResolver

當執行一個基于字符串的權限檢查時,大部分Shiro默認的Realm將會在執行權限隱含邏輯之前首先把這個字符串轉換成一個常用的權限實例。

 

這是因為權限被認為是基于隱含邏輯而不是相等檢查(查看Permission章節了解更多隱含與相等的對比)。隱含邏輯用代碼表示要比通過字符串對比好,因此,大部分Realm需要轉換一個提交的權限字符串為對應的權限實例。

 

為了這個轉換目的,Shiro支持PermissionResolver,大部分Shiro Realm使用一個PermissionResolver來支持它們對Authorizer接口中基于字符串的權限方法的實現:當這些方法在Realm上被調用時,將使用PermissionResolver將字符串轉換為權限實例,并執行檢查。

 

 

所有的Shiro Realm默認使用內部的WildcardPermissionResolver,它采用ShiroWildcardPermission字符串格式。

 

如果你想創建你自己的PermissionResolver 實現,比如說你想創建你自己的權限字符串語法,你希望所有配置的Realm實例都支持這個語法,你可以對所有的Realms把自己的PermissionResolver設置成全局。

 

如,在shiro.ini中:

shiro.ini

globalPermissionResolver = com.foo.bar.authz.MyPermissionResolver

...

securityManager.authorizer.permissionResolver = $globalPermissionResolver

...

 

 

PermissionResolverAware

如果你想配置一個全局的PermissionResolver,每一個會接收這個PermissionResolver配置的Realm必須實現PermissionResolverAware接口,這確保被配置PermissionResolver的實例可以傳遞給支持這種配置的每一個Realm

 

如果你不想使用一個全局的PermissionResolver或者你不想被PermissionResolverAware接口麻煩,你可以明確地為單個的Realm配置PermissionResolver接口(可看作是JavaBeansetPermissionResolver方法):

permissionResolver = com.foo.bar.authz.MyPermissionResolver

 

realm = com.foo.bar.realm.MyCustomRealm

realm.permissionResolver = $permissionResolver

...

 

配置全局的RolePermissionResolver

PermissionResolver類似,RolePermissionResolver有能力對執行檢查的Realm所需的權限實例進行表示。

 

最主要的不同在于輸入的字符串是一個角色名,而不是一個權限字符串。

 

RolePermissionResolverRealm在需要時用來轉換一個角色名到一組明確的權限實例。

 

這是非常有用的,它支持那些遺留的或者不靈活的沒有權限概念的數據源。

 

例如,許多LDAP目錄存儲角色名稱(或組名)但不支持角色名和權限的聯合因為沒有權限的概念。一個使用shiro的程序可以使用存儲于LDAP的角色名,但需要實現一個RolePermissionResolver來轉換LDAP名到一組確切的權限中以執行明確的訪問控制,權限的聯合將被存儲于其它的數據存儲中,比如說本地數據庫。

 

因為這種將角色名轉換為權限的理念是專用的,Shiro默認的Realm沒有使用它們。

 

然而,如果你想創建你自己的 RolePermissionResolver 并且希望用它配置多個Realm 實現,你可以將你的 RolePermissionResolver設置成全局。

shiro.ini

globalRolePermissionResolver = com.foo.bar.authz.MyPermissionResolver

...

securityManager.authorizer.rolePermissionResolver = $globalRolePermissionResolver

...

 

RolePermissionResolverAware

如果你想配置一個全局的RolePermissionResolver,每一個會接收這個RolePermissionResolver配置的Realm必須實現RolePermissionResolverAware接口,這確保被配置了RolePermissionResolver的實例可以傳遞給支持這種配置的每一個Realm

 

如果你不想使用全局的RolePermissionResolver 或者你不想麻煩實現RolePermissionResolverAware接口,你可以單獨為一個Realm配置RolePermissionResolver(可以看作JavaBeansetRolePermissionResolver方法)。

 

rolePermissionResolver = com.foo.bar.authz.MyRolePermissionResolver

 

realm = com.foo.bar.realm.MyCustomRealm

realm.rolePermissionResolver = $rolePermissionResolver

...

 

定制Authorizer

如果你的程序使用多于一個Realm來執行授權而ModularRealmAuthorizer默認的簡單迭代、短路授權的行為不能滿足你的需求,你可以創建自己的Authorizer并配置給相應的SecurityManager

例如,在shiro.ini中:

[main]

...

authorizer = com.foo.bar.authz.CustomAuthorizer

 

securityManager.authorizer = $authorizer

 本文由用戶 fmms 自行上傳分享,僅供網友學習交流。所有權歸原作者,若您的權利被侵害,請聯系管理員。
 轉載本站原創文章,請注明出處,并保留原始鏈接、圖片水印。
 本站是一個以用戶分享為主的開源技術平臺,歡迎各類分享!