SpringBoot 整合 Shiro 實現動態(tài)權限加載更新+ Session 共享 + 單點登錄
點擊上方藍色“小哈學Java”,選擇“設為星標”
回復“資源”獲取獨家整理的學習資料!


來源: juejin.im/post/5d087d605188256de9779e64
一。說明 二。項目環(huán)境 二。編寫項目基礎類 三。編寫Shiro核心類 四。實現權限控制 五.POSTMAN測試 六。項目源碼
一。說明
Shiro是一個安全框架,項目中主要用它做認證,授權,加密,以及用戶的會話管理,雖然Shiro沒有SpringSecurity功能更豐富,但是它輕量,簡單,在項目中通常業(yè)務需求Shiro也能夠勝任。
二。項目環(huán)境
MyBatis-Plus版本:3.1.0
SpringBoot版本:2.1.5
JDK版本:1.8
Shiro版本:1.4
Shiro-redis插件版本:3.1.0
數據表(SQL文件在項目中):數據庫中測試號的密碼進行了加密,密碼皆為123456
| 數據表名 | 中文表名 | 備注說明 |
|---|---|---|
| sys_user | 系統(tǒng)用戶表 | 基礎表 |
| sys_menu | 權限表 | 基礎表 |
| sys_role | 角色表 | 基礎表 |
| sys_role_menu | 角色與權限關系表 | 中間表 |
| sys_user_role | 用戶與角色關系表 | 中間表 |
Maven依賴如下:
<dependencies>
????????<dependency>
????????????<groupId>org.springframework.bootgroupId>
????????????<artifactId>spring-boot-starter-webartifactId>
????????dependency>
????????<dependency>
????????????<groupId>mysqlgroupId>
????????????<artifactId>mysql-connector-javaartifactId>
????????????<scope>runtimescope>
????????dependency>
????????
????????<dependency>
????????????<groupId>org.springframework.bootgroupId>
????????????<artifactId>spring-boot-starter-aopartifactId>
????????dependency>
????????
????????<dependency>
????????????<groupId>org.projectlombokgroupId>
????????????<artifactId>lombokartifactId>
????????????<optional>trueoptional>
????????dependency>
????????
????????<dependency>
????????????<groupId>org.springframework.bootgroupId>
????????????<artifactId>spring-boot-starter-data-redis-reactiveartifactId>
????????dependency>
????????
????????<dependency>
????????????<groupId>com.baomidougroupId>
????????????<artifactId>mybatis-plus-boot-starterartifactId>
????????????<version>3.1.0version>
????????dependency>
????????
????????<dependency>
????????????<groupId>com.alibabagroupId>
????????????<artifactId>druidartifactId>
????????????<version>1.1.6version>
????????dependency>
????????
????????<dependency>
????????????<groupId>org.apache.shirogroupId>
????????????<artifactId>shiro-springartifactId>
????????????<version>1.4.0version>
????????dependency>
????????
????????<dependency>
????????????<groupId>org.crazycakegroupId>
????????????<artifactId>shiro-redisartifactId>
????????????<version>3.1.0version>
????????dependency>
????????
????????<dependency>
????????????<groupId>org.apache.commonsgroupId>
????????????<artifactId>commons-lang3artifactId>
????????????<version>3.5version>
????????dependency>
dependencies>
配置如下:
#?配置端口
server:
??port:?8764
spring:
??#?配置數據源
??datasource:
????driver-class-name:?com.mysql.cj.jdbc.Driver
????url:?jdbc:mysql://localhost:3306/my_shiro?serverTimezone=UTC&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false
????username:?root
????password:?root
????type:?com.alibaba.druid.pool.DruidDataSource
??#?Redis數據源
??redis:
????host:?localhost
????port:?6379
????timeout:?6000
????password:?123456
????jedis:
??????pool:
????????max-active:?1000??#?連接池最大連接數(使用負值表示沒有限制)
????????max-wait:?-1??????#?連接池最大阻塞等待時間(使用負值表示沒有限制)
????????max-idle:?10??????#?連接池中的最大空閑連接
????????min-idle:?5???????#?連接池中的最小空閑連接
#?mybatis-plus相關配置
mybatis-plus:
??#?xml掃描,多個目錄用逗號或者分號分隔(告訴?Mapper?所對應的?XML?文件位置)
??mapper-locations:?classpath:mapper/*.xml
??#?以下配置均有默認值,可以不設置
??global-config:
????db-config:
??????#主鍵類型?AUTO:"數據庫ID自增"?INPUT:"用戶輸入ID",ID_WORKER:"全局唯一ID?(數字類型唯一ID)",?UUID:"全局唯一ID?UUID";
??????id-type:?auto
??????#字段策略?IGNORED:"忽略判斷"??NOT_NULL:"非?NULL?判斷")??NOT_EMPTY:"非空判斷"
??????field-strategy:?NOT_EMPTY
??????#數據庫類型
??????db-type:?MYSQL
??configuration:
????#?是否開啟自動駝峰命名規(guī)則映射:從數據庫列名到Java屬性駝峰命名的類似映射
????map-underscore-to-camel-case:?true
????#?返回map時true:當查詢數據為空時字段返回為null,false:不加這個查詢數據為空時,字段將被隱藏
????call-setters-on-nulls:?true
????#?這個配置會將執(zhí)行的sql打印出來,在開發(fā)或測試的時候可以用
????log-impl:?org.apache.ibatis.logging.stdout.StdOutImpl
二。編寫項目基礎類
用戶實體,Dao,Service等在這里省略,請參考源碼
編寫異常類來處理Shiro權限攔截異常
/**
?*?@Description?自定義異常
?*?@Author?Sans
?*?@CreateTime?2019/6/15?22:56
?*/
@ControllerAdvice
public?class?MyShiroException?{
????/**
?????*?處理Shiro權限攔截異常
?????*?如果返回JSON數據格式請加上?@ResponseBody注解
?????*?@Author?Sans
?????*?@CreateTime?2019/6/15?13:35
?????*?@Return?Map
????@ResponseBody
????@ExceptionHandler(value?=?AuthorizationException.class)
????public?Map<String,Object>?defaultErrorHandler(){
????????Map?map?=?new?HashMap<>();
????????map.put("403","權限不足");
????????return?map;
????}
}
創(chuàng)建SHA256Util加密工具
/**
?*?@Description?Sha-256加密工具
?*?@Author?Sans
?*?@CreateTime?2019/6/12?9:27
?*/
public?class?SHA256Util?{
????/**??私有構造器?**/
????private?SHA256Util(){};
????/**??加密算法?**/
????public?final?static?String?HASH_ALGORITHM_NAME?=?"SHA-256";
????/**??循環(huán)次數?**/
????public?final?static?int?HASH_ITERATIONS?=?15;
????/**??執(zhí)行加密-采用SHA256和鹽值加密?**/
????public?static?String?sha256(String?password,?String?salt)?{
????????return?new?SimpleHash(HASH_ALGORITHM_NAME,?password,?salt,?HASH_ITERATIONS).toString();
????}
}
創(chuàng)建彈簧工具
/**
?*?@Description?Spring上下文工具類
?*?@Author?Sans
?*?@CreateTime?2019/6/17?13:40
?*/
@Component
public?class?SpringUtil?implements?ApplicationContextAware?{
????private?static?ApplicationContext?context;
????/**
?????*?Spring在bean初始化后會判斷是不是ApplicationContextAware的子類
?????*?如果該類是,setApplicationContext()方法,會將容器中ApplicationContext作為參數傳入進去
?????*?@Author?Sans
?????*?@CreateTime?2019/6/17?16:58
?????*/
????@Override
????public?void?setApplicationContext(ApplicationContext?applicationContext)?throws?BeansException?{
????????context?=?applicationContext;
????}
????/**
?????*?通過Name返回指定的Bean
?????*?@Author?Sans
?????*?@CreateTime?2019/6/17?16:03
?????*/
????public?static??T?getBean(Class?beanClass) ?{
????????return?context.getBean(beanClass);
????}
}
創(chuàng)建Shiro工具
/**
?*?@Description?Shiro工具類
?*?@Author?Sans
?*?@CreateTime?2019/6/15?16:11
?*/
public?class?ShiroUtils?{
????/**?私有構造器?**/
????private?ShiroUtils(){}
????private?static?RedisSessionDAO?redisSessionDAO?=?SpringUtil.getBean(RedisSessionDAO.class);
????/**
?????*?獲取當前用戶Session
?????*?@Author?Sans
?????*?@CreateTime?2019/6/17?17:03
?????*?@Return?SysUserEntity?用戶信息
?????*/
????public?static?Session?getSession()?{
????????return?SecurityUtils.getSubject().getSession();
????}
????/**
?????*?用戶登出
?????*?@Author?Sans
?????*?@CreateTime?2019/6/17?17:23
?????*/
????public?static?void?logout()?{
????????SecurityUtils.getSubject().logout();
????}
????/**
????*?獲取當前用戶信息
????*?@Author?Sans
????*?@CreateTime?2019/6/17?17:03
????*?@Return?SysUserEntity?用戶信息
????*/
????public?static?SysUserEntity?getUserInfo()?{
??????return?(SysUserEntity)?SecurityUtils.getSubject().getPrincipal();
????}
????/**
?????*?刪除用戶緩存信息
?????*?@Author?Sans
?????*?@CreateTime?2019/6/17?13:57
?????*?@Param??username??用戶名稱
?????*?@Param??isRemoveSession?是否刪除Session
?????*?@Return?void
?????*/
????public?static?void?deleteCache(String?username,?boolean?isRemoveSession){
????????//從緩存中獲取Session
????????Session?session?=?null;
????????Collection?sessions?=?redisSessionDAO.getActiveSessions();
????????SysUserEntity?sysUserEntity;
????????Object?attribute?=?null;
????????for(Session?sessionInfo?:?sessions){
????????????//遍歷Session,找到該用戶名稱對應的Session
????????????attribute?=?sessionInfo.getAttribute(DefaultSubjectContext.PRINCIPALS_SESSION_KEY);
????????????if?(attribute?==?null)?{
????????????????continue;
????????????}
????????????sysUserEntity?=?(SysUserEntity)?((SimplePrincipalCollection)?attribute).getPrimaryPrincipal();
????????????if?(sysUserEntity?==?null)?{
????????????????continue;
????????????}
????????????if?(Objects.equals(sysUserEntity.getUsername(),?username))?{
????????????????session=sessionInfo;
????????????????break;
????????????}
????????}
????????if?(session?==?null||attribute?==?null)?{
????????????return;
????????}
????????//刪除session
????????if?(isRemoveSession)?{
????????????redisSessionDAO.delete(session);
????????}
????????//刪除Cache,在訪問受限接口時會重新授權
????????DefaultWebSecurityManager?securityManager?=?(DefaultWebSecurityManager)?SecurityUtils.getSecurityManager();
????????Authenticator?authc?=?securityManager.getAuthenticator();
????????((LogoutAware)?authc).onLogout((SimplePrincipalCollection)?attribute);
????}
}
創(chuàng)建Shiro的SessionId生成器
/**
?*?@Description?自定義SessionId生成器
?*?@Author?Sans
?*?@CreateTime?2019/6/11?11:48
?*/
public?class?ShiroSessionIdGenerator?implements?SessionIdGenerator?{
????/**
?????*?實現SessionId生成
?????*?@Author?Sans
?????*?@CreateTime?2019/6/11?11:54
?????*/
????@Override
????public?Serializable?generateId(Session?session)?{
????????Serializable?sessionId?=?new?JavaUuidSessionIdGenerator().generateId(session);
????????return?String.format("login_token_%s",?sessionId);
????}
}
三。編寫Shiro核心類
創(chuàng)建Realm用于授權和認證
/**
?*?@Description?Shiro權限匹配和賬號密碼匹配
?*?@Author?Sans
?*?@CreateTime?2019/6/15?11:27
?*/
public?class?ShiroRealm?extends?AuthorizingRealm?{
????@Autowired
????private?SysUserService?sysUserService;
????@Autowired
????private?SysRoleService?sysRoleService;
????@Autowired
????private?SysMenuService?sysMenuService;
????/**
?????*?授權權限
?????*?用戶進行權限驗證時候Shiro會去緩存中找,如果查不到數據,會執(zhí)行這個方法去查權限,并放入緩存中
?????*?@Author?Sans
?????*?@CreateTime?2019/6/12?11:44
?????*/
????@Override
????protected?AuthorizationInfo?doGetAuthorizationInfo(PrincipalCollection?principalCollection)?{
????????SimpleAuthorizationInfo?authorizationInfo?=?new?SimpleAuthorizationInfo();
????????SysUserEntity?sysUserEntity?=?(SysUserEntity)?principalCollection.getPrimaryPrincipal();
????????//獲取用戶ID
????????Long?userId?=sysUserEntity.getUserId();
????????//這里可以進行授權和處理
????????Set?rolesSet?=?new?HashSet<>();
????????Set?permsSet?=?new?HashSet<>();
????????//查詢角色和權限(這里根據業(yè)務自行查詢)
????????List?sysRoleEntityList?=?sysRoleService.selectSysRoleByUserId(userId);
????????for?(SysRoleEntity?sysRoleEntity:sysRoleEntityList)?{
????????????rolesSet.add(sysRoleEntity.getRoleName());
????????????List?sysMenuEntityList?=?sysMenuService.selectSysMenuByRoleId(sysRoleEntity.getRoleId());
????????????for?(SysMenuEntity?sysMenuEntity?:sysMenuEntityList)?{
????????????????permsSet.add(sysMenuEntity.getPerms());
????????????}
????????}
????????//將查到的權限和角色分別傳入authorizationInfo中
????????authorizationInfo.setStringPermissions(permsSet);
????????authorizationInfo.setRoles(rolesSet);
????????return?authorizationInfo;
????}
????/**
?????*?身份認證
?????*?@Author?Sans
?????*?@CreateTime?2019/6/12?12:36
?????*/
????@Override
????protected?AuthenticationInfo?doGetAuthenticationInfo(AuthenticationToken?authenticationToken)?throws?AuthenticationException?{
????????//獲取用戶的輸入的賬號.
????????String?username?=?(String)?authenticationToken.getPrincipal();
????????//通過username從數據庫中查找?User對象,如果找到進行驗證
????????//實際項目中,這里可以根據實際情況做緩存,如果不做,Shiro自己也是有時間間隔機制,2分鐘內不會重復執(zhí)行該方法
????????SysUserEntity?user?=?sysUserService.selectUserByName(username);
????????//判斷賬號是否存在
????????if?(user?==?null)?{
????????????throw?new?AuthenticationException();
????????}
????????//判斷賬號是否被凍結
????????if?(user.getState()==null||user.getState().equals("PROHIBIT")){
????????????throw?new?LockedAccountException();
????????}
????????//進行驗證
????????SimpleAuthenticationInfo?authenticationInfo?=?new?SimpleAuthenticationInfo(
????????????????user,??????????????????????????????????//用戶名
????????????????user.getPassword(),????????????????????//密碼
????????????????ByteSource.Util.bytes(user.getSalt()),?//設置鹽值
????????????????getName()
????????);
????????//驗證成功開始踢人(清除緩存和Session)
????????ShiroUtils.deleteCache(username,true);
????????return?authenticationInfo;
????}
}
創(chuàng)建SessionManager類
/**
?*?@Description?自定義獲取Token
?*?@Author?Sans
?*?@CreateTime?2019/6/13?8:34
?*/
public?class?ShiroSessionManager?extends?DefaultWebSessionManager?{
????//定義常量
????private?static?final?String?AUTHORIZATION?=?"Authorization";
????private?static?final?String?REFERENCED_SESSION_ID_SOURCE?=?"Stateless?request";
????//重寫構造器
????public?ShiroSessionManager()?{
????????super();
????????this.setDeleteInvalidSessions(true);
????}
????/**
?????*?重寫方法實現從請求頭獲取Token便于接口統(tǒng)一
?????*?每次請求進來,Shiro會去從請求頭找Authorization這個key對應的Value(Token)
?????*?@Author?Sans
?????*?@CreateTime?2019/6/13?8:47
?????*/
????@Override
????public?Serializable?getSessionId(ServletRequest?request,?ServletResponse?response)?{
????????String?token?=?WebUtils.toHttp(request).getHeader(AUTHORIZATION);
????????//如果請求頭中存在token?則從請求頭中獲取token
????????if?(!StringUtils.isEmpty(token))?{
????????????request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_SOURCE,?REFERENCED_SESSION_ID_SOURCE);
????????????request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID,?token);
????????????request.setAttribute(ShiroHttpServletRequest.REFERENCED_SESSION_ID_IS_VALID,?Boolean.TRUE);
????????????return?token;
????????}?else?{
????????????//?這里禁用掉Cookie獲取方式
????????????//?按默認規(guī)則從Cookie取Token
????????????//?return?super.getSessionId(request,?response);
????????????return?null;
????????}
????}
}
創(chuàng)建ShiroConfig配置類
/**
?*?@Description?Shiro配置類
?*?@Author?Sans
?*?@CreateTime?2019/6/10?17:42
?*/
@Configuration
public?class?ShiroConfig?{
????private?final?String?CACHE_KEY?=?"shiro:cache:";
????private?final?String?SESSION_KEY?=?"shiro:session:";
????private?final?int?EXPIRE?=?1800;
????//Redis配置
????@Value("${spring.redis.host}")
????private?String?host;
????@Value("${spring.redis.port}")
????private?int?port;
????@Value("${spring.redis.timeout}")
????private?int?timeout;
????@Value("${spring.redis.password}")
????private?String?password;
????/**
?????*?開啟Shiro-aop注解支持
?????*?@Attention?使用代理方式所以需要開啟代碼支持
?????*?@Author?Sans
?????*?@CreateTime?2019/6/12?8:38
?????*/
????@Bean
????public?AuthorizationAttributeSourceAdvisor?authorizationAttributeSourceAdvisor(SecurityManager?securityManager)?{
????????AuthorizationAttributeSourceAdvisor?authorizationAttributeSourceAdvisor?=?new?AuthorizationAttributeSourceAdvisor();
????????authorizationAttributeSourceAdvisor.setSecurityManager(securityManager);
????????return?authorizationAttributeSourceAdvisor;
????}
????/**
?????*?Shiro基礎配置
?????*?@Author?Sans
?????*?@CreateTime?2019/6/12?8:42
?????*/
????@Bean
????public?ShiroFilterFactoryBean?shiroFilterFactory(SecurityManager?securityManager){
????????ShiroFilterFactoryBean?shiroFilterFactoryBean?=?new?ShiroFilterFactoryBean();
????????shiroFilterFactoryBean.setSecurityManager(securityManager);
????????Map?filterChainDefinitionMap?=?new?LinkedHashMap<>();
????????//?注意過濾器配置順序不能顛倒
????????//?配置過濾:不會被攔截的鏈接
????????filterChainDefinitionMap.put("/static/**",?"anon");
????????filterChainDefinitionMap.put("/userLogin/**",?"anon");
????????filterChainDefinitionMap.put("/**",?"authc");
????????//?配置shiro默認登錄界面地址,前后端分離中登錄界面跳轉應由前端路由控制,后臺僅返回json數據
????????shiroFilterFactoryBean.setLoginUrl("/userLogin/unauth");
????????shiroFilterFactoryBean.setFilterChainDefinitionMap(filterChainDefinitionMap);
????????return?shiroFilterFactoryBean;
????}
????/**
?????*?安全管理器
?????*?@Author?Sans
?????*?@CreateTime?2019/6/12?10:34
?????*/
????@Bean
????public?SecurityManager?securityManager()?{
????????DefaultWebSecurityManager?securityManager?=?new?DefaultWebSecurityManager();
????????//?自定義Ssession管理
????????securityManager.setSessionManager(sessionManager());
????????//?自定義Cache實現
????????securityManager.setCacheManager(cacheManager());
????????//?自定義Realm驗證
????????securityManager.setRealm(shiroRealm());
????????return?securityManager;
????}
????/**
?????*?身份驗證器
?????*?@Author?Sans
?????*?@CreateTime?2019/6/12?10:37
?????*/
????@Bean
????public?ShiroRealm?shiroRealm()?{
????????ShiroRealm?shiroRealm?=?new?ShiroRealm();
????????shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
????????return?shiroRealm;
????}
????/**
?????*?憑證匹配器
?????*?將密碼校驗交給Shiro的SimpleAuthenticationInfo進行處理,在這里做匹配配置
?????*?@Author?Sans
?????*?@CreateTime?2019/6/12?10:48
?????*/
????@Bean
????public?HashedCredentialsMatcher?hashedCredentialsMatcher()?{
????????HashedCredentialsMatcher?shaCredentialsMatcher?=?new?HashedCredentialsMatcher();
????????//?散列算法:這里使用SHA256算法;
????????shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);
????????//?散列的次數,比如散列兩次,相當于?md5(md5(""));
????????shaCredentialsMatcher.setHashIterations(SHA256Util.HASH_ITERATIONS);
????????return?shaCredentialsMatcher;
????}
????/**
?????*?配置Redis管理器
?????*?@Attention?使用的是shiro-redis開源插件
?????*?@Author?Sans
?????*?@CreateTime?2019/6/12?11:06
?????*/
????@Bean
????public?RedisManager?redisManager()?{
????????RedisManager?redisManager?=?new?RedisManager();
????????redisManager.setHost(host);
????????redisManager.setPort(port);
????????redisManager.setTimeout(timeout);
????????redisManager.setPassword(password);
????????return?redisManager;
????}
????/**
?????*?配置Cache管理器
?????*?用于往Redis存儲權限和角色標識
?????*?@Attention?使用的是shiro-redis開源插件
?????*?@Author?Sans
?????*?@CreateTime?2019/6/12?12:37
?????*/
????@Bean
????public?RedisCacheManager?cacheManager()?{
????????RedisCacheManager?redisCacheManager?=?new?RedisCacheManager();
????????redisCacheManager.setRedisManager(redisManager());
????????redisCacheManager.setKeyPrefix(CACHE_KEY);
????????//?配置緩存的話要求放在session里面的實體類必須有個id標識
????????redisCacheManager.setPrincipalIdFieldName("userId");
????????return?redisCacheManager;
????}
????/**
?????*?SessionID生成器
?????*?@Author?Sans
?????*?@CreateTime?2019/6/12?13:12
?????*/
????@Bean
????public?ShiroSessionIdGenerator?sessionIdGenerator(){
????????return?new?ShiroSessionIdGenerator();
????}
????/**
?????*?配置RedisSessionDAO
?????*?@Attention?使用的是shiro-redis開源插件
?????*?@Author?Sans
?????*?@CreateTime?2019/6/12?13:44
?????*/
????@Bean
????public?RedisSessionDAO?redisSessionDAO()?{
????????RedisSessionDAO?redisSessionDAO?=?new?RedisSessionDAO();
????????redisSessionDAO.setRedisManager(redisManager());
????????redisSessionDAO.setSessionIdGenerator(sessionIdGenerator());
????????redisSessionDAO.setKeyPrefix(SESSION_KEY);
????????redisSessionDAO.setExpire(expire);
????????return?redisSessionDAO;
????}
????/**
?????*?配置Session管理器
?????*?@Author?Sans
?????*?@CreateTime?2019/6/12?14:25
?????*/
????@Bean
????public?SessionManager?sessionManager()?{
????????ShiroSessionManager?shiroSessionManager?=?new?ShiroSessionManager();
????????shiroSessionManager.setSessionDAO(redisSessionDAO());
????????return?shiroSessionManager;
????}
}
四。實現權限控制
Shiro可以使用代碼或注解來控制權限,通常我們使用注解控制,既簡單方便,而且更加靈活。
| 注解名稱 | 說明 |
|---|---|
| 需要認證 | 使用該注解標注的類,方法等在訪問時,當前主題必須在當前會話中已經過認證。 |
| 需要客人 | 使用該注解標注的類,方法等在訪問時,當前主題可以是“ gust”身份,不需要經過認證或者在原先的會話中存在記錄。 |
| 要求用戶 | 驗證用戶是否被記憶,有兩種含義:一種是成功登錄的(subject.isAuthenticated()結果為true);另一種是被記憶的(subject.isRemembered()結果為true)。 |
| 需要權限 | 當前Subject需要擁有某些特定的權限時,才能執(zhí)行被該注解注釋的方法。如果沒有權限,則方法將不會執(zhí)行引發(fā)AuthorizationException異常。 |
| 需要角色 | 當前Subject必須擁有所有指定的角色時,才能訪問被該注解標注的方法。如果沒有角色,則方法將不會執(zhí)行引發(fā)拋出AuthorizationException異常。 |
一般情況下我們在項目中做權限控制,使用最多的是RequiresPermissions和RequiresRoles,允許存在多個角色和權限,成為邏輯是AND,也就是同時擁有這些才可以訪問方法,可以在注解中以參數的形式設置成OR
示例
//擁有一個角色就可以訪問
@RequiresRoles(value={"ADMIN","USER"},logical?=?Logical.OR)
//擁有所有權限才可以訪問
@RequiresPermissions(value={"sys:user:info","sys:role:info"},logical?=?Logical.AND)
使用順序:Shiro注解是存在順序的,當多個注解在一個方法上的時候,會逐個檢查,知道全部通過為止,交替攔截順序是:RequiresRoles-> RequiresPermissions-> RequiresAuthentication-> RequiresUser-> RequiresGuest
示例
//擁有ADMIN角色同時還要有sys:role:info權限
@RequiresRoles(value={"ADMIN")
@RequiresPermissions("sys:role:info")
創(chuàng)建UserRoleController角色攔截測試類
/**
?*?@Description?角色測試
?*?@Author?Sans
?*?@CreateTime?2019/6/19?11:38
?*/
@RestController
@RequestMapping("/role")
public?class?UserRoleController?{
????@Autowired
????private?SysUserService?sysUserService;
????@Autowired
????private?SysRoleService?sysRoleService;
????@Autowired
????private?SysMenuService?sysMenuService;
????@Autowired
????private?SysRoleMenuService?sysRoleMenuService;
????/**
?????*?管理員角色測試接口
?????*?@Author?Sans
?????*?@CreateTime?2019/6/19?10:38
?????*?@Return?Map?返回結果
?????*/
????@RequestMapping("/getAdminInfo")
????@RequiresRoles("ADMIN")
????public?Map?getAdminInfo() {
????????Map?map?=?new?HashMap<>();
????????map.put("code",200);
????????map.put("msg","這里是只有管理員角色能訪問的接口");
????????return?map;
????}
????/**
?????*?用戶角色測試接口
?????*?@Author?Sans
?????*?@CreateTime?2019/6/19?10:38
?????*?@Return?Map?返回結果
?????*/
????@RequestMapping("/getUserInfo")
????@RequiresRoles("USER")
????public?Map?getUserInfo() {
????????Map?map?=?new?HashMap<>();
????????map.put("code",200);
????????map.put("msg","這里是只有用戶角色能訪問的接口");
????????return?map;
????}
????/**
?????*?角色測試接口
?????*?@Author?Sans
?????*?@CreateTime?2019/6/19?10:38
?????*?@Return?Map?返回結果
?????*/
????@RequestMapping("/getRoleInfo")
????@RequiresRoles(value={"ADMIN","USER"},logical?=?Logical.OR)
????@RequiresUser
????public?Map?getRoleInfo() {
????????Map?map?=?new?HashMap<>();
????????map.put("code",200);
????????map.put("msg","這里是只要有ADMIN或者USER角色能訪問的接口");
????????return?map;
????}
????/**
?????*?登出(測試登出)
?????*?@Author?Sans
?????*?@CreateTime?2019/6/19?10:38
?????*?@Return?Map?返回結果
?????*/
????@RequestMapping("/getLogout")
????@RequiresUser
????public?Map?getLogout() {
????????ShiroUtils.logout();
????????Map?map?=?new?HashMap<>();
????????map.put("code",200);
????????map.put("msg","登出");
????????return?map;
????}
}
創(chuàng)建UserMenuController權限攔截測試類
/**
?*?@Description?權限測試
?*?@Author?Sans
?*?@CreateTime?2019/6/19?11:38
?*/
@RestController
@RequestMapping("/menu")
public?class?UserMenuController?{
????@Autowired
????private?SysUserService?sysUserService;
????@Autowired
????private?SysRoleService?sysRoleService;
????@Autowired
????private?SysMenuService?sysMenuService;
????@Autowired
????private?SysRoleMenuService?sysRoleMenuService;
????/**
?????*?獲取用戶信息集合
?????*?@Author?Sans
?????*?@CreateTime?2019/6/19?10:36
?????*?@Return?Map?返回結果
?????*/
????@RequestMapping("/getUserInfoList")
????@RequiresPermissions("sys:user:info")
????public?Map?getUserInfoList() {
????????Map?map?=?new?HashMap<>();
????????List?sysUserEntityList?=?sysUserService.list();
????????map.put("sysUserEntityList",sysUserEntityList);
????????return?map;
????}
????/**
?????*?獲取角色信息集合
?????*?@Author?Sans
?????*?@CreateTime?2019/6/19?10:37
?????*?@Return?Map?返回結果
?????*/
????@RequestMapping("/getRoleInfoList")
????@RequiresPermissions("sys:role:info")
????public?Map?getRoleInfoList() {
????????Map?map?=?new?HashMap<>();
????????List?sysRoleEntityList?=?sysRoleService.list();
????????map.put("sysRoleEntityList",sysRoleEntityList);
????????return?map;
????}
????/**
?????*?獲取權限信息集合
?????*?@Author?Sans
?????*?@CreateTime?2019/6/19?10:38
?????*?@Return?Map?返回結果
?????*/
????@RequestMapping("/getMenuInfoList")
????@RequiresPermissions("sys:menu:info")
????public?Map?getMenuInfoList() {
????????Map?map?=?new?HashMap<>();
????????List?sysMenuEntityList?=?sysMenuService.list();
????????map.put("sysMenuEntityList",sysMenuEntityList);
????????return?map;
????}
????/**
?????*?獲取所有數據
?????*?@Author?Sans
?????*?@CreateTime?2019/6/19?10:38
?????*?@Return?Map?返回結果
?????*/
????@RequestMapping("/getInfoAll")
????@RequiresPermissions("sys:info:all")
????public?Map?getInfoAll() {
????????Map?map?=?new?HashMap<>();
????????List?sysUserEntityList?=?sysUserService.list();
????????map.put("sysUserEntityList",sysUserEntityList);
????????List?sysRoleEntityList?=?sysRoleService.list();
????????map.put("sysRoleEntityList",sysRoleEntityList);
????????List?sysMenuEntityList?=?sysMenuService.list();
????????map.put("sysMenuEntityList",sysMenuEntityList);
????????return?map;
????}
????/**
?????*?添加管理員角色權限(測試動態(tài)權限更新)
?????*?@Author?Sans
?????*?@CreateTime?2019/6/19?10:39
?????*?@Param??username?用戶ID
?????*?@Return?Map?返回結果
?????*/
????@RequestMapping("/addMenu")
????public?Map?addMenu() {
????????//添加管理員角色權限
????????SysRoleMenuEntity?sysRoleMenuEntity?=?new?SysRoleMenuEntity();
????????sysRoleMenuEntity.setMenuId(4L);
????????sysRoleMenuEntity.setRoleId(1L);
????????sysRoleMenuService.save(sysRoleMenuEntity);
????????//清除緩存
????????String?username?=?"admin";
????????ShiroUtils.deleteCache(username,false);
????????Map?map?=?new?HashMap<>();
????????map.put("code",200);
????????map.put("msg","權限添加成功");
????????return?map;
????}
}
創(chuàng)建UserLoginController登錄類
/**
?*?@Description?用戶登錄
?*?@Author?Sans
?*?@CreateTime?2019/6/17?15:21
?*/
@RestController
@RequestMapping("/userLogin")
public?class?UserLoginController?{
????/**
?????*?登錄
?????*?@Author?Sans
?????*?@CreateTime?2019/6/20?9:21
?????*/
????@RequestMapping("/login")
????public?Map?login(@RequestBody?SysUserEntity?sysUserEntity) {
????????Map?map?=?new?HashMap<>();
????????//進行身份驗證
????????try{
????????????//驗證身份和登錄
????????????Subject?subject?=?SecurityUtils.getSubject();
????????????UsernamePasswordToken?token?=?new?UsernamePasswordToken(sysUserEntity.getUsername(),?sysUserEntity.getPassword());
????????????//驗證成功進行登錄操作
????????????subject.login(token);
????????}catch?(IncorrectCredentialsException?e)?{
????????????map.put("code",500);
????????????map.put("msg","用戶不存在或者密碼錯誤");
????????????return?map;
????????}?catch?(LockedAccountException?e)?{
????????????map.put("code",500);
????????????map.put("msg","登錄失敗,該用戶已被凍結");
????????????return?map;
????????}?catch?(AuthenticationException?e)?{
????????????map.put("code",500);
????????????map.put("msg","該用戶不存在");
????????????return?map;
????????}?catch?(Exception?e)?{
????????????map.put("code",500);
????????????map.put("msg","未知異常");
????????????return?map;
????????}
????????map.put("code",0);
????????map.put("msg","登錄成功");
????????map.put("token",ShiroUtils.getSession().getId().toString());
????????return?map;
????}
????/**
?????*?未登錄
?????*?@Author?Sans
?????*?@CreateTime?2019/6/20?9:22
?????*/
????@RequestMapping("/unauth")
????public?Map?unauth() {
????????Map?map?=?new?HashMap<>();
????????map.put("code",500);
????????map.put("msg","未登錄");
????????return?map;
????}
}
五.POSTMAN測試
登錄成功后會返回TOKEN,因為是單點登錄,再次登錄的話會返回新的TOKEN,然后Redis的TOKEN就會失效了

當第一次訪問接口后我們可以看到緩存中已經有權限數據了,在次訪問接口的時候,Shiro會直接去緩存中拿取權限,注意訪問接口時候要設置請求頭。


ADMIN這個號現在沒有sys:info:all這個權限的,所以無法訪問getInfoAll接口,我們要動態(tài)分配權限后,要清掉緩存,在訪問接口時候,Shiro會去重新執(zhí)行授權方法,之后再次把權限和角色數據放入緩存中

訪問添加權限測試接口,因為是測試,我把增加權限的用戶ADMIN寫死在里面了,權限添加后,調用工具類清掉緩存,我們可以發(fā)現,Redis中已經沒有緩存了


首次訪問getInfoAll接口,因為緩存中沒有數據,Shiro會重新授權查詢權限,攔截通過

六。項目源碼
編碼云:https : //gitee.com/liselotte/spring-boot-shiro-demo
GitHub:https : //github.com/xuyulong2017/my-java-demo
END
有熱門推薦?
2.?突然就懵了!面試官問我:線程池中多余的線程是如何回收的?
3.?星巴克不使用兩階段提交
最近面試BAT,整理一份面試資料《Java面試BATJ通關手冊》,覆蓋了Java核心技術、JVM、Java并發(fā)、SSM、微服務、數據庫、數據結構等等。
獲取方式:點“在看”,關注公眾號并回復?Java?領取,更多內容陸續(xù)奉上。
文章有幫助的話,在看,轉發(fā)吧。
謝謝支持喲 (*^__^*)

