`
jinnianshilongnian
  • 浏览: 21424436 次
  • 性别: Icon_minigender_1
博客专栏
5c8dac6a-21dc-3466-8abb-057664ab39c7
跟我学spring3
浏览量:2403176
D659df3e-4ad7-3b12-8b9a-1e94abd75ac3
Spring杂谈
浏览量:2996176
43989fe4-8b6b-3109-aaec-379d27dd4090
跟开涛学SpringMVC...
浏览量:5630413
1df97887-a9e1-3328-b6da-091f51f886a1
Servlet3.1规范翻...
浏览量:257187
4f347843-a078-36c1-977f-797c7fc123fc
springmvc杂谈
浏览量:1592662
22722232-95c1-34f2-b8e1-d059493d3d98
hibernate杂谈
浏览量:248795
45b32b6f-7468-3077-be40-00a5853c9a48
跟我学Shiro
浏览量:5845661
Group-logo
跟我学Nginx+Lua开...
浏览量:697478
5041f67a-12b2-30ba-814d-b55f466529d5
亿级流量网站架构核心技术
浏览量:779851
社区版块
存档分类
最新评论

第十九章 动态URL权限控制——《跟我学Shiro》

阅读更多

 

目录贴: 跟我学Shiro目录贴

 

用过Spring Security的朋友应该比较熟悉对URL进行全局的权限控制,即访问URL时进行权限匹配;如果没有权限直接跳到相应的错误页面。Shiro也支持类似的机制,不过需要稍微改造下来满足实际需求。不过在Shiro中,更多的是通过AOP进行分散的权限控制,即方法级别的;而通过URL进行权限控制是一种集中的权限控制。本章将介绍如何在Shiro中完成动态URL权限控制。

 

本章代码基于《第十六章 综合实例》,请先了解相关数据模型及基本流程后再学习本章。

 

表及数据SQL

请运行shiro-example-chapter19/sql/ shiro-schema.sql 表结构

请运行shiro-example-chapter19/sql/ shiro-schema.sql 数据

 

实体

具体请参考com.github.zhangkaitao.shiro.chapter19包下的实体。 

public class UrlFilter implements Serializable {
    private Long id;
    private String name; //url名称/描述
    private String url; //地址
    private String roles; //所需要的角色,可省略
    private String permissions; //所需要的权限,可省略
} 

表示拦截的URL和角色/权限之间的关系,多个角色/权限之间通过逗号分隔,此处还可以扩展其他的关系,另外可以加如available属性表示是否开启该拦截。

 

DAO

具体请参考com.github.zhangkaitao.shiro.chapter19.dao包下的DAO接口及实现。

 

Service

具体请参考com.github.zhangkaitao.shiro.chapter19.service包下的Service接口及实现。  

public interface UrlFilterService {
    public UrlFilter createUrlFilter(UrlFilter urlFilter);
    public UrlFilter updateUrlFilter(UrlFilter urlFilter);
    public void deleteUrlFilter(Long urlFilterId);
    public UrlFilter findOne(Long urlFilterId);
    public List<UrlFilter> findAll();
}

基本的URL拦截的增删改查实现。 

 

@Service
public class UrlFilterServiceImpl implements UrlFilterService {
    @Autowired
private ShiroFilerChainManager shiroFilerChainManager;

    @Override
    public UrlFilter createUrlFilter(UrlFilter urlFilter) {
        urlFilterDao.createUrlFilter(urlFilter);
        initFilterChain();
        return urlFilter;
    }
    //其他方法请参考源码
    @PostConstruct
    public void initFilterChain() {
        shiroFilerChainManager.initFilterChains(findAll());
    }
} 

UrlFilterServiceImpl在进行新增、修改、删除时会调用initFilterChain来重新初始化Shiro的URL拦截器链,即同步数据库中的URL拦截器定义到Shiro中。此处也要注意如果直接修改数据库是不会起作用的,因为只要调用这几个Service方法时才同步。另外当容器启动时会自动回调initFilterChain来完成容器启动后的URL拦截器的注册。

  

ShiroFilerChainManager 

@Service
public class ShiroFilerChainManager {
    @Autowired private DefaultFilterChainManager filterChainManager;
    private Map<String, NamedFilterList> defaultFilterChains;
    @PostConstruct
    public void init() {
        defaultFilterChains = 
          new HashMap<String, NamedFilterList>(filterChainManager.getFilterChains());
    }
    public void initFilterChains(List<UrlFilter> urlFilters) {
        //1、首先删除以前老的filter chain并注册默认的
        filterChainManager.getFilterChains().clear();
        if(defaultFilterChains != null) {
            filterChainManager.getFilterChains().putAll(defaultFilterChains);
        }
        //2、循环URL Filter 注册filter chain
        for (UrlFilter urlFilter : urlFilters) {
            String url = urlFilter.getUrl();
            //注册roles filter
            if (!StringUtils.isEmpty(urlFilter.getRoles())) {
                filterChainManager.addToChain(url, "roles", urlFilter.getRoles());
            }
            //注册perms filter
            if (!StringUtils.isEmpty(urlFilter.getPermissions())) {
                filterChainManager.addToChain(url, "perms", urlFilter.getPermissions());
            }
        }
    }
} 

1、init:Spring容器启动时会调用init方法把在spring配置文件中配置的默认拦截器保存下来,之后会自动与数据库中的配置进行合并。

2、initFilterChains:UrlFilterServiceImpl会在Spring容器启动或进行增删改UrlFilter时进行注册URL拦截器到Shiro。

 

拦截器及拦截器链知识请参考《第八章 拦截器机制》,此处再介绍下Shiro拦截器的流程:

AbstractShiroFilter //如ShiroFilter/ SpringShiroFilter都继承该Filter

   doFilter //Filter的doFilter

     doFilterInternal //转调doFilterInternal

       executeChain(request, response, chain) //执行拦截器链

         FilterChain chain = getExecutionChain(request, response, origChain) //使用原始拦截器链获取新的拦截器链

           chain.doFilter(request, response) //执行新组装的拦截器链

 

getExecutionChain(request, response, origChain) //获取拦截器链流程

       FilterChainResolver resolver = getFilterChainResolver(); //获取相应的FilterChainResolver

       FilterChain resolved = resolver.getChain(request, response, origChain); //通过FilterChainResolver根据当前请求解析到新的FilterChain拦截器链

 

默认情况下如使用ShiroFilterFactoryBean创建shiroFilter时,默认使用PathMatchingFilterChainResolver进行解析,而它默认是根据当前请求的URL获取相应的拦截器链,使用Ant模式进行URL匹配;默认使用DefaultFilterChainManager进行拦截器链的管理。

 

PathMatchingFilterChainResolver默认流程:

public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
    //1、首先获取拦截器链管理器
    FilterChainManager filterChainManager = getFilterChainManager();
    if (!filterChainManager.hasChains()) {
        return null;
    }
    //2、接着获取当前请求的URL(不带上下文)
    String requestURI = getPathWithinApplication(request);
    //3、循环拦截器管理器中的拦截器定义(拦截器链的名字就是URL模式)
    for (String pathPattern : filterChainManager.getChainNames()) {
        //4、如当前URL匹配拦截器名字(URL模式)
        if (pathMatches(pathPattern, requestURI)) {
            //5、返回该URL模式定义的拦截器链
            return filterChainManager.proxy(originalChain, pathPattern);
        }
    }
    return null;
} 

默认实现有点小问题:

如果多个拦截器链都匹配了当前请求URL,那么只返回第一个找到的拦截器链;后续我们可以修改此处的代码,将多个匹配的拦截器链合并返回。

 

DefaultFilterChainManager内部使用Map来管理URL模式-拦截器链的关系;也就是说相同的URL模式只能定义一个拦截器链,不能重复定义;而且如果多个拦截器链都匹配时是无序的(因为使用map.keySet()获取拦截器链的名字,即URL模式)。

 

FilterChainManager接口: 

public interface FilterChainManager {
    Map<String, Filter> getFilters(); //得到注册的拦截器
    void addFilter(String name, Filter filter); //注册拦截器
    void addFilter(String name, Filter filter, boolean init); //注册拦截器
    void createChain(String chainName, String chainDefinition); //根据拦截器链定义创建拦截器链
    void addToChain(String chainName, String filterName); //添加拦截器到指定的拦截器链
    void addToChain(String chainName, String filterName, String chainSpecificFilterConfig) throws ConfigurationException; //添加拦截器(带有配置的)到指定的拦截器链
    NamedFilterList getChain(String chainName); //获取拦截器链
    boolean hasChains(); //是否有拦截器链
    Set<String> getChainNames(); //得到所有拦截器链的名字
    FilterChain proxy(FilterChain original, String chainName); //使用指定的拦截器链代理原始拦截器链
} 

此接口主要三个功能:注册拦截器,注册拦截器链,对原始拦截器链生成代理之后的拦截器链,比如  

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
……
    <property name="filters">
        <util:map>
            <entry key="authc" value-ref="formAuthenticationFilter"/>
            <entry key="sysUser" value-ref="sysUserFilter"/>
        </util:map>
    </property>
    <property name="filterChainDefinitions">
        <value>
            /login = authc
            /logout = logout
            /authenticated = authc
            /** = user,sysUser
        </value>
    </property>
</bean> 

filters属性定义了拦截器;filterChainDefinitions定义了拦截器链;如/**就是拦截器链的名字;而user,sysUser就是拦截器名字列表。

 

之前说过默认的PathMatchingFilterChainResolver和DefaultFilterChainManager不能满足我们的需求,我们稍微扩展了一下:

  

CustomPathMatchingFilterChainResolver 

public class CustomPathMatchingFilterChainResolver
             extends PathMatchingFilterChainResolver {
  private CustomDefaultFilterChainManager customDefaultFilterChainManager;
  public void setCustomDefaultFilterChainManager(
        CustomDefaultFilterChainManager customDefaultFilterChainManager) {
      this.customDefaultFilterChainManager = customDefaultFilterChainManager;
      setFilterChainManager(customDefaultFilterChainManager);
  }

  public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
      FilterChainManager filterChainManager = getFilterChainManager();
      if (!filterChainManager.hasChains()) {
          return null;
      }
      String requestURI = getPathWithinApplication(request);
      List<String> chainNames = new ArrayList<String>();
      for (String pathPattern : filterChainManager.getChainNames()) {
        if (pathMatches(pathPattern, requestURI)) {
        chainNames.add(pathPattern);
        }
      }
      if(chainNames.size() == 0) {
        return null;
      }
      return customDefaultFilterChainManager.proxy(originalChain, chainNames);
  }
} 

和默认的PathMatchingFilterChainResolver区别是,此处得到所有匹配的拦截器链,然后通过调用CustomDefaultFilterChainManager.proxy(originalChain, chainNames)进行合并后代理。

 

CustomDefaultFilterChainManager    

public class CustomDefaultFilterChainManager extends DefaultFilterChainManager {
    private Map<String, String> filterChainDefinitionMap = null;
    private String loginUrl;
    private String successUrl;
    private String unauthorizedUrl;
    public CustomDefaultFilterChainManager() {
        setFilters(new LinkedHashMap<String, Filter>());
        setFilterChains(new LinkedHashMap<String, NamedFilterList>());
        addDefaultFilters(true);
    }
    public Map<String, String> getFilterChainDefinitionMap() {
        return filterChainDefinitionMap;
    }
    public void setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) {
        this.filterChainDefinitionMap = filterChainDefinitionMap;
    }
    public void setCustomFilters(Map<String, Filter> customFilters) {
        for(Map.Entry<String, Filter> entry : customFilters.entrySet()) {
            addFilter(entry.getKey(), entry.getValue(), false);
        }
}
    public void setDefaultFilterChainDefinitions(String definitions) {
        Ini ini = new Ini();
        ini.load(definitions);
        Ini.Section section = ini.getSection(IniFilterChainResolverFactory.URLS);
        if (CollectionUtils.isEmpty(section)) {
            section = ini.getSection(Ini.DEFAULT_SECTION_NAME);
        }
        setFilterChainDefinitionMap(section);
    }
    public String getLoginUrl() {
        return loginUrl;
    }
    public void setLoginUrl(String loginUrl) {
        this.loginUrl = loginUrl;
    }
    public String getSuccessUrl() {
        return successUrl;
    }
    public void setSuccessUrl(String successUrl) {
        this.successUrl = successUrl;
    }
    public String getUnauthorizedUrl() {
        return unauthorizedUrl;
    }
    public void setUnauthorizedUrl(String unauthorizedUrl) {
        this.unauthorizedUrl = unauthorizedUrl;
    }
    @PostConstruct
    public void init() {
        Map<String, Filter> filters = getFilters();
        if (!CollectionUtils.isEmpty(filters)) {
            for (Map.Entry<String, Filter> entry : filters.entrySet()) {
                String name = entry.getKey();
                Filter filter = entry.getValue();
                applyGlobalPropertiesIfNecessary(filter);
                if (filter instanceof Nameable) {
                    ((Nameable) filter).setName(name);
                }
                addFilter(name, filter, false);
            }
        }
        Map<String, String> chains = getFilterChainDefinitionMap();
        if (!CollectionUtils.isEmpty(chains)) {
            for (Map.Entry<String, String> entry : chains.entrySet()) {
                String url = entry.getKey();
                String chainDefinition = entry.getValue();
                createChain(url, chainDefinition);
            }
        }
    }
    protected void initFilter(Filter filter) {
        //ignore 
    }

    public FilterChain proxy(FilterChain original, List<String> chainNames) {
        NamedFilterList configured = new SimpleNamedFilterList(chainNames.toString());
        for(String chainName : chainNames) {
            configured.addAll(getChain(chainName));
        }
        return configured.proxy(original);
    }
    private void applyGlobalPropertiesIfNecessary(Filter filter) {
        applyLoginUrlIfNecessary(filter);
        applySuccessUrlIfNecessary(filter);
        applyUnauthorizedUrlIfNecessary(filter);
    }
    private void applyLoginUrlIfNecessary(Filter filter) {
        //请参考源码
    }
    private void applySuccessUrlIfNecessary(Filter filter) {
        //请参考源码
    }
    private void applyUnauthorizedUrlIfNecessary(Filter filter) {
        //请参考源码
    }
} 

1、CustomDefaultFilterChainManager:调用其构造器时,会自动注册默认的拦截器;

2、loginUrl、successUrl、unauthorizedUrl:分别对应登录地址、登录成功后默认跳转地址、未授权跳转地址,用于给相应拦截器的;

3、filterChainDefinitionMap:用于存储如ShiroFilterFactoryBean在配置文件中配置的拦截器链定义,即可以认为是默认的静态拦截器链;会自动与数据库中加载的合并;

4、setDefaultFilterChainDefinitions:解析配置文件中传入的字符串拦截器链配置,解析为相应的拦截器链;

5、setCustomFilters:注册我们自定义的拦截器;如ShiroFilterFactoryBean的filters属性;

6、init:初始化方法,Spring容器启动时会调用,首先其会自动给相应的拦截器设置如loginUrl、successUrl、unauthorizedUrl;其次根据filterChainDefinitionMap构建默认的拦截器链;

7、initFilter:此处我们忽略实现initFilter,因为交给spring管理了,所以Filter的相关配置会在Spring配置中完成;

8、proxy:组合多个拦截器链为一个生成一个新的FilterChain代理。

 

Web层控制器 

请参考com.github.zhangkaitao.shiro.chapter19.web.controller包,相对于第十六章添加了UrlFilterController用于UrlFilter的维护。另外,移除了控制器方法上的权限注解,而是使用动态URL拦截进行控制。

 

Spring配置——spring-config-shiro.xml   

<bean id="filterChainManager" 
    class="com.github.zhangkaitao.shiro.spring.CustomDefaultFilterChainManager">
    <property name="loginUrl" value="/login"/>
    <property name="successUrl" value="/"/>
    <property name="unauthorizedUrl" value="/unauthorized.jsp"/>
    <property name="customFilters">
        <util:map>
            <entry key="authc" value-ref="formAuthenticationFilter"/>
            <entry key="sysUser" value-ref="sysUserFilter"/>
        </util:map>
    </property>
    <property name="defaultFilterChainDefinitions">
        <value>
            /login = authc
            /logout = logout
            /unauthorized.jsp = authc
            /** = user,sysUser
        </value>
    </property>
</bean> 

filterChainManager是我们自定义的CustomDefaultFilterChainManager,注册相应的拦截器及默认的拦截器链。 

<bean id="filterChainResolver" 
    class="com.github.zhangkaitao.shiro.spring.CustomPathMatchingFilterChainResolver">
    <property name="customDefaultFilterChainManager" ref="filterChainManager"/>
</bean> 

filterChainResolver是自定义的CustomPathMatchingFilterChainResolver,使用上边的filterChainManager进行拦截器链的管理。 

<bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean">
    <property name="securityManager" ref="securityManager"/>
</bean> 

shiroFilter不再定义filters及filterChainDefinitions,而是交给了filterChainManager进行完成。 

<bean class="org.springframework.beans.factory.config.MethodInvokingFactoryBean">
    <property name="targetObject" ref="shiroFilter"/>
    <property name="targetMethod" value="setFilterChainResolver"/>
    <property name="arguments" ref="filterChainResolver"/>
</bean> 

最后把filterChainResolver注册给shiroFilter,其使用它进行动态URL权限控制。

 

其他配置和第十六章一样,请参考第十六章。

 

测试

1、首先执行shiro-data.sql初始化数据。

2、然后再URL管理中新增如下数据: 

3、访问http://localhost:8080/chapter19/user时要求用户拥有aa角色,此时是没有的所以会跳转到未授权页面;

4、添加aa角色然后授权给用户,此时就有权限访问http://localhost:8080/chapter19/user。

 

实际项目可以在此基础上进行扩展。

 

     

 

 

示例源代码:https://github.com/zhangkaitao/shiro-example;可加群 231889722 探讨Spring/Shiro技术。

        

  

18
4
分享到:
评论
43 楼 7546519a 2018-11-29  
您太厉害了 ,膜拜大神 ~~~~~~~~~~
42 楼 jcrack 2018-01-14  
672ade71e449bf2929c110e29671d1146687领教了
41 楼 xuezhongyoufeng 2018-01-10  
您好  我想问一下:重写AuthorizingRealm 中设置
SimpleAuthorizationInfo info = new SimpleAuthorizationInfo();
info.setStringPermissions(permsSet);
这个setStringPermissions()方法可不可以起到动态URL的控制作用
40 楼 WChao226 2017-06-05  
举例:
<bean id="filterChainManager" class="com.jsoa.security.shiroPlus.CustomDefaultFilterChainManager">  
    <property name="loginUrl" value="/manage/oaLogin.html"/>
    <property name="successUrl" value="/manage/oaIndex.html"/>
    <property name="unauthorizedUrl" value="/manage/unauthorized.html"/>
    <property name="defaultFilterChainDefinitions">  
        <value>  
           /demo/index.html = anon
           /demo/** = authc
        </value>  
    </property>  
</bean>

按道理我们希望demo/index.html直接通过,但demo/index.html很可能会被跳转到登录页面

ShiroFilerChainManager类中的HashMap(无序)改为 LinkedHashMap(有序)
不然过滤器链会出现乱序,我搞了几个小时才解决
init(){
//defaultFilterChains = new HashMap<String, NamedFilterList>(filterChainManager.getFilterChains());
 defaultFilterChains = new LinkedHashMap<String, NamedFilterList>(filterChainManager.getFilterChains());
}


39 楼 251859257 2017-06-04  
429537044 写道
liyuejin 写道
如果配置了 anon 的url,会有问题啊,会被后面的拦截到并且跳转到login, 有好的办法解决吗?

好像顺序有问题,我跑起来的时候 需要把限制范围最大的放到最上面,

需要改两个地方>>CustomPathMatchingFilterChainResolver
public FilterChain getChain(ServletRequest request, ServletResponse response, FilterChain originalChain) {
        FilterChainManager filterChainManager = getFilterChainManager();
        if (!filterChainManager.hasChains()) {
            return null;
        }

        String requestURI = getPathWithinApplication(request);

        //the 'chain names' in this implementation are actually path patterns defined by the user.  We just use them
        //as the chain name for the FilterChainManager's requirements
        for (String pathPattern : filterChainManager.getChainNames()) {

            // If the path does match, then pass on to the subclass implementation for specific checks:
            if (pathMatches(pathPattern, requestURI)) {
                return customDefaultFilterChainManager.proxy(originalChain, pathPattern);
            }
        }
        return null;

    }


ShiroFilerChainManager >>
public void initFilterChains(List<UrlFilter> urlFilters) {
        //1、首先删除以前老的filter chain并注册默认的
        filterChainManager.getFilterChains().clear();
        //2、循环URL Filter 注册filter chain
        for (UrlFilter urlFilter : urlFilters) {
            String url = urlFilter.getUrl();
            //注册roles filter
            if (!StringUtils.isEmpty(urlFilter.getRoles())) {
                filterChainManager.addToChain(url, "roles", urlFilter.getRoles());
            }
            //注册perms filter
            if (!StringUtils.isEmpty(urlFilter.getPermissions())) {
                filterChainManager.addToChain(url, "perms", urlFilter.getPermissions());
            }
        }
        if(defaultFilterChains != null) {
            filterChainManager.getFilterChains().putAll(defaultFilterChains);
        }


    }
38 楼 Flyingideal 2017-01-12  
Flyingideal 写道
使用springBoot配置的时候,MethodInvokingFactoryBean配置了以后一直报循环引用的错误,这个在springBoot中如何配置?谢谢


感谢@ousui 35楼回答,已解决  定义ShiroFilterFactoryBean的时候set一下就OK了
try {
((AbstractShiroFilter) factoryBean.getObject()).setFilterChainResolver(filterChainResolver());
} catch (Exception e) {
System.out.println("Register FilterChainResolver fail");
e.printStackTrace();
}
37 楼 Flyingideal 2017-01-11  
使用springBoot配置的时候,MethodInvokingFactoryBean配置了以后一直报循环引用的错误,这个在springBoot中如何配置?谢谢
36 楼 ousui 2016-06-12  
TAO哥写的教程都很棒,但是这篇感觉好乱。。。
35 楼 ousui 2016-06-12  
qq294825811 写道
shiro 1.2.4  的 org.apache.shiro.spring.web.ShiroFilterFactoryBean 并没有  setFilterChainResolver 这个方法


setFilterChainResolver 实际上调用的是
getObject().setFilterChainResolver()

34 楼 bmcsy 2016-05-08  
qq294825811 写道
shiro 1.2.4  的 org.apache.shiro.spring.web.ShiroFilterFactoryBean 并没有  setFilterChainResolver 这个方法


我也是被这卡住了。
33 楼 429537044 2016-04-12  
@楼主,我把/login改成/system/login之后,登录的时候不能被表单拦截器拦截
32 楼 429537044 2016-04-12  
liyuejin 写道
如果配置了 anon 的url,会有问题啊,会被后面的拦截到并且跳转到login, 有好的办法解决吗?

好像顺序有问题,我跑起来的时候 需要把限制范围最大的放到最上面,
31 楼 qq294825811 2016-03-26  
shiro 1.2.4  的 org.apache.shiro.spring.web.ShiroFilterFactoryBean 并没有  setFilterChainResolver 这个方法
30 楼 liyuejin 2016-02-02  
如果配置了 anon 的url,会有问题啊,会被后面的拦截到并且跳转到login, 有好的办法解决吗?
29 楼 lujicong 2015-12-06  
zerok_cn 写道
CustomPathMatchingFilterChainResolver 里如果这么搞的话,如果配置了 anon 的url,会有问题啊,会被后面的拦截到并且跳转到login, 有好的办法解决吗?

解决了没有?
28 楼 lujicong 2015-12-04  
zilongzilong 写道
自定义ShiroFilerChainManager中私有变量 defaultFilterChains必须为linkedHashMap,不然在XML中定义的拦截器就全部乱序了

如果去实现?有例子不?
27 楼 alpha_hw 2015-11-24  
url管理中是否只是实现了角色控制,其中权限控制没有实现?例如 权限列表中设置为user:view,但是能有所有的权限,还是基于角色控制的
26 楼 liangxianning 2015-11-02  
更新maven依赖就好了
25 楼 liangxianning 2015-11-02  
在shiro-example-chapter19中好像没有import net.sf.ehcache.Ehcache;这个相关的jar包。所以程序出错。
24 楼 zerok_cn 2015-10-22  
CustomPathMatchingFilterChainResolver 里如果这么搞的话,如果配置了 anon 的url,会有问题啊,会被后面的拦截到并且跳转到login, 有好的办法解决吗?

相关推荐

Global site tag (gtag.js) - Google Analytics