適合才最美:Shiro安全框架使用心得

威哥爱编程發表於2024-11-06

大家好,我是 V 哥。Apache Shiro 是一個強大且靈活的 Java 安全框架,專注於提供認證、授權、會話管理和加密功能。它常用於保護 Java 應用的訪問控制,特別是在 Web 應用中。相比於 Spring Security,Shiro 的設計更簡潔,適合輕量級應用,並且在許多方面具有更好的易用性和擴充套件性,今天 V 哥就來聊聊 Shiro 安全框架。

Shiro 的核心概念

按照慣例,和 V 哥一起來了解一下 Shiro 的核心概念:

  1. Subject
    Subject 是 Shiro 框架中一個核心的介面,表示應用中的“使用者”或“實體”,用於互動和儲存認證狀態。通常透過 SecurityUtils.getSubject() 獲取當前的 Subject。它代表了使用者的身份資訊和許可權資料。

  2. SecurityManager
    SecurityManager 是 Shiro 的核心控制器,負責管理所有的安全操作和認證。透過配置 SecurityManager,可以控制使用者的認證、授權、會話等管理。

  3. Realm
    Realm 是 Shiro 從資料來源獲取使用者、角色和許可權資訊的途徑。透過實現自定義的 Realm,可以將 Shiro 與資料庫、LDAP、檔案等資料來源整合。Shiro 會把使用者的認證和授權資料從 Realm 中獲取。

  4. Session
    Shiro 自帶會話管理,不依賴於 Servlet 容器提供的會話。即使在非 Web 環境下,也可以使用 Shiro 的會話管理。Shiro 的會話管理提供了更細緻的控制,比如會話超時、儲存和共享等功能。

  5. Authentication(認證)
    認證是指驗證使用者身份的過程。Shiro 提供了簡單的 API 來實現認證過程,比如 subject.login(token)。在實際應用中,通常透過使用者名稱和密碼的組合進行認證,但 Shiro 也支援其他方式(如 OAuth2、JWT 等)。

  6. Authorization(授權)
    授權是指驗證使用者是否具備某些許可權或角色的過程。Shiro 支援基於角色和基於許可權的授權,允許更精細的許可權控制。透過 subject.hasRolesubject.isPermitted 方法,開發者可以檢查使用者的角色和許可權。

  7. Cryptography(加密)
    Shiro 內建了加密功能,提供對密碼和敏感資訊的加密和解密支援。它支援多種加密演算法,並且在密碼儲存時支援雜湊和鹽值。

Shiro 的主要功能和優勢

V 哥總結幾點Shiro 的主要功能和優勢,這個在面試時吹牛逼用得到。

  1. 易於整合
    Shiro 的 API 設計簡單,易於整合到各種 Java 應用中。開發者可以基於 Shiro 提供的預設實現快速搭建一個基本的安全架構,也可以根據需要自定義各種功能。

  2. 獨立的會話管理
    與基於 Web 容器的會話管理不同,Shiro 提供了跨環境的會話管理,可以應用於 Web 和非 Web 的環境,增加了應用的靈活性。

  3. 許可權控制簡單而靈活
    Shiro 的許可權管理可以透過配置檔案、註解或程式碼實現,提供了細粒度的訪問控制。透過許可權和角色的組合,開發者可以非常靈活地控制訪問許可權。

  4. 支援多種資料來源
    Shiro 可以從多種資料來源(如資料庫、LDAP、檔案等)獲取使用者和許可權資訊,方便與各種現有系統整合。

  5. 支援 Web 和非 Web 環境
    Shiro 不僅可以在 Web 應用中使用,也支援在桌面應用或微服務等環境中使用。

Shiro 的基本使用示例

光講概念不是 V 哥風格,接下來,透過一個典型的 Shiro 應用來了解一下如何使用,包含配置 SecurityManager、配置 Realm、進行認證和授權等步驟。

  1. 配置 Shiro 環境
    可以透過 shiro.ini 檔案配置 Shiro,也可以透過程式碼進行配置。
   [main]
   # 配置 SecurityManager
   securityManager = org.apache.shiro.mgt.DefaultSecurityManager

   # 配置 Realm
   myRealm = com.wg.MyCustomRealm
   securityManager.realms = $myRealm
  1. 建立自定義 Realm

    自定義 Realm 透過繼承 AuthorizingRealm 並實現 doGetAuthenticationInfodoGetAuthorizationInfo 方法來提供使用者和許可權資料。

   public class MyCustomRealm extends AuthorizingRealm {
       @Override
       protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
           // 獲取使用者名稱和密碼等資訊,查詢資料庫進行認證
           return new SimpleAuthenticationInfo(username, password, getName());
       }

       @Override
       protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
           // 獲取使用者角色和許可權資訊
           SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
           info.addRole("admin");
           info.addStringPermission("user:read");
           return info;
       }
   }
  1. 使用 Shiro 進行認證和授權
   Subject currentUser = SecurityUtils.getSubject();
   if (!currentUser.isAuthenticated()) {
       UsernamePasswordToken token = new UsernamePasswordToken("username", "password");
       try {
           currentUser.login(token);
           System.out.println("認證成功");
       } catch (AuthenticationException ae) {
           System.out.println("認證失敗");
       }
   }

   // 檢查許可權
   if (currentUser.hasRole("admin")) {
       //用輸出模擬一下哈
       System.out.println("使用者擁有 admin 角色");
   }
   if (currentUser.isPermitted("user:read")) {
       //用輸出模擬一下哈
       System.out.println("使用者具有 user:read 許可權");
   }

透過這個簡單的案例學習,咱們可以瞭解 Shiro 的基本使用,但這不是全部,聽V哥繼續慢慢道來。

場景案例

這點很重要,強調一下哈,Shiro 適合需要簡潔易用、安全控制要求靈活的 Java 應用,如中小型 Web 應用、桌面應用、分散式微服務等。對於大型企業應用或需要整合多種認證方式(如 OAuth2、JWT 等)的專案,Spring Security 可能會更合適。

要在微服務架構中實現基於 Apache Shiro 的安全認證和授權,比如一個訂單管理系統為例。這個系統包含兩個主要服務:

  1. 使用者服務:負責使用者的註冊、登入、認證等操作。
  2. 訂單服務:允許使用者建立、檢視、刪除訂單,並限制訪問許可權。

咱們來看一下,這個應該怎麼設計呢?

微服務案例設計

在這個場景中,我們需要以下幾項核心功能:

  1. 使用者認證:使用者透過使用者名稱和密碼登入。
  2. 許可權控制:僅管理員能刪除訂單,普通使用者只能檢視和建立訂單。
  3. Token機制:使用 JWT Token(JSON Web Token)來管理使用者的登入狀態,實現無狀態認證,使得在分散式環境下不依賴於單一會話。
  4. 跨服務認證:訂單服務在接收到請求時,檢查並驗證使用者的身份和許可權。

架構和技術選型

  • Spring Boot:用於快速搭建微服務。
  • Shiro:實現認證、授權。
  • JWT:生成和驗證 Token,保持無狀態認證。
  • Spring Data JPA:訪問資料庫儲存使用者和訂單資料。

系統結構

+------------------+      +---------------------+
|   使用者服務        |      |     訂單服務        |
|                  |      |                     |
| 使用者註冊、登入    |      |   檢視、建立、刪除訂單|
+------------------+      +---------------------+
            |                    |
            |----使用者 Token ------|

步驟:實現微服務中的認證和授權

1. 引入必要的依賴

pom.xml 檔案中,新增 Shiro、JWT 和 Spring Data JPA 等依賴:

<dependency>
    <groupId>org.apache.shiro</groupId>
    <artifactId>shiro-spring</artifactId>
    <version>1.8.0</version>
</dependency>

<dependency>
    <groupId>io.jsonwebtoken</groupId>
    <artifactId>jjwt</artifactId>
    <version>0.9.1</version>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
    <groupId>com.h2database</groupId>
    <artifactId>h2</artifactId>
</dependency>

2. 配置 Shiro 與 JWT 過濾器

使用 Shiro 的自定義 JWT 過濾器實現無狀態認證,透過 Token 驗證使用者。

public class JwtFilter extends BasicHttpAuthenticationFilter {

    @Override
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
        HttpServletRequest httpServletRequest = (HttpServletRequest) request;
        String token = httpServletRequest.getHeader("Authorization");

        if (StringUtils.isBlank(token)) {
            return false;
        }

        try {
            // 解析 JWT token
            JwtToken jwtToken = new JwtToken(token);
            getSubject(request, response).login(jwtToken);
            return true;
        } catch (Exception e) {
            return false;
        }
    }
}

3. 實現自定義 Realm

自定義 Realm,從資料庫獲取使用者和角色資訊,並使用 JWT Token 進行無狀態認證。

public class JwtRealm extends AuthorizingRealm {

    @Override
    public boolean supports(AuthenticationToken token) {
        return token instanceof JwtToken;
    }

    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
        String jwtToken = (String) token.getPrincipal();

        // 驗證 Token
        String username = JwtUtil.getUsernameFromToken(jwtToken);
        if (username == null) {
            throw new AuthenticationException("Token 無效");
        }

        // 查詢使用者
        User user = userService.findByUsername(username);
        if (user == null) {
            throw new AuthenticationException("使用者不存在");
        }

        return new SimpleAuthenticationInfo(jwtToken, jwtToken, getName());
    }

    @Override
    protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
        String username = JwtUtil.getUsernameFromToken(principals.toString());

        SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
        User user = userService.findByUsername(username);

        // 新增角色和許可權
        authorizationInfo.addRole(user.getRole());
        authorizationInfo.addStringPermission(user.getPermission());

        return authorizationInfo;
    }
}

4. 建立 JWT 工具類

編寫一個工具類,用於生成和解析 JWT Token。

public class JwtUtil {

    //這裡替換一下你自己的secret_key
    private static final String SECRET_KEY = "這裡打碼了"; 

    public static String generateToken(String username) {
        return Jwts.builder()
                .setSubject(username)
                .setIssuedAt(new Date())
                .setExpiration(new Date(System.currentTimeMillis() + 3600000)) // 1 hour
                .signWith(SignatureAlgorithm.HS256, SECRET_KEY)
                .compact();
    }

    public static String getUsernameFromToken(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
        return claims.getSubject();
    }

    public static boolean isTokenExpired(String token) {
        Claims claims = Jwts.parser()
                .setSigningKey(SECRET_KEY)
                .parseClaimsJws(token)
                .getBody();
        return claims.getExpiration().before(new Date());
    }
}

5. 編寫使用者服務和訂單服務介面

使用者服務介面

使用者服務提供註冊和登入 API。

@RestController
@RequestMapping("/user")
public class UserController {

    @PostMapping("/register")
    public ResponseEntity<?> register(@RequestBody User user) {
        userService.save(user);
        return ResponseEntity.ok("使用者註冊成功");
    }

    @PostMapping("/login")
    public ResponseEntity<?> login(@RequestBody User user) {
        User dbUser = userService.findByUsername(user.getUsername());
        if (dbUser != null && dbUser.getPassword().equals(user.getPassword())) {
            String token = JwtUtil.generateToken(user.getUsername());
            return ResponseEntity.ok(token);
        }
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("登入失敗");
    }
}
訂單服務介面

訂單服務在操作訂單時會驗證使用者的角色和許可權。

@RestController
@RequestMapping("/order")
public class OrderController {

    @GetMapping("/{orderId}")
    public ResponseEntity<?> getOrder(@PathVariable Long orderId) {
        Subject currentUser = SecurityUtils.getSubject();
        if (currentUser.isPermitted("order:read")) {
            // 查詢訂單
            return ResponseEntity.ok("訂單詳情");
        }
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body("無許可權檢視訂單");
    }

    @DeleteMapping("/{orderId}")
    public ResponseEntity<?> deleteOrder(@PathVariable Long orderId) {
        Subject currentUser = SecurityUtils.getSubject();
        if (currentUser.hasRole("admin")) {
            // 刪除訂單
            return ResponseEntity.ok("訂單已刪除");
        }
        return ResponseEntity.status(HttpStatus.FORBIDDEN).body("無許可權刪除訂單");
    }
}

最後

這個案例中咱們透過如何使用 Shiro、JWT 和 Spring Boot 來構建一個無狀態的微服務認證授權機制。透過 Shiro 實現使用者認證和許可權控制,使用 JWT 實現無狀態 Token 驗證。在輕量級的分散式微服務應用中,是不是使用 Shiro 感覺更加清爽呢,歡迎評論區一起討論,關注威哥愛程式設計,愛上Java,一輩子。

相關文章