shiroFilter生命周期
- 基本说明
- 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();
- 验证securityManager不为空,验证 securityManager 必须是 WebSecurityManager
- 创建 过滤链管理器(责任链模式),将shiro默认的验证类型和用户自己定义的过滤器添加到管理器
- 返回一个 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
- 设置filterConfig和servletContext
- 确保 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();
- 当 init-param 的 staticSecurityManagerEnabled 不为空就设置 this.staticSecurityManagerEnabled
- init是空的,留给子类去实现,当前情况下没有子类去实现,就是空的
- 当 securityManager 为空的时候,创建默认的securityManager
- 判断 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
- 判断是否有 filterName().FILTERED,不为空就 执行下一个过滤器
- 判断 没有启用过滤器 或者 request 没有过滤器就执行下一个过滤器
- 设置 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);
- 用 ShiroHttpServletRequest 包装servletRequest,ShiroHttpServletResponse 包装servletResponse (这是一个典型的装饰模式)
- 创建当前用户 Subject
- 当前用户执行访问目标操作
在这里创建用户的部分可以参考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);
获得当前链的操作 做了一下几个步骤
- 获取 FilterChainManager (在ShiroFilterFactoryBean的getObject下面的createInstance创建的)
- 获取 requestURI
- 遍历配置的 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);
- 这里判断是否存在当前匹配器 ,例如 全局都没有配置 /user = authc, 那么就表示没有authc匹配器
- 遍历匹配器的key,用请求的uri和配置的匹配器进行匹配
- 进入下一阶段获取是否可以继续执行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;}
访问控制器
- 判断是否允许访问, 允许访问就返回true
- 拒绝访问(无论如何都返回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
- 判断subject 是否已经认证
- 判断是不是loginUrl
- 判断是否有授权
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生命周期相关推荐
- LTV 即用户生命周期价值
20220321 https://mp.weixin.qq.com/s/kPoojfRCbvCCV4zpnCimmQ 指标计算详细介绍 数据分析|如何做好用户生命周期价值分析 LTV https:// ...
- Harmony生命周期
Harmony生命周期 系统管理或用户操作等行为,均会引起Page实例在其生命周期的不同状态之间进行转换.Ability类提供的回调机制能够让Page及时感知外界变化,从而正确地应对状态变化(比如释放 ...
- Activity在有Dialog时按Home键的生命周期
当一个Activity弹出Dialog对话框时,程序的生命周期依然是onCreate() - onStart() - onResume(),在弹出Dialog的时候并没有onPause()和onSto ...
- 横竖屏切换时Activity的生命周期
1.不设置Activity的android:configChanges时,切屏会重新调用各个生命周期,切横屏执行一次,切竖屏执行两次. 2.设置Activity的android:configChang ...
- Android中Service生命周期、启动、绑定、混合使用
一.Activity和Service如何绑定: 1.Service和Activity之间的连接可以用ServiceConnection来实现.实现一个ServiceConnection对象实例,重写o ...
- Cocos生命周期回调
Cocos Creator 为组件脚本提供了生命周期的回调函数.用户只要定义特定的回调函数,Creator 就会在特定的时期自动执行相关脚本,用户不需要手工调用它们. 目前提供给用户的生命周期回调函数 ...
- Fragment 使用 replace 的方式实现切换 以及切换的时候Fragment 生命周期
这个主要代码在activity里面 如下 public class ReplaceActivity extends AppCompatActivity implements View.OnClickL ...
- Fragment 使用 show 和 hide 的方式实现切换 以及切换的时候Fragment 生命周期
实现的效果如下图 主要的代码在activity 这里贴出来了 public class ShowActvity extends AppCompatActivity implements View.On ...
- ViewPager与Fragment结合使用,以及切换的时候Fragment 的生命周期
下面要做的效果图下图 首先我们创建一个适配器如下 public class FraPagerAdapter extends FragmentPagerAdapter {private List< ...
最新文章
- Django模板系统和admin模块
- 对AFNetworking的简单封装
- 第二单元 考点6-7商业银行和投资理财
- 大豆和黄豆芽还能吃吗?
- 4.2 One-Shot 学习-深度学习第四课《卷积神经网络》-Stanford吴恩达教授
- B Convex Polygon
- jzoj4248-n染色【数学,快速幂】
- 剪切文件_lammps模拟带缺陷镍板剪切变形(in文件及注释)
- linux 下/proc/cpuinfo三级缓存,51CTO博客-专业IT技术博客创作平台-技术成就梦想
- spark textFile方法
- 图解设计模式:空对象模式
- 移植emwin到stm32f205上卡死在gui_init();
- crontab 误删除恢复
- 中国式危机公关9加1策略(第十三章 建立系统实用的危机管理机制)
- AI 重聚知名已故歌手,发布四首原创歌曲
- Alphago进化史 漫画告诉你Zero为什么这么牛
- Ubuntu下桌面死机的解决方法,无须重启
- Xpdf 中文字体解决方案(TTF字库) - 图文教程
- gtav登录请确认不是机器人_关于GTA5登录要接收R星验证码
- 元胞自动机交通模型【matlab实现】