SpringBoot整合Shiro實現動態權限加載更新+共享+單點登錄

關注Java後端技術棧

回覆“面試”獲取最新資料

回覆“加羣”邀您進技術交流羣

  • 一.說明

  • 二.項目環境

  • 二.編寫項目基礎類

  • 三.編寫Shiro核心類

  • 四.實現權限控制

  • 五.POSTMAN測試

  • 六.項目源碼

一.說明

Shiro是一個安全框架,項目中主要用它做認證,授權,加密,以及用戶的會話管理,雖然Shiro沒有SpringSecurity功能更豐富,但是它輕量,簡單,在項目中通常業務需求Shiro也都能勝任.

二.項目環境

MyBatis-Plus版本: 3.1.0

SpringBoot版本:2.1.5

JDK版本:1.8

Shiro版本:1.4

Shiro-redis插件版本:3.1.0

數據表(SQL文件在項目中):數據庫中測試號的密碼進行了加密,密碼皆爲123456

數據表名中文表名備註說明
sys_user系統用戶表基礎表
sys_menu權限表基礎表
sys_role角色表基礎表
sys_role_menu角色與權限關係表中間表
sys_user_role用戶與角色關係表中間表

Maven依賴如下:

<dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <scope>runtime</scope>
        </dependency>
        <!-- AOP依賴,一定要加,否則權限攔截驗證不生效 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!-- lombok插件 -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
        <!-- Redis -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis-reactive</artifactId>
        </dependency>
        <!-- mybatisPlus 核心庫 -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!-- 引入阿里數據庫連接池 -->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>1.1.6</version>
        </dependency>
        <!-- Shiro 核心依賴 -->
        <dependency>
            <groupId>org.apache.shiro</groupId>
            <artifactId>shiro-spring</artifactId>
            <version>1.4.0</version>
        </dependency>
        <!-- Shiro-redis插件 -->
        <dependency>
            <groupId>org.crazycake</groupId>
            <artifactId>shiro-redis</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!-- StringUtilS工具 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.5</version>
        </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:
    # 是否開啓自動駝峯命名規則映射:從數據庫列名到Java屬性駝峯命名的類似映射
    map-underscore-to-camel-case:true
    # 返回map時true:當查詢數據爲空時字段返回爲null,false:不加這個查詢數據爲空時,字段將被隱藏
    call-setters-on-nulls:true
    # 這個配置會將執行的sql打印出來,在開發或測試的時候可以用
    log-impl:org.apache.ibatis.logging.stdout.StdOutImpl

二.編寫項目基礎類

用戶實體,Dao,Service等在這裏省略,請參考源碼

編寫Exception類來處理Shiro權限攔截異常

/**
 * @Description 自定義異常
 * @Author Sans
 * @CreateTime 2019/6/15 22:56
 */
@ControllerAdvice
publicclass MyShiroException {
    /**
     * 處理Shiro權限攔截異常
     * 如果返回JSON數據格式請加上 @ResponseBody註解
     * @Author Sans
     * @CreateTime 2019/6/15 13:35
     * @Return Map<Object> 返回結果集
     */
    @ResponseBody
    @ExceptionHandler(value = AuthorizationException.class)
    public Map<String,Object> defaultErrorHandler(){
        Map<String,Object> map = new HashMap<>();
        map.put("403","權限不足");
        return map;
    }
}

創建SHA256Util加密工具

/**
 * @Description Sha-256加密工具
 * @Author Sans
 * @CreateTime 2019/6/12 9:27
 */
publicclass SHA256Util {
    /**  私有構造器 **/
    private SHA256Util(){};
    /**  加密算法 **/
    publicfinalstatic String HASH_ALGORITHM_NAME = "SHA-256";
    /**  循環次數 **/
    publicfinalstaticint HASH_ITERATIONS = 15;
    /**  執行加密-採用SHA256和鹽值加密 **/
    public static String sha256(String password, String salt) {
        returnnew SimpleHash(HASH_ALGORITHM_NAME, password, salt, HASH_ITERATIONS).toString();
    }
}

創建Spring工具

/**
 * @Description Spring上下文工具類
 * @Author Sans
 * @CreateTime 2019/6/17 13:40
 */
@Component
publicclass SpringUtil implements ApplicationContextAware {
    privatestatic 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
     */
    publicstatic <T> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }
}

創建Shiro工具

/**
 * @Description Shiro工具類
 * @Author Sans
 * @CreateTime 2019/6/15 16:11
 */
publicclass ShiroUtils {

    /** 私有構造器 **/
    private ShiroUtils(){}

    privatestatic 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<Session> 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);
    }
}

創建Shiro的SessionId生成器

/**
 * @Description 自定義SessionId生成器
 * @Author Sans
 * @CreateTime 2019/6/11 11:48
 */
publicclass 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核心類

創建Realm用於授權和認證

/**
 * @Description Shiro權限匹配和賬號密碼匹配
 * @Author Sans
 * @CreateTime 2019/6/15 11:27
 */
publicclass ShiroRealm extends AuthorizingRealm {
    @Autowired
    private SysUserService sysUserService;
    @Autowired
    private SysRoleService sysRoleService;
    @Autowired
    private SysMenuService sysMenuService;
    /**
     * 授權權限
     * 用戶進行權限驗證時候Shiro會去緩存中找,如果查不到數據,會執行這個方法去查權限,並放入緩存中
     * @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<String> rolesSet = new HashSet<>();
        Set<String> permsSet = new HashSet<>();
        //查詢角色和權限(這裏根據業務自行查詢)
        List<SysRoleEntity> sysRoleEntityList = sysRoleService.selectSysRoleByUserId(userId);
        for (SysRoleEntity sysRoleEntity:sysRoleEntityList) {
            rolesSet.add(sysRoleEntity.getRoleName());
            List<SysMenuEntity> 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分鐘內不會重複執行該方法
        SysUserEntity user = sysUserService.selectUserByName(username);
        //判斷賬號是否存在
        if (user == null) {
            thrownew AuthenticationException();
        }
        //判斷賬號是否被凍結
        if (user.getState()==null||user.getState().equals("PROHIBIT")){
            thrownew LockedAccountException();
        }
        //進行驗證
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user,                                  //用戶名
                user.getPassword(),                    //密碼
                ByteSource.Util.bytes(user.getSalt()), //設置鹽值
                getName()
        );
        //驗證成功開始踢人(清除緩存和Session)
        ShiroUtils.deleteCache(username,true);
        return authenticationInfo;
    }
}

創建SessionManager類

/**
 * @Description 自定義獲取Token
 * @Author Sans
 * @CreateTime 2019/6/13 8:34
 */
publicclass ShiroSessionManager extends DefaultWebSessionManager {
    //定義常量
    privatestaticfinal String AUTHORIZATION = "Authorization";
    privatestaticfinal String REFERENCED_SESSION_ID_SOURCE = "Stateless request";
    //重寫構造器
    public ShiroSessionManager() {
        super();
        this.setDeleteInvalidSessions(true);
    }
    /**
     * 重寫方法實現從請求頭獲取Token便於接口統一
     * 每次請求進來,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獲取方式
            // 按默認規則從Cookie取Token
            // return super.getSessionId(request, response);
            returnnull;
        }
    }
}


創建ShiroConfig配置類

/**
 * @Description Shiro配置類
 * @Author Sans
 * @CreateTime 2019/6/10 17:42
 */
@Configuration
publicclass ShiroConfig {

    privatefinal String CACHE_KEY = "shiro:cache:";
    privatefinal String SESSION_KEY = "shiro:session:";
    privatefinalint EXPIRE = 1800;

    //Redis配置
    @Value("${spring.redis.host}")
    private String host;
    @Value("${spring.redis.port}")
    privateint port;
    @Value("${spring.redis.timeout}")
    privateint 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<String, String> 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(){
        returnnew 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可以用代碼或者註解來控制權限,通常我們使用註解控制,不僅簡單方便,而且更加靈活.Shiro註解一共有五個:

註解名稱說明
RequiresAuthentication使用該註解標註的類,方法等在訪問時,當前Subject必須在當前session中已經過認證.
RequiresGuest使用該註解標註的類,方法等在訪問時,當前Subject可以是“gust”身份,不需要經過認證或者在原先的session中存在記錄.
RequiresUser驗證用戶是否被記憶,有兩種含義:一種是成功登錄的(subject.isAuthenticated()結果爲true);另外一種是被記憶的(subject.isRemembered()結果爲true).
RequiresPermissions當前Subject需要擁有某些特定的權限時,才能執行被該註解標註的方法.如果沒有權限,則方法不會執行還會拋出AuthorizationException異常.
RequiresRoles當前Subject必須擁有所有指定的角色時,才能訪問被該註解標註的方法.如果沒有角色,則方法不會執行還會拋出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")


創建UserRoleController角色攔截測試類

/**
 * @Description 角色測試
 * @Author Sans
 * @CreateTime 2019/6/19 11:38
 */
@RestController
@RequestMapping("/role")
publicclass 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<String,Object> 返回結果
     */
    @RequestMapping("/getAdminInfo")
    @RequiresRoles("ADMIN")
    public Map<String,Object> getAdminInfo(){
        Map<String,Object> map = new HashMap<>();
        map.put("code",200);
        map.put("msg","這裏是只有管理員角色能訪問的接口");
        return map;
    }

    /**
     * 用戶角色測試接口
     * @Author Sans
     * @CreateTime 2019/6/19 10:38
     * @Return Map<String,Object> 返回結果
     */
    @RequestMapping("/getUserInfo")
    @RequiresRoles("USER")
    public Map<String,Object> getUserInfo(){
        Map<String,Object> map = new HashMap<>();
        map.put("code",200);
        map.put("msg","這裏是只有用戶角色能訪問的接口");
        return map;
    }

    /**
     * 角色測試接口
     * @Author Sans
     * @CreateTime 2019/6/19 10:38
     * @Return Map<String,Object> 返回結果
     */
    @RequestMapping("/getRoleInfo")
    @RequiresRoles(value={"ADMIN","USER"},logical = Logical.OR)
    @RequiresUser
    public Map<String,Object> getRoleInfo(){
        Map<String,Object> map = new HashMap<>();
        map.put("code",200);
        map.put("msg","這裏是只要有ADMIN或者USER角色能訪問的接口");
        return map;
    }

    /**
     * 登出(測試登出)
     * @Author Sans
     * @CreateTime 2019/6/19 10:38
     * @Return Map<String,Object> 返回結果
     */
    @RequestMapping("/getLogout")
    @RequiresUser
    public Map<String,Object> getLogout(){
        ShiroUtils.logout();
        Map<String,Object> map = new HashMap<>();
        map.put("code",200);
        map.put("msg","登出");
        return map;
    }
}


創建UserMenuController權限攔截測試類

/**
 * @Description 權限測試
 * @Author Sans
 * @CreateTime 2019/6/19 11:38
 */
@RestController
@RequestMapping("/menu")
publicclass 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<String,Object> 返回結果
     */
    @RequestMapping("/getUserInfoList")
    @RequiresPermissions("sys:user:info")
    public Map<String,Object> getUserInfoList(){
        Map<String,Object> map = new HashMap<>();
        List<SysUserEntity> sysUserEntityList = sysUserService.list();
        map.put("sysUserEntityList",sysUserEntityList);
        return map;
    }

    /**
     * 獲取角色信息集合
     * @Author Sans
     * @CreateTime 2019/6/19 10:37
     * @Return Map<String,Object> 返回結果
     */
    @RequestMapping("/getRoleInfoList")
    @RequiresPermissions("sys:role:info")
    public Map<String,Object> getRoleInfoList(){
        Map<String,Object> map = new HashMap<>();
        List<SysRoleEntity> sysRoleEntityList = sysRoleService.list();
        map.put("sysRoleEntityList",sysRoleEntityList);
        return map;
    }

    /**
     * 獲取權限信息集合
     * @Author Sans
     * @CreateTime 2019/6/19 10:38
     * @Return Map<String,Object> 返回結果
     */
    @RequestMapping("/getMenuInfoList")
    @RequiresPermissions("sys:menu:info")
    public Map<String,Object> getMenuInfoList(){
        Map<String,Object> map = new HashMap<>();
        List<SysMenuEntity> sysMenuEntityList = sysMenuService.list();
        map.put("sysMenuEntityList",sysMenuEntityList);
        return map;
    }

    /**
     * 獲取所有數據
     * @Author Sans
     * @CreateTime 2019/6/19 10:38
     * @Return Map<String,Object> 返回結果
     */
    @RequestMapping("/getInfoAll")
    @RequiresPermissions("sys:info:all")
    public Map<String,Object> getInfoAll(){
        Map<String,Object> map = new HashMap<>();
        List<SysUserEntity> sysUserEntityList = sysUserService.list();
        map.put("sysUserEntityList",sysUserEntityList);
        List<SysRoleEntity> sysRoleEntityList = sysRoleService.list();
        map.put("sysRoleEntityList",sysRoleEntityList);
        List<SysMenuEntity> sysMenuEntityList = sysMenuService.list();
        map.put("sysMenuEntityList",sysMenuEntityList);
        return map;
    }

    /**
     * 添加管理員角色權限(測試動態權限更新)
     * @Author Sans
     * @CreateTime 2019/6/19 10:39
     * @Param  username 用戶ID
     * @Return Map<String,Object> 返回結果
     */
    @RequestMapping("/addMenu")
    public Map<String,Object> addMenu(){
        //添加管理員角色權限
        SysRoleMenuEntity sysRoleMenuEntity = new SysRoleMenuEntity();
        sysRoleMenuEntity.setMenuId(4L);
        sysRoleMenuEntity.setRoleId(1L);
        sysRoleMenuService.save(sysRoleMenuEntity);
        //清除緩存
        String username = "admin";
        ShiroUtils.deleteCache(username,false);
        Map<String,Object> map = new HashMap<>();
        map.put("code",200);
        map.put("msg","權限添加成功");
        return map;
    }
}

創建UserLoginController登錄類

/**
 * @Description 用戶登錄
 * @Author Sans
 * @CreateTime 2019/6/17 15:21
 */
@RestController
@RequestMapping("/userLogin")
publicclass UserLoginController {

    /**
     * 登錄
     * @Author Sans
     * @CreateTime 2019/6/20 9:21
     */
    @RequestMapping("/login")
    public Map<String,Object> login(@RequestBody SysUserEntity sysUserEntity){
        Map<String,Object> 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<String,Object> unauth(){
        Map<String,Object> map = new HashMap<>();
        map.put("code",500);
        map.put("msg","未登錄");
        return map;
    }
}

五.POSTMAN測試

登錄成功後會返回TOKEN,因爲是單點登錄,再次登陸的話會返回新的TOKEN,之前Redis的TOKEN就會失效了

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

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

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

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

六.項目源碼

碼雲:https://gitee.com/liselotte/spring-boot-shiro-demo

GitHub:https://github.com/xuyulong2017/my-java-demo

來源:juejin.im/post/5d087d605188256de9779e64

推薦閱讀

Spring 最核心的註解,都是乾貨!

網絡常見的 9 大命令

volatile 手摸手帶你解析

不可不知的 7 個 JDK 命令

目前10000+ 人已關注加入我們

       

       

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章