博客 / 詳情

返回

Shiro學習筆記(一) 基本概念與使用

Shiro能幫助我們幹什麼?

Apache Shiro™ is a powerful and easy-to-use Java security framework that performs authentication, authorization, cryptography, and session management. With Shiro’s easy-to-understand API, you can quickly and easily secure any application – from the smallest mobile applications to the largest web and enterprise applications. -《Apache Shiro 官網》

Apache Shiro 是Java領域內的一款簡單易用而又強大的一款安全框架,主要用於登錄驗證、授權、加密、會話管理。Shiro擁有簡單應用的API,您可以快送的使用它來保護您的應用,不管是小到手機應用還是到大的Web企業級應用。

從上面的一段話我們可以提取到以下信息:

  • Shiro簡單易用而又強大
  • 主要應用於登錄驗證、授權、加密、會話管理。

第一點需要在中使用中慢慢體會,第二點是我們主要關注的,這裏我們一點一點的講。

登錄驗證 authentication 與 會話管理

這裏我們回憶一下HTTP協議和Servlet, 早期的HTTP協議是無狀態的, 這個無狀態我們可以這麼理解,你一分鐘前訪問和現在訪問一個網站,服務端並不認識你是誰,但是對於Web服務端開發者來説, 這便無法實現訪問控制,某些信息只能登錄用户才能看,各自看各自的。這着實的有點限制住了Web的發展,為了讓HTTP協議有狀態,RFC-6235提案被獲得批准,這個提案引入了Cookie, 是服務器發送到用户瀏覽器並保存在本地的一小塊數據,它會再瀏覽器下次向同一服務器再發起請求時被攜帶併發送到服務器上,服務端就可以實現“識別”用户了。

在原生的Servlet場景中如下:

登錄流程圖

代碼示例:

/**
 * 攔截所有的請求
 */
@WebFilter(urlPatterns = "/*")
public class LoginCheckFilter implements Filter {

    /**
     * 不重寫init方法,過濾器無法起作用
     * @param filterConfig
     * @throws ServletException
     */
    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("----login check filter init-----");
    }
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest servletRequest = (HttpServletRequest) request;
        HttpSession session = servletRequest.getSession();
        // 這裏採取了暫時寫死,目前只放行登錄請求的URL
        String requestUri = servletRequest.getRequestURI();
        if ("/login".equals(requestUri)){
            // 放行,此請求進入下一個過濾器
            chain.doFilter(request,response);
        }else {
            Object attribute = session.getAttribute("currentUser");
            if (Objects.nonNull(attribute)){
                chain.doFilter(request,response);
            }else {
                request.getRequestDispatcher("/login.jsp").forward(request,response);
            }
        }
    }
}
@WebFilter(urlPatterns = "/login")
public class LoginFilter implements Filter {

    @Override
    public void init(FilterConfig filterConfig) throws ServletException {
        System.out.println("------login filter init-----");
    }

    /**
     * 由登錄過濾器來執行登錄驗證操作
     * 要求用户名和密碼不為空的情況下才進行下一步操縱
     * 此處省略判空操作
     * 只做模擬登錄
     */
    @Override
    public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
        HttpServletRequest servletRequest = (HttpServletRequest) request;
        String userName = (String) servletRequest.getAttribute("userName");
        String password = (String)servletRequest.getAttribute("password");
        // 這裏假裝去數據庫去查賬號和密碼
        HttpSession session = servletRequest.getSession();
        // 生成session,
        session.setAttribute("currentUser",userName + password);
        // 放到下一個過濾器,如果這是最後一個過濾器,那麼這個請求會被放行到對應的Servlet中
        chain.doFilter(request,response);
    }
}
@WebServlet(value = "/hello")
public class HttpServletDemo extends HttpServlet {
   
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("hello world");
        super.doGet(req, resp);
    }

    @Override
    protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.doPost(req, resp);
    }
}

前面提到, 為了實現讓HTTP協議有“狀態”, 瀏覽器引入了Cookie, 存放服務端發送給客户端的數據, 為了使服務端區分不同的用户, 服務端程序引入了Session這個概念,瀏覽器首次請求服務端,服務端會在HTTP協議中告訴客户端,需要再Cooke裏面記錄一個SessionID,以後每次請求把這個SessionId發送到服務器,這樣服務器就能區分不同的客户端了。

JSessionId

有了這樣的對應關係,我們就可以在Session中保存當前用户的信息。其實我們這裏只做了登錄,還沒有做登出,登出應該將對應的Session中清除掉。這套邏輯Shiro幫我們做好了,那自然是極好的,但僅僅是如此的話,還不足以讓我們使用Shiro, 我們接着翻官方文檔。

一個小插曲

因為很久沒寫Servlet了,這次去下載Tomcat,下了一個10.0的, 但是發現10.0版本跟JDK 8不太兼容,折騰了許久, 請求都沒辦法到達Servlet,要不就是報Servlet初始化失敗,最高只好退回8.5版本,但是還是發現請求到達不了我寫的Servlet中, 我是基於註解的形式配置的映射,但是web.xml的有個屬性我忘記了,那就是metadata-complete, 此屬性為true, 不會掃描基於註解的Servlet,此註解為false, 才會啓用此Servlet。

meta-complete = false

Shiro的特性

ShiroFeatures

Authentication和Session Management: Shiro幫我們做好了, 那自然是極好的。

Cryptography: 加密, 其實Java標準庫也提供了實現,那Shiro能提供一套更簡單易用的API更好。

Authorization:授權, 這個值得我們注意一下,寫到這裏我想起我第一個構建的Web系統,當時只考慮了普通的用户,沒有考慮資源控制的問題,當時已經快到預定的時間了,説來慚愧, 只好做了一個非常粗糙的權限控制,為用户添加了一個權限標識字段,1代表什麼角色,2代表什麼角色能看哪些頁面,也就是説資源是固定的,相當的僵硬。後面才瞭解到在這方面有一套比較成熟而自然的RBAC(Role-Based Access Controle 基於角色的訪問控制)模型,即角色-用户-權限(資源),也就是説一個用户擁有若干角色,每一個角色擁有若干權限和資源,這樣我們就實現了權限和用户的解耦合。

基本概念我們大致論述完之後,我們來進一步深入的看這些特性:

  • Authentication 登錄

    • Subject Based – Almost everything you do in Shiro is based on the currently executing user, called a Subject.
      And you can easily retrieve the Subject anywhere in your code. This makes it easier for you to understand and work with Shiro in your applications.

      基於主體,在Shiro中做的任何事情都基於當前正在活動的用户,在Shiro中稱之為主體,你可以在代碼的任何地方取到當前的主體。

      這將讓你在使用和理解shiro變的輕鬆起來。

    • Single Method call – The authentication process is a single method call.
      Needing only one method call keeps the API simple and your application code clean, saving you time and effort.

      簡單的方法調用,非常簡單,節省時間和精力。

    • Rich Exception Hierarchy – Shiro offers a rich exception hierarchy to offered detailed explanations for why a login failed.
      The hierarchy can help you more easily diagnose code bugs or customer services issues related to authentication. In addition, the richness can help you create more complex authentication functionality if needed.

      豐富的異常體系,Shiro提供了完備的異常體系來解釋登錄為什麼失敗。這個異常體系可以幫助診斷定製服務的相關bug和issue。除此之外,還可以幫助創建功能更加豐富的應用。

    • ‘Remember Me’ built in – Standard in the Shiro API is the ability to remember your users if they return to your application.
      You can offer a better user experience to them with minimal development effort.

      記住我,標準的Shiro API提供了記錄密碼的功能,只需少量的配置,就能給用户提供更佳的體驗。

    • Pluggable data sources – Shiro uses pluggable data access objects (DAOs), called Realms, to connect to security data sources like LDAP and Active Directory.
      To help you avoid building and maintaining integrations yourself, Shiro provides out-of-the-box realms for popular data sources like LDAP, Active Directory, and JDBC. If needed, you can also create your own realms to support specific functionality not included in the basic realms.

      可插拔的數據源,Shiro提供了一個可插拔的數據權限對象,在shiro中我們稱之為Realms,我們用這個去安全的連接像LDAP、Active Directory的數據源。

    為了避免開發者做重複的工作,Shiro 提供了開箱即用的連接指定數據源的Realm是,像LDAP、Active Directory 、JDBC。 如果你需要你也可以創建自定義的Realms。

    • Login with one or more realms – Using Shiro, you can easily authenticate a user against one or more realms and return one unified view of their identity.
      In addition, you can customize the authentication process with Shiro’s notion of an authentication strategy. The strategies can be setup in configuration files so changes don’t require source code modifications – reducing complexity and maintenance effort.

      支持一個和多個Realm登錄,使用Shiro 你可以輕鬆的完成用户使用多個Realm登錄,並且方式統一。除此之外,你也可以可以使用Shiro的身份驗證策略自定義登錄過程,驗證策略支持寫在配置文件中,所以當驗證策略改變的時候,不需要更改代碼。

看來上面的特性概述,你會發現原來登錄需要考慮這麼多,原先我們的視角可能只在數據庫的數據源,事實上對於WEB系統來説還可以引入其他數據源,但是你不用擔心,這麼多需要考慮的東西,原先你自己來寫登錄,可能還會考慮遺漏的地方,但是Shiro都幫你寫好。我們還有什麼理由不學它呢。這裏還是需要重點講一下Shiro的Realms概念,我們回憶一下JDBC,可以讓我們Java程序員寫一套API就能做到跨數據庫,那多個數據源呢,我們能否也抽象出一個接口,做到登錄的時候跨數據源呢? 其實Realms就是這個思路,也抽象出了一個接口,對接不同的數據源來實現登錄認證:

Realm繼承圖

本質上是一個特定的安全DAO(Data Access Object), 封裝與數據源連接的細節,得到Shiro所需要的相關數據。在配置Shiro的時候,我們必須指定至少Realm來實現認證(authentication) 和授權(authorization).

我們只重點介紹一個特性來體會Shiro的強大,其他特性我們只簡單介紹轉眼的點:

  • Cryptography:Shiro指出Java的密碼體系比較難用(The Java Cryptography Extension (JCE) can be complicated and difficult to use unless you’re a cryptography expert, Java的擴展非常難用,當然你要是個密碼專家就當我沒説),Shiro設計的密碼學的API更簡單易用。

    PS: 其實還指出了Java密碼學擴展的一些問題,算是對Java密碼學相關庫的吐槽了。有興致可以去看看,我們這裏不做過多介紹。

  • SessionManagement

    可以被用於SSO,獲取用户登錄和退出都相當方便。SSO是單點登錄,方便的和各種應用系統做集成。
  • Authorization

    下面是授權的幾個經典問題:

    這個用户是否可以編輯這個賬號

    這個用户是否有權限看這頁面

    這個用户是否有權限使用這個按鈕

    Shiro回答了以上問題,並且非常靈活、簡單、容易使用。

    Shiro幫我們做了這麼多,而且簡單,我們可以省了很多工作,這就是學習Shiro的理由。

Shiro核心概念概述

在Shiro中的架構中有三個主要概念:

  • subject(當前主體, 可以理解為當前登錄用户,)

    在Shiro中可以使用下面代碼來獲取當前登錄用户:

  Subject currentUser = SecurityUtils.getSubject();
  • SecurityManager
為所有用户提供安全保護,內嵌了很多安全組件,那麼如何設置它呢? 也取決於不同的環境, Web 程序中通常是在Web.xml中指定Shiro的Servlet過濾器,這就完成了一個SecurityManager 實例,其他類型的應用程序我們也有其他選項。
  • Realms
Realm 事實上是Shiro和你應用安全數據的橋樑或連接器。

Realms

三者之間的關係:

三個驗證狀態

用起來

首先引入maven依賴:

  <dependency>
      <groupId>org.apache.shiro</groupId>
      <artifactId>shiro-core</artifactId>
      <version>1.9.0</version>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-simple</artifactId>
      <version>1.7.21</version>
      <scope>test</scope>
    </dependency>
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>jcl-over-slf4j</artifactId>
      <version>1.7.21</version>
      <scope>test</scope>
    </dependency>

當然是從Hello World開始

    public static void testAuthentication(){
        // 設置SecurityManager 
        // SecurityManager 負責將用户提交過來的username、password和realm中的進行對比
        // 判斷是否可以登錄
        DefaultSecurityManager defaultSecurityManager = new DefaultSecurityManager();
    
        // 這是一個簡單的Realm,直接在代碼裏面存儲賬號和密碼
        SimpleAccountRealm simpleAccountRealm = new SimpleAccountRealm();
        simpleAccountRealm.addAccount("hello world","hello world");
        // 將realm 納入到 DefaultManager的管轄之下
        defaultSecurityManager.setRealm(simpleAccountRealm);
        // 通過此方法設置SecurityManager
        SecurityUtils.setSecurityManager(defaultSecurityManager);
        
        // 用户登錄和密碼憑證
        UsernamePasswordToken token = new UsernamePasswordToken("hello world", "hello world");
        
        // 獲取subject
        Subject subject = SecurityUtils.getSubject();
        
        // 由subject將token提交給SecurityManager
        subject.login(token);
        // 登錄成功會返回true
        System.out.println("login status:"+subject.isAuthenticated());
        // 退出
        subject.logout();
        // 退出之後是false
        System.out.println("login status:"+subject.isAuthenticated());
    }

加密示例

Cryptography is the process of hiding or obfuscating data so prying eyes can’t understand it. Shiro’s goal in cryptography is to simplify and make usable the JDK’s cryptography support.

密碼學是隱藏或混淆數據的過程,防止數據被竊取。Shiro在密碼學方面的目標是簡化JDK標準密碼學庫的使用

接下來讓我們下用Shiro的密碼學相關的API有多簡單易用.

  • MD5 JDK標準庫的實現
 private static void testMD5JDK() {
        try {
            String code = "hello world";
            // MD5 是 MessageDigest的第五個版本
            MessageDigest md = MessageDigest.getInstance("MD5");
            byte[] targetBytes = md.digest(code.getBytes());
            //輸出的是MD5的十六進制形式
            System.out.println(targetBytes);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
    }
  • Shiro的實現:
private static void testMD5Shiro() {
    String hex = new Md5Hash("hello world").toHex();
    System.out.println(hex.getBytes());
}

這麼一看Shiro的實現確實簡單一些,更直觀。

授權示例

在Shiro中將授權分成以下兩種:

  • Permission Defined

Permissions are the most atomic level of a security policy and they are statements of functionality. Permissions represent what can be done in your application. A well formed permission describes a resource types and what actions are possible when you interact with those resources. Can you open a door? Can you read a file? Can you delete a customer record? Can you push a button?

Common actions for data-related resources are create, read, update, and delete, commonly referred to as CRUD.

It is important to understand that permissions do not have knowledge of who can perform the actions– they are just statements of what actions can be performed.

准許是安全策略的原子級別,表現為功能的聲明。准許的意思是你可以在這個系統中做什麼。形式良好的准許描述了資源類型以及可以對這些資源進行的操作。比如: 你是否可以打開這扇門? 你是否可以讀一個文件? 你是否可以刪除一個客户記錄? 你是否能點擊按鈕?

一般來説對資源的操作有新增、讀取、更新、刪除,這些操作通常被稱作為CRUD。

一定要理解,准許是不知道是誰可以操作這些資源的,它們只是説明這些資源可以執行哪些操作。

​ 權限的粒度:

  1. Resource Level - This is the broadest and easiest to build. A user can edit customer records or open doors. The resource is specified but not a specific instance of that resource.
資源級別: 這是最廣泛的而且是最容易構建的。用户可以編輯客户記錄或者打開一扇門。資源指定了,但是沒有指定到具體的人禍角色上。
  1. Instance Level - The permission specifies the instance of a resource. A user can edit the customer record for IBM or open the kitchen door.
實例級別: 某個准許指定了具體的人或者角色, 某個用户可以編輯IBM的用户記錄或打開廚房的門。
  1. Attribute Level - The permission now specifies an attribute of an instance or resource. A user can edit the address on the IBM customer record.
屬性級別: 某人被允許編輯資源的某個屬性,某個用户可以編輯IVM用户記錄的地址。
  • Role Defined

In the context of Authorization, Roles are effectively a collection of permissions used to simplify the management of permissions and users. So users can be assigned roles instead of being assigned permissions directly, which can get complicated with larger user bases and more complex applications. So, for example, a bank application might have an administrator role or a bank teller role.

在授權的上下門,角色實際上是權限的集合,簡化權限和用户的管理,因此用户可以被分配角色,而不是直接被分配權限,因為直接分配權限這對於更大的用户羣和更復雜的應用程序來説會比較複雜。

Shiro支持如下兩種角色:

  • Implicit Roles 隱式角色
大多數人的角色在我們眼中屬於是隱式角色,隱含了一組權限,通常的説,如果你具有管理員的角色,你可以查看患者數據。如你具有“銀行出納員”的角色,那麼你可以創建賬號。
  • Explicit Roles 顯式角色
顯式角色就是系統明顯分配的權限,如果你可以查看患者數據,那是因為你被分配到了“管理員角色”的“查看患者數據”權限。
小小總結一下

上面是我翻譯的Shiro對角色和權限的論述,權限的話可以理解為對某個資源的CRUD, 粒度級別有整個資源,比如一行記錄的操縱權限,這行記錄只有某個人才能操縱,某個人只能操縱這行記錄的一部分。而角色則是權限的集合, 在我看來是實現權限和用户的解耦,比如我想對一批用户授權,我可以選一個角色,進行批量授權。那麼問題又來了,我該怎麼做權限控制呢,或者在Shiro中判斷某個是否有這個角色呢?

  • 我們可以從Subject這個類的方法中判斷當前用户是否具備某個角色或者具備某個權限

    subject.hasRole("admin") // 當前用户是否有admin這個角色
    subject.isPermitted("user:create") // 判斷當前用户是否允許添加用户
  • JDK 的註解, 在方法中加註解
// 判斷當前角色是否有admin 這個角色
@RequiresRoles("admin")
private static void requireSpecialRole() {

}
 // 判斷當前用户是否允許添加用户
@RequiresPermissions("user:create")
private static void requireSpecialRole() {
 }
  • JSP 標籤(前後端分離時代了, 這個不做介紹)

Shiro 定義了一組權限描述語法來描述權限,格式為: 資源:(create || update || delete ||query)。

示例:

user:create,update // 表示具備創建和更新用户的權限
user:create,update:test110 // ID為test110有 創建和更新用户的權限   

自定義Realm

我們前面一直再説Realm是Shiro和用户數據之間的橋樑, 我們大致看下這個登錄過程,重點來體會一下橋樑:

  1. subject.login(token). 目前Subject只有一個實現類,那就是DelegatingSubject。

DelegatingSubject

我們上面用的是DefaultSecurityManager, 所以我們需要進入該類的方法來看是怎麼執行登錄的, DefaultSecurityManager.login調用authenticate(token)來獲取用户信息,authenticate()方法來自DefaultSecurityManager的父類AuthenticatingSecurityManager, 該類擁有一個Authenticator成員變量,由該成員變量調用authenticate,獲取用户信息

AuthenticatingSecurityManager

這在某種成都上很像是模板方法模式, 父類定義骨架或通用方法,子類做調用。Authenticator是一個接口有許多實現類,但是調用的authenticate就只有兩個實現:

AbstractAuthenticator

doAuthenticate

獲取初始化進來Realms

最終調到了對應的Realms

Realm這個接口太頂層了,我們要做自己的Realm的話還是找一個抽象類,我們上面的SimpleAccountRealm就是繼承自AuthorizingRealm,重寫了doGetAuthenticationInfo用於登錄驗證,doGetAuthorizationInfo用於做權限驗證。我們自己寫的如下:

public class MyRealm extends AuthorizingRealm {

    /**
     * 設置Realm的名稱
     */
    public MyRealm() {
        super.setName("myRealm");
    }

    /**
     * 我們這個自定義reamls就能實現權限控制了
     * @param principals
     * @return
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        // 獲取登錄憑證
        String userName = (String) principals.getPrimaryPrincipal();
        // 假裝這roles 是從數據庫中查的
        Set<String> roles  = new HashSet<>();
        // 假裝是從數據庫查的
        Set<String> permissions  = new HashSet<>();
        SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo();
        simpleAuthorizationInfo.setStringPermissions(permissions);
        simpleAuthorizationInfo.setRoles(roles);
        return simpleAuthorizationInfo;
    }

    /**
     * 隨便寫個空實現
     * @param userName
     * @return
     */
    private String getPassWordByUserName(String userName) {
        return "";
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        // 獲取賬號
        String userName = (String) token.getPrincipal();
        String password = null;
        if (userName != null && userName != ""){
            password = getPassWordByUserName(userName);
        }else {
            return null;
        }
        if (Objects.isNull(password)){
            return null;
        }
        // 假裝去查了數據庫
        SimpleAuthenticationInfo simpleAuthenticationInfo = new SimpleAuthenticationInfo(userName,password,"myRealm");
        return simpleAuthenticationInfo;
    }
}

寫在最後

我記得剛開始學Shiro的時候是去B站搜對應的視頻, 但是視頻大多都是從Shiro的基本組件開始講,我其實只是想知道Shiro能幫我幹啥,去看教程也是先從Shiro是一個安全框架講起,然後講架構,看了半天視頻我只收穫了一堆名詞,我想看的東西都沒看到,實在消耗我的耐心。所以本篇文章是以問題為導向,即Shiro最終幫我們做了什麼,同時也調整了一些文章介紹風格,省略掉一些比較大的詞,這篇文章獻給當初在網上找Shiro,找了半天也沒找到合心意的教程的自己。

參考資料

  • Shiro 官方文檔 https://shiro.apache.org/
  • RBAC用户、角色、權限、組設計方案 https://zhuanlan.zhihu.com/p/...
  • Shiro安全框架【快速入門】就這一篇! https://zhuanlan.zhihu.com/p/...
  • 理解Apache Shiro中的權限--官網 https://www.jianshu.com/p/1e6...
user avatar docker_app 頭像 cunyu1943 頭像 yadong_zhang 頭像
3 位用戶收藏了這個故事!

發佈 評論

Some HTML is okay.