將Shiro作爲應用的權限基礎框架

Shiro 是 JAVA 世界中新近出現的權限框架,較之 JAAS 和 Spring Security,Shiro 在保持強大功能的同時,還在簡單性和靈活性方面擁有巨大優勢。本文就帶領讀者一睹 Shiro 的風采。

可能大家早先會見過 J-security,這個是 Shiro 的前身。在 2009 年 3 月初之前,這個安全框架叫做 J-security,由於某些原因,更名爲 Shiro(或者 Ki,意爲 Fortress),是 Apache 的孵化項目,鑑於本文編寫時 Shiro 的還沒有正式發佈的版本,本文使用的是 Jsecurity 的穩定版本 0.9,本文中 Shiro 等同於 Jsecurity。

本文將涉及 Shiro 的整體框架、安全模型、關鍵概念類,同時給出了 Shiro 以及 Grails Shiro Plugin 的使用示例,可以下載文中使用的源代碼。

本文代碼的開發環境:

  • Jsecurity 0.9
  • Grails 1.2.0
  • Grails Shiro Plugin 1.0.1
  • SpringSource Tool Suite 2.3

Shiro 是一個強大而靈活的開源安全框架,能夠非常清晰的處理認證、授權、管理會話以及密碼加密。如下是它所具有的特點:

  1. 易於理解的 Java Security API;
  2. 簡單的身份認證(登錄),支持多種數據源(LDAP,JDBC,Kerberos,ActiveDirectory 等);
  3. 對角色的簡單的籤權(訪問控制),支持細粒度的籤權;
  4. 支持一級緩存,以提升應用程序的性能;
  5. 內置的基於 POJO 企業會話管理,適用於 Web 以及非 Web 的環境;
  6. 異構客戶端會話訪問;
  7. 非常簡單的加密 API;
  8. 不跟任何的框架或者容器捆綁,可以獨立運行。

目前還有其他出現較早的安全框架,比如 JAAS,Spring Security。

JAAS —面世的時間最早,但是鑑於其在使用上有很大的限制,很少有人真正的使用它。可以說它不是一個好的應用程序級別的安全框架;

Spring Security —目前是 Java 安全框架領域當之無愧的老大,已經非常成熟了;如果使用 Spring 框架,可以首選 Spring Security,但是對於單應用來說,Shiro 更顯簡單方便。

下面就開始我們的 Shiro 之旅吧!

首先,我們來看看的 Shiro 的整體架構,見下圖:


圖 1. 整體架構
圖 1. 整體架構

從上圖可以看出,Shiro 主要有四個組件:

  1. SecurityManager

    典型的 Facade,Shiro 通過它對外提供安全管理的各種服務。

  2. Authenticator

    對“Who are you ?”進行覈實。通常涉及用戶名和密碼。

    這個組件負責收集 principals 和 credentials,並將它們提交給應用系統。如果提交的 credentials 跟應用系統中提供的 credentials 吻合,就能夠繼續訪問,否則需要重新提交 principals 和 credentials,或者直接終止訪問。

  3. Authorizer

    身份份驗證通過後,由這個組件對登錄人員進行訪問控制的篩查,比如“who can do what”, 或者“who can do which actions”。Shiro 採用“基於 Realm”的方法,即用戶(又稱 Subject)、用戶組、角色和 permission 的聚合體。

  4. Session Manager

    這個組件保證了異構客戶端的訪問,配置簡單。它是基於 POJO/J2SE 的,不跟任何的客戶端或者協議綁定。

Shiro 的認證和籤權可以通過 JDBC、LDAP 或者 Active Directory 來訪問數據庫、目錄服務器或者 Active Directory 中的人員以及認證 / 籤權信息。SessionManager 通過會話 DAO 可以將會話保存在 cache 中,或者固化到數據庫或文件系統中。

從 Shiro 的框架圖,已經能夠體會到這個工具的簡單了。下面讓我們來看看 Shiro 是如何工作的。先了解一下它的安全模型吧!見下圖:


圖 2. 安全模型
圖 2. 安全模型

上圖中,涉及了 Shiro 的五個概念:

  • Subject 是安全領域術語,除了代表人,它還可以是應用。在單應用中,可將其視爲 User 的同義詞。
  • Principal 是 Subject 的標識,一般情況下是唯一標識,比如用戶名。
  • Role 和 Permission 分別代表了不同粒度的權限,從上圖中可以看出 Role 的粒度更大些,Permission 代表了系統的原子權限,比如數據的修改、刪除權限。對於簡單的權限應用,可以不需要 Permission。
  • Realm 是一個執行者,負責真正的認證和鑑權。

實現應用的安全模塊的關鍵在於:定義合適的 role 和 permission,這就需要遵循如下原則:

  1. role 沒有實質內容,只是代表一組 permission,目的是爲了管理的方便,一般都是動態定義;
  2. permission 一般都是預先定義好的,不允許動態改變,除非源代碼改動,它纔會變化,它是整個安全模塊的基礎;
  3. 要使 permission 也能動態定義,並非不可能,但是這將使鑑權非常複雜,甚至可能導致鑑權語句遍佈整個程序,得不償失;
  4. 當然有一個例外:如果知道 permission 動態定義的規則和鑑權規則,如 Grails 的 fileter 中“${controllerName}:${actionName}:${params.id}”也可實現 permission 的動態定義

理解 Shiro 的架構和安全模型了,我們來看看更具體些的內容。下圖顯示了 Shiro 中的關鍵概念類(參考資料 -- JSecurity Mini Guide)。


圖 3. 關鍵類
圖 3. 關鍵類

AuthenticationToken 和 AuthenticationInfo

前者在認證前使用,描述認證所需的信息,最常用的就是 username 和 password 對;後者在認證後使用,內容同前,但是表示已經經過認證的信息。

RememberMe

代表的是一種可能狀態,並不表示該 Subject 已經經過了認證。對於一些普通的操作,這種可能狀態並無大礙,但一旦涉及安全敏感的操作,必須經過認證。

Credentials 和 CredentialsMatcher

Credentials 是 Subject 的證書,在認證時使用,最常用的就是 password。在通常情況下,爲了安全起見,Subject 的 credentials 都需要加密保存,於是 CredentialsMatcher 的作用就體現出來了,見下圖:


圖 4. CredentialsMatcher 的作用
圖 4. CredentialsMatcher 的作用

這裏 CredentialsMatcher 需要將加密後的證書跟用戶登錄時提供的證書進行比對,完成認證的過程。

PAM= Pluggable Authentication Modules

在有多個 Realm 的時候使用。由認證策略決定認證結果,即 PAM= Relams + 認證策略。一般的策略有 3 種:AllSuccessful、AtLeastOneSuccessful 和 FirstSuccessful。

AuthorizationInfo

可以看成是 Role + Permission 的組合體。

PermissionResolver 和 Permission

它們之間的關係如下:


圖 5. PermissionResolver 和 Permission 的關係
圖 5. PermissionResolver 和 Permission 的關係

在 Shiro 中,權限被轉化爲一種字符串描述(字符串分級表示,稱之爲 WildcardPermission),從而將權限轉化爲類似於對象 equals 的操作(Shiro 中的 implies 方法)。

內置的權限有 2 個:

  • AllPermission,總是返回 true
  • WildcardPermission,權限字符串的表示方式。

這裏重點聲明一下。WildcardPermission 是 Shiro 的精妙之處,我們可以將權限表示成字符串,這樣對權限的控制可以不拘泥於物理存儲,比如對 messagge 類具有修改和刪除權限可以標識爲:message:update,delete:*,其中‘ * ’表示所有;第一級分隔符爲‘ : ’;第二級分隔符爲‘ , ’,而對於權限字符串的解釋完全可以由應用自己來定。

如果要比較權限字符串,可以使用 permission1.implies(permission2),它分別比較對應位置的字符串,在如下情況中,結果會返回 true:

  • permission1 中的子串有 * 或 permission1 子串 ==permission2 子串;
  • permission1 無子串,permission2 有;
  • permission1 有子串,permission2 無,permission1 的所有子串都是 *。

總的說來,Shiro 中的 Permission 需要注意如下內容:

  1. 權限的比較實際是字符串的比較,只不過是考慮到了字符串的分級
  2. 字符串的分級劃分完全由使用者自己決定,Shiro 的慣例是 3 級:資源 : 操作 : 實例。
  3. 字符串的使用必須一致,分隔符之間不要有空格,避免無意間引入的不一致。如:定義使用“file : create, update : 1”,而驗證使用“file : update”,那麼分解之後一個是“ update ”,一個是“ update”,因空格而引起不等。

Realm

這是一個實際訪問安全實體的組件,一般是應用相關的,跟數據源的關係是 1-1。它負責完成認證和鑑權,getAuthenticationInfo 代表了 login 的嘗試,鑑權方法則由 Authorizer 繼承而來。此處也體現了 Shiro 代碼的另一個特點,通過繼承來擴充功能。以常用的 JdbcRealm 爲例,其繼承鏈如下:


圖 6. JdbcRealm 的繼承鏈
圖 6. JdbcRealm 的繼承鏈

Session

它關聯一個 Subject 的上下文,其作用類似於在 HttpSession 中保存用戶標識,session 一旦過期,則重新登錄。Shiro 的 Session 是獨立的,其目的是做到環境無關性。爲了利用 Web 環境中,Shiro 實現了一個直接使用 HttpSession 的 WebSession。

SecurityManager

這是一個 Façade 接口,=Authenticator + Authorizer + SessionFactory。在整體框架圖中已經看到了它在 Shiro 中所處的位置。其特點同 Realm,一樣是使用繼承不斷地擴充功能。對於 Web 應用一般使用 DefaultWebSecurityManager。

Filter

在 Web 環境下使用 filter 進行認證和權限檢查是毋庸置疑的,而 Shiro 的特點則在於由一個主 Filter 將一羣子 filter 串起來:


圖 7. Filter 的作用
圖 7. Filter 的作用

在實際使用時,須注意:

  1. web.xml 中只需配置 JSecurityFilter。對於 Spring 應用,則使用 SpringJSecurityFilter;
  2. 子 filter 作爲主 filter 的配置參數值出現,特點是:順序相關
    1.  
      • 對於多個 URL,驗證順序是由上至下,類似 Exception 的匹配。因此,使用順序應該是由細到粗。
      • 對於同一 URL,子 filter 的驗證順序是從左至右的 AND 操作。
  3. 如果配置值中含有分隔符,如 Permission,就需要使用引號來轉義。

Subject

subject 代表了一個用戶的狀態和操作,它提供了所有安全相關的操作,包括認證和籤權。可以將其視爲另一種形式的 Façade。缺省實現是將這些操作委派給其內部包含的 SecurityManager。

Configuration

configuration 負責將所有這些組件串起來,最終創建 SecurityManager。在 Shiro 中,缺省格式是 ini。整個配置關係如下圖:


圖 8. 配置關係
圖 8. 配置關係

其中:

  • JSecurityFilter 創建 Configuration 實例,並將 ini 參數值傳給 Configuation。在 Spring 環境中,分別使用 SpringJSecurityFilter 和 SpringIniWebConfiguration。
  • Configuration 實際就是 SecurityManager 的 Factroy,對 SpringIniWebConfiguration 而言,它需要知道 SecurityManager 的 BeanName,該值由 SpringJSecurityFilter 的初始化參數“securityManagerBeanName”值決定。即 SpringJSecurityFilter,實際有兩個初始化參數:
  •  
    • config,是 ini 配置文件內容
    • securityManagerBeanName,是 SecurityManager 的 BeanName

SecurityUtils

這是 Shiro 中最重要的工具類,由它可以方便地獲得 Subject 和 SecurityManager。

雜項

  • AOP,提供 AOP 方面的支持,實現對某個類某個方法的攔截,從而使權限控制延伸至類的方法。
  • Cache,提供緩存支持
  • Codec,提供編碼方面的支持
  • Crypto,提供加密支持
  • IO,從多個資源位置讀寫原始數據
  • JNDI,提供 jndi 支持
  • util,工具類支持
  • 標籤類,用於 Web 頁面

對 Shiro 有了一個感官認識後,下面我們就親自動手試試這個框架吧!下面給大家舉了兩個使用案例。

在開始案例的學習之前,先作好準備工作 -- 獲得 Shiro 相關的 jar 包,獲取途徑有兩種:

  1. 直接到 J-security 的網站上 下 載,本文用到的就是這個;
  2. 由於 Shiro 目前是 Apache 的孵化項目,還沒有發佈正式的版本,但是我們可以到 Subversion 上下載代碼,之後使用 Maven 構建

    mkdir shiro

    cd shiro

    svn co https://svn.apache.org/repos/asf/incubator/shiro/trunk/

    mvn install

之後會得到 shiro-all-1.0-incubating-SNAPSHOT.jar,就可以使用 Shiro 了。

示例一:讓 Shiro 爲你的應用服務

這個案例中,我們使用 Grails 向大家講述 Shiro 的使用。我們要實現如下功能:

  1. 用戶登錄後方可進入系統;
  2. 假定一個 message 的安全內容,用戶可以創建 message 的內容,但是如果需要修改 / 刪除 message 的內容就必須具有相應的權限;
  3. Admin 具有所有的權限;
  4. message 的權限跟角色關聯。

示例程序執行的流程如下:


圖 9 程序執行的流程
圖 9 程序執行的流程

從上圖中可以看到,任何人要訪問應用中受保護的 URL,首先要通過 Filter 檢查用戶是否經過認證;對於沒有認證的用戶會將訪問定向到登錄頁面;對於已經認證的用戶,會對用戶進行鑑權,這個用戶是否具有訪問其所提交的 URL 的權限;而管理員可以給角色授權。

好了,開始程序的編寫啦!

創建安全領域類

最常見的就是 User、Role 和 Permission,見清單 1。


清單 1. User/Role/Permission 的 Domain class

				
class User {
String username
String password
static hasMany= [roles: Role]
static belongsTo= Role
……
}
class Role {
String rolename
static hasMany= [users: User, permissions: Permission]
……
}
class Permission {
String permission

static hasMany= [roles: Role]
static belongsTo= Role
……
}

 

這裏使用了最簡單的情形,即權限傳遞結構爲:Permission -> Role -> User。通常情況下,Permission 也可以分配給單個 User。

創建一個安全實體

實體名爲 message,見清單 2。只有經過授權的用戶才能對這個實體進行修改和刪除。


清單 2. message 的 Domain class

				
class Message {
String details
User user
static constraints = {
}
}

 

配置 web.xml


清單 3. 在 web.xml 加入 SecurityFilter 的內容:

				
<filter>
<filter-name>SecurityFilter</filter-name>
<filter-class>
org.jsecurity.spring.SpringJSecurityFilter
</filter-class>
<init-param>
<param-name>securityManagerBeanName</param-name>
<param-value>jsecSecurityManager</param-value>
</init-param>
</filter>
<filter-mapping>
<filter-name>SecurityFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>

 

這裏需要注意:

  • 這個 Filter 應該在 Grails 的 web.xml 中所有缺省的 Filter 最後;
  • url-pattern 不要使用“/**”,因爲這樣會造成登錄頁的 css 和圖片無法訪問。解決辦法,可以通過遵循“只能通過 Controller/Action 訪問”這個規則,並使用 Grails 的 Filter 機制,可以保證所有安全 URL 不被非法訪問。

創建 realm


清單 4. conf/spring/resources.groovy

				
beans = {
credentialMatcher(
org.jsecurity.authc.credential.Sha1CredentialsMatcher) {
storedCredentialsHexEncoded = true
}

permissionResolver(
org.jsecurity.authz.permission.WildcardPermissionResolver)

realm(org.jsecurity.realm.jdbc.JdbcRealm){
permissionResolver = ref("permissionResolver")
dataSource = ref("dataSource")
permissionsLookupEnabled= true
permissionsQuery= "select permission from
permission, role_permissions, role where
permission.id= permission_id and role_id= role.id and rolename= ?"
userRolesQuery= "select rolename from role, role_users, user
where role.id=role_id and user_id= user.id and username=?"
authenticationQuery= "select password from user where username=?"
}

jsecSecurityManager(
org.jsecurity.web.DefaultWebSecurityManager) {
bean ->bean.destroyMethod = "destroy"
realms = [ ref("realm") ]
}
}

 

這裏使用了 JdbcRealm,同時根據應用情況修改了相應的 SQL。如果允許 Permission 直

接分配給 User,即 Permission 和 User 之間是多對多關係,那麼 permissionsQuery 應該使用 union,即“role 相關 permission union user 相關 permission”。對於 User 有多個 Role 的情況,JdbcRealm 會循環得出總的結果。

安全守護神:SecurityFilters

下面就是我們的安全守護神:SecurityFilters,這裏遵循 Grails 的 Filter 語法。見清單 5。


清單 5. SecurityFilters

				
import org.jsecurity.SecurityUtils

class SecurityFilters {
def filters = {
authc(controller:'*', action:'*', ) {
before = {
if(controllerName!='auth'){
def subject = SecurityUtils.subject
if (!subject.authenticated) {
redirect(
controller: 'auth',
action: 'login',
params: [
targetUri: request.forwardURI - request.contextPath
])
return false
}
}
}
}

admin(controller: 'user|role|permission', action: '*'){
before = {
def subject= SecurityUtils.subject
if(!subject.hasRole('admin')){
redirect(controller: 'auth', action: 'unauthorized')
return false
}
}
}

editmessage(controller: 'message', action: 'update|delete'){
before = {
def subject= SecurityUtils.subject
if(!subject.isPermitted(
"${controllerName}:${actionName}:${params.id}")){
redirect(controller: 'auth', action: 'unauthorized')
return false
}
}
}
}
}

 

代碼中 :

  • authc 表示的是所有用戶對應用系統的任何訪問都要經過 auth 認證,對於沒有認證的用戶的訪問會重定向到登錄界面;
  • admin 表示的是屬於 admin 角色的用戶對 user/role/permission 具有所有權限,對於非 admin 角色的用戶會對其提示沒有 user/role/permission 的訪問權限;
  • editmessage 表示當用戶對 message 進行修改或者刪除的時候對其進行鑑權,只有具有 message 的 update/delete 權限的用戶才能對 message 進行修改。

在上述代碼中還可以看出,我們通常可以由 SecurityUtils 爲出發點獲得 Subject 和 SecurityManager。需要注意的是,認證的 Filter(authc)要打頭陣。

認證的代碼


清單 6. 認證代碼

				
def signIn = {
// 創建 AuthenticationToken
def authToken = new UsernamePasswordToken(
params.username,
params.password)
if (params.rememberMe) {
authToken.rememberMe = true
}
try{
// 使用 SecurityManager 的 login 登錄
this.jsecSecurityManager.login(authToken)
flash.message = message(code: "$params.username")
def targetUri = params.targetUri ?: "/"
log.info "Redirecting to '${targetUri}'."
redirect(uri: targetUri)
}
catch (AuthenticationException ex){
// 如果出現異常,顯示出錯信息
log.info "Authentication failure for user '${params.username}'."
flash.message = message(code: "login.failed")
def m = [ username: params.username ]
if (params.rememberMe) {
m['rememberMe'] = true
}
if (params.targetUri) {
m['targetUri'] = params.targetUri
}
redirect(action: 'login', params: m)
}
}

 

授權部分很簡單,即對安全實體進行 CRUD。其中 Permission 的權限字符串根據實際情況形成,在本例中:

  •  
    •  
      • 如果對所有 message 具有修改權限,權限字符串可以爲:message:delete,update:*;
      • 如果針對某一個 message 具有修改權限,權限字符串可以爲:message:update,delete:messageid。

示例二:使用 Shiro Plugin 快速構建安全模塊

在示例一中,所有的代碼都是自己手動寫的,這就對初學者要求有些高了。但可喜的是 Grails 社區有了 Shiro 的 plugin,讓我們的工作變得非常簡單。同樣示例一的功能,看看 plugin 能夠給我們帶來什麼樣的驚喜?

使用步驟如下:

  1. 安裝 Shiro Plugin,在你的 grails 項目中運行:grails install-plugin shiro, 會創建 grails-app/realms 目錄,並提供如下新的 Grails 命令:
    • grails create-auth-controller,創建 AuthController 以及登錄窗口,Controller 提供了登錄、登出和權限驗證失敗處理等 Action。
    • grails create-db-realm,創建一個訪問數據庫的 Realm
    • grails create-ldap-realm,創建一個訪問 ldap 的 Realm
    • grails create-wildcard-realm,創建一個訪問數據庫的 Realm,但使用的是 Shiro 的 WildcardPermission。
    • grails quick-start,它是 create-auth-controller 和 create-db-realm 的集合體,是 Shiro 的快速入門,在接下來的內容中將詳細介紹它的使用。
  2. 下面進行 Shiro 快速入門。在 grails 項目的目錄下執行:grails quick-start,這個命令會創 建如下內容:
    • 在 grails-app/realms 下創建 ShiroDbRealm,這個 realm 是訪問控制的信息庫,用來決定一個用戶有權訪問哪些內容;
    • 在 grails-app/domain 創建 ShiroRole.groovy 和 ShiroUser.groovy;
    • 在 grails-app/controllers 下創建 AuthController,這裏提供了登錄和退出的 action;
    • 在 grails-app/views 下創建 auth 文件夾以及 login.gsp;
    • 在 grails-app/conf/ 下創建 SecurityFilters.groovy,這裏管理着對所有 controller 和 action 的訪問控制。
  3. 啓動程序,當訪問 controller 的時候,頁面就會重定向到登錄頁面。但是這個時候是無法登錄,因爲我們沒有添加用戶。
  4. 進入到 grails-app/conf/ 修改 BootStrap.groovy, 

    清單 7. 修改後的 BootStrap.groovy
    				
    def init = { servletContext ->
    def user = new User(username: "user",
    passwordHash: new Sha1Hash("user").toHex())
    user.save()
    def role= new Role(name:"admin").addToUsers(user).save()
    }

    重新啓動程序,就能使用 user/user 登陸了。

  5. 在 plugin 缺省創建的 SecurityFilters 中使用了對所有 URL 進行 before 攔截,但是我們根據實際情況進行修改,例如我們要示例一的 filter 內容,就可以做如下更改: 

    清單 8. 更改後的 SecurityFilters
    				
    auth(controller: "*", action: "*"){
    before={
    accessControl{true}
    }
    }
        management(controller: "user|role|permission", action: "*"){
    before={
    accessControl{role("admin")}
    }
    }
    message(controller: "message", action: "delete|update"){
    before={
    accessControl{   
            permission("message:${actionName}:${params.id}")
    }
    }
    }

    看到這裏,讀者可能已經注意到了,這裏的代碼比示例一中的 Filter 的代碼簡單的許多。對,Shiro 插件已經將示例一中的類似代碼封裝到 ShiroGrailsPlugin.groovy 中了,我們使用的時候只需要:

    調用 accessControl 方法,該方法的參數可以是 Map、Closure 或者 Map+Closure;

    使用 role( …… ),驗證訪問對象是否具有相應的角色;

    使用 permission( …… ),驗證訪問對象是否具有相應的 Permission。

  6. 授權部分內容參見上一示例,這裏不做冗述。
  7. Shiro Plugin 中除了爲我們提供了上述方便之外,還提供了一些常用的 taglib 來增強用戶界面 , 這些 taglib 都在 pluginPath/grails-app/taglib /ShiroTagLib.groovy 中定義,我們可以直接在 gsp 中使用它們。

    比如,只有具有修改權限的用戶才能夠看到一些修改類的按鈕顯示,可以這樣寫:



    清單 9. Taglib 的使用
    				
    <shiro:hasPermission permission="message:update,delete">
    <span class="button">
    <g:actionSubmit class="edit" value="Edit" />
    </span>
    <span class="button">
    <g:actionSubmit class="delete"
    οnclick="return confirm('Are you sure?');"
    value="Delete" />
    </span>
    </shiro:hasPermission>

    如下是經常使用到的 Tag:

    • principal, 輸出當前用戶的標識
    • hasRole,判斷當前用戶是否屬於給定的角色,參數:name
    • hasPermission, 判斷當前用戶是否具有指定的權限,參數:type,action 或者 permission
    • isLoggedIn, 判斷當前用戶是否已經登錄
    • hasAnyRole,判斷當前用戶是否屬於給定的某個角色,參數:in

更多的 Tag 請參考 Shiro Plugin 的 gapi 的文檔。

如果您已經非常瞭解了 Shiro,可以採用示例一的方式自己寫所有的代碼,但是對於初學者,作者還建議使用 Shiro plugin,這個插件幫助我們生成了安全相關的基本內容,以及提供了方便的 Tag。

讀到這裏,是不是覺得 Shiro 真的不錯?!

這裏給大家舉的示例只是拋磚引玉。Shiro 真正的精髓還需要在項目中慢慢的體會。本文是引領我們走向 Shiro 的第一步。在這裏要感謝胡鍵對本文編寫的大力支持。

源代碼中,shiroApp 是示例一的源代碼,ShiroPlugin 是示例二的源代碼。

下載本文中的示例代碼

發佈了126 篇原創文章 · 獲贊 23 · 訪問量 107萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章