一:什么是ACL和RBAC:
ACL: Access Control List 訪問控制列表
以前盛行的一種權限設計,它的核心在於用戶直接和權限掛鈎
優點:簡單易用,開發便捷
缺點:用戶和權限直接掛鈎,導致在授予時的復雜性,比較分散,不便於管理
例子:常見的文件系統權限設計, 直接給用戶加權限
RBAC: Role Based Access Control
基於角色的訪問控制系統。權限與角色相關聯,用戶通過成為適當角色的成員而得到這些角色的權限
優點:簡化了用戶與權限的管理,通過對用戶進行分類,使得角色與權限關聯起來
缺點:開發對比ACL相對復雜
例子:基於RBAC模型的權限驗證框架與應用 Apache Shiro、spring Security
BAT企業 ACL,一般是對報表系統,阿里的ODPS
總結:不能過於復雜,規則過多,維護性和性能會下降, 更多分類 ABAC、PBAC等
二:Apache Shiro基礎知識和架構
Shiro是一個強大易用的Java安全框架,提供了認證、授權、加密和會話管理等功能
shiro包含四大核心模塊:身份認證,授權,會話管理和加密
直達Apache Shiro官網 http://shiro.apache.org/introduction.html
什么是身份認證
Authentication,身份證認證,一般就是登錄
什么是授權
Authorization,給用戶分配角色或者訪問某些資源的權限
什么是會話管理
Session Management, 用戶的會話管理員,多數情況下是web session
什么是加密
Cryptography, 數據加解密,比如密碼加解密等
shiro架構圖
三.用戶訪問Shiro權限控制運行流程
Subject
我們把用戶或者程序稱為主體(如用戶,第三方服務,cron作業),主體去訪問系統或者資源
SecurityManager
安全管理器,Subject的認證和授權都要在安全管理器下進行
Authenticator
認證器,主要負責Subject的認證
Realm
數據域,Shiro和安全數據的連接器,好比jdbc連接數據庫; 通過realm獲取認證授權相關信息
Authorizer
授權器,主要負責Subject的授權, 控制subject擁有的角色或者權限
Cryptography
加解密,Shiro的包含易於使用和理解的數據加解密方法,簡化了很多復雜的api
Cache Manager
緩存管理器,比如認證或授權信息,通過緩存進行管理,提高性能
四.Springboot2.x整合Apache Shiro實戰
項目環境:
Maven3.5 + Jdk8 + Springboot 2.X + IDEA
相關依賴
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.0.1</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<!--注釋掉-->
<!--<scope>runtime</scope>-->
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!--阿里巴巴druid數據源-->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.6</version>
</dependency>
<!--spring整合shiro-->
<dependency>
<groupId>org.apache.shiro</groupId>
<artifactId>shiro-spring</artifactId>
<version>1.4.0</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.16</version>
</dependency>
<!-- shiro+redis緩存插件 主要用於CacheManager,以及緩存sessionManage中配置session持久化-->
<dependency>
<groupId>org.crazycake</groupId>
<artifactId>shiro-redis</artifactId>
<version>3.1.0</version>
</dependency>
Shiro各個組件說明
Realm:
realm作用:Shiro 從 Realm 獲取安全數據
默認自帶的realm:idae查看realm繼承關系,有默認實現和自定義繼承的realm
兩個概念
principal : 主體的標示,可以有多個,但是需要具有唯一性,常見的有用戶名,手機號,郵箱等
credential:憑證, 一般就是密碼
所以一般我們說 principal + credential 就賬號 + 密碼
開發中,往往是自定義realm , 即繼承 AuthorizingRealm,重寫doGetAuthenticationInfo(認證方法)和doGetAuthorizationInfo(授權方法)
當用戶登陸的時候會調用 doGetAuthenticationInfo
進行權限校驗的時候會調用: doGetAuthorizationInfo
UsernamePasswordToken : 對應就是 shiro的token中有Principal和Credential
UsernamePasswordToken-》HostAuthenticationToken-》AuthenticationToken
SimpleAuthorizationInfo:代表用戶角色權限信息
SimpleAuthenticationInfo :代表該用戶的認證信息
Shiro內置的Filter
核心過濾器類:DefaultFilter, 配置哪個路徑對應哪個攔截器進行處理
authc:org.apache.shiro.web.filter.authc.FormAuthenticationFilter
需要認證登錄才能訪問
user:org.apache.shiro.web.filter.authc.UserFilter
用戶攔截器,表示必須存在用戶。
anon:org.apache.shiro.web.filter.authc.AnonymousFilter
匿名攔截器,不需要登錄即可訪問的資源,匿名用戶或游客,一般用於過濾靜態資源。
roles:org.apache.shiro.web.filter.authz.RolesAuthorizationFilter
角色授權攔截器,驗證用戶是或否擁有角色。
參數可寫多個,表示某些角色才能通過,多個參數時寫 roles["admin,user"],當有多個參數時必須每個
參數都通過才算通過
perms:org.apache.shiro.web.filter.authz.PermissionsAuthorizationFilter
權限授權攔截器,驗證用戶是否擁有權限
參數可寫多個,表示需要某些權限才能通過,多個參數時寫 perms["user, admin"],當有多個參數時必
須每個參數都通過才算可以
authcBasic:org.apache.shiro.web.filter.authc.BasicHttpAuthenticationFilter
httpBasic 身份驗證攔截器。
logout:org.apache.shiro.web.filter.authc.LogoutFilter
退出攔截器,執行后會直接跳轉到shiroFilterFactoryBean.setLoginUrl(); 設置的 url
port:org.apache.shiro.web.filter.authz.PortFilter
端口攔截器, 可通過的端口。
ssl:org.apache.shiro.web.filter.authz.SslFilter
ssl攔截器,只有請求協議是https才能通過。
Filter的配置路徑說明:
/admin/video /user /pub
路徑通配符支持 ?、*、**,注意通配符匹配不 包括目錄分隔符“/”
心 可以匹配所有,不加*可以進行前綴匹配,但多個冒號就需要多個 * 來匹配
URL權限采取第一次匹配優先的方式
? : 匹配一個字符,如 /user? , 匹配 /user3,但不匹配/user/;
* : 匹配零個或多個字符串,如 /add* ,匹配 /addtest,但不匹配 /user/1
** : 匹配路徑中的零個或多個路徑,如 /user/** 將匹 配 /user/xxx 或 /user/xxx/yyy
例子
/user/**=filter1
/user/add=filter2
請求 /user/add 命中的是filter1攔截器
性能問題:通配符比字符串匹配會復雜點,所以性能也會稍弱,推薦是使用字符串匹配方式
數據加解密器CredentialsMatche
一般會自定義驗證規則
@Bean
public HashedCredentialsMatcher hashedCredentialsMatcher(){
HashedCredentialsMatcher hashedCredentialsMatcher = new
HashedCredentialsMatcher();
//散列算法,使用MD5算法;
hashedCredentialsMatcher.setHashAlgorithmName("md5");
//散列的次數,比如散列兩次,相當於 md5(md5("xxx"));
hashedCredentialsMatcher.setHashIterations(2);
return hashedCredentialsMatcher;
}
Shiro的緩存模塊CacheManager
shiro中提供了對認證信息和授權信息的緩存。
默認是關閉認證信息緩存的,對於授權信息的緩存shiro默認開啟的(因為授權的數據量大,每次都要查詢數據庫,性能受到影響)
AuthenticatingRealm 及 AuthorizingRealm 分別提供了對AuthenticationInfo 和 AuthorizationInfo 信息的緩
存。
Shiro中的SessionManager
用戶和程序直接的鏈接,程序可以根據session識別到哪個用戶,和javaweb中的session類似
什么是會話管理器SessionManager
會話管理器管理所有subject的所有操作,是shiro的核心組件,前后端分離的應用中session通常用token代替,用於會話管理
RBAC權限控制架構設計
1.數據庫設計:老五張表
用戶,角色,權限,用戶角色表,角色權限表
2.shiro相關配置
-配置ShiroFilterFactoryBean
配置流程和思路
shiroFilterFactoryBean-》
-SecurityManager-》
-CustomSessionManager
-CustomRealm-》hashedCredentialsMatcher
SessionManager
-DefaultSessionManager: 默認實現,常用於javase
-ServletContainerSessionManager: web環境
-DefaultWebSessionManager:常用於自定義實現(一般用這種)
ShiroConfig配置
@Configuration public class ShiroConfig { /** * 配置ShiroFilterFactoryBean * * @param securityManager * @return */ @Bean public ShiroFilterFactoryBean shiroFilter(SecurityManager securityManager) { System.out.println("執行 ShiroFilterFactoryBean shiroFilter"); ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean(); //必須設置securityManager shiroFilterFactoryBean.setSecurityManager(securityManager); //需要登錄的接口:如果訪問某個接口,需要登錄卻沒有登錄,則調用此接口,如果前端后端不分離,則跳轉到html頁面 shiroFilterFactoryBean.setLoginUrl("/pub/need_login"); //登錄成功 跳轉url,如果前后端分離,則沒這個調用 --這里設置為首頁就行了 shiroFilterFactoryBean.setSuccessUrl("/"); //登錄成功,但是沒有權限,未授權就會調用這個接口,如果不是前后端分離,則跳轉到403頁面 shiroFilterFactoryBean.setUnauthorizedUrl("/pub/not_permit"); //設置自定義filter Map<String, Filter> filterMap = new LinkedHashMap<>(); filterMap.put("roleOrFilter", new CustomRoleFilter()); //shiroFilterFactoryBean綁定自定義的filter shiroFilterFactoryBean.setFilters(filterMap); //過濾器鏈的map //攔截器(過濾器路徑,坑一必須要用LinkedhashMap),部分路徑無法進行攔截 Map<String, String> filterChainDefinitionMap = new LinkedHashMap<>(); /* ******************************************** 通常是配置這些過濾器,也可以用個數據庫的動態加載,這些數據 ********************************************/ //退出過濾器 filterChainDefinitionMap.put("/logout", "logout"); //匿名可以訪問,也就是游客模式 filterChainDefinitionMap.put("/pub/**", "anon"); //登錄用戶才可以訪問 filterChainDefinitionMap.put("/authc/**", "authc"); //管理員角色才可以訪問 filterChainDefinitionMap.put("/admin/**", "roles[admin]"); //有編輯權限才可以訪問 filterChainDefinitionMap.put("/video/update", "perms[video_update]"); //坑二:過濾器是順序執行,從上而下,一般來說/** 放到最下面 //authc: url定義必須通過認證才可以訪問 //anno: url可以匿名訪問 filterChainDefinitionMap.put("/**", "authc"); //配置過濾器 shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap); return shiroFilterFactoryBean; } /** * 數據域 * * @return */ @Bean public CustomRealm customRealm() { CustomRealm customRealm = new CustomRealm(); //設置加密器--因為數據庫中的密碼不是明文存儲 customRealm.setCredentialsMatcher(hashedCredentialsMatcher()); return customRealm; } /** * 自定義seesionManager * -配置session持久化 * @return */ @Bean public SessionManager sessionManager() { //自定義CustomSessionManager 繼承 DefaultWebSessionManager CustomSessionManager customSessionManager = new CustomSessionManager(); //配置session持久化 customSessionManager.setSessionDAO(redisSessionDAO()); //超時時間,默認 30分鍾,會話超時;方法里面的單位是毫秒 customSessionManager.setGlobalSessionTimeout(20000); return customSessionManager; } /** * 密碼加解密規則 CredentialMatcher-配置在數據域中,用於數據的加解密 * * @return */ @Bean public HashedCredentialsMatcher hashedCredentialsMatcher() { HashedCredentialsMatcher hashedCredentialsMatcher = new HashedCredentialsMatcher(); //設置散列算法:這里使用MD5算法 hashedCredentialsMatcher.setHashAlgorithmName("md5"); //散列次數,好比散列兩次 相當於md5(md5(x)) hashedCredentialsMatcher.setHashIterations(2); return hashedCredentialsMatcher; } /** * 配置redisManager */ @Bean public RedisManager getRedisManager() { RedisManager redisManager = new RedisManager(); //默認就是localhost:6379 不寫也行 redisManager.setHost("localhost"); redisManager.setPort(6379); return redisManager; } /** * 配置具體cache實現類RedisCacheManager * 為什么要使用緩存: * 緩存組件位於SecurityManager中,在CustomRealm數據域中,由於授權方法中每次都要查詢數據庫,性能受影響,因此將數據緩存起來,提高查詢效率 * 除了使用Redis緩存,還能使用shiro-ehcache * * @return */ public RedisCacheManager redisCacheManager() { RedisCacheManager redisCacheManager = new RedisCacheManager(); redisCacheManager.setRedisManager(getRedisManager()); //設置過期時間,單位是秒,20s, redisCacheManager.setExpire(20); return redisCacheManager; } /** * 自定義session持久化 * * @return */ public RedisSessionDAO redisSessionDAO() { /* 為啥session也要持久化? 重啟應用,用戶無感知,可以繼續以原先的狀態繼續訪問 注意點: DO對象需要實現序列化接口 Serializable logout接口和以前一樣調用,請求logout后會刪除redis里面的對應的key,即刪除對應的token */ RedisSessionDAO redisSessionDAO = new RedisSessionDAO(); redisSessionDAO.setRedisManager(getRedisManager()); //配置自定義sessionId,shiro自動生成色sessionId不滿足條件時可以使用 redisSessionDAO.setSessionIdGenerator(new CustomSessionIdGenerator()); return redisSessionDAO; } /** * LifecycleBeanPostProcessor * 管理shiro一些bean的生命周期 即bean初始化 與銷毀 * * @return */ @Bean public LifecycleBeanPostProcessor lifecycleBeanPostProcessor() { return new LifecycleBeanPostProcessor(); } /** * AuthorizationAttributeSourceAdvisor * 作用:加入shiro注解的使用,不加入這個AOP注解不生效(shiro的注解 例如 @RequiresGuest) */ @Bean public AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor() { AuthorizationAttributeSourceAdvisor authorizationAttributeSourceAdvisor = new AuthorizationAttributeSourceAdvisor(); authorizationAttributeSourceAdvisor.setSecurityManager(securityManager()); return authorizationAttributeSourceAdvisor; } /** * DefaultAdvisorAutoProxyCreator * 作用: 用來掃描上下文尋找所有的Advistor(通知器), 將符合條件的Advisor應用到切入點的Bean中,需 * 要在LifecycleBeanPostProcessor創建后才可以創建 * * @return */ @Bean @DependsOn("lifecycleBeanPostProcessor") public DefaultAdvisorAutoProxyCreator getDefaultAdvisorAutoProxyCreator() { DefaultAdvisorAutoProxyCreator defaultAdvisorAutoProxyCreator = new DefaultAdvisorAutoProxyCreator(); defaultAdvisorAutoProxyCreator.setUsePrefix(true); return defaultAdvisorAutoProxyCreator; } }
自定義Filter==>>>CustomRoleFilter
/** * 自定義Filter * 問題:為什么要自定義filter: * 因為配置role[admin,root] ,只有同時滿足admin和root才能夠訪問,顯然這是不合理的 * 實際是超級管理員有全部權限 */ public class CustomRoleFilter extends AuthorizationFilter { @Override public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException { //獲取當前訪問路徑所有角色的集合 Subject subject = getSubject(request, response); String[] rolesArray = (String[]) mappedValue; //沒有角色顯示,可以直接訪問 if (rolesArray == null || rolesArray.length == 0) { //no roles specified, so nothing to check - allow access. return true; } //當前subject是roles中的任意一個,則有權限訪問 Set<String> roles = CollectionUtils.asSet(rolesArray); for (String role : roles) { if (subject.hasRole(role)) { return true; } } return subject.hasAllRoles(roles); } }
自定義SessionManager:===>>CustomSessionManager
/** @Discription 自定義SessionMananger 為什么要自定義自定義SessionMananger? * 原因: * 因為前后端分離的情況下 不是靠session,而是靠token去交互,因此需要自定義這個sessionId的獲取 * 即重寫父類的方法(父類是從頭中拿sessionId) * */ public class CustomSessionManager extends DefaultWebSessionManager { //這個key 放在請求頭中,可以自己定義 ,通常是設置為token或者authorization private static final String AUTHORIZATION = "token"; public CustomSessionManager() { super(); } @Override protected Serializable getSessionId(ServletRequest request, ServletResponse response) { //將ServletRequest轉換成HttpServletRequest String sessionId = WebUtils.toHttp(request).getHeader(AUTHORIZATION); if (sessionId != null) { // request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, "cookie"); // // request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId); // //automatically mark it valid here. If it is invalid, the // //onUnknownSession method below will be invoked and we'll remove the attribute at that time. // request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE, ShiroHttpServletRequest.COOKIE_SESSION_ID_SOURCE); request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID, sessionId); //automatically mark it valid here. If it is invalid, the //onUnknownSession method below will be invoked and we'll remove the attribute at that time. request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID, Boolean.TRUE); return sessionId; } else { return super.getSessionId(request, response); } } }
自定義數據域 CustomRealm
** * 自定義的realm 數據域 */ public class CustomRealm extends AuthorizingRealm { @Autowired private UserService userService; /** * 權限校驗的時候會調用 * * @param principals * @return */ @Override protected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) { System.out.println("授權 doGetAuthorizationInfo"); //從token中獲取用戶信息,token代表用戶輸入 // String username = (String) principals.getPrimaryPrincipal(); User newUser = (User) principals.getPrimaryPrincipal(); // 使用原因? // 授權的時候每次都去查詢數據庫,對於頻繁訪問的接口,性能和響應速度比較慢,所以使用緩存 //提高性能的方法1-使用redis緩存: // 將信息放到緩存,例如redis,但是要設置緩存失效時間,因為可能更新數據庫了,但是緩存沒有更新 //提高性能的方法2-使用shiro-redis集成的緩存: // shiro-redis的緩存配置在SecurityManager中 User user = userService.findAllUserInfoByUsername(newUser.getUsername()); List<String> stringRoleList = new ArrayList<>(); List<String> stringPermissionList = new ArrayList<>(); List<Role> roleList = user.getRoleList(); for (Role role : roleList) { stringRoleList.add(role.getName()); List<Permission> permissionList = role.getPermissionList(); for (Permission p : permissionList) { if (p != null) { stringPermissionList.add(p.getName()); } } } SimpleAuthorizationInfo simpleAuthorizationInfo = new SimpleAuthorizationInfo(); //將用戶對應的角色和權限信息 放到權限器中 simpleAuthorizationInfo.addStringPermissions(stringPermissionList); simpleAuthorizationInfo.addRoles(stringRoleList); return simpleAuthorizationInfo; } /** * 用戶登錄的時候會調用 * * @param token * @return * @throws AuthenticationException */ @Override protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException { System.out.println("認證 doGetAuthenticationInfo"); //從token中獲取用戶信息,token代表用戶輸入 String username = (String) token.getPrincipal(); User user = userService.findAllUserInfoByUsername(username); //取密碼 String password = user.getPassword(); if (password == null || "".equals(password)) { return null; } return new SimpleAuthenticationInfo(user, password, this.getClass().getName()); } } /* * 原有的問題 class java.lang.String must has getter for field: authCacheKey or id\nWe need a field to identify this Cache Object in Redis. So you need to defined an id field which you can get unique id to identify this principal. For example, if you use UserInfo as Principal class, the id field maybe userId, userName, email, etc. For example, getUserId(), getUserName(), getEmail(), etc.\nDefault value is authCacheKey or id, that means your principal object has a method called \"getAuthCacheKey()\" or \"getId()\"" 改造原有的邏輯,修改緩存的唯一key doGetAuthorizationInfo 方法 原有: String username = (String)principals.getPrimaryPrincipal(); User user = userService.findAllUserInfoByUsername(username); 改為 User newUser = (User)principals.getPrimaryPrincipal(); User user = userService.findAllUserInfoByUsername(newUser.getUsername()); doGetAuthenticationInfo方法 原有: return new SimpleAuthenticationInfo(username, user.getPassword(), this.getClass().getName()); 改為 return new SimpleAuthenticationInfo(user, user.getPassword(), this.getClass().getName()); * * * */
代碼地址:
https://github.com/AlenYang123456/Springboot-Shiro