1. 程式人生 > >SpringBoot集成Apache Shiro

SpringBoot集成Apache Shiro

基本 table this trac fig imp div 研究 環境

筆者因為項目轉型的原因,對Apache Shiro安全框架做了一點研究工作,故想寫點東西以便將來查閱。之所以選擇Shiro也是看了很多人的推薦,號稱功能豐富強大,而且易於使用。實踐下來的確如大多數人所說簡約優美,小巧精悍。

介紹demo項目前,簡單說明一下Shiro框架的特性。

1. Apache Shiro Features

技術分享圖片

從上圖可以看出Shiro具備應用程序安全框架的四大基石”:身份驗證、授權、會話管理和密碼。

Authentication有時被稱為‘登錄’,這是需要明確用戶是誰

Authorization訪問控制,即確定‘誰’對‘什麽’有訪問權限。

Session Management

管理特定用戶的會話,即使在非web或EJB應用程序中也是如此。

Cryptography使用加密算法保持數據安全,但易於使用。

在不同的應用程序環境中,還有更多的特性來支持和增強這些關註點,特別是:

Web SupportShiro的Web支持API幫助輕松地保護web應用程序。

Caching緩存是ApacheShiro的API中的第一等公民,以確保安全操作同時保持快速和高效。

ConcurrencyApacheShiro支持具有並發特性的多線程應用程序。

Testing提供測試支持,以幫助編寫單元和集成測試,並確保代碼如預期的安全。

Run as允許用戶假定另一個用戶的身份(如果允許的話)的特性,有時在管理場景中很有用。

Remember Me記住用戶在會話中的身份,這樣他們就只需要在強制的情況下輸入口令登錄。

2. High-Level Overview

Shiro的體系結構有三個主要概念:Subject、SecurityManager和Realms。下圖展現了它的運行原理,

技術分享圖片

主題:主題本質上是當前正在執行的用戶。雖然“用戶”這個詞通常意味著一個人,一個主題可以是一個人,但它也可以代表一個第三方服務、守護進程帳戶、cron作業或任何類似的東西-基本上是任何當前與軟件交互的東西。Subject實例都綁定到(並且需要)一個SecurityManager。當與主題交互時,這些交互轉化為與SecurityManager的特定主題交互。

SecurityManagerSecurityManager是Shiro體系結構的核心,它將其內部安全組件協調在一起形成一個對象圖。然而,一旦為應用程序配置了SecurityManager及其內部對象圖,它通常會被單獨使用,應用程序開發人員將幾乎所有的時間都花在Subject API上。當與一個主題交互時,實際上是幕後的SecurityManager為任何主題安全操作做了所有繁重的工作。

領域:領域充當Shiro和應用程序安全數據之間的“橋梁”或“連接器”。當涉及到實際與用戶帳戶等安全相關的數據交互以執行身份驗證(登錄)和授權(訪問控制)時,Shiro從一個或多個為應用程序配置的領域中查找數據。從這個意義上說,領域本質上是一個特定於安全的DAO:它封裝數據源的連接細節,並根據需要將相關數據提供給Shiro。配置Shiro時,必須指定至少一個用於身份驗證和/或授權的域。SecurityManager可以配置多個Realm,但至少需要一個。Shiro提供了開箱即用的領域,以連接到許多安全數據源(也稱為目錄),如LDAP、關系數據庫(JDBC)、INI和屬性文件等文本配置源。

3. Detailed Architecture

技術分享圖片

4. 過濾器

當 Shiro 被運用到 web 項目時,Shiro 會自動創建一些默認的過濾器對客戶端請求進行過濾。以下是 Shiro 內置過濾器:

過濾器簡稱

對應的 Java 類

anon

org.apache.shiro.web.filter.authc.AnonymousFilter

authc

org.apache.shiro.web.filter.authc.FormAuthenticationFilter

authcBasic

org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter

perms

org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter

port

org.apache.shiro.web.filter.authz.PortFilter

rest

org.apache.shiro.web.filter.authz.HttpMethodPermissionFilter

roles

org.apache.shiro.web.filter.authz.RolesAuthorizationFilter

ssl

org.apache.shiro.web.filter.authz.SslFilter

user

org.apache.shiro.web.filter.authc.UserFilter

logout

org.apache.shiro.web.filter.authc.LogoutFilter


項目中常用的解釋一下,

/test/**=anon ~~所有url 可以匿名訪問

/test/**=authc ~~url需要認證才能訪問

/test/**=perms[user:add] ~~url需要認證用戶擁有 user:add 權限才能訪問

/test/**=roles[admin] ~~url需要認證用戶擁有 admin 角色才能訪問

/test/**=user ~~url 需要認證或通過記住我認證才能訪問

5. DEMO

開發工具為Eclipse+Maven,新建Springboot項目,版本號為1.5.14

模板引擎使用Thymeleaf

為了突出shiro,數據訪問層略過,服務層模擬查詢數據,Realm裏面硬編碼權限和角色信息簡化代碼。

整個系統的核心在於兩個class(MyShiroRealm + ShiroConfiguration), 項目結構如下,

技術分享圖片

5.1 在pom.xml裏面添加好shiro-core, shiro-spring

<dependency>

         <groupId>org.apache.shiro</groupId>

         <artifactId>shiro-core</artifactId>

         <version>1.4.0</version>

</dependency>

<!-- shiro權限控制框架 -->

<dependency>

         <groupId>org.apache.shiro</groupId>

         <artifactId>shiro-spring</artifactId>

         <version>1.4.0</version>

</dependency>

5.2 顯示層

模板引擎Thymeleaf,故application配置文件如下:

1 spring.thymeleaf.cache=true
2 spring.thymeleaf.prefix=classpath:/templates/
3 spring.thymeleaf.suffix=.html
4 spring.thymeleaf.mode=HTML5
5 spring.thymeleaf.encoding=UTF-8
6 spring.thymeleaf.content-type=text/html

5.3 顯示層靜態頁面如下:

403.html

add.html

delete.html

details.html

edit.html

index.html

login.html

logout.html

5.4 幾乎所有靜態頁面就是一個空殼,大體如add.html

技術分享圖片
 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4 <meta charset="UTF-8"></meta>
 5 <title>Add Page</title>
 6 </head>
 7 <body>
 8     <h1>Add Page</h1>
 9 </body>
10 </html>
add.html 技術分享圖片
 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4 <meta charset="UTF-8"></meta>
 5 <title>Login Page</title>
 6 <style type="text/css">
 7 table {
 8     width: 360px;
 9     min-height: 25px;
10     line-height: 25px;
11     text-align: center;
12     border-color: #b6ff00;
13     border-collapse: collapse;
14     
15 }
16 </style>
17 </head>
18 <body>
19     <div>
20         <form action="/home/check" method="post">
21             <table border="1">
22                 <tr>
23                     <th>User Name</th>
24                     <th><input type="text" name="name" /></th>
25                 </tr>
26                 <tr>
27                     <td>Password</td>
28                     <td><input type="password" name="password" /></td>
29                 </tr>
30                 <tr>
31                     <td><input type="submit" value="Submit" /></td>
32                     <td></td>
33                 </tr>
34             </table>
35         </form>
36     </div>
37 </body>
38 </html>
login.html 技術分享圖片
 1 <!DOCTYPE html>
 2 <html>
 3 <head>
 4 <meta charset="UTF-8"></meta>
 5 <title>Index Page</title>
 6 <style type="text/css">
 7 p {
 8     font-family: Times, TimesNR, ‘New Century Schoolbook‘, Georgia,
 9         ‘New York‘, serif;
10     font-size: 20px;
11 }
12 </style>
13 
14 </head>
15 <body>
16     <h1>This is index page.</h1>
17     <p>
18         Customer Name: <span th:text="${name}"></span> --- Role: <span
19             th:text="${role}"></span>
20     </p>
21     <table border="1">
22         <tr>
23             <th>角色</th>
24             <th>權限</th>
25         </tr>
26         <tr>
27             <td>admin</td>
28             <td>增加,刪除,編輯,查看</td>
29         </tr>
30         <tr>
31             <td>operator</td>
32             <td>編輯,查看</td>
33         </tr>
34         <tr>
35             <td>viewer</td>
36             <td>查看</td>
37         </tr>
38     </table>
39     <ul>
40         <li><a th:href="@{/customer/index}">Index</a></li>
41         <li><a th:href="@{/customer/details}">Details</a></li>
42         <li><a th:href="@{/customer/add}">Add</a></li>
43         <li><a th:href="@{/customer/edit}">Edit</a></li>
44         <li><a th:href="@{/customer/delete}">Delete</a></li>
45     </ul>
46 </body>
47 </html>
index.html

5.5 Model

技術分享圖片
public class Customer implements Serializable {
    private static final long serialVersionUID = 7429292944316962328L;
    private String name;
    private String password;
    private String role;
    
    public String getName() {
        return name;
    }
    public void setName(String name) {
        this.name = name;
    }
    public String getPassword() {
        return password;
    }
    public void setPassword(String password) {
        this.password = password;
    }
    
    public String getRole() {
        return role;
    }
    public void setRole(String role) {
        this.role = role;
    }
    
    public Customer(String name, String password, String role) {
        super();
        this.name = name;
        this.password = password;
        this.role = role;
    }
    
    @Override
    public String toString() {
        return "Name : --- " + name + ", Password : --- " + password + ", Role : *** " + role;
    }
    
}
Customer.java

5.6 Service

技術分享圖片
 1 @Service
 2 public class CustomerService {
 3     public Customer findByName(String name) {
 4         // 模擬查詢數據庫
 5         // tom is admin, alice is operator, lucy is viewer
 6         if (name.equals("alice")) {
 7             return new Customer(name, "123", "operator");
 8         }
 9         return null;
10     }
11 }
CustomerService.java

5.7 Controller

技術分享圖片
@Controller
@RequestMapping("customer")
public class CustomerController {    
    @RequestMapping("/index")
    @RequiresPermissions("customer:index")//權限管理; 
    public String index(Model model) {    
        Subject subject = SecurityUtils.getSubject();
        Customer customer = (Customer)subject.getPrincipal();
        if(customer != null) {
            model.addAttribute("name", customer.getName());
            model.addAttribute("role", customer.getRole());
        }
        return "index";
    }
    
    @RequestMapping("/details")
    @RequiresPermissions("customer:details")//權限管理; 
    public String details() {    
        return "details";
    }
    
    @RequestMapping("/add")    
    @RequiresRoles("admin")
    public String add() {    
        return "add";
    }
    
    @RequestMapping("/edit")
    @RequiresPermissions("customer:edit")//權限管理; 
    public String edit() {    
        return "edit";
    }
    
    @RequestMapping("/delete")
    @RequiresPermissions("customer:delete")//權限管理;
    public String delete() {    
        return "delete";
    }    
}
CustomerController.java 技術分享圖片
@Controller
@RequestMapping("home")
public class HomeController {
    @RequestMapping("/login")
    public String login() {
        return "login";
    }

    @RequestMapping("/check")
    public String check(HttpServletRequest request) throws Exception {
        System.out.println("HomeController.check()");

        String name = request.getParameter("name");
        String password = request.getParameter("password");
        UsernamePasswordToken token = new UsernamePasswordToken(name, password);
        Subject subject = SecurityUtils.getSubject();

        try {
            subject.login(token);
        } catch (Exception ex) {
            System.out.println(ex.getMessage());
            System.out.println(ex.getStackTrace());
            return "login";
        }

        return "redirect:/customer/index";
    }
}
HomeController.java

5.8 最重要的兩個類如下:

技術分享圖片
 1 package com.example.demo.config;
 2 
 3 import java.util.LinkedHashMap;
 4 import java.util.Map;
 5 
 6 import org.apache.shiro.mgt.SecurityManager;
 7 import org.apache.shiro.spring.LifecycleBeanPostProcessor;
 8 import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor;
 9 import org.apache.shiro.spring.web.ShiroFilterFactoryBean;
10 import org.apache.shiro.web.mgt.DefaultWebSecurityManager;
11 import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator;
12 import org.springframework.context.annotation.Bean;
13 import org.springframework.context.annotation.Configuration;
14 
15 @Configuration
16 public class ShiroConfiguration {
17 
18     @Bean
19     public ShiroFilterFactoryBean shirFilter(SecurityManager securityManager) {
20         System.out.println("ShiroConfiguration.shirFilter()");
21         ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
22         shiroFilterFactoryBean.setSecurityManager(securityManager);
23         // 過濾器.
24         Map<String, String> filterChainDefinitionMap = new LinkedHashMap<String, String>();
25         // 配置不會被攔截的鏈接 順序判斷
26         filterChainDefinitionMap.put("/static/**", "anon");
27         filterChainDefinitionMap.put("/home/**", "anon");
28         filterChainDefinitionMap.put("/test/**", "anon");
29         filterChainDefinitionMap.put("/customer/**", "authc");
30         shiroFilterFactoryBean.setLoginUrl("/home/login");
31         // 登錄成功後要跳轉的鏈接
32         shiroFilterFactoryBean.setSuccessUrl("/customer/index");
33         
34         shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
35         return shiroFilterFactoryBean;
36     }
37 
38     @Bean
39     public MyShiroRealm myShiroRealm() {
40         MyShiroRealm myShiroRealm = new MyShiroRealm();
41         return myShiroRealm;
42     }
43 
44     @Bean
45     public SecurityManager securityManager() {
46         DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager();
47         securityManager.setRealm(myShiroRealm());
48         return securityManager;
49     }
50 
51     // 開啟Shiro AOP註解支持.
52     @Bean
53     public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor(SecurityManager securityManager) {
54         System.out.println("OPNE AOP......");
55         AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor();
56         authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
57         return authorizationAttributeSourceAdvisor;
58     }
59     
60     @Bean
61     public static DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() {
62         return new DefaultAdvisorAutoProxyCreator();
63     }
64 
65     // 管理shiro生命周期
66     @Bean
67     public static LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() {
68         return new LifecycleBeanPostProcessor();
69     }
70 }
ShiroConfiguration.java

因為在Shiro中,最終是通過Realm來獲取應用程序中的用戶、角色及權限信息的。通常情況下,在Realm中會直接從我們的數據源中獲取Shiro需要的驗證信息。可以說,Realm是專用於安全框架的DAO.

Shiro的認證過程最終會交由Realm執行,這時會調用Realm的getAuthenticationInfo(token)方法。

該方法主要執行以下操作:

1) 根據口令信息檢查標識主體(帳戶標識信息)

2) 在數據源中查找相應的帳戶信息

3) 確保令牌提供的憑據與存儲在數據存儲中的憑據匹配

4) 如果憑證匹配,則返回一個AuthenticationInfo實例,該實例將帳戶數據封裝為Shiro理解的格式

5) 如果憑證不匹配,則引發身份驗證異常

在應用程序中需要自定義一個Realm類,繼承AuthorizingRealm抽象類,覆蓋doGetAuthenticationInfo(),重寫獲取用戶信息的方法。

shiro的權限授權是通過繼承AuthorizingRealm抽象類,覆蓋doGetAuthorizationInfo()。當訪問到頁面的時候,URL配置了相應的權限或者shiro標簽才會執行此方法否則不會執行,所以如果只是簡單的身份認證沒有權限的控制的話,那麽這個方法可以不進行實現,直接返回null即可。

在這個方法中主要是使用類:SimpleAuthorizationInfo進行角色的添加和權限的添加。SecurityManager將權限或角色檢查的任務委托給Authorizer,默認為ModularRealmAuthorizer。

應用程序則可以通過角色或者權限進行訪問控制。

技術分享圖片
 1 package com.example.demo.config;
 2 
 3 import java.util.HashSet;
 4 import java.util.Set;
 5 
 6 import org.apache.shiro.authc.AuthenticationException;
 7 import org.apache.shiro.authc.AuthenticationInfo;
 8 import org.apache.shiro.authc.AuthenticationToken;
 9 import org.apache.shiro.authc.SimpleAuthenticationInfo;
10 import org.apache.shiro.authz.AuthorizationInfo;
11 import org.apache.shiro.authz.SimpleAuthorizationInfo;
12 import org.apache.shiro.realm.AuthorizingRealm;
13 import org.apache.shiro.subject.PrincipalCollection;
14 import org.springframework.beans.factory.annotation.Autowired;
15 
16 import com.example.demo.model.Customer;
17 import com.example.demo.service.CustomerService;
18 
19 public class MyShiroRealm extends AuthorizingRealm {
20 
21     @Autowired
22     private CustomerService customerService;
23     
24     @Override
25     protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {
26         System.out.println("MyShiroRealm.doGetAuthenticationInfo()");
27 
28         // 獲取用戶的輸入的賬號.
29         String name = (String) token.getPrincipal();
30         System.out.println(token.getCredentials());
31         Customer c = customerService.findByName(name);
32         System.out.println("Customer info : " + c);
33         if (c == null) {
34             return null;
35         }
36         SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(c, // 用戶名
37                 c.getPassword(), // 密碼
38                 getName() // realm name
39         );
40 
41         return authenticationInfo;
42     }
43 
44     @Override
45     protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {
46 
47         System.out.println("權限管理-->MyShiroRealm.doGetAuthorizationInfo()");
48         SimpleAuthorizationInfo authorizationInfo = new SimpleAuthorizationInfo();
49         Customer customer = (Customer) principals.getPrimaryPrincipal();
50         System.out.println("Customer is : " + customer);
51         // 權限單個添加;
52         // 添加一個角色,不是配置意義上的添加,而是證明該用戶擁有admin角色
53         // 模擬查詢數據庫,得到用戶角色為admin或者operator或者viewer
54         // admin有所有權限,operator有查看和編輯權限,沒有添加和刪除權限
55         // viewer只有查看權限
56         authorizationInfo.addRole("operator");
57         // 添加權限
58         Set<String> permissionSet = new HashSet<String>();
59         permissionSet.add("customer:details");
60         permissionSet.add("customer:index");
61         permissionSet.add("customer:edit");
62         //permissionSet.add("customer:add");
63         //permissionSet.add("customer:delete");
64         
65         authorizationInfo.setStringPermissions(permissionSet);
66         return authorizationInfo;
67     }
68 }
MyShiroRealm

6. 參考資料

http://shiro.apache.org/introduction.html

SpringBoot集成Apache Shiro