• 基本说明

    • spring 配置信息
    • 获取shiroFilter
  • shiroFilter
    • init
    • doFilter
      • executeChain步骤(一)
      • executeChain步骤(二)
    • destory
  • 附注
    • 当前用户调用代理
    • 路径匹配
    • 默认过滤器
    • 认证过滤器
    • 访问控制器
      • isAccessAllowed
      • onAccessDenied
    • 权限过滤器

基本说明

filter执行的是代理bean的id为shiroFilter相应的方法

接下来看看 shiroFilter 是什么

再来分析shiroFilter 这个实现了 javax.servlet.Filter 的生命周期

spring 配置信息

 <bean id="shiroFilter" class="org.apache.shiro.spring.web.ShiroFilterFactoryBean"><property name="securityManager" ref="securityManager" /><!-- loginUrl认证提交地址,如果没有认证将会请求此地址进行认证,请求此地址将由formAuthenticationFilter进行表单认证 --><property name="loginUrl" value="/login.action" /><!-- 认证成功统一跳转到first.action,建议不配置,shiro认证成功自动到上一个请求路径 --><property name="successUrl" value="/index.action" /><!-- 通过unauthorizedUrl指定没有权限操作时跳转页面 --><property name="unauthorizedUrl" value="/refuse.action" /><!-- 过虑器链定义,从上向下顺序执行,一般将/**放在最下边 --><property name="filterChainDefinitions"><value><!-- 静态资源放行 -->/login/account = anon/user = perms["user:view"]<!--商品查询需要商品查询权限 ,取消url拦截配置,使用注解授权方式 --><!-- /itemEdit.action = perms[item:edit] --><!-- 请求 logout.action地址,shiro去清除session -->/logout = logout<!-- /** = authc 所有url都必须认证通过才可以访问 -->/** = authc<!-- 所有url都可以匿名访问 --><!-- /** = anon --></value></property></bean><!-- securityManager安全管理器 --><bean id="securityManager" class="org.apache.shiro.web.mgt.DefaultWebSecurityManager"><!-- 注入realm --><property name="realm" ref="customRealm" /><property name="sessionManager" ref="sessionManager"/></bean><!-- 会话管理器 --><bean id="sessionManager"class="org.apache.shiro.web.session.mgt.DefaultWebSessionManager"><!-- session的失效时长,单位毫秒 --><property name="globalSessionTimeout" value="600000" /><!-- 删除失效的session --><property name="deleteInvalidSessions" value="true" /></bean><!-- realm --><bean id="customRealm" class="org.apache.shiro.realm.text.IniRealm"><constructor-arg index="0" value="classpath:shiro.ini"/></bean>

获取shiroFilter

ShiroFilterFactoryBean 实现了 FactoryBean ,那么实际上getBean的时候,返回的是getObject的内容而不是ShiroFilterFactoryBean

(如果不理解实现 FactoryBean 会返回 getObject 的同学, 期待接下来的spring源码分析,或者自行百度)

接下来分析 ShiroFilterFactoryBean 的getObject 返回的对象是什么,才能知道 init,doFilter,destory 的时候做了什么

ShiroFilterFactoryBean

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

级别2

instance = createInstance();

  1. 验证securityManager不为空,验证 securityManager 必须是 WebSecurityManager
  2. 创建 过滤链管理器(责任链模式),将shiro默认的验证类型和用户自己定义的过滤器添加到管理器
  3. 返回一个 SpringShiroFilter 对象

ShiroFilterFactoryBean

 protected AbstractShiroFilter createInstance() throws Exception {log.debug("Creating Shiro Filter instance.");SecurityManager securityManager = getSecurityManager();if (securityManager == null) {String msg = "SecurityManager property must be set.";throw new BeanInitializationException(msg);}if (!(securityManager instanceof WebSecurityManager)) {String msg = "The security manager does not implement the WebSecurityManager interface.";throw new BeanInitializationException(msg);}FilterChainManager manager = createFilterChainManager();//Expose the constructed FilterChainManager by first wrapping it in a// FilterChainResolver implementation. The AbstractShiroFilter implementations// do not know about FilterChainManagers - only resolvers:PathMatchingFilterChainResolver chainResolver = new PathMatchingFilterChainResolver();chainResolver.setFilterChainManager(manager);//Now create a concrete ShiroFilter instance and apply the acquired SecurityManager and built//FilterChainResolver.  It doesn't matter that the instance is an anonymous inner class//here - we're just using it because it is a concrete AbstractShiroFilter instance that accepts//injection of the SecurityManager and FilterChainResolver:return new SpringShiroFilter((WebSecurityManager) securityManager, chainResolver);}

所以在web中配置的 shiroFilter 实际上是一个 SpringShiroFilter 对象,我们要分析的shiro在web中的使用,也就是分析 SpringShiroFilter 的生命周期

shiroFilter

init

  1. 设置filterConfig和servletContext
  2. 确保 this.securityManager 已被初始化

SpringShiroFilter

    public void setFilterConfig(FilterConfig filterConfig) {this.filterConfig = filterConfig;setServletContext(filterConfig.getServletContext());}public final void init(FilterConfig filterConfig) throws ServletException {setFilterConfig(filterConfig);try {onFilterConfigSet();} catch (Exception e) {if (e instanceof ServletException) {throw (ServletException) e;} else {if (log.isErrorEnabled()) {log.error("Unable to start Filter: [" + e.getMessage() + "].", e);}throw new ServletException(e);}}}

级别2

onFilterConfigSet();

  1. 当 init-param 的 staticSecurityManagerEnabled 不为空就设置 this.staticSecurityManagerEnabled
  2. init是空的,留给子类去实现,当前情况下没有子类去实现,就是空的
  3. 当 securityManager 为空的时候,创建默认的securityManager
  4. 判断 this.staticSecurityManagerEnabled 为true,就设置 SecurityUtils.securityManager

AbstractShiroFilter

// 当 init-param 的 staticSecurityManagerEnabled 不为空就设置 this.staticSecurityManagerEnabled
private void applyStaticSecurityManagerEnabledConfig() {String value = getInitParam(STATIC_INIT_PARAM_NAME);if (value != null) {Boolean b = Boolean.valueOf(value);if (b != null) {setStaticSecurityManagerEnabled(b);}}}
//当 securityManager 为空的时候,创建默认的securityManagerprivate void ensureSecurityManager() {WebSecurityManager securityManager = getSecurityManager();if (securityManager == null) {log.info("No SecurityManager configured.  Creating default.");securityManager = createDefaultSecurityManager();setSecurityManager(securityManager);}}protected final void onFilterConfigSet() throws Exception {//added in 1.2 for SHIRO-287:applyStaticSecurityManagerEnabledConfig();init();ensureSecurityManager();//added in 1.2 for SHIRO-287:if (isStaticSecurityManagerEnabled()) {SecurityUtils.setSecurityManager(getSecurityManager());}}

doFilter

父类 OncePerRequestFilter 实现了 doFilter

  1. 判断是否有 filterName().FILTERED,不为空就 执行下一个过滤器
  2. 判断 没有启用过滤器 或者 request 没有过滤器就执行下一个过滤器
  3. 设置 filterName().FILTERED = true, 执行当前过滤器

OncePerRequestFilter

 public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)throws ServletException, IOException {String alreadyFilteredAttributeName = getAlreadyFilteredAttributeName();if ( request.getAttribute(alreadyFilteredAttributeName) != null ) {log.trace("Filter '{}' already executed.  Proceeding without invoking this filter.", getName());filterChain.doFilter(request, response);} else //noinspection deprecationif (/* added in 1.2: */ !isEnabled(request, response) ||/* retain backwards compatibility: */ shouldNotFilter(request) ) {log.debug("Filter '{}' is not enabled for the current request.  Proceeding without invoking this filter.",getName());filterChain.doFilter(request, response);} else {// Do invoke this filter...log.trace("Filter '{}' not yet executed.  Executing now.", getName());request.setAttribute(alreadyFilteredAttributeName, Boolean.TRUE);try {doFilterInternal(request, response, filterChain);} finally {// Once the request has finished, we're done and we don't// need to mark as 'already filtered' any more.request.removeAttribute(alreadyFilteredAttributeName);}}}

级别2

doFilterInternal(request, response, filterChain);

  1. 用 ShiroHttpServletRequest 包装servletRequest,ShiroHttpServletResponse 包装servletResponse (这是一个典型的装饰模式)
  2. 创建当前用户 Subject
  3. 当前用户执行访问目标操作

在这里创建用户的部分可以参考shiro 基础章节, 就是从 this.securityManager.createSubject(this.subjectContext);

访问目标 subject.execute 就是接下来进行重点讲解的了

AbstractShiroFilter

 protected void doFilterInternal(ServletRequest servletRequest, ServletResponse servletResponse, final FilterChain chain)throws ServletException, IOException {Throwable t = null;try {final ServletRequest request = prepareServletRequest(servletRequest, servletResponse, chain);final ServletResponse response = prepareServletResponse(request, servletResponse, chain);final Subject subject = createSubject(request, response);//noinspection uncheckedsubject.execute(new Callable() {public Object call() throws Exception {updateSessionLastAccessTime(request, response);executeChain(request, response, chain);return null;}});} catch (ExecutionException ex) {t = ex.getCause();} catch (Throwable throwable) {t = throwable;}if (t != null) {if (t instanceof ServletException) {throw (ServletException) t;}if (t instanceof IOException) {throw (IOException) t;}//otherwise it's not one of the two exceptions expected by the filter method signature - wrap it in one:String msg = "Filtered request failed.";throw new ServletException(msg, t);}}

级别3-1

subject.execute

这里就是一个很典型的代理模式 ,用 SubjectCallable 代理了原来的 callable.

然后再执行真正的call

public SubjectCallable(Subject subject, Callable<V> delegate) {// 设置TheadStatethis(new SubjectThreadState(subject), delegate);}public <V> Callable<V> associateWith(Callable<V> callable) {//返回代理类return new SubjectCallable<V>(this, callable);}public <V> V execute(Callable<V> callable) throws ExecutionException {Callable<V> associated = associateWith(callable);try {// 代理类 callreturn associated.call();} catch (Throwable t) {throw new ExecutionException(t);}}

级别3-2

updateSessionLastAccessTime(request, response);

通过名字就可以判断出来, 更新session的最后访问时间。

但是访问的时候带sessionId会报异常,不过没什么影响,可以忽略,或者关闭这个日志

http://localhost:8080/login.action;JSESSIONID=2bf853ec-f439-4981-8c01-7bfa7456c878

 org.apache.shiro.session.UnknownSessionException: There is no session with id [b81ed452-882c-4cd1-a8e8-b1bfd7e3cee8]at org.apache.shiro.session.mgt.eis.AbstractSessionDAO.readSession(AbstractSessionDAO.java:170)

级别3-3

executeChain(request, response, chain);

这段内容也比较简单, 就是用当前的 FilterChainResolver 去获取 FilterChain ,

然后返回去执行 chain.doFilter(request, response);

这一段的重点内容也就在于 resolver.getChain(request, response, origChain);

这里会具体进行角色权限的判断,判断用户是不是有url的访问权限

    protected void executeChain(ServletRequest request, ServletResponse response, FilterChain origChain)throws IOException, ServletException {FilterChain chain = getExecutionChain(request, response, origChain);chain.doFilter(request, response);}

executeChain步骤(一)

级别1

     protected FilterChain getExecutionChain(ServletRequest request, ServletResponse response, FilterChain origChain) {FilterChain chain = origChain;FilterChainResolver resolver = getFilterChainResolver();if (resolver == null) {log.debug("No FilterChainResolver configured.  Returning original FilterChain.");return origChain;}FilterChain resolved = resolver.getChain(request, response, origChain);if (resolved != null) {log.trace("Resolved a configured FilterChain for the current request.");chain = resolved;} else {log.trace("No FilterChain configured for the current request.  Using the default.");}return chain;}

级别2

FilterChain resolved = resolver.getChain(request, response, origChain);

获得当前链的操作 做了一下几个步骤

  1. 获取 FilterChainManager (在ShiroFilterFactoryBean的getObject下面的createInstance创建的)
  2. 获取 requestURI
  3. 遍历配置的 filterChainDefinitions 属性的key。 判断key是否能匹配requestURI。 如果能匹配 就返回value对应的Filter
    接下来解析如何通过value获取对应的filter

如果有更多的兴趣: 可以去查看 如何匹配请求uri的filter

PathMatchingFilterChainResolver

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 requirementsfor (String pathPattern : filterChainManager.getChainNames()) {// If the path does match, then pass on to the subclass implementation for specific checks:if (pathMatches(pathPattern, requestURI)) {if (log.isTraceEnabled()) {log.trace("Matched path pattern [" + pathPattern + "] for requestURI [" + requestURI + "].  " +"Utilizing corresponding filter chain...");}return filterChainManager.proxy(originalChain, pathPattern);}}return null;}

级别3

return filterChainManager.proxy(originalChain, pathPattern);

这里就是通过 配置的key获取对应的Filter

例如: /* = authc 就是通过/* 获取shiro内置权限认证对象: NamedFilterList

DefaultFilterChainManager

    public NamedFilterList getChain(String chainName) {return this.filterChains.get(chainName);}public FilterChain proxy(FilterChain original, String chainName) {NamedFilterList configured = getChain(chainName);if (configured == null) {String msg = "There is no configured chain under the name/key [" + chainName + "].";throw new IllegalArgumentException(msg);}return configured.proxy(original);}

级别4

return configured.proxy(original);

这里可以看到,是通过 NamedFilterList 和 FilterChain 创建了一个代理对象 ProxiedFilterChain

SimpleNamedFilterList

    public FilterChain proxy(FilterChain orig) {return new ProxiedFilterChain(orig, this);}

executeChain步骤(二)

chain.doFilter(request, response);

通过 executeChain步骤(一) 就已经获取到了 FilterChain 对象。
返回的是一个 ProxiedFilterChain 对象。
这一步实际上跟进的就是 ProxiedFilterChain.doFilter

到这里为止,其实就已经分析完成了。因为类似: /** = authc 可以有各种各样的配置。
在最下面的附注会附上 perms 和 authc 的doFilter 分析

ProxiedFilterChain

    public void doFilter(ServletRequest request, ServletResponse response) throws IOException, ServletException {if (this.filters == null || this.filters.size() == this.index) {//we've reached the end of the wrapped chain, so invoke the original one:if (log.isTraceEnabled()) {log.trace("Invoking original filter chain.");}this.orig.doFilter(request, response);} else {if (log.isTraceEnabled()) {log.trace("Invoking wrapped filter at index [" + this.index + "]");}this.filters.get(this.index++).doFilter(request, response, this);}}

destory

卸载的时候什么都不做

AbstractFilter

   public void destroy() {}

附注

当前用户调用代理

做的事情非常简单,就是在执行目标前后进行 threadState 的设置和还原

其实就是设置当前线程的 Subject 和 SecurityManager

SubjectCallable

    protected V doCall(Callable<V> target) throws Exception {return target.call();}public V call() throws Exception {try {threadState.bind();return doCall(this.callable);} finally {threadState.restore();}}

路径匹配

默认过滤器

shiro内置权限认证对象

认证过滤器

authc对应的filter: FormAuthenticationFilter

FormAuthenticationFilter 继承 OncePerRequestFilter 那么 doFilter 实际上就是执行 doFilterInternal

    public final void doFilter(ServletRequest request, ServletResponse response, FilterChain filterChain)throws ServletException, IOException {//TODO 省略doFilterInternal(httpRequest, httpResponse, filterChain);}

级别2

doFilterInternal(httpRequest, httpResponse, filterChain);

这里的代码整体流程也非常清晰
1. 预处理请求 (进行url匹配,匹配成功后进行认证。 认证成功后 continueChain=true)
2. 通过访问控制器判断是否要执行调用链,
3. 清理调用链 (在这里什么都没做)

那么这里就可以确定最最重要的部分 perHandler

AdviceFilter

    public void doFilterInternal(ServletRequest request, ServletResponse response, FilterChain chain)throws ServletException, IOException {Exception exception = null;try {boolean continueChain = preHandle(request, response);if (log.isTraceEnabled()) {log.trace("Invoked preHandle method.  Continuing chain?: [" + continueChain + "]");}if (continueChain) {executeChain(request, response, chain);}postHandle(request, response);if (log.isTraceEnabled()) {log.trace("Successfully invoked postHandle method");}} catch (Exception e) {exception = e;} finally {cleanup(request, response, exception);}}

级别3

boolean continueChain = preHandle(request, response);

  1. 这里判断是否存在当前匹配器 ,例如 全局都没有配置 /user = authc, 那么就表示没有authc匹配器
  2. 遍历匹配器的key,用请求的uri和配置的匹配器进行匹配
  3. 进入下一阶段获取是否可以继续执行filterChain

PathMatchingFilter

    protected boolean preHandle(ServletRequest request, ServletResponse response) throws Exception {if (this.appliedPaths == null || this.appliedPaths.isEmpty()) {if (log.isTraceEnabled()) {log.trace("appliedPaths property is null or empty.  This Filter will passthrough immediately.");}return true;}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);}}//no path matched, allow the request to go through:return true;}

级别4

return isFilterChainContinued(request, response, path, config);

onPreHandle 由 访问控制器分析 实现.

PathMatchingFilter

 private boolean isFilterChainContinued(ServletRequest request, ServletResponse response,String path, Object pathConfig) throws Exception {if (isEnabled(request, response, path, pathConfig)) { //isEnabled check added in 1.2if (log.isTraceEnabled()) {log.trace("Filter '{}' is enabled for the current request under path '{}' with config [{}].  " +"Delegating to subclass implementation for 'onPreHandle' check.",new Object[]{getName(), path, pathConfig});}//The filter is enabled for this specific request, so delegate to subclass implementations//so they can decide if the request should continue through the chain or not:return onPreHandle(request, response, pathConfig);}if (log.isTraceEnabled()) {log.trace("Filter '{}' is disabled for the current request under path '{}' with config [{}].  " +"The next element in the FilterChain will be called immediately.",new Object[]{getName(), path, pathConfig});}//This filter is disabled for this specific request,//return 'true' immediately to indicate that the filter will not process the request//and let the request/response to continue through the filter chain:return true;}

访问控制器

  1. 判断是否允许访问, 允许访问就返回true
  2. 拒绝访问(无论如何都返回false)

接下来就要进行两部分分析了 isAccessAllowed 和 onAccessDenied
AccessControlFilter

    public boolean onPreHandle(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {return isAccessAllowed(request, response, mappedValue) || onAccessDenied(request, response, mappedValue);}

isAccessAllowed

  1. 判断subject 是否已经认证
  2. 判断是不是loginUrl
  3. 判断是否有授权
    protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {return super.isAccessAllowed(request, response, mappedValue) ||(!isLoginRequest(request, response) && isPermissive(mappedValue));}//判断subject 是否已经认证protected boolean isAccessAllowed(ServletRequest request, ServletResponse response, Object mappedValue) {Subject subject = getSubject(request, response);return subject.isAuthenticated();}//判断是不是loginUrlprotected boolean isLoginRequest(ServletRequest request, ServletResponse response) {return pathsMatch(getLoginUrl(), request);}protected boolean isPermissive(Object mappedValue) {if(mappedValue != null) {String[] values = (String[]) mappedValue;return Arrays.binarySearch(values, PERMISSIVE) >= 0;}return false;}// 是否有访问权限,protected boolean isPermissive(Object mappedValue) {if(mappedValue != null) {String[] values = (String[]) mappedValue;return Arrays.binarySearch(values, PERMISSIVE) >= 0;}return false;}

onAccessDenied

调用子类实现,发现子类无论如何都会返回false

也就是说 当isAccessAllowed的结果为false(不允许访问的时候)
1. 当前用户的凭证信息为空的时候, 跳转到登录页
2. 判断是否已经配置 unauthorizedUrl (未授权跳转到的url),如果已经配置就跳转到 unauthorizedUrl, 如果没有配置没权限页面,返回错误码401 (HttpServletResponse.SC_UNAUTHORIZED)

AccessControlFilter

protected boolean onAccessDenied(ServletRequest request, ServletResponse response, Object mappedValue) throws Exception {return onAccessDenied(request, response);}

AuthorizationFilter

    protected boolean onAccessDenied(ServletRequest request, ServletResponse response) throws IOException {Subject subject = getSubject(request, response);// If the subject isn't identified, redirect to login URLif (subject.getPrincipal() == null) {saveRequestAndRedirectToLogin(request, response);} else {// If subject is known but not authorized, redirect to the unauthorized URL if there is one// If no unauthorized URL is specified, just return an unauthorized HTTP status codeString unauthorizedUrl = getUnauthorizedUrl();//SHIRO-142 - ensure that redirect _or_ error code occurs - both cannot happen due to response commit:if (StringUtils.hasText(unauthorizedUrl)) {WebUtils.issueRedirect(request, response, unauthorizedUrl);} else {WebUtils.toHttp(response).sendError(HttpServletResponse.SC_UNAUTHORIZED);}}return false;}

权限过滤器

shiroFilter生命周期相关推荐

  1. LTV 即用户生命周期价值

    20220321 https://mp.weixin.qq.com/s/kPoojfRCbvCCV4zpnCimmQ 指标计算详细介绍 数据分析|如何做好用户生命周期价值分析 LTV https:// ...

  2. Harmony生命周期

    Harmony生命周期 系统管理或用户操作等行为,均会引起Page实例在其生命周期的不同状态之间进行转换.Ability类提供的回调机制能够让Page及时感知外界变化,从而正确地应对状态变化(比如释放 ...

  3. Activity在有Dialog时按Home键的生命周期

    当一个Activity弹出Dialog对话框时,程序的生命周期依然是onCreate() - onStart() - onResume(),在弹出Dialog的时候并没有onPause()和onSto ...

  4. 横竖屏切换时Activity的生命周期

    1.不设置Activity的android:configChanges时,切屏会重新调用各个生命周期,切横屏执行一次,切竖屏执行两次. 2.设置Activity的android:configChang ...

  5. Android中Service生命周期、启动、绑定、混合使用

    一.Activity和Service如何绑定: 1.Service和Activity之间的连接可以用ServiceConnection来实现.实现一个ServiceConnection对象实例,重写o ...

  6. Cocos生命周期回调

    Cocos Creator 为组件脚本提供了生命周期的回调函数.用户只要定义特定的回调函数,Creator 就会在特定的时期自动执行相关脚本,用户不需要手工调用它们. 目前提供给用户的生命周期回调函数 ...

  7. Fragment 使用 replace 的方式实现切换 以及切换的时候Fragment 生命周期

    这个主要代码在activity里面 如下 public class ReplaceActivity extends AppCompatActivity implements View.OnClickL ...

  8. Fragment 使用 show 和 hide 的方式实现切换 以及切换的时候Fragment 生命周期

    实现的效果如下图 主要的代码在activity 这里贴出来了 public class ShowActvity extends AppCompatActivity implements View.On ...

  9. ViewPager与Fragment结合使用,以及切换的时候Fragment 的生命周期

    下面要做的效果图下图 首先我们创建一个适配器如下 public class FraPagerAdapter extends FragmentPagerAdapter {private List< ...

最新文章

  1. Django模板系统和admin模块
  2. 对AFNetworking的简单封装
  3. 第二单元 考点6-7商业银行和投资理财
  4. 大豆和黄豆芽还能吃吗?
  5. 4.2 One-Shot 学习-深度学习第四课《卷积神经网络》-Stanford吴恩达教授
  6. B Convex Polygon
  7. jzoj4248-n染色【数学,快速幂】
  8. 剪切文件_lammps模拟带缺陷镍板剪切变形(in文件及注释)
  9. linux 下/proc/cpuinfo三级缓存,51CTO博客-专业IT技术博客创作平台-技术成就梦想
  10. spark textFile方法
  11. 图解设计模式:空对象模式
  12. 移植emwin到stm32f205上卡死在gui_init();
  13. crontab 误删除恢复
  14. 中国式危机公关9加1策略(第十三章 建立系统实用的危机管理机制)
  15. AI 重聚知名已故歌手,发布四首原创歌曲
  16. Alphago进化史 漫画告诉你Zero为什么这么牛
  17. Ubuntu下桌面死机的解决方法,无须重启
  18. Xpdf 中文字体解决方案(TTF字库) - 图文教程
  19. gtav登录请确认不是机器人_关于GTA5登录要接收R星验证码
  20. 元胞自动机交通模型【matlab实现】

热门文章

  1. 移动端和PC端的pdf预览与下载
  2. 【练习题】第二章--变量,表达式,语句(Think Python)
  3. FFHQ数据解压问题记录
  4. 7.消费者的确认机制
  5. CCF-野外环境中的蝴蝶自动识别-排名54
  6. J204B接口数据链路的建立
  7. 计算机购销存系统论文,企业进销存管理系统毕业论文.doc
  8. SMSAlarm短信猫语音猫快速连接
  9. python自动发送短信验证码、短信通知、营销短信、语音短信
  10. react实现echarts的疫情地图