前言

这篇看看shiro的工作流程,研究一下config文件中的3个Bean是怎么运作的。


开头

回顾一下配置文件里的三个Bean,他们的返回类型和参数类型是互相吻合的:

@Bean
public ShiroFilterFactoryBean myShiroFilterFactoryBean(DefaultWebSecurityManager securityManager) {
    ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
    shiroFilterFactoryBean.setSecurityManager(securityManager);
    Map<String, String> map = new HashMap<>();
    map.put("/shiro", "authc");
    map.put("/logout", "authc");
    shiroFilterFactoryBean.setLoginUrl("/index");
    shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
    return shiroFilterFactoryBean;
}

@Bean
public DefaultWebSecurityManager myDefaultWebSecurityManager(Realm realm) {
    DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
    defaultWebSecurityManager.setRealm(realm);
    return defaultWebSecurityManager;
}

@Bean
public Realm myRealm() {
    return new MyRealm();
}

通过调试可以发现,它们是套娃调用,上一个作为下一个的参数,最后合并成一个ShiroFilterFactoryBean。

配置加载-BeanPostProcessor注册

这个ShiroFilterFactoryBean对象继承了FactoryBean和BeanPostProcessor两个接口,BeanPostProcessor接口可以用于在实例化Bean时对其进行拦截、修改,具有比较高的加载优先度。

在加载BeanPostProcessor的registerBeanPostProcessors函数中,会通过beanFactory的getBeanNamesForType函数获取要加载的BeanPostProcessor名称:

String[] postProcessorNames = beanFactory.getBeanNamesForType(BeanPostProcessor.class, true, false);

通过调试可以发现,这里获取到了我们自定义的Bean,名字叫做&myShiroFilterFactoryBean:

进入这个getBeanNamesForType函数,来到doGetBeanNamesForType函数:

private String[] doGetBeanNamesForType(ResolvableType type, boolean includeNonSingletons, boolean allowEagerInit) {
    List<String> result = new ArrayList<>();

    // Check all bean definitions.
    for (String beanName : this.beanDefinitionNames) {
        // Only consider bean as eligible if the bean name is not defined as alias for some other bean.
        if (!isAlias(beanName)) {
            try {
                RootBeanDefinition mbd = getMergedLocalBeanDefinition(beanName);
                // Only check bean definition if it is complete.
                if (!mbd.isAbstract() && (allowEagerInit ||
                                          (mbd.hasBeanClass() || !mbd.isLazyInit() || isAllowEagerClassLoading()) &&
                                          !requiresEagerInitForType(mbd.getFactoryBeanName()))) {
                    boolean isFactoryBean = isFactoryBean(beanName, mbd);
                    BeanDefinitionHolder dbd = mbd.getDecoratedDefinition();
                    boolean matchFound = false;
                    boolean allowFactoryBeanInit = (allowEagerInit || containsSingleton(beanName));
                    boolean isNonLazyDecorated = (dbd != null && !mbd.isLazyInit());
                    if (!isFactoryBean) {
                        if (includeNonSingletons || isSingleton(beanName, mbd, dbd)) {
                            matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit);
                        }
                    }
                    else {
                        if (includeNonSingletons || isNonLazyDecorated ||
                            (allowFactoryBeanInit && isSingleton(beanName, mbd, dbd))) {
                            matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit);
                        }
                        if (!matchFound) {
                            // In case of FactoryBean, try to match FactoryBean instance itself next.
                            beanName = FACTORY_BEAN_PREFIX + beanName;
                            matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit);
                        }
                    }
                    if (matchFound) {
                        result.add(beanName);
                    }
                }
            }
            ...
        }
    }

    ...

    return StringUtils.toStringArray(result);
}

这里遍历了定义的Bean进行确认,根据调试中看到的Bean名前多出来的&,我们可以确定当遍历到myShiroFilterFactoryBean时进入了这段代码:

if (!matchFound) {
    // In case of FactoryBean, try to match FactoryBean instance itself next.
    beanName = FACTORY_BEAN_PREFIX + beanName;
    matchFound = isTypeMatch(beanName, type, allowFactoryBeanInit);
}

首先是这一段判断:

// Only check bean definition if it is complete.
if (!mbd.isAbstract() && (allowEagerInit ||
                          (mbd.hasBeanClass() || !mbd.isLazyInit() || isAllowEagerClassLoading()) &&
                          !requiresEagerInitForType(mbd.getFactoryBeanName())))

根据注释,这里只是确认该Bean是否完整,略过,然后是第二个判断用的布尔值isFactoryBean,其通过isFactoryBean函数赋值:

boolean isFactoryBean = isFactoryBean(beanName, mbd);

跟进去看看:

protected boolean isFactoryBean(String beanName, RootBeanDefinition mbd) {
    Boolean result = mbd.isFactoryBean;
    if (result == null) {
        ...
    }
    return result;
}

可以看到结果直接来自mbd对象的isFactoryBean属性,回头看看这个对象是通过getMergedLocalBeanDefinition函数生成的:

protected RootBeanDefinition getMergedLocalBeanDefinition(String beanName) throws BeansException {
    // Quick check on the concurrent map first, with minimal locking.
    RootBeanDefinition mbd = this.mergedBeanDefinitions.get(beanName);
    if (mbd != null && !mbd.stale) {
        return mbd;
    }
    return getMergedBeanDefinition(beanName, getBeanDefinition(beanName));
}

调试可以发现,此时的mbd是直接从内部map中取出的,虽然不会直接返回,但是其isFactoryBean属性和resolvedTargetType属性等已经有了值:

调试发现对这些属性的赋值是在注册BeanPostProcessor之前,会加载Bean,期间还有一次调用doGetBeanNamesForType函数的操作:

这次调用isFactoryBean函数时就会因为其isFactoryBean属性未赋值而进入predictBeanType函数:

protected Class<?> predictBeanType(String beanName, RootBeanDefinition mbd, Class<?>... typesToMatch) {
    Class<?> targetType = determineTargetType(beanName, mbd, typesToMatch);
    ...
    return targetType;
}

然后到determineTargetType函数:

protected Class<?> determineTargetType(String beanName, RootBeanDefinition mbd, Class<?>... typesToMatch) {
    Class<?> targetType = mbd.getTargetType();
    if (targetType == null) {
        targetType = (mbd.getFactoryMethodName() != null ?
                      getTypeForFactoryMethod(beanName, mbd, typesToMatch) :
                      resolveBeanClass(mbd, beanName, typesToMatch));
        if (ObjectUtils.isEmpty(typesToMatch) || getTempClassLoader() == null) {
            mbd.resolvedTargetType = targetType;
        }
    }
    return targetType;
}

再到getTypeForFactoryMethod函数,这里会遍历myShiroFilterFactoryBean所属的BeanFactory,即myShiroConfig类中的函数,找到与该Bean同名的函数:

for (Method candidate : candidates) {
    if (Modifier.isStatic(candidate.getModifiers()) == isStatic && mbd.isFactoryMethod(candidate) &&
        candidate.getParameterCount() >= minNrOfArgs) {
        // Declared type variables to inspect?
        if (candidate.getTypeParameters().length > 0) {
            ...
        }
        else {
            uniqueCandidate = (commonType == null ? candidate : null);
            commonType = ClassUtils.determineCommonAncestor(candidate.getReturnType(), commonType);
            if (commonType == null) {
                // Ambiguous return types found: return null to indicate "not determinable".
                return null;
            }
        }
    }
}

再:

cachedReturnType = (uniqueCandidate != null ?
                    ResolvableType.forMethodReturnType(uniqueCandidate) : ResolvableType.forClass(commonType));
mbd.factoryMethodReturnType = cachedReturnType;
return cachedReturnType.resolve();

简单来说就是返回了该函数的返回类型,即ShiroFilterFactoryBean。然后回到determineTargetType函数将其放入mbd的resolvedTargetType属性中,再回到isFactoryBean函数中,因为ShiroFilterFactoryBean是FactoryBean类的子类,所以mbd的isFactoryBean属性被赋值为了true。

回到doGetBeanNamesForType函数,最后会通过isTypeMatch函数确认myShiroFilterFactoryBean是否满足BeanPostProcessor这个Type的要求。

当bean名称前面有一个&符号时,isFactoryDereference就会是true,代表这个bean是一个FactoryBean:

boolean isFactoryDereference = BeanFactoryUtils.isFactoryDereference(name);
...
public static boolean isFactoryDereference(@Nullable String name) {
    return (name != null && name.startsWith(BeanFactory.FACTORY_BEAN_PREFIX));
}

然后整一个typesToMatch变量:

// Setup the types that we want to match against
Class<?> classToMatch = typeToMatch.resolve();
if (classToMatch == null) {
    classToMatch = FactoryBean.class;
}
Class<?>[] typesToMatch = (FactoryBean.class == classToMatch ?
                           new Class<?>[] {classToMatch} : new Class<?>[] {FactoryBean.class, classToMatch});

此时classToMatch变量值为BeanPostProcessor,因为不等于FactoryBean,所以typesToMatch是一个数组,需要该bean同时继承BeanPostProcessor和FactoryBean接口。

因为myShiroFilterFactoryBean是一个FactoryBean,所以会跳过下面的判断到:

// If we couldn't use the target type, try regular prediction.
if (predictedType == null) {
    predictedType = predictBeanType(beanName, mbd, typesToMatch);
    if (predictedType == null) {
        return false;
    }
}

predictBeanType函数如下:

protected Class<?> predictBeanType(String beanName, RootBeanDefinition mbd, Class<?>... typesToMatch) {
    Class<?> targetType = mbd.getTargetType();
    if (targetType != null) {
        return targetType;
    }
    if (mbd.getFactoryMethodName() != null) {
        return null;
    }
    return resolveBeanClass(mbd, beanName, typesToMatch);
}
...
public Class<?> getTargetType() {
    if (this.resolvedTargetType != null) {
        return this.resolvedTargetType;
    }
    ResolvableType targetType = this.targetType;
    return (targetType != null ? targetType.resolve() : null);
}

返回值来自mbd的resolvedTargetType属性,即ShiroFilterFactoryBean,再下一步:

// We don't have an exact type but if bean definition target type or the factory
// method return type matches the predicted type then we can use that.
if (beanType == null) {
    ResolvableType definedType = mbd.targetType;
    if (definedType == null) {
        definedType = mbd.factoryMethodReturnType;
    }
    if (definedType != null && definedType.resolve() == predictedType) {
        beanType = definedType;
    }
}

// If we have a bean type use it so that generics are considered
if (beanType != null) {
    return typeToMatch.isAssignableFrom(beanType);
}

beanType来自mbd的factoryMethodReturnType属性为ShiroFilterFactoryBean,而typeToMatch是来自参数的BeanPostProcessor,因为ShiroFilterFactoryBean继承了BeanPostProcessor接口,所以返回值为true。

回到doGetBeanNamesForType函数,将&myShiroFilterFactoryBean加入到了要注册的BeanPostProcessor中,最后返回registerBeanPostProcessors函数进行注册:

String ppName;
BeanPostProcessor pp;
for(int var10 = 0; var10 < var9; ++var10) {
    ppName = var8[var10];
    if (beanFactory.isTypeMatch(ppName, PriorityOrdered.class)) {
        pp = (BeanPostProcessor)beanFactory.getBean(ppName, BeanPostProcessor.class);
        priorityOrderedPostProcessors.add(pp);
        if (pp instanceof MergedBeanDefinitionPostProcessor) {
            internalPostProcessors.add(pp);
        }
    } else if (beanFactory.isTypeMatch(ppName, Ordered.class)) {
        orderedPostProcessorNames.add(ppName);
    } else {
        nonOrderedPostProcessorNames.add(ppName);
    }
}

因为myShiroFilterFactoryBean没有继承PriorityOrdered、Ordered之类的优先级接口,所以会放入低优先级的nonOrderedPostProcessorNames队列中,再到后面进行实例化:

List<BeanPostProcessor> nonOrderedPostProcessors = new ArrayList(nonOrderedPostProcessorNames.size());
Iterator var17 = nonOrderedPostProcessorNames.iterator();

while(var17.hasNext()) {
    ppName = (String)var17.next();
    pp = (BeanPostProcessor)beanFactory.getBean(ppName, BeanPostProcessor.class);
    nonOrderedPostProcessors.add(pp);
    if (pp instanceof MergedBeanDefinitionPostProcessor) {
        internalPostProcessors.add(pp);
    }
}

实例化用的是getBean函数,给myShiroFilterFactoryBean函数下个断点,找到关键实例化函数instantiateUsingFactoryMethod:

String factoryBeanName = mbd.getFactoryBeanName();
if (factoryBeanName != null) {
    if (factoryBeanName.equals(beanName)) {
        throw new BeanDefinitionStoreException(mbd.getResourceDescription(), beanName,
                                               "factory-bean reference points back to the same bean definition");
    }
    factoryBean = this.beanFactory.getBean(factoryBeanName);
    if (mbd.isSingleton() && this.beanFactory.containsSingleton(beanName)) {
        throw new ImplicitlyAppearedSingletonException();
    }
    this.beanFactory.registerDependentBean(factoryBeanName, beanName);
    factoryClass = factoryBean.getClass();
    isStatic = false;
}

首先实例化了一个myShiroConfig对象,然后:

// Need to determine the factory method...
// Try all methods with this name to see if they match the given arguments.
factoryClass = ClassUtils.getUserClass(factoryClass);

List<Method> candidates = null;
if (mbd.isFactoryMethodUnique) {
    if (factoryMethodToUse == null) {
        factoryMethodToUse = mbd.getResolvedFactoryMethod();
    }
    if (factoryMethodToUse != null) {
        candidates = Collections.singletonList(factoryMethodToUse);
    }
}

从mbd中获取了myShiroFilterFactoryBean函数放在candidates中作为下面要调用的factoryMethod,再然后:

for (Method candidate : candidates) {
    int parameterCount = candidate.getParameterCount();

    if (parameterCount >= minNrOfArgs) {
        ArgumentsHolder argsHolder;

        Class<?>[] paramTypes = candidate.getParameterTypes();
        if (explicitArgs != null) {
            ...
        }
        else {
            // Resolved constructor arguments: type conversion and/or autowiring necessary.
            try {
                String[] paramNames = null;
                ParameterNameDiscoverer pnd = this.beanFactory.getParameterNameDiscoverer();
                if (pnd != null) {
                    paramNames = pnd.getParameterNames(candidate);
                }
                argsHolder = createArgumentArray(beanName, mbd, resolvedValues, bw,
                                                 paramTypes, paramNames, candidate, autowiring, candidates.size() == 1);
            }
            ...
        }

        ...
    }
}

这里会调用createArgumentArray处理参数:

MethodParameter methodParam = MethodParameter.forExecutable(executable, paramIndex);
// No explicit match found: we're either supposed to autowire or
// have to fail creating an argument array for the given constructor.
if (!autowiring) {
    ...
}
try {
    Object autowiredArgument = resolveAutowiredArgument(
        methodParam, beanName, autowiredBeanNames, converter, fallback);
    args.rawArguments[paramIndex] = autowiredArgument;
    args.arguments[paramIndex] = autowiredArgument;
    args.preparedArguments[paramIndex] = autowiredArgumentMarker;
    args.resolveNecessary = true;
}

根据函数名resolveAutowiredArgument可以看出来,下一步通过自动装载获取参数。那什么时候会进入这段代码来获取参数?往上找找,可以看到要求为

if (valueHolder != null)

而valueHolder为null的要求为:

if (resolvedValues != null) {
    valueHolder = resolvedValues.getArgumentValue(paramIndex, paramType, paramName, usedValueHolders);
    // If we couldn't find a direct match and are not supposed to autowire,
    // let's try the next generic, untyped argument value as fallback:
    // it could match after type conversion (for example, String -> int).
    if (valueHolder == null && (!autowiring || paramTypes.length == resolvedValues.getArgumentCount())) {
        valueHolder = resolvedValues.getGenericArgumentValue(null, null, usedValueHolders);
    }
}

就是取决于来自参数的变量resolvedValues,回到上一级函数instantiateUsingFactoryMethod中:

// We don't have arguments passed in programmatically, so we need to resolve the
// arguments specified in the constructor arguments held in the bean definition.
if (mbd.hasConstructorArgumentValues()) {
    ConstructorArgumentValues cargs = mbd.getConstructorArgumentValues();
    resolvedValues = new ConstructorArgumentValues();
    minNrOfArgs = resolveConstructorArguments(beanName, mbd, bw, cargs, resolvedValues);
}
else {
    minNrOfArgs = 0;
}

简单来说就是代码中找不到参数,所以要通过自动装载从其他bean中获取参数,也就是说注册BeanPostProcessor要调用myShiroFilterFactoryBean这个bean,而由于缺失参数所以要调用myDefaultWebSecurityManager,又因为缺失参数要调用myRealm,从而形成了一种bean之间的依赖传递的情况。

所以我们把三个bean简化成一个也还是可以用的:

@Configuration
public class MyShiroConfig {
    @Bean
    public ShiroFilterFactoryBean myShiroFilterFactoryBean() {
        DefaultWebSecurityManager defaultWebSecurityManager = new DefaultWebSecurityManager();
        defaultWebSecurityManager.setRealm(new MyRealm());
        ShiroFilterFactoryBean shiroFilterFactoryBean = new ShiroFilterFactoryBean();
        shiroFilterFactoryBean.setSecurityManager(defaultWebSecurityManager);
        Map<String, String> map = new HashMap<>();
        map.put("/shiro", "authc");
        map.put("/logout", "authc");
        shiroFilterFactoryBean.setLoginUrl("/index");
        shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
        return shiroFilterFactoryBean;
    }
}

至此,我们编写的三个bean已经执行完毕,最后的执行结果就是在内存中形成了一个属于myShiroFilterFactoryBean这个bean的ShiroFilterFactoryBean对象。

权限控制

用于配置权限控制的map会被放入ShiroFilterFactoryBean对象的filterChainDefinitionMap属性中:

shiroFilterFactoryBean.setFilterChainDefinitionMap(map);
...
public void setFilterChainDefinitionMap(Map<String, String> filterChainDefinitionMap) {
    this.filterChainDefinitionMap = filterChainDefinitionMap;
}

而createFilterChainManager函数会取出此map进行下一步操作,先不看具体的处理代码,下个断点看看调用栈,找到ServletContextInitializerBeans类的addAdaptableBeans函数:

protected void addAdaptableBeans(ListableBeanFactory beanFactory) {
    MultipartConfigElement multipartConfig = this.getMultipartConfig(beanFactory);
    this.addAsRegistrationBean(beanFactory, Servlet.class, new ServletContextInitializerBeans.ServletRegistrationBeanAdapter(multipartConfig));
    this.addAsRegistrationBean(beanFactory, Filter.class, new ServletContextInitializerBeans.FilterRegistrationBeanAdapter());
    ...
}

可以看到这个函数用于处理跟servlet、filter相关的bean,跟下去可以发现用于获取要处理的bean名称所用的同样是getBeanNamesForType函数:

String[] names = beanFactory.getBeanNamesForType(type, true, false);

不过不同的是这次筛选要求的type为Filter,而且在通过isTypeMatch函数判断是否符合要求时,会进入这段代码:

// Check manually registered singletons.
Object beanInstance = getSingleton(beanName, false);
if (beanInstance != null && beanInstance.getClass() != NullBean.class) {
    if (beanInstance instanceof FactoryBean) {
        if (!isFactoryDereference) {
            Class<?> type = getTypeForFactoryBean((FactoryBean<?>) beanInstance);
            return (type != null && typeToMatch.isAssignableFrom(type));
        }
        else {
            return typeToMatch.isInstance(beanInstance);
        }
    }
    ...
}

由于内存中已经保存了myShiroFilterFactoryBean这个bean对应的组装好的ShiroFilterFactoryBean对象,所以getSingleton函数就会获取到这个对象,getTypeForFactoryBean函数如下:

protected Class<?> getTypeForFactoryBean(FactoryBean<?> factoryBean) {
    try {
        if (System.getSecurityManager() != null) {
            ...
        }
        else {
            return factoryBean.getObjectType();
        }
    }
    ...
}

而ShiroFilterFactoryBean类是一个用于生产SpringShiroFilter的类,其getObject函数和getObjectType函数如下:

public Object getObject() throws Exception {
    if (instance == null) {
        instance = createInstance();
    }
    return instance;
}

public Class getObjectType() {
    return SpringShiroFilter.class;
}

getObjectType函数告知Spring这个工厂bean会生产一个SpringShiroFilter对象,以满足前面type要求的Filter,getObject函数用于后面的生产工作。

回到getOrderedBeansOfType:

Map<String, T> map = new LinkedHashMap();
String[] var6 = names;
int var7 = names.length;

for(int var8 = 0; var8 < var7; ++var8) {
    String name = var6[var8];
    if (!excludes.contains(name) && !ScopedProxyUtils.isScopedTarget(name)) {
        T bean = beanFactory.getBean(name, type);
        if (!excludes.contains(bean)) {
            map.put(name, bean);
        }
    }
}

获取到符合filter要求的bean后,会调用getBean函数,同样由于内存中存在这个ShiroFilterFactoryBean对象,会一步步进入doGetObjectFromFactoryBean函数:

private Object doGetObjectFromFactoryBean(FactoryBean<?> factory, String beanName) throws BeanCreationException {
    Object object;
    try {
        if (System.getSecurityManager() != null) {
            ...
        }
        else {
            object = factory.getObject();
        }
    }
    ...
    return object;
}

调用ShiroFilterFactoryBean类的getObject开始生产,一步步来到我们要看的createFilterChainManager函数:

//build up the chains:
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();
        manager.createChain(url, chainDefinition);
    }
}

// create the default chain, to match anything the path matching would have missed
manager.createDefaultChain("/**"); // TODO this assumes ANT path matching, which might be OK here

return manager;

url为要拦截的url,chainDefinition为所需的权限。然后调用manager的createChain函数开始创建权限规则,这是一个DefaultFilterChainManager类,经过createChain、addToChain等函数来到addToChain函数:

public void addToChain(String chainName, String filterName, String chainSpecificFilterConfig) {
    if (!StringUtils.hasText(chainName)) {
        throw new IllegalArgumentException("chainName cannot be null or empty.");
    } else {
        Filter filter = this.getFilter(filterName);
        if (filter == null) {
            ...
        } else {
            this.applyChainConfig(chainName, filter, chainSpecificFilterConfig);
            NamedFilterList chain = this.ensureChain(chainName);
            chain.add(filter);
        }
    }
}

通过定义的权限获取对应的filter,比如authc获取到的就是FormAuthenticationFilter,然后调用applyChainConfig函数应用新的规则,一路来到processPathConfig函数中:

public Filter processPathConfig(String path, String config) {
    String[] values = null;
    if (config != null) {
        values = StringUtils.split(config);
    }

    this.appliedPaths.put(path, values);
    return this;
}

其实就是将要拦截的路径放入appliedPaths这个map中,当请求到来时就会调用preHandle函数:

for (String path : this.appliedPaths.keySet()) {
    // If the path does match, then pass on to the subclass implementation for specific checks
    //(first match 'wins'):
    if (pathsMatch(path, request)) {
        log.trace("Current requestURI matches pattern '{}'.  Determining filter chain execution...", path);
        Object config = this.appliedPaths.get(path);
        return isFilterChainContinued(request, response, path, config);
    }
}

将请求路径跟appliedPaths中的配置相匹配,然后调用isAccessAllowed函数进行权限判断:

protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    return super.isAccessAllowed(request, response, mappedValue) ||
        (!isLoginRequest(request, response) && isPermissive(mappedValue));
}

mappedValue通常为null,所以继续看父类的isAccessAllowed函数:

protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {
    Subject subject = getSubject(request, response);
    return subject.isAuthenticated() && subject.getPrincipal() != null;
}
...
protected Subject getSubject(ServletRequest request, ServletResponse response) {
    return SecurityUtils.getSubject();
}

跟我们控制器中的写法差不多,简单来说就是通过读取session来检验是否登录。

身份认证

懒得看了,应该差不多那个样子。


参考

https://blog.csdn.net/lzx1991610/article/details/100743003

https://www.jianshu.com/p/cb77412fde4d


Web Java Shiro

本博客所有文章除特别声明外,均采用 CC BY-SA 3.0协议 。转载请注明出处!

shiro-550 1.2.4反序列化漏洞
shiro入门