JWT的權限控制與Shiro入門


一、前端權限控制

1.1介紹

在vue工程中,需要根據登錄用戶所擁有的權限信息動態的加載菜單列表(路由列表)

  1. 登錄成功后獲取用戶信息,包含權限列表(菜單權限,按鈕權限等)
  2. 根據用戶的權限,去動態的渲染頁面(根據路由名稱和權限標識比較)
  3. 頁面按鈕權限通過自定義方法控制可見性

1.2具體

  1. 此時需要准備所有使用該系統的用戶的等級:
  • saas管理員----擁有所有權限
  • 企業管理員----創建租戶企業的權限
  • 普通用戶-----被分配的權限
  1. 此時前端訪問登錄界面后,就開始渲染頁面,使用了路由
  2. 路由首先把公共的組件路由出來,然后調用前置構字函數(beforeEach),獲取用戶的信息存儲起來。
  3. 最后判斷當前的模塊路由是否具有訪問權限,來決定是否渲染此路由。

二、有狀態服務和無狀態服務

對服務器程序來說,究竟是有狀態服務,還是無狀態服務,其判斷依據——兩個來自相同發起者的請求在服務器端是否具備上下文關系。

2.1無狀態服務

無狀態服務對於客戶端的單次請求,不會依賴其他請求的數據。即:處理請求的所需信息全部包含在該請求里。

如:cookie保存token的方式傳輸數據,對於服務端來講,每次只是驗證token是否合法,是否是我這個服務端頒發出的內容,從而辨別是否有權限,而不會保存用戶的信息。

2.2有狀態服務

有些數據會被記錄在服務端,先后的請求是有關聯的

當客戶端登錄后,服務端會頒發一個sessionId,此時在服務端就存儲了該sessionId對應的session信息。客戶端一般把返回的sessionId存儲在cookie中。從而將http的無狀態服務變相轉換為有狀態服務。

三、基於JWT的API鑒權

3.1JWT(JSON Web Token)

3.1.1 介紹

JSON Web Token (JWT)是一個開放標准(RFC 7519),它定義了一種緊湊的、自包含的方式,用於作為JSON對象在各方之間安全地傳輸信息。該信息可以被驗證和信任,因為它是數字簽名的。

JWT是由三段信息構成的,將這三段信息文本用.鏈接一起就構成了Jwt字符串。就像這樣:

eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJzdWIiOiIxMjM0NTY3ODkwIiwibmFtZSI6IkpvaG4gRG9lIiwiYWRtaW4iOnRydWV9.TJVA95OrM7E2cBab30RMHrHDcEfxjoYZgeFONFh7HgQ

3.1.2JWT的構成

第一部分我們稱它為頭部(header),第二部分我們稱其為載荷(payload, 類似於飛機上承載的物品),第三部分是簽證(signature).

1.header

jwt的頭部承載兩部分信息:

  • 聲明類型,這里是jwt
  • 聲明加密的算法 通常直接使用 HMAC SHA256

完整的頭部就像下面這樣的JSON:

{
  'typ': 'JWT',
  'alg': 'HS256'
}

然后將頭部進行base64加密(該加密是可以對稱解密的),構成了第一部分.

2.playload

載荷就是存放有效信息的地方。這個名字像是特指飛機上承載的貨品,這些有效信息包含三個部分

  • 標准中注冊的聲明
  • 公共的聲明
  • 私有的聲明

定義一個payload:

{
  "sub": "1234567890",
  "name": "John Doe",
  "admin": true
}

然后將其進行base64加密,得到Jwt的第二部分。

3.signature

jwt的第三部分是一個簽證信息,這個簽證信息由三部分組成:

  • header (base64后的)
  • payload (base64后的)
  • secret

這個部分需要base64加密后的header和base64加密后的payload使用.連接組成的字符串,然后通過header中聲明的加密方式進行加鹽secret組合加密,然后就構成了jwt的第三部分。

注意

secret是保存在服務器端的,jwt的簽發生成也是在服務器端的,secret就是用來進行jwt的簽發和jwt的驗證,所以,它就是你服務端的私鑰,在任何場景都不應該流露出去。一旦客戶端得知這個secret, 那就意味着客戶端是可以自我簽發jwt了。

3.1.3JWT與Session的差異

  1. Session是在服務器端的,而JWT是在客戶端的。
  2. Session方式存儲用戶信息的最大問題在於要占用大量服務器內存,增加服務器的開銷。而JWT方式將用戶狀態分散到了客戶端中,可以明顯減輕服務端的內存壓力。
  3. Session的狀態是存儲在服務器端,客戶端只有session id;而Token的狀態是存儲在客戶端。

3.1.4 工作流程

-每一次請求都需要token -Token應該放在請求header中 -我們還需要將服務器設置為接受來自所有域的請求,用Access-Control-Allow-Origin: *

3.1.5 用Token的好處

  • 無狀態和可擴展性:Tokens存儲在客戶端。完全無狀態,可擴展。我們的負載均衡器可以將用戶傳遞到任意服務器,因為在任何地方都沒有狀態或會話信息。
  • 安全:Token不是Cookie。(The token, not a cookie.)每次請求的時候Token都會被發送。而且,由於沒有Cookie被發送,還有助於防止CSRF攻擊。
    CSRF(Cross-site request forgery),中文名稱:跨站請求偽造。CSRF攻擊:攻擊者盜用了你的身份,以你的名義發送惡意請求。
  • token在一段時間以后會過期,這個時候用戶需要重新登錄。這有助於我們保持安全。

在spring攔截器中,進行攔截

四、Shiro安全框架

4.1介紹

shiro 是一個功能強大和易於使用的Java安全框架,為開發人員提供一個直觀而全面的解決方案的認證,授權,加密,會話管理。

4.2 作用

  • Authentication:驗證用戶核實身份
  • Authorization:對用戶進行訪問控制:如判斷用戶是否被允許做某件事
  • SessionManager:會話管理,即用戶登錄后就是一次會話,在沒有退出之前,它的所有信息都在會話中;
  • Cryptography:加密,保護數據的安全性,如密碼加密存儲到數據庫,而不是明文存儲

4.3 特點

Web Support:Web支持,可以非常容易的集成到Web環境;

Caching:緩存,比如用戶登錄后,其用戶信息、擁有的角色/權限不必每次去查,這樣可以提高效率;

Concurrency:shiro支持多線程應用的並發驗證,即如在一個線程中開啟另一個線程,能把權限自動傳播過去;

Testing:提供測試支持;

Run As:允許一個用戶假裝為另一個用戶(如果他們允許)的身份進行訪問;

Remember Me:記住我,這個是非常常見的功能,即一次登錄后,下次再來的話不用登錄了。

SSO:單點登錄功能

4.4 快速入門

1.導入依賴

<dependencies>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
</dependencies>

2.創建配置文件

[users]
#模擬從數據庫查詢用戶
#數據格式: 用戶名=密碼
zhangsan=123456
lisi=1234

3.測試login

 /**
     * 測試用戶認證
     *  認證: 用戶登錄
     *  1.通過配置文件創建SecurityManagerFactory
     *  2.通過工廠獲取securityManager
     *  3.將securityManager綁定到當前運行環境
     *  4.從當前運行環境中構造subject
     *  5.構造shiro登錄的數據
     *  6.登錄
     */
@Test
public void testLogin(){
    //1.獲取工廠
    Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-test-1.ini");
    //2.獲取安全管理
    SecurityManager securityManager = factory.getInstance();
    //3.綁定
    SecurityUtils.setSecurityManager(securityManager);
    //4.構造subject
    Subject subject = SecurityUtils.getSubject();
    //5.構造數據
    String username = "zhangsan";
    String password = "123456";
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    //6,登錄
    subject.login(token);
    //判斷是否登錄成功
    System.out.println(subject.isAuthenticated());
    System.out.println(subject.getPrincipal());
}

4.測試用戶權限

[users]
#數據格式: 用戶名=密碼,角色名列表
zhangsan=123456,role1,role2
lisi=1234,role2
[roles]
#角色
#角色名=權限列表
role1=user:save,user:update
role2=user:find
/**
 * 測試用戶權限
 */
@Test
public void testAble(){
    //1.獲取工廠
    Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-test-2.ini");
    //2.獲取安全管理
    SecurityManager securityManager = factory.getInstance();
    //3.綁定
    SecurityUtils.setSecurityManager(securityManager);
    //4.構造subject
    Subject subject = SecurityUtils.getSubject();
    //5.構造數據
    String username = "lisi";
    String password = "1234";
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    //6,登錄
    subject.login(token);
    //登錄成功
    if (subject.isAuthenticated()){
        //查看是否有role1角色
        System.out.println(subject.hasRole("role2"));
        //是否具有權限
        System.out.println(subject.isPermitted("user:find"));
    }
}
//結果:true
//    false

4.5使用

1.導入maven坐標

<dependencies>
    <dependency>
        <groupId>org.apache.shiro</groupId>
        <artifactId>shiro-core</artifactId>
        <version>1.3.2</version>
    </dependency>
    <dependency>
        <groupId>junit</groupId>
        <artifactId>junit</artifactId>
        <version>4.12</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>org.slf4j</groupId>
        <artifactId>slf4j-simple</artifactId>
        <version>1.7.25</version>
        <scope>test</scope>
    </dependency>
    <dependency>
        <groupId>commons-logging</groupId>
        <artifactId>commons-logging</artifactId>
        <version>1.2</version>
    </dependency>
</dependencies>

2.創建ini配置文件

其中指定編寫的realm域的位置,並且將realm綁定到SecurityManager

[main]
#自定寫的realm域,全包名
permReam=gyb.shiro.PermissionRealm
#注冊realm到SecurityManager
securityManager.realms=$permReam

3.編寫PermissionRealm類,繼承AuthorizingRealm,重寫方法

package gyb.shiro;

import org.apache.shiro.authc.*;
import org.apache.shiro.authz.AuthorizationInfo;
import org.apache.shiro.authz.SimpleAuthorizationInfo;
import org.apache.shiro.realm.AuthorizingRealm;
import org.apache.shiro.subject.PrincipalCollection;

import java.util.ArrayList;
import java.util.List;

/**
 * @Author: 郜宇博
 * @Date: 2021/9/19 16:30
 */
public class PermissionRealm extends AuthorizingRealm {
    /**
     * 一般重寫setName方法
     */
    public void setName(){
        super.setName("permissionRealm");
    }
    /**
     * 重寫抽象doGetAuthorizationInfo:授權(獲取到用戶的授權數據)
     * 目的:根據認證數據獲取用戶的權限信息
     * principals:包含已認證安全數據
     * AuthorizationInfo 授權數據
     */
    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        System.out.println("執行授權方法");
        //1.獲取安全數據
        //2.通過安全數據內的username查詢數據庫,獲取到權限和角色
        //模擬查數據庫的內容
        List<String> perms = new ArrayList<String>();
        perms.add("user:save");
        perms.add("user:update");
        List<String> roles = new ArrayList<String>();
        roles.add("role1");
        roles.add("role2");
        //3.構造返回
        SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
        //4.設置權限、角色集合
        info.addStringPermissions(perms);
        info.addRoles(roles);
        return info;
    }

    /**
     * doGetAuthenticationInfo :認證(根據用戶名密碼登錄,將用戶數據(安全數據)保存)
     * 目的:比較用戶名和密碼是否和數據庫中的一致,並將安全數據存入到shiro中進行保管
     * AuthenticationToken:登錄時構造的token
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        System.out.println("執行認證方法");
        //1.構造upToken
        UsernamePasswordToken upToken = (UsernamePasswordToken) token;
        //2.獲取輸入的用戶名密碼
        String username = upToken.getUsername();
        String password = new String(upToken.getPassword());
        //3.與數據庫比較
        if("123456".equals(password)){
            //一致,向shiro存入安全數據
            //三個參數:1.安全數據,2.密碼,3,當前realm域名稱
            SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(username,password,this.getName());
            return info;
        }else {
            throw new RuntimeException("用戶名或密碼錯誤");
        }
    }
}

4.使用

/**
 * 通過認證授權方法進行判斷
 */
@Test
public void testShiro(){
    //1.獲取工廠
    Factory<SecurityManager> factory = new IniSecurityManagerFactory("classpath:shiro-test-3.ini");
    //2.獲取安全管理
    SecurityManager securityManager = factory.getInstance();
    //3.綁定
    SecurityUtils.setSecurityManager(securityManager);
    //4.構造subject
    Subject subject = SecurityUtils.getSubject();
    //5.構造數據
    String username = "zhangsan";
    String password = "123456";
    UsernamePasswordToken token = new UsernamePasswordToken(username, password);
    //6,執行認證方法(登錄),此時自動找到配置的Shiro類(在ini文件中),然后執行認證方法
    subject.login(token);
    //7.執行授權方法
    System.out.println(subject.hasRole("role1"));
    System.out.println(subject.isPermitted("user:save"));
}

4.6工作流程

1.認證

  1. subject調用login方法,傳遞token,會自動委任給SecurityManager
  2. SecurityManager中使用認證器
  3. 認證器找到了所有的realm域,此時就找到了自己寫好的。
  4. 然后就可以執行自己實現的realm域

2.授權

首先調用isPermitted或hasRole方法,然后會委任給SecurityManager。

SecurityManager中使用授權器


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM