SpringBoot集成Apache Shiro
筆者因為項目轉型的原因,對Apache Shiro安全框架做了一點研究工作,故想寫點東西以便將來查閱。之所以選擇Shiro也是看了很多人的推薦,號稱功能豐富強大,而且易於使用。實踐下來的確如大多數人所說簡約優美,小巧精悍。
介紹demo項目前,簡單說明一下Shiro框架的特性。
1. Apache Shiro Features
從上圖可以看出Shiro具備應用程序安全框架的四大基石”:身份驗證、授權、會話管理和密碼。
Authentication:有時被稱為‘登錄’,這是需要明確用戶是誰
Authorization:訪問控制,即確定‘誰’對‘什麽’有訪問權限。
Session Management
Cryptography:使用加密算法保持數據安全,但易於使用。
在不同的應用程序環境中,還有更多的特性來支持和增強這些關註點,特別是:
Web Support:Shiro的Web支持API幫助輕松地保護web應用程序。
Caching:緩存是ApacheShiro的API中的第一等公民,以確保安全操作同時保持快速和高效。
Concurrency:ApacheShiro支持具有並發特性的多線程應用程序。
Testing:提供測試支持,以幫助編寫單元和集成測試,並確保代碼如預期的安全。
Run as:允許用戶假定另一個用戶的身份(如果允許的話)的特性,有時在管理場景中很有用。
Remember Me:記住用戶在會話中的身份,這樣他們就只需要在強制的情況下輸入口令登錄。
2. High-Level Overview
Shiro的體系結構有三個主要概念:Subject、SecurityManager和Realms。下圖展現了它的運行原理,
主題:主題本質上是當前正在執行的用戶。雖然“用戶”這個詞通常意味著一個人,一個主題可以是一個人,但它也可以代表一個第三方服務、守護進程帳戶、cron作業或任何類似的東西-基本上是任何當前與軟件交互的東西。Subject實例都綁定到(並且需要)一個SecurityManager。當與主題交互時,這些交互轉化為與SecurityManager的特定主題交互。
SecurityManager:SecurityManager是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