Shiro
Shiro
主要分爲 安全認證 和 接口授權 兩個部分,其中的核心組件爲 Subject
、SecurityManager
、Realms
,公共部分 Shiro
都已經爲我們封裝好了,我們只需要按照一定的規則去編寫響應的代碼即可…
- Subject 即表示主體,將用戶的概念理解爲當前操作的主體,因爲它即可以是一個通過瀏覽器請求的用戶,也可能是一個運行的程序,外部應用與 Subject 進行交互,記錄當前操作用戶。Subject 代表了當前用戶的安全操作,SecurityManager 則管理所有用戶的安全操作。
- SecurityManager 即安全管理器,對所有的 Subject 進行安全管理,並通過它來提供安全管理的各種服務(認證、授權等)
- Realm 充當了應用與數據安全間的 橋樑 或 連接器。當對用戶執行認證(登錄)和授權(訪問控制)驗證時,Shiro 會從應用配置的 Realm 中查找用戶及其權限信息。
本章目標
利用 Spring Boot
與 Shiro
實現安全認證和授權….
導入依賴
依賴 spring-boot-starter-web
…
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <java.version>1.8</java.version> <shiro.version>1.4.0</shiro.version> </properties> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <!-- shiro 相關包 --> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-core</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-spring</artifactId> <version>${shiro.version}</version> </dependency> <dependency> <groupId>org.apache.shiro</groupId> <artifactId>shiro-ehcache</artifactId> <version>${shiro.version}</version> </dependency> <!-- End --> </dependencies> |
屬性配置
緩存配置
Shiro 爲我們提供了 CacheManager
即緩存管理,將用戶權限數據存儲在緩存,可以提高它的性能。支持 EhCache
、Redis
等常規緩存,這裏爲了簡單起見就用 EhCache
了 , 在resources
目錄下創建一個 ehcache-shiro.xml
文件
<?xml version="1.0" encoding="UTF-8"?> <ehcache updateCheck="false" name="shiroCache"> <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="false" diskPersistent="false" diskExpiryThreadIntervalSeconds="120" /> </ehcache> |
實體類
創建一個 User.java
,標記爲數據庫用戶
package com.battcn.entity; /** * @author Levin * @since 2018/6/28 0028 */ public class User { /** 自增ID */ private Long id; /** 賬號 */ private String username; /** 密碼 */ private String password; /** 角色名:Shiro 支持多個角色,而且接收參數也是 Set<String> 集合,但這裏爲了簡單起見定義成 String 類型了 */ private String roleName; /** 是否禁用 */ private boolean locked; // 省略 GET SET 構造函數... } |
僞造數據
支持 roles
、permissions
,比如你一個接口可以允許用戶擁有某一個角色,也可以是擁有某一個 permission
…
package com.battcn.config; import com.battcn.entity.User; import java.util.*; /** * 主要不想連接數據庫.. * * @author Levin * @since 2018/6/28 0028 */ public class DBCache { /** * K 用戶名 * V 用戶信息 */ public static final Map<String, User> USERS_CACHE = new HashMap<>(); /** * K 角色ID * V 權限編碼 */ public static final Map<String, Collection<String>> PERMISSIONS_CACHE = new HashMap<>(); static { // TODO 假設這是數據庫記錄 USERS_CACHE.put("u1", new User(1L, "u1", "p1", "admin", true)); USERS_CACHE.put("u2", new User(2L, "u2", "p2", "admin", false)); USERS_CACHE.put("u3", new User(3L, "u3", "p3", "test", true)); PERMISSIONS_CACHE.put("admin", Arrays.asList("user:list", "user:add", "user:edit")); PERMISSIONS_CACHE.put("test", Collections.singletonList("user:list")); } } |
ShiroConfiguration
Shiro 的主要配置信息都在此文件內實現;
package com.battcn.config; import org.apache.shiro.cache.ehcache.EhCacheManager; import org.apache.shiro.spring.LifecycleBeanPostProcessor; import org.apache.shiro.spring.security.interceptor.AuthorizationAttributeSourceAdvisor; import org.apache.shiro.spring.web.ShiroFilterFactoryBean; import org.apache.shiro.web.mgt.DefaultWebSecurityManager; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.aop.framework.autoproxy.DefaultAdvisorAutoProxyCreator; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import java.util.LinkedHashMap; import java.util.Map; /** * Shiro 配置 * * @author Levin */ @Configuration public class ShiroConfiguration { private static final Logger log = LoggerFactory.getLogger(ShiroConfiguration.class); @Bean public EhCacheManager getEhCacheManager() { EhCacheManager em = new EhCacheManager(); em.setCacheManagerConfigFile("classpath:ehcache-shiro.xml"); return em; } @Bean(name = "lifecycleBeanPostProcessor") public LifecycleBeanPostProcessor getLifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * 加密器:這樣一來數據庫就可以是密文存儲,爲了演示我就不開啓了 * * @return HashedCredentialsMatcher */ // @Bean // public HashedCredentialsMatcher hashedCredentialsMatcher() { // HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); // //散列算法:這裏使用MD5算法; // hashedCredentialsMatcher.setHashAlgorithmName("md5"); // //散列的次數,比如散列兩次,相當於 md5(md5("")); // hashedCredentialsMatcher.setHashIterations(2); // return hashedCredentialsMatcher; // } @Bean public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator autoProxyCreator = new DefaultAdvisorAutoProxyCreator(); autoProxyCreator.setProxyTargetClass(true); return autoProxyCreator; } @Bean(name = "authRealm") public AuthRealm authRealm(EhCacheManager cacheManager) { AuthRealm authRealm = new AuthRealm(); authRealm.setCacheManager(cacheManager); return authRealm; } @Bean(name = "securityManager") public DefaultWebSecurityManager getDefaultWebSecurityManager(AuthRealm authRealm) { DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager(); defaultWebSecurityManager.setRealm(authRealm); // <!-- 用戶授權/認證信息Cache, 採用EhCache 緩存 --> defaultWebSecurityManager.setCacheManager(getEhCacheManager()); return defaultWebSecurityManager; } @Bean public AuthorizationAttributeSourceAdvisor getAuthorizationAttributeSourceAdvisor( DefaultWebSecurityManager securityManager) { AuthorizationAttributeSourceAdvisor advisor = new AuthorizationAttributeSourceAdvisor(); advisor.setSecurityManager(securityManager); return advisor; } /** * ShiroFilter<br/> * 注意這裏參數中的 StudentService 和 IScoreDao 只是一個例子,因爲我們在這裏可以用這樣的方式獲取到相關訪問數據庫的對象, * 然後讀取數據庫相關配置,配置到 shiroFilterFactoryBean 的訪問規則中。實際項目中,請使用自己的Service來處理業務邏輯。 * * @param securityManager 安全管理器 * @return ShiroFilterFactoryBean */ @Bean(name = "shiroFilter") public ShiroFilterFactoryBean getShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) { ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); // 必須設置 SecurityManager shiroFilterFactoryBean.setSecurityManager(securityManager); // 如果不設置默認會自動尋找Web工程根目錄下的"/login"頁面 shiroFilterFactoryBean.setLoginUrl("/login"); // 登錄成功後要跳轉的連接 shiroFilterFactoryBean.setSuccessUrl("/index"); shiroFilterFactoryBean.setUnauthorizedUrl("/denied"); loadShiroFilterChain(shiroFilterFactoryBean); return shiroFilterFactoryBean; } /** * 加載shiroFilter權限控制規則(從數據庫讀取然後配置) */ private void loadShiroFilterChain(ShiroFilterFactoryBean shiroFilterFactoryBean) { /////////////////////// 下面這些規則配置最好配置到配置文件中 /////////////////////// // TODO 重中之重啊,過濾順序一定要根據自己需要排序 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); // 需要驗證的寫 authc 不需要的寫 anon filterChainDefinitionMap.put("/resource/**", "anon"); filterChainDefinitionMap.put("/install", "anon"); filterChainDefinitionMap.put("/hello", "anon"); // anon:它對應的過濾器裏面是空的,什麼都沒做 log.info("##################從數據庫讀取權限規則,加載到shiroFilter中##################"); // 不用註解也可以通過 API 方式加載權限規則 Map<String, String> permissions = new LinkedHashMap<>(); permissions.put("/users/find", "perms[user:find]"); filterChainDefinitionMap.putAll(permissions); filterChainDefinitionMap.put("/**", "authc"); shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); } } |
AuthRealm
上面介紹過 Realm
,安全認證和權限驗證的核心處理就是重寫 AuthorizingRealm
中的 doGetAuthenticationInfo(登錄認證)
與 doGetAuthorizationInfo(權限驗證)
package com.battcn.config; import com.battcn.entity.User; import org.apache.shiro.SecurityUtils; 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.session.Session; import org.apache.shiro.subject.PrincipalCollection; import org.apache.shiro.util.ByteSource; import org.springframework.context.annotation.Configuration; import java.util.*; /** * 認證領域 * * @author Levin * @version 2.5.1 * @since 2018-01-10 */ @Configuration public class AuthRealm extends AuthorizingRealm { /** * 認證回調函數,登錄時調用 * 首先根據傳入的用戶名獲取User信息;然後如果user爲空,那麼拋出沒找到帳號異常UnknownAccountException; * 如果user找到但鎖定了拋出鎖定異常LockedAccountException;最後生成AuthenticationInfo信息, * 交給間接父類AuthenticatingRealm使用CredentialsMatcher進行判斷密碼是否匹配, * 如果不匹配將拋出密碼錯誤異常IncorrectCredentialsException; * 另外如果密碼重試此處太多將拋出超出重試次數異常ExcessiveAttemptsException; * 在組裝SimpleAuthenticationInfo信息時, 需要傳入:身份信息(用戶名)、憑據(密文密碼)、鹽(username+salt), * CredentialsMatcher使用鹽加密傳入的明文密碼和此處的密文密碼進行匹配。 */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { String principal = (String) token.getPrincipal(); User user = Optional.ofNullable(DBCache.USERS_CACHE.get(principal)).orElseThrow(UnknownAccountException::new); if (!user.isLocked()) { throw new LockedAccountException(); } // 從數據庫查詢出來的賬號名和密碼,與用戶輸入的賬號和密碼對比 // 當用戶執行登錄時,在方法處理上要實現 user.login(token) // 然後會自動進入這個類進行認證 // 交給 AuthenticatingRealm 使用 CredentialsMatcher 進行密碼匹配,如果覺得人家的不好可以自定義實現 // TODO 如果使用 HashedCredentialsMatcher 這裏認證方式就要改一下 SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, "密碼", ByteSource.Util.bytes("密碼鹽"), getName()); SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(principal, user.getPassword(), getName()); Session session = SecurityUtils.getSubject().getSession(); session.setAttribute("USER_SESSION", user); return authenticationInfo; } /** * 只有需要驗證權限時纔會調用, 授權查詢回調函數, 進行鑑權但緩存中無用戶的授權信息時調用.在配有緩存的情況下,只加載一次. * 如果需要動態權限,但是又不想每次去數據庫校驗,可以存在ehcache中.自行完善 */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principal) { Session session = SecurityUtils.getSubject().getSession(); User user = (User) session.getAttribute("USER_SESSION"); // 權限信息對象info,用來存放查出的用戶的所有的角色(role)及權限(permission) SimpleAuthorizationInfo info = new SimpleAuthorizationInfo(); // 用戶的角色集合 Set<String> roles = new HashSet<>(); roles.add(user.getRoleName()); info.setRoles(roles); // 用戶的角色對應的所有權限,如果只使用角色定義訪問權限,下面可以不要 // 只有角色並沒有顆粒度到每一個按鈕 或 是操作選項 PERMISSIONS 是可選項 final Map<String, Collection<String>> permissionsCache = DBCache.PERMISSIONS_CACHE; final Collection<String> permissions = permissionsCache.get(user.getRoleName()); info.addStringPermissions(permissions); return info; } } |
控制器
常用註解
- @RequiresGuest 代表無需認證即可訪問,同理的就是
/path = anon
- @RequiresAuthentication 需要認證,只要登錄成功後就允許你操作
- @RequiresPermissions 需要特定的權限,沒有則拋出
AuthorizationException
- @RequiresRoles 需要特定的橘色,沒有則拋出
AuthorizationException
- @RequiresUser 不太清楚,不常用…
LoginController
package com.battcn.controller; import com.battcn.config.ShiroConfiguration; import org.apache.shiro.SecurityUtils; import org.apache.shiro.authc.*; import org.apache.shiro.subject.Subject; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import org.springframework.web.servlet.mvc.support.RedirectAttributes; /** * @author Levin * @since 2018/6/28 0028 */ @RestController public class LoginController { private static final Logger log = LoggerFactory.getLogger(ShiroConfiguration.class); @GetMapping(value = "/hello") public String hello() { log.info("不登錄也可以訪問..."); return "hello..."; } @GetMapping(value = "/index") public String index() { log.info("登陸成功了..."); return "index"; } @GetMapping(value = "/denied") public String denied() { log.info("小夥子權限不足,別無謂掙扎了..."); return "denied..."; } @GetMapping(value = "/login") public String login(String username, String password, RedirectAttributes model) { // 想要得到 SecurityUtils.getSubject() 的對象..訪問地址必須跟 shiro 的攔截地址內.不然後會報空指針 Subject sub = SecurityUtils.getSubject(); // 用戶輸入的賬號和密碼,,存到UsernamePasswordToken對象中..然後由shiro內部認證對比, // 認證執行者交由 com.battcn.config.AuthRealm 中 doGetAuthenticationInfo 處理 // 當以上認證成功後會向下執行,認證失敗會拋出異常 UsernamePasswordToken token = new UsernamePasswordToken(username, password); try { sub.login(token); } catch (UnknownAccountException e) { log.error("對用戶[{}]進行登錄驗證,驗證未通過,用戶不存在", username); token.clear(); return "UnknownAccountException"; } catch (LockedAccountException lae) { log.error("對用戶[{}]進行登錄驗證,驗證未通過,賬戶已鎖定", username); token.clear(); return "LockedAccountException"; } catch (ExcessiveAttemptsException e) { log.error("對用戶[{}]進行登錄驗證,驗證未通過,錯誤次數過多", username); token.clear(); return "ExcessiveAttemptsException"; } catch (AuthenticationException e) { log.error("對用戶[{}]進行登錄驗證,驗證未通過,堆棧軌跡如下", username, e); token.clear(); return "AuthenticationException"; } return "success"; } } |
UserController
package com.battcn.controller; import org.apache.shiro.authz.annotation.Logical; import org.apache.shiro.authz.annotation.RequiresRoles; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; /** * @author Levin * @since 2018/6/28 0028 */ @RestController @RequestMapping("/users") public class UserController { @GetMapping public String get() { return "get....."; } /** * RequiresRoles 是所需角色 包含 AND 和 OR 兩種 * RequiresPermissions 是所需權限 包含 AND 和 OR 兩種 * * @return msg */ @RequiresRoles(value = {"admin", "test"}, logical = Logical.OR) //@RequiresPermissions(value = {"user:list", "user:query"}, logical = Logical.OR) @GetMapping("/query") public String query() { return "query....."; } @GetMapping("/find") public String find() { return "find....."; } } |
主函數
package com.battcn; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; /** * @author Levin */ @SpringBootApplication public class Chapter25Application { public static void main(String[] args) { SpringApplication.run(Chapter25Application.class, args); } } |
測試
啓動 Chapter25Application.java
中的 main
方法,爲了更好的演示效果這裏打開了 postman
做的測試,只演示其中一個流程,剩下的可以自己複製代碼測試…
先登錄,由於 u3
在 DBCache
中擁有的角色是 test
,只有 user:list
這一個權限
訪問 /users/query
成功,因爲我們符合響應的角色/權限
訪問 /users/find
失敗,並重定向到了 /denied
接口,問題來了爲什麼 /users/find
沒有寫註解也權限不足呢?
細心的朋友肯定會發現 在 ShiroConfiguration 中寫了一句 permissions.put(“/users/find”, “perms[user:find]”); 意味着我們不僅可以通過註解方式,同樣可以通過初始化時加載數據庫中的權限樹做控制,看各位喜好了….