一、Shiro整合到Springboot步驟
1、準備SpringBoot 環境,這一步省略
2、引入Shiro 依賴
? ? ?因為是Web 項目,所以需要引入web 相關依賴?shiro-spring-boot-web-starter,如下所示:
? ? ?
3、準備Realm
? ? ?因為實例化?ShiroFilterFactoryBean 時需要注入??SecurityManager 的bean,而?
? ? ?SecurityManager 實例化時需要綁定Realm。
? ? ? 在真正工作中,我們一般需要從數據庫中查詢用戶信息、角色信息和用戶權限信息,
? ? ? 即一般自定義Relam,自定義Realm如下:
@Component
public class CustomRealm extends AuthorizingRealm {@Autowiredprivate UserService userService;@Autowiredprivate RoleService roleService;@Autowiredprivate PermissionService permissionService;{//用于密碼加密和比對HashedCredentialsMatcher matcher = new HashedCredentialsMatcher();matcher.setHashAlgorithmName("MD5");matcher.setHashIterations(1024);this.setCredentialsMatcher(matcher);}/*** 授權* todo 注意:* 1、授權是在認證之后的操作,授權方法需要用到認證方法返回的 AuthenticationInfo 中的用戶信息* 2、該方法是在父類 AuthorizingRealm.getAuthorizationInfo() 方法中調用的,在 getAuthorizationInfo()* 方法中,回先從緩存中查詢權限信息,若緩存中數據不存在,再執行改當前方法從數據庫中查詢權限數據* 針對這個邏輯,我們可以擴展Shiro 把數據放到緩存中(一般放到redis 中)*** @param principals 即 doGetAuthenticationInfo 方法返回的 AuthenticationInfo 中的用戶信息(這里是User )* @return*/@Overrideprotected AuthorizationInfo doGetAuthorizationInfo(PrincipalCollection principals) {System.out.println("查詢數據庫,根據用戶信息查詢角色和權限信息~~~~~~~~~~~~~~~~~~~~~~~~");//0、判斷是否完成認證Subject subject = SecurityUtils.getSubject();if(subject == null || subject.isAuthenticated())return null;//1. 獲取認證用戶的信息User user = (User) principals.getPrimaryPrincipal();//2. 基于用戶信息獲取當前用戶擁有的角色。Set<Role> roleSet = roleService.findRolesByUid(user.getId());Set<Integer> roleIdSet = new HashSet<>();Set<String> roleNameSet = new HashSet<>();for (Role role : roleSet) {roleIdSet.add(role.getId());roleNameSet.add(role.getRoleName());}//3. 基于用戶擁有的角色查詢權限信息Set<Permission> permSet = permissionService.findPermsByRoleSet(roleIdSet);Set<String> permNameSet = new HashSet<>();for (Permission permission : permSet) {permNameSet.add(permission.getPermName());}//4. 聲明AuthorizationInfo對象作為返回值,傳入角色信息和權限信息SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();info.setRoles(roleNameSet);info.setStringPermissions(permNameSet);//5. 返回return info;}/*** 認證 用戶執行認證操作傳入的用戶名和密碼* 只需要完成用戶名校驗即可,密碼校驗由Shiro內部完成** @param token* @return* @throws AuthenticationException*/@Overrideprotected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken token) throws AuthenticationException {//1、獲取用戶名稱String userName = (String) token.getPrincipal();//2、判斷用戶名稱是否為空if(StringUtils.isEmpty(userName)){// 返回null,會默認拋出一個異常,org.apache.shiro.authc.UnknownAccountExceptionreturn null;}//4、如果用戶名稱不為空,則基于用戶名稱去查詢用戶信息//這一步一般是自己的UserService 服務//模擬查詢用戶信息User user = userService.findByUsername(userName);if(user == null){return null;}//5、構建 AuthenticationInfo 對象,并填充用戶信息/*** todo 注意:* SimpleAuthenticationInfo 第一個參數是用戶信息,第二個參數是用戶密碼,第三個參數是Realm名稱(這個參數沒有意義)*/SimpleAuthenticationInfo info = new SimpleAuthenticationInfo(user,user.getPassword(),"CustomRealm!!!");//設置鹽info.setCredentialsSalt(ByteSource.Util.bytes(user.getSalt()));//返回 AuthenticationInfo 對象return info;}}
4、準備Shiro相關的配置文件
? ? ? 定義Shiro 配置文件,用于實例化?SecurityManager 對象 與 配置攔截器鏈。
? ? ? 雖然SpringBoot 自動裝配機制會自動裝配?SecurityManager,但自動裝載?SecurityManager 時
? ? ? 只會注入 Shiro 自身默認提供的Relam,這里需要把自定義的Realm注入到?SecurityManager
? ? ?,所以需要我們手動裝載?SecurityManager 去覆蓋Springboot 自動裝載的?SecurityManager。
? ? ?另外在?shiro-spring-boot-web-starter 包下的文件 spring.factores 中的配置類
? ? ?ShiroWebAutoConfiguration中 springboot自動裝配的攔截器鏈中只配置了一種請求,
? ? ?如圖所示:
? ? ? ? ? ?
? ? ?所以我們也需要在自定義的shiro配置文件中,手動配置我們需要的攔截器鏈。
? ? ?shiro配置文件如下:
? ? ?
/***************************************************** Shiro 配置類* 由前邊 Shiro 與Spring Web 整合可以發現,Shiro與Spring 整合的核心是向spring中注入* ShiroFilterFactoryBean;但在spring boot 中,spring boot 會自動將 ShiroFilterFactoryBean* 注入到spring 中(在 AbstractShiroWebFilterConfiguration 中完成的)* 在 AbstractShiroWebFilterConfiguration 中發現,實例化 ShiroFilterFactoryBean 時需要* 提供 SecurityManager(使用的是 DefaultWebSecurityManager) 和 ShiroFilterChainDefinition(攔截器鏈)* SecurityManager 實例化需要提供 Realm ,** 定義 Shiro 配置類,用于實例化 DefaultWebSecurityManager 和 ShiroFilterChainDefinition** @author lbf* @date ****************************************************/
@Configuration
public class ShiroConfig {/*** 注入* 實例化 WebSecurityManager 時需要用到Releam bean,所以在這之前 Releam 一定要存在* @param realm* @return*/@Beanpublic DefaultWebSecurityManager securityManager(CustomRealm realm, SessionManager sessionManager){DefaultWebSecurityManager securityManager = new DefaultWebSecurityManager(realm);return securityManager;}/*** 添加攔截器鏈* @return*/@Beanpublic ShiroFilterChainDefinition filterChainDefinition(){DefaultShiroFilterChainDefinition filterChainDefinition = new DefaultShiroFilterChainDefinition();//添加攔截器信息Map<String, String> filterChainDefinitionMap = new LinkedHashMap();/*** anon 和 authc 都是Shiro 自帶的過濾器* Shiro 自帶的過濾器可以在枚舉 DefaultFilter 中查看*///anon: 放行filterChainDefinitionMap.put("/login.html","anon");filterChainDefinitionMap.put("/user/**","anon");//authc:認證之后放行filterChainDefinitionMap.put("/**","authc");filterChainDefinition.addPathDefinitions(filterChainDefinitionMap);return filterChainDefinition;}}
? ??
5、測試
? ? ??
二、Shiro 授權方式
? ? ? ?Shiro 常用的授權方式有2種,即
? ? ? ? ? ?1)基于連接器鏈的權限角色校驗,就是上邊配置攔截器鏈的方式;將需要校驗的請求
? ? ? ? ? ? ? ? ?配置到攔截器鏈?ShiroFilterChainDefinition 種,如下圖所示:
? ? ? ? ? ? ? ? ? ? ?
? ? ? ? ? ?2)基于注解的權限角色校驗
? ?
? ? 2、基于注解的權限角色校驗
? ? ? ? ? Shiro 提供了基于注解的方式來簡化權限和角色的校驗,可以在類或方法上直接聲明所需要
? ? ? ? ? 的角色或權限;
? ? ? ? ??注解進行權限或角色校驗時,是基于對Controller類進行代理,在前置增強中對請求進行權限
? ? ? ? ? 校驗,是在攔截器鏈方式的后邊執行。
? ? ? ? ? Shiro 提供了如下幾個注解用于權限和角色校驗
? ? ? ? ? ? ? ? ?1)@RequiresAuthentication
? ? ? ? ? ? ? ? ? ? ? ?要求當前 Subject 已經通過認證(即用戶已登錄)
? ? ? ? ? ? ? ? ?2)@RequiresUser
? ? ? ? ? ? ? ? ? ? ? ?要求當前 Subject 是一個應用程序用戶(已認證或通過記住我功能登錄)
? ? ? ? ? ? ? ? ?3)@RequiresGuest
? ? ? ? ? ? ? ? ? ? ? ?要求當前 Subject 是一個"訪客",即未認證或未通過記住我登錄
? ? ? ? ? ? ? ? ?4)@RequiresRoles
? ? ? ? ? ? ? ? ? ? ??要求當前 Subject 擁有指定的角色,即角色校驗,常用
? ? ? ? ? ? ? ? ?5)@RequiresPermissions
? ? ? ? ? ? ? ? ? ? ??要求當前 Subject 擁有指定的權限,即權限校驗,常用
? ? ? ? ? 示例代碼如下:
@RestController
@RequestMapping("/item")
public class ItemController {/*** 基于過濾器鏈的角色、權限校驗* @return*/@GetMapping("/select")public String select(){return "item Select!!!";}@GetMapping("/delete")public String delete(){return "item Delete!!!";}/*** 基于注解的角色校驗* @return*/@GetMapping("/update")//默認多個角色是and 的關系@RequiresRoles(value = {"超級管理員","運營"})public String update(){return "item Update!!!";}@GetMapping("/insert")@RequiresRoles(value = {"超級管理員","運營"},logical = Logical.OR)public String insert(){return "item Update!!!";}/*** 基于注解的權限校驗* logical=用于指定多個權限是同時滿足,還是滿足其中一個,默認是and* Logical.OR含義是:只有用于 admin 權限或del權限的用戶才能執行刪除操作* @return*/@GetMapping("/update")@RequiresPermissions(value = {"item:admin","item:del"},logical = Logical.OR)public String del(){return "item del !!!";}
}
? ? ? ? ?
3、基于注解方式的權限角色校驗要注意的點
? ? ?1)基于注解的方式與基于配置鏈的方式是可以配合使用的,并不沖突,基于配置的方式在
? ? ? ? ? ?攔截器鏈之后執行。
? ? ?2)注解只能用在 Spring 管理的 Bean 上(如 Controller、Service 等),對于靜態方法
? ? ? ? ? ?或非 Spring 管理的類,注解不會生效
? ? ?3)當權限校驗失敗時,Shiro 會拋出相應的異常,我們需要自己配置異常處理器處理這些異常
? ? ? ? ? ,如:通過 @RestControllerAdvice,@ControllerAdvice
? ? ? ? ? ? ?示例代碼如下:
@RestControllerAdvice(basePackages = "com.msb.controller") //指定要處理異常的包路徑
public class AuthExceptionHandler {//處理授權異常@ExceptionHandler(AuthorizationException.class)public ResponseEntity<String> handleAuthorizationException(AuthorizationException e) {return ResponseEntity.status(HttpStatus.FORBIDDEN).body("無權訪問: " + e.getMessage());}//處理認證異常@ExceptionHandler(AuthenticationException.class)public ResponseEntity<String> handleAuthenticationException(AuthenticationException e) {return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("認證失敗: " + e.getMessage());}
}
? ??
三、RolesAuthorizationFilter 分析
? ? ? ?以默認的角色攔截器?RolesAuthorizationFilter 分析下 Shiro 種是如何進行角色認證的
? ? ? ?1)角色校驗方法?RolesAuthorizationFilter.isAccessAllowed
//mappedValue: 需要校驗的角色列表,即在連接器鏈種指定的角色列表
public boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws IOException {//獲取請求主體 Subject,即用戶Subject subject = this.getSubject(request, response);//需要校驗的角色列表String[] rolesArray = (String[])((String[])mappedValue);if (rolesArray != null && rolesArray.length != 0) {Set<String> roles = CollectionUtils.asSet(rolesArray);//調用 Subject.hasAllRoles 校驗用戶是否具有所有的 mappedValue 角色return subject.hasAllRoles(roles);} else {return true;}}
? ? ? ? 2)AuthorizingRealm.hasAllRoles
? ? ? ? ? ? ? 從Subject.hasAllRoles 方法一直點進去 ,如下所示:
? ? ? ? ? ? ? ? ? ? Subject—>DelegatingSubject.hasAllRoles —> Authorizer.hasAllRoles
? ? ? ? ? ? ? ? ? ? ?—> AuthorizingRealm.hasAllRoles
? ? ? ? ? ? ? 一直到?AuthorizingRealm.hasAllRoles 方法,在該方法種調用了getAuthorizationInfo?
? ? ? ? ? ? ?方法來獲取?AuthorizationInfo,如下圖所示:
? ? ? ? ? ? ? ? ??
? ? ? ? 3)AuthorizingRealm.getAuthorizationInfo 方法
? ? ? ? ? ? ?在?getAuthorizationInfo 方法種,我們重點看下?doGetAuthorizationInfo 方法;
? ? ? ? ? ? ?到這里是不是有點眼熟,doGetAuthorizationInfo是一個抽象方法,它有多個實現,
? ? ? ? ? ? 其中有一個實現就是上邊我們自定義的CustomRealm中的方法
? ? ? ? ? ? ?如下圖所示:
? ? ? ? ? ? ? ??
四、自定義攔截器
? ? ? ?在工作中Shiro 默認提供的校驗攔截器往往不能滿足我們實際的需要,這就需要我們自定義
? ? ? ?Shiro攔截器,如:上邊RolesAuthorizationFilter 是校驗用戶具有角色列表中的所有角色才
? ? ? ?校驗通過,而在工作中常常有 “存在一個角色在角色列表中就行” 的場景;
1、自定義Shiro攔截器解決 “存在一個角色在角色列表中就行” 的場景
? ? ? 自定義過濾器需要繼承類?AuthorizationFilter,并重寫?isAccessAllowed 方法
? ? ? 示例代碼如下:
? ? ??
public class RolesOrAuthorizationFilter extends AuthorizationFilter {/*** 用戶角色校驗* @param request* @param response* @param mappedValue 指定的角色列表,* @return* @throws Exception*/@Overrideprotected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {//獲取校驗主題,可以認為就是用戶信息Subject subject = this.getSubject(request, response);/*** 獲取用戶指定的角色列表,就是在 初始化 ShiroFilterChainDefinition過程中指定的角色列表* (如:roles[超級管理員,運營],[]中的內容)*/String[] rolesArray = (String[])((String[])mappedValue);if(rolesArray != null && rolesArray.length > 0){for (String role:rolesArray){//有一個角色校驗通過,則表示校驗通過,返回trueif(subject.hasRole(role)){return true;}}}else {return true;}return false;}
}
2、將自定義Filter 交給Shiro 管理
? ? ??自定義的角色校驗器(過濾器)如何交給Shiro管理?
? ? ? 由 配置類 ShiroWebFilterConfiguration 初始化 ShiroFilterFactoryBean 時可以發現,過濾器
? ? ? 是在 ShiroFilterFactoryBean 實例化時交給 ShiroFilterFactoryBean 管理的;到這里就明白
? ? ? 了,我們可以手動初始化 ShiroFilterFactoryBean 來覆蓋springboot 的自動初始化
? ? ? ShiroFilterFactoryBean,并把自定義的 RolesOrAuthorizationFilter 過濾器交給
? ? ? ShiroFilterFactoryBean 管理。
? ? ? 在配置類ShiroConfig 中手動注入ShiroFilterFactoryBean 代碼如下:
//手動注入 ShiroFilterFactoryBean,覆蓋 Springboot 自動裝載ShiroFilterFactoryBean;/*** 初始化一些url* ShiroFilterFactoryBean 實例化需要的url,這些url可以在配置文件中配置*///登錄url@Value("#{ @environment['shiro.loginUrl'] ?: '/login.jsp' }")protected String loginUrl;//登錄成功后跳轉url@Value("#{ @environment['shiro.successUrl'] ?: '/' }")protected String successUrl;//校驗失敗的url@Value("#{ @environment['shiro.unauthorizedUrl'] ?: null }")protected String unauthorizedUrl;/*** 手動注入 ShiroFilterFactoryBean,覆蓋 Springboot 自動裝載ShiroFilterFactoryBean;* 用于把自定義的過濾器交給 Shiro 管理* @param securityManager* @param filterChainDefinition 過濾器鏈* @return*/@Beanpublic ShiroFilterFactoryBean shiroFilterFactoryBean(SecurityManager securityManager,ShiroFilterChainDefinition filterChainDefinition){//創建 ShiroFilterFactoryBeanShiroFilterFactoryBean filterFactoryBean = new ShiroFilterFactoryBean();//設置大量的urlfilterFactoryBean.setLoginUrl(this.loginUrl);filterFactoryBean.setSuccessUrl(this.successUrl);filterFactoryBean.setUnauthorizedUrl(this.unauthorizedUrl);//設置安全管理器filterFactoryBean.setSecurityManager(securityManager);//設置過濾器鏈filterFactoryBean.setFilterChainDefinitionMap(filterChainDefinition.getFilterChainMap());//設置自定義過濾器 ,// todo 注意:這里一定要手動的new出來這個自定義過濾器,如果使用Spring自動注入自定義過濾器,// 會造成無法獲取到Subject// 因為spring 自動注入 自定義過濾器 RolesOrAuthorizationFilter 初始化太早了,而RolesOrAuthorizationFilter// 初始化時需要做一些Shiro處理后,RolesOrAuthorizationFilter 實例才能拿到SubjectfilterFactoryBean.getFilters().put("roleOr",new RolesOrAuthorizationFilter());return filterFactoryBean;}
3、在攔截器鏈?ShiroFilterChainDefinition 配置使用自定義過濾器
? ? ?由上邊可以知道 自定義Shiro 過濾器?RolesOrAuthorizationFilter 的名稱是 “roleOr”;
? ? ?在攔截器鏈?ShiroFilterChainDefinition 中的配置如下:
@Beanpublic ShiroFilterChainDefinition filterChainDefinition(){DefaultShiroFilterChainDefinition filterChainDefinition = new DefaultShiroFilterChainDefinition();//添加攔截器信息,LinkedHashMap有序Map<String, String> filterChainDefinitionMap = new LinkedHashMap();/*** anon 和 authc 都是Shiro 自帶的過濾器* Shiro 自帶的過濾器可以在枚舉 DefaultFilter 中查看*///anon: 放行filterChainDefinitionMap.put("/login.html","anon");filterChainDefinitionMap.put("/user/**","anon");//使用自定義的過濾器,有一個角色在[超級管理員,運營] 中就校驗通過filterChainDefinitionMap.put("/item/select","rolesOr[超級管理員,運營]");//filterChainDefinitionMap.put("/item/select","roles[超級管理員,運營]");filterChainDefinitionMap.put("/item/delete","perms[item:delete,item:insert]");//authc:認證之后放行filterChainDefinitionMap.put("/**","authc");filterChainDefinition.addPathDefinitions(filterChainDefinitionMap);return filterChainDefinition;}