SpringBoot整合Shiro實(shí)現(xiàn)動(dòng)態(tài)權(quán)限加載更新+Session共享+單點(diǎn)登錄

一. 說明

Shiro是一個(gè)安全框架,項(xiàng)目中主要用它做認(rèn)證,授權(quán),加密,以及用戶的會(huì)話管理,雖然Shiro沒有SpringSecurity功能更豐富,但是它輕量,簡(jiǎn)單,在項(xiàng)目中通常業(yè)務(wù)需求Shiro也都能勝任.

二. 項(xiàng)目環(huán)境

  • MyBatis-Plus版本: 3.1.0

  • SpringBoot版本:2.1.5

  • JDK版本:1.8

  • Shiro版本:1.4

  • Shiro-redis插件版本:3.1.0

數(shù)據(jù)表(SQL文件在項(xiàng)目中):數(shù)據(jù)庫(kù)中測(cè)試號(hào)的密碼進(jìn)行了加密,密碼皆為123456

image

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依賴,一定要加,否則權(quán)限攔截驗(yàn)證不生效 -->
        <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 核心庫(kù) -->
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.1.0</version>
        </dependency>
        <!-- 引入阿里數(shù)據(jù)庫(kù)連接池 -->
        <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>
        <!-- StringUitlS工具 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>3.5</version>
        </dependency>
</dependencies>

配置如下:

# 配置端口
server:
  port: 8764
spring:
  # 配置數(shù)據(jù)源
  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數(shù)據(jù)源
  redis:
    host: localhost
    port: 6379
    timeout: 6000
    password: 123456
    jedis:
      pool:
        max-active: 1000  # 連接池最大連接數(shù)(使用負(fù)值表示沒有限制)
        max-wait: -1      # 連接池最大阻塞等待時(shí)間(使用負(fù)值表示沒有限制)
        max-idle: 10      # 連接池中的最大空閑連接
        min-idle: 5       # 連接池中的最小空閑連接
# mybatis-plus相關(guān)配置
mybatis-plus:
  # xml掃描,多個(gè)目錄用逗號(hào)或者分號(hào)分隔(告訴 Mapper 所對(duì)應(yīng)的 XML 文件位置)
  mapper-locations: classpath:mapper/*.xml
  # 以下配置均有默認(rèn)值,可以不設(shè)置
  global-config:
    db-config:
      #主鍵類型 AUTO:"數(shù)據(jù)庫(kù)ID自增" INPUT:"用戶輸入ID",ID_WORKER:"全局唯一ID (數(shù)字類型唯一ID)", UUID:"全局唯一ID UUID";
      id-type: auto
      #字段策略 IGNORED:"忽略判斷"  NOT_NULL:"非 NULL 判斷")  NOT_EMPTY:"非空判斷"
      field-strategy: NOT_EMPTY
      #數(shù)據(jù)庫(kù)類型
      db-type: MYSQL
  configuration:
    # 是否開啟自動(dòng)駝峰命名規(guī)則映射:從數(shù)據(jù)庫(kù)列名到Java屬性駝峰命名的類似映射
    map-underscore-to-camel-case: true
    # 如果查詢結(jié)果中包含空值的列账忘,則 MyBatis 在映射的時(shí)候志膀,不會(huì)映射這個(gè)字段
    call-setters-on-nulls: true
    # 這個(gè)配置會(huì)將執(zhí)行的sql打印出來,在開發(fā)或測(cè)試的時(shí)候可以用
    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl

三. 編寫項(xiàng)目基礎(chǔ)類

用戶實(shí)體,Dao,Service等在這里省略,請(qǐng)參考源碼

編寫Exception類來處理Shiro權(quán)限攔截異常

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

創(chuàng)建SHA256Util加密工具

/**
 * @Description Sha-256加密工具
 * @Author Sans
 * @CreateTime 2019/6/12 9:27
 */
public class SHA256Util {
    /**  私有構(gòu)造器 **/
    private SHA256Util(){};
    /**  加密算法 **/
    public final static String HASH_ALGORITHM_NAME = "SHA-256";
    /**  循環(huán)次數(shù) **/
    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)建Spring工具

/**
 * @Description Spring上下文工具類
 * @Author Sans
 * @CreateTime 2019/6/17 13:40
 */
@Component
public class SpringUtil implements ApplicationContextAware {
    private static ApplicationContext context;
    /**
     * Spring在bean初始化后會(huì)判斷是不是ApplicationContextAware的子類
     * 如果該類是,setApplicationContext()方法,會(huì)將容器中ApplicationContext作為參數(shù)傳入進(jìn)去
     * @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> T getBean(Class<T> beanClass) {
        return context.getBean(beanClass);
    }
}

創(chuàng)建Shiro工具

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

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

    private static RedisSessionDAO redisSessionDAO = SpringUtil.getBean(RedisSessionDAO.class);

    /**
     * 獲取當(dāng)前用戶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();
    }

    /**
    * 獲取當(dāng)前用戶信息
    * @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,找到該用戶名稱對(duì)應(yīng)的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;
            }
        }
        if (session == null||attribute == null) {
            return;
        }
        //刪除session
        if (isRemoveSession) {
            redisSessionDAO.delete(session);
        }
        //刪除Cache鳖擒,在訪問受限接口時(shí)會(huì)重新授權(quán)
        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 {
    /**
     * 實(shí)現(xiàn)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用于授權(quán)和認(rèn)證

/**
 * @Description Shiro權(quán)限匹配和賬號(hào)密碼匹配
 * @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;
    /**
     * 授權(quán)權(quán)限
     * 用戶進(jìn)行權(quán)限驗(yàn)證時(shí)候Shiro會(huì)去緩存中找,如果查不到數(shù)據(jù),會(huì)執(zhí)行這個(gè)方法去查權(quán)限,并放入緩存中
     * @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();
        //這里可以進(jìn)行授權(quán)和處理
        Set<String> rolesSet = new HashSet<>();
        Set<String> permsSet = new HashSet<>();
        //查詢角色和權(quán)限(這里根據(jù)業(yè)務(wù)自行查詢)
        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());
            }
        }
        //將查到的權(quán)限和角色分別傳入authorizationInfo中
        authorizationInfo.setStringPermissions(permsSet);
        authorizationInfo.setRoles(rolesSet);
        return authorizationInfo;
    }
    
    /**
     * 身份認(rèn)證
     * @Author Sans
     * @CreateTime 2019/6/12 12:36
     */
    @Override
    protected AuthenticationInfo doGetAuthenticationInfo(AuthenticationToken authenticationToken) throws AuthenticationException {
        //獲取用戶的輸入的賬號(hào).
        String username = (String) authenticationToken.getPrincipal();
        //通過username從數(shù)據(jù)庫(kù)中查找 User對(duì)象溉浙,如果找到進(jìn)行驗(yàn)證
        //實(shí)際項(xiàng)目中,這里可以根據(jù)實(shí)際情況做緩存,如果不做,Shiro自己也是有時(shí)間間隔機(jī)制,2分鐘內(nèi)不會(huì)重復(fù)執(zhí)行該方法
        SysUserEntity user = sysUserService.selectUserByName(username);
        //判斷賬號(hào)是否存在
        if (user == null) {
            throw new AuthenticationException();
        }
        //判斷賬號(hào)是否被凍結(jié)
        if (user.getState()==null||user.getState().equals("PROHIBIT")){
            throw new LockedAccountException();
        }
        //進(jìn)行驗(yàn)證
        SimpleAuthenticationInfo authenticationInfo = new SimpleAuthenticationInfo(
                user,                                  //用戶名
                user.getPassword(),                    //密碼
                ByteSource.Util.bytes(user.getSalt()), //設(shè)置鹽值
                getName()
        );
        //驗(yàn)證成功開始踢人(清除緩存和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";
    //重寫構(gòu)造器
    public ShiroSessionManager() {
        super();
        this.setDeleteInvalidSessions(true);
    }
    /**
     * 重寫方法實(shí)現(xiàn)從請(qǐng)求頭獲取Token便于接口統(tǒng)一
     * 每次請(qǐng)求進(jìn)來,Shiro會(huì)去從請(qǐng)求頭找Authorization這個(gè)key對(duì)應(yīng)的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);
        //如果請(qǐng)求頭中存在token 則從請(qǐng)求頭中獲取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 {
            //否則按默認(rèn)規(guī)則從cookie取token
            return super.getSessionId(request, response);
        }
    }
}

創(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基礎(chǔ)配置
     * @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<>();
        // 注意過濾器配置順序不能顛倒
        // 配置過濾:不會(huì)被攔截的鏈接
        filterChainDefinitionMap.put("/static/**", "anon");
        filterChainDefinitionMap.put("/userLogin/**", "anon");
        filterChainDefinitionMap.put("/**", "authc");
        // 配置shiro默認(rèn)登錄界面地址,前后端分離中登錄界面跳轉(zhuǎn)應(yīng)由前端路由控制蒋荚,后臺(tái)僅返回json數(shù)據(jù)
        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實(shí)現(xiàn)
        securityManager.setCacheManager(cacheManager());
        // 自定義Realm驗(yàn)證
        securityManager.setRealm(shiroRealm());
        return securityManager;
    }

    /**
     * 身份驗(yàn)證器
     * @Author Sans
     * @CreateTime 2019/6/12 10:37
     */
    @Bean
    public ShiroRealm shiroRealm() {
        ShiroRealm shiroRealm = new ShiroRealm();
        shiroRealm.setCredentialsMatcher(hashedCredentialsMatcher());
        return shiroRealm;
    }

    /**
     * 憑證匹配器
     * 將密碼校驗(yàn)交給Shiro的SimpleAuthenticationInfo進(jìn)行處理,在這里做匹配配置
     * @Author Sans
     * @CreateTime 2019/6/12 10:48
     */
    @Bean
    public HashedCredentialsMatcher hashedCredentialsMatcher() {
        HashedCredentialsMatcher shaCredentialsMatcher = new HashedCredentialsMatcher();
        // 散列算法:這里使用SHA256算法;
        shaCredentialsMatcher.setHashAlgorithmName(SHA256Util.HASH_ALGORITHM_NAME);
        // 散列的次數(shù)戳稽,比如散列兩次,相當(dāng)于 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存儲(chǔ)權(quán)限和角色標(biāo)識(shí)
     * @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里面的實(shí)體類必須有個(gè)id標(biāo)識(shí)
        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;
    }
}

五. 實(shí)現(xiàn)權(quán)限控制

Shiro可以用代碼或者注解來控制權(quán)限,通常我們使用注解控制,不僅簡(jiǎn)單方便,而且更加靈活.Shiro注解一共有五個(gè):

image

一般情況下我們?cè)陧?xiàng)目中做權(quán)限控制,使用最多的是RequiresPermissions和RequiresRoles,允許存在多個(gè)角色和權(quán)限,默認(rèn)邏輯是AND,也就是同時(shí)擁有這些才可以訪問方法,可以在注解中以參數(shù)的形式設(shè)置成OR

示例:

//擁有一個(gè)角色就可以訪問
@RequiresRoles(value={"ADMIN","USER"},logical = Logical.OR)
//擁有所有權(quán)限才可以訪問
@RequiresPermissions(value={"sys:user:info","sys:role:info"},logical = Logical.AND)

使用順序:Shiro注解是存在順序的,當(dāng)多個(gè)注解在一個(gè)方法上的時(shí)候,會(huì)逐個(gè)檢查,知道全部通過為止,默認(rèn)攔截順序是:RequiresRoles->RequiresPermissions->RequiresAuthentication->
RequiresUser->RequiresGuest

示例:

//擁有ADMIN角色同時(shí)還要有sys:role:info權(quán)限
@RequiresRoles(value={"ADMIN")
@RequiresPermissions("sys:role:info")

創(chuàng)建UserRoleController角色攔截測(cè)試類

/**
 * @Description 角色測(cè)試
 * @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;

    /**
     * 管理員角色測(cè)試接口
     * @Author Sans
     * @CreateTime 2019/6/19 10:38
     * @Return Map<String,Object> 返回結(jié)果
     */
    @RequestMapping("/getAdminInfo")
    @RequiresRoles("ADMIN")
    public Map<String,Object> getAdminInfo(){
        Map<String,Object> map = new HashMap<>();
        map.put("code",200);
        map.put("msg","這里是只有管理員角色能訪問的接口");
        return map;
    }

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

    /**
     * 角色測(cè)試接口
     * @Author Sans
     * @CreateTime 2019/6/19 10:38
     * @Return Map<String,Object> 返回結(jié)果
     */
    @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;
    }

    /**
     * 登出(測(cè)試登出)
     * @Author Sans
     * @CreateTime 2019/6/19 10:38
     * @Return Map<String,Object> 返回結(jié)果
     */
    @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;
    }
}

創(chuàng)建UserMenuController權(quán)限攔截測(cè)試類

/**
 * @Description 權(quán)限測(cè)試
 * @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<String,Object> 返回結(jié)果
     */
    @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> 返回結(jié)果
     */
    @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;
    }

    /**
     * 獲取權(quán)限信息集合
     * @Author Sans
     * @CreateTime 2019/6/19 10:38
     * @Return Map<String,Object> 返回結(jié)果
     */
    @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;
    }

    /**
     * 獲取所有數(shù)據(jù)
     * @Author Sans
     * @CreateTime 2019/6/19 10:38
     * @Return Map<String,Object> 返回結(jié)果
     */
    @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;
    }

    /**
     * 添加管理員角色權(quán)限(測(cè)試動(dòng)態(tài)權(quán)限更新)
     * @Author Sans
     * @CreateTime 2019/6/19 10:39
     * @Param  username 用戶ID
     * @Return Map<String,Object> 返回結(jié)果
     */
    @RequestMapping("/addMenu")
    public Map<String,Object> addMenu(){
        //添加管理員角色權(quán)限
        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","權(quán)限添加成功");
        return map;
    }
}

創(chuàng)建UserLoginController登錄類

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

    @Autowired
    private SysUserService sysUserService;

    /**
     * 登錄
     * @Author Sans
     * @CreateTime 2019/6/20 9:21
     */
    @RequestMapping("/login")
    public Map<String,Object> login(@RequestBody SysUserEntity sysUserEntity){
        Map<String,Object> map = new HashMap<>();
        //進(jìn)行身份驗(yàn)證
        try{
            //驗(yàn)證身份和登陸
            Subject subject = SecurityUtils.getSubject();
            UsernamePasswordToken token = new UsernamePasswordToken(sysUserEntity.getUsername(), sysUserEntity.getPassword());
            //驗(yàn)證成功進(jìn)行登錄操作
            subject.login(token);
        }catch (IncorrectCredentialsException e) {
            map.put("code",500);
            map.put("msg","用戶不存在或者密碼錯(cuò)誤");
            return map;
        } catch (LockedAccountException e) {
            map.put("code",500);
            map.put("msg","登錄失敗期升,該用戶已被凍結(jié)");
            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測(cè)試

登錄成功后會(huì)返回TOKEN,因?yàn)槭菃吸c(diǎn)登錄,再次登陸的話會(huì)返回新的TOKEN,之前Redis的TOKEN就會(huì)失效了

image

當(dāng)?shù)谝淮卧L問接口后我們可以看到緩存中已經(jīng)有權(quán)限數(shù)據(jù)了,在次訪問接口的時(shí)候,Shiro會(huì)直接去緩存中拿取權(quán)限,注意訪問接口時(shí)候要設(shè)置請(qǐng)求頭.

image
image

ADMIN這個(gè)號(hào)現(xiàn)在沒有sys:info:all這個(gè)權(quán)限的,所以無(wú)法訪問getInfoAll接口,我們要?jiǎng)討B(tài)分配權(quán)限后,要清掉緩存,在訪問接口時(shí)候,Shiro會(huì)去重新執(zhí)行授權(quán)方法,之后再次把權(quán)限和角色數(shù)據(jù)放入緩存中

image

訪問添加權(quán)限測(cè)試接口,因?yàn)槭菧y(cè)試,我把增加權(quán)限的用戶ADMIN寫死在里面了,權(quán)限添加后,調(diào)用工具類清掉緩存,我們可以發(fā)現(xiàn),Redis中已經(jīng)沒有緩存了

image
image

再次訪問getInfoAll接口,因?yàn)榫彺嬷袥]有數(shù)據(jù),Shiro會(huì)重新授權(quán)查詢權(quán)限,攔截通過

image

七. 項(xiàng)目源碼

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

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

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請(qǐng)聯(lián)系作者
  • 序言:七十年代末惊奇,一起剝皮案震驚了整個(gè)濱河市互躬,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌颂郎,老刑警劉巖吼渡,帶你破解...
    沈念sama閱讀 218,858評(píng)論 6 508
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場(chǎng)離奇詭異乓序,居然都是意外死亡诞吱,警方通過查閱死者的電腦和手機(jī),發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,372評(píng)論 3 395
  • 文/潘曉璐 我一進(jìn)店門竭缝,熙熙樓的掌柜王于貴愁眉苦臉地迎上來房维,“玉大人,你說我怎么就攤上這事抬纸×” “怎么了?”我有些...
    開封第一講書人閱讀 165,282評(píng)論 0 356
  • 文/不壞的土叔 我叫張陵湿故,是天一觀的道長(zhǎng)阿趁。 經(jīng)常有香客問我,道長(zhǎng)坛猪,這世上最難降的妖魔是什么脖阵? 我笑而不...
    開封第一講書人閱讀 58,842評(píng)論 1 295
  • 正文 為了忘掉前任,我火速辦了婚禮墅茉,結(jié)果婚禮上命黔,老公的妹妹穿的比我還像新娘。我一直安慰自己就斤,他們只是感情好悍募,可當(dāng)我...
    茶點(diǎn)故事閱讀 67,857評(píng)論 6 392
  • 文/花漫 我一把揭開白布。 她就那樣靜靜地躺著洋机,像睡著了一般坠宴。 火紅的嫁衣襯著肌膚如雪。 梳的紋絲不亂的頭發(fā)上绷旗,一...
    開封第一講書人閱讀 51,679評(píng)論 1 305
  • 那天喜鼓,我揣著相機(jī)與錄音,去河邊找鬼衔肢。 笑死庄岖,一個(gè)胖子當(dāng)著我的面吹牛,可吹牛的內(nèi)容都是我干的膀懈。 我是一名探鬼主播顿锰,決...
    沈念sama閱讀 40,406評(píng)論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼,長(zhǎng)吁一口氣:“原來是場(chǎng)噩夢(mèng)啊……” “哼启搂!你這毒婦竟也來了硼控?” 一聲冷哼從身側(cè)響起,我...
    開封第一講書人閱讀 39,311評(píng)論 0 276
  • 序言:老撾萬(wàn)榮一對(duì)情侶失蹤胳赌,失蹤者是張志新(化名)和其女友劉穎牢撼,沒想到半個(gè)月后,有當(dāng)?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體疑苫,經(jīng)...
    沈念sama閱讀 45,767評(píng)論 1 315
  • 正文 獨(dú)居荒郊野嶺守林人離奇死亡熏版,尸身上長(zhǎng)有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點(diǎn)故事閱讀 37,945評(píng)論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時(shí)候發(fā)現(xiàn)自己被綠了捍掺。 大學(xué)時(shí)的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片撼短。...
    茶點(diǎn)故事閱讀 40,090評(píng)論 1 350
  • 序言:一個(gè)原本活蹦亂跳的男人離奇死亡,死狀恐怖挺勿,靈堂內(nèi)的尸體忽然破棺而出曲横,到底是詐尸還是另有隱情,我是刑警寧澤不瓶,帶...
    沈念sama閱讀 35,785評(píng)論 5 346
  • 正文 年R本政府宣布禾嫉,位于F島的核電站,受9級(jí)特大地震影響蚊丐,放射性物質(zhì)發(fā)生泄漏熙参。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點(diǎn)故事閱讀 41,420評(píng)論 3 331
  • 文/蒙蒙 一麦备、第九天 我趴在偏房一處隱蔽的房頂上張望孽椰。 院中可真熱鬧,春花似錦凛篙、人聲如沸弄屡。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,988評(píng)論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽(yáng)膀捷。三九已至,卻和暖如春削彬,著一層夾襖步出監(jiān)牢的瞬間全庸,已是汗流浹背。 一陣腳步聲響...
    開封第一講書人閱讀 33,101評(píng)論 1 271
  • 我被黑心中介騙來泰國(guó)打工融痛, 沒想到剛下飛機(jī)就差點(diǎn)兒被人妖公主榨干…… 1. 我叫王不留壶笼,地道東北人。 一個(gè)月前我還...
    沈念sama閱讀 48,298評(píng)論 3 372
  • 正文 我出身青樓雁刷,卻偏偏與公主長(zhǎng)得像覆劈,于是被迫代替她去往敵國(guó)和親。 傳聞我的和親對(duì)象是個(gè)殘疾皇子,可洞房花燭夜當(dāng)晚...
    茶點(diǎn)故事閱讀 45,033評(píng)論 2 355

推薦閱讀更多精彩內(nèi)容