Spring 里那么多种 CORS 的配置方式,到底有什么区别
作为一个后端开发,我们经常遇到的一个问题就是需要配置CORS
,好让我们的前端能够访问到我们的 API,并且不让其他人访问。而在Spring
中,我们见过很多种CORS
的配置,很多资料都只是告诉我们可以这样配置、可以那样配置,但是这些配置有什么区别?
CORS 是什么
首先我们要明确,CORS
是什么,以及规范是如何要求的。这里只是梳理一下流程,具体的规范请看这里。
CORS
全称是Cross-Origin Resource Sharing
,直译过来就是跨域资源共享。要理解这个概念就需要知道域、资源和同源策略这三个概念。
- 域,指的是一个站点,由
protocal
、host
和port
三部分组成,其中host
可以是域名,也可以是ip
;port
如果没有指明,则是使用protocal
的默认端口 - 资源,是指一个
URL
对应的内容,可以是一张图片、一种字体、一段HTML
代码、一份JSON
数据等等任何形式的任何内容 - 同源策略,指的是为了防止
XSS
,浏览器、客户端应该仅请求与当前页面来自同一个域的资源,请求其他域的资源需要通过验证。
了解了这三个概念,我们就能理解为什么有CORS
规范了:从站点 A 请求站点 B 的资源的时候,由于浏览器的同源策略的影响,这样的跨域请求将被禁止发送;为了让跨域请求能够正常发送,我们需要一套机制在不破坏同源策略的安全性的情况下、允许跨域请求正常发送,这样的机制就是CORS
。
预检请求
在CORS
中,定义了一种预检请求,即preflight request
,当实际请求不是一个简单请求
时,会发起一次预检请求。预检请求是针对实际请求的 URL 发起一次OPTIONS
请求,并带上下面三个headers
:
-
Origin
:值为当前页面所在的域,用于告诉服务器当前请求的域。如果没有这个header
,服务器将不会进行CORS
验证。 -
Access-Control-Request-Method
:值为实际请求将会使用的方法 -
Access-Control-Request-Headers
:值为实际请求将会使用的header
集合
如果服务器端CORS
验证失败,则会返回客户端错误,即4xx
的状态码。
否则,将会请求成功,返回200
的状态码,并带上下面这些headers
:
-
Access-Control-Allow-Origin
:允许请求的域,多数情况下,就是预检请求中的Origin
的值 -
Access-Control-Allow-Credentials
:一个布尔值,表示服务器是否允许使用cookies
-
Access-Control-Expose-Headers
:实际请求中可以出现在响应中的headers
集合 -
Access-Control-Max-Age
:预检请求返回的规则可以被缓存的最长时间,超过这个时间,需要再次发起预检请求 -
Access-Control-Allow-Methods
:实际请求中可以使用到的方法集合
浏览器会根据预检请求的响应,来决定是否发起实际请求。
小结
到这里, 我们就知道了跨域请求会经历的故事:
- 访问另一个域的资源
- 有可能会发起一次预检请求(非简单请求,或超过了
Max-Age
) - 发起实际请求
接下来,我们看看在 Spring 中,我们是如何让CORS
机制在我们的应用中生效的。
几种配置的方式
Spring 提供了多种配置CORS
的方式,有的方式针对单个 API,有的方式可以针对整个应用;有的方式在一些情况下是等效的,而在另一些情况下却又出现不同。我们这里例举几种典型的方式来看看应该如何配置。
假设我们有一个 API:
@RestController
class HelloController {
@GetMapping("hello")
fun hello(): String {
return "Hello, CORS!"
}
}
@CrossOrigin
注解
使用@CorssOrigin
注解需要引入Spring Web
的依赖,该注解可以作用于方法或者类,可以针对这个方法或类对应的一个或多个 API 配置CORS
规则:
@RestController
class HelloController {
@GetMapping("hello")
@CrossOrigin(origins = ["http://localhost:8080"])
fun hello(): String {
return "Hello, CORS!"
}
}
实现WebMvcConfigurer.addCorsMappings
方法
WebMvcConfigurer
是一个接口,它同样来自于Spring Web
。我们可以通过实现它的addCorsMappings
方法来针对全局 API 配置CORS
规则:
@Configuration
@EnableWebMvc
class MvcConfig: WebMvcConfigurer {
override fun addCorsMappings(registry: CorsRegistry) {
registry.addMapping("/hello")
.allowedOrigins("http://localhost:8080")
}
}
注入CorsFilter
CorsFilter
同样来自于Spring Web
,但是实现WebMvcConfigurer.addCorsMappings
方法并不会使用到这个类,具体原因我们后面来分析。我们可以通过注入一个CorsFilter
来使用它:
@Configuration
class CORSConfiguration {
@Bean
fun corsFilter(): CorsFilter {
val configuration = CorsConfiguration()
configuration.allowedOrigins = listOf("http://localhost:8080")
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/hello", configuration)
return CorsFilter(source)
}
}
注入CorsFilter
不止这一种方式,我们还可以通过注入一个FilterRegistrationBean
来实现,这里就不给例子了。
在仅仅引入
Spring Web
的情况下,实现WebMvcConfigurer.addCorsMappings
方法和注入CorsFilter
这两种方式可以达到同样的效果,二选一即可。它们的区别会在引入Spring Security
之后会展现出来,我们后面再来分析。
Spring Security 中的配置
在引入了Spring Security
之后,我们会发现前面的方法都不能正确的配置CORS
,每次preflight request
都会得到一个401
的状态码,表示请求没有被授权。这时,我们需要增加一点配置才能让CORS
正常工作:
@Configuration
class SecurityConfig : WebSecurityConfigurerAdapter() {
override fun configure(http: HttpSecurity?) {
http?.cors()
}
}
或者,干脆不实现WebMvcConfigurer.addCorsMappings
方法或者注入CorsFilter
,而是注入一个CorsConfigurationSource
,同样能与上面的代码配合,正确的配置CORS
:
@Bean
fun corsConfigurationSource(): CorsConfigurationSource {
val configuration = CorsConfiguration()
configuration.allowedOrigins = listOf("http://localhost:8080")
val source = UrlBasedCorsConfigurationSource()
source.registerCorsConfiguration("/hello", configuration)
return source
}
到此,我们已经看过了几种典型的例子了,完整的内容可以在Demo中查看,我们接下来看看 Spring 到底是如何实现CORS
验证的。
这些配置有什么区别
我们会主要分析实现WebMvcConfigurer.addCorsMappings
方法和调用HttpSecurity.cors
方法这两种方式是如何实现CORS
的,但在进行之前,我们要先复习一下Filter
与Interceptor
的概念。
Filter 与 Interceptor
上图很形象的说明了Filter
与Interceptor
的区别,一个作用在DispatcherServlet
调用前,一个作用在调用后。
但实际上,它们本身并没有任何关系,是完全独立的概念。
Filter
由Servlet
标准定义,要求Filter
需要在Servlet
被调用之前调用,作用顾名思义,就是用来过滤请求。在Spring Web
应用中,DispatcherServlet
就是唯一的Servlet
实现。
Interceptor
由 Spring 自己定义,由DispatcherServlet
调用,可以定义在Handler
调用前后的行为。这里的Handler
,在多数情况下,就是我们的Controller
中对应的方法。
对于Filter
和Interceptor
的复习就到这里,我们只需要知道它们会在什么时候被调用到,就能理解后面的内容了。
WebMvcConfigurer.addCorsMappings
方法做了什么
我们从WebMvcConfigurer.addCorsMappings
方法的参数开始,先看看CORS
配置是如何保存到 Spring 上下文中的,然后在了解一下 Spring 是如何使用的它们。
注入 CORS 配置
CorsRegistry 和 CorsRegistration
WebMvcConfigurer.addCorsMappings
方法的参数CorsRegistry
用于注册CORS
配置,它的源码如下:
public class CorsRegistry {
private final List<CorsRegistration> registrations = new ArrayList<>();
public CorsRegistration addMapping(String pathPattern) {
CorsRegistration registration = new CorsRegistration(pathPattern);
this.registrations.add(registration);
return registration;
}
protected Map<String, CorsConfiguration> getCorsConfigurations() {
Map<String, CorsConfiguration> configs = new LinkedHashMap<>(this.registrations.size());
for (CorsRegistration registration : this.registrations) {
configs.put(registration.getPathPattern(), registration.getCorsConfiguration());
}
return configs;
}
}
我们发现这个类仅仅有两个方法:
-
addMapping
接收一个pathPattern
,创建一个CorsRegistration
实例,保存到列表后将其返回。在我们的代码中,这里的pathPattern
就是/hello
-
getCorsConfigurations
方法将保存的CORS
规则转换成Map
后返回
CorsRegistration
这个类,同样很简单,我们看看它的部分源码:
public class CorsRegistration {
private final String pathPattern;
private final CorsConfiguration config;
public CorsRegistration(String pathPattern) {
this.pathPattern = pathPattern;
this.config = new CorsConfiguration().applyPermitDefaultValues();
}
public CorsRegistration allowedOrigins(String... origins) {
this.config.setAllowedOrigins(Arrays.asList(origins));
return this;
}
}
不难发现,这个类仅仅保存了一个pathPattern
字符串和CorsConfiguration
,很好理解,它保存的是一个pathPattern
对应的CORS
规则。
在它的构造函数中,调用的CorsConfiguration.applyPermitDefaultValues
方法则用于配置默认的CORS
规则:
- allowedOrigins 默认为所有域
- allowedMethods 默认为
GET
、HEAD
和POST
- allowedHeaders 默认为所有
- maxAge 默认为 30 分钟
- exposedHeaders 默认为 null,也就是不暴露任何 header
- credentials 默认为 null
创建CorsRegistration
后,我们可以通过它的allowedOrigins
、allowedMethods
等方法修改它的CorsConfiguration
,覆盖掉上面的默认值。
现在,我们已经通过WebMvcConfigurer.addCorsMappings
方法配置好CorsRegistry
了,接下来看看这些配置会在什么地方被注入到 Spring 上下文中。
WebMvcConfigurationSupport
CorsRegistry.getCorsConfigurations
方法,会被WebMvcConfigurationSupport.getConfigurations
方法调用,这个方法如下:
protected final Map<String, CorsConfiguration> getCorsConfigurations() {
if (this.corsConfigurations == null) {
CorsRegistry registry = new CorsRegistry();
addCorsMappings(registry);
this.corsConfigurations = registry.getCorsConfigurations();
}
return this.corsConfigurations;
}
addCorsMappings(registry)
调用的是自己的方法,由子类DelegatingWebMvcConfiguration
通过委托的方式调用到WebMvcConfigurer.addCorsMappings
方法,我们的配置也由此被读取到。
getCorsConfigurations
是一个protected
方法,是为了在扩展该类时,仍然能够直接获取到CORS
配置。而这个方法在这个类里被四个地方调用到,这四个调用的地方,都是为了注册一个HandlerMapping
到 Spring 容器中。每一个地方都会调用mapping.setCorsConfigurations
方法来接收CORS
配置,而这个setCorsConfigurations
方法,则由AbstractHandlerMapping
提供,CorsConfigurations
也被保存在这个抽象类中。
到此,我们的CORS
配置借由AbstractHandlerMapping
被注入到了多个HandlerMapping
中,而这些HandlerMapping
以 Spring 组件的形式被注册到了 Spring 容器中,当请求来临时,将会被调用。
获取 CORS 配置
还记得前面关于Filter
和Interceptor
那张图吗?当请求来到Spring Web
时,一定会到达DispatcherServlet
这个唯一的Servlet
。
在DispatcherServlet.doDispatch
方法中,会调用所有HandlerMapping.getHandler
方法。好巧不巧,这个方法又是由AbstractHandlerMapping
实现的:
@Override
@Nullable
public final HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
// 省略代码
if (CorsUtils.isCorsRequest(request)) {
CorsConfiguration globalConfig = this.corsConfigurationSource.getCorsConfiguration(request);
CorsConfiguration handlerConfig = getCorsConfiguration(handler, request);
CorsConfiguration config = (globalConfig != null ? globalConfig.combine(handlerConfig) : handlerConfig);
executionChain = getCorsHandlerExecutionChain(request, executionChain, config);
}
return executionChain;
}
在这个方法中,关于CORS
的部分都在这个if
中。我们来看看最后这个getCorsHandlerExecutionChain
做了什么:
protected HandlerExecutionChain getCorsHandlerExecutionChain(HttpServletRequest request,
HandlerExecutionChain chain, @Nullable CorsConfiguration config) {
if (CorsUtils.isPreFlightRequest(request)) {
HandlerInterceptor[] interceptors = chain.getInterceptors();
chain = new HandlerExecutionChain(new PreFlightHandler(config), interceptors);
}
else {
chain.addInterceptor(new CorsInterceptor(config));
}
return chain;
}
可以看到:
- 针对
preflight request
,由于不会有对应的Handler
来处理,所以这里就创建了一个PreFlightHandler
来作为这次请求的handler
- 对于其他的跨域请求,因为会有对应的
handler
,所以就在handlerExecutionChain
中加入一个CorsInterceptor
来进行CORS
验证
这里的PreFlightHandler
和CorsInterceptor
都是AbstractHandlerMapping
的内部类,实现几乎一致,区别仅仅在于一个是HttpRequestHandler
,一个是HandlerInterceptor
;它们对CORS
规则的验证都交由CorsProcessor
接口完成,这里采用了默认实现DefaultCorsProcessor
。
DefaultCorsProcessor
则是依照CORS
标准来实现,并在验证失败的时候打印debug
日志并拒绝请求。我们只需要关注一下标准中没有定义的验证失败时的状态码:
protected void rejectRequest(ServerHttpResponse response) throws IOException {
response.setStatusCode(HttpStatus.FORBIDDEN);
response.getBody().write("Invalid CORS request".getBytes(StandardCharsets.UTF_8));
}
CORS
验证失败时调用这个方法,并设置状态码为403
。
小结
通过对源码的研究,我们发现实现WebMvcConfigurer.addCorsMappings
方法的方式配置CORS
,会在Interceptor
或者Handler
层进行CORS
验证。
HtttpSecurity.cors
方法做了什么
在研究这个方法的行为之前,我们先来回想一下,我们调用这个方法解决的是什么问题。
前面我们通过某种方式配置好CORS
后,引入Spring Security
,CORS
就失效了,直到调用这个方法后,CORS
规则才重新生效。
下面这些原因,导致了preflight request
无法通过身份验证,从而导致CORS
失效:
-
preflight request
不会携带认证信息 -
Spring Security
通过Filter
来进行身份验证 -
Interceptor
和HttpRequestHanlder
在DispatcherServlet
之后被调用 -
Spring Security
中的Filter
优先级比我们注入的CorsFilter
优先级高
接下来我们就来看看HttpSecurity.cors
方法是如何解决这个问题的。
CorsConfigurer 如何配置 CORS 规则
HttpSecurity.cors
方法中其实只有一行代码:
public CorsConfigurer<HttpSecurity> cors() throws Exception {
return getOrApply(new CorsConfigurer<>());
}
这里调用的getOrApply
方法会将SecurityConfigurerAdapter
的子类实例加入到它的父类AbstractConfiguredSecurityBuilder
维护的一个Map
中,然后一个个的调用configure
方法。所以,我们来关注一下CorsConfigurer.configure
方法就好了。
@Override
public void configure(H http) throws Exception {
ApplicationContext context = http.getSharedObject(ApplicationContext.class);
CorsFilter corsFilter = getCorsFilter(context);
if (corsFilter == null) {
throw new IllegalStateException(
"Please configure either a " + CORS_FILTER_BEAN_NAME + " bean or a "
+ CORS_CONFIGURATION_SOURCE_BEAN_NAME + "bean.");
}
http.addFilter(corsFilter);
}
这段代码很好理解,就是在当前的 Spring Context 中找到一个CorsFilter
,然后将它加入到http
对象的filters
中。由上面的HttpSecurity.cors
方法可知,这里的http
对象实际类型就是HttpSecurity
。
getCorsFilter 方法做了什么
也许你会好奇,HttpSecurity
要如何保证CorsFilter
一定在Spring Security
的Filters
之前调用。但是在研究这个之前,我们先来看看同样重要的getCorsFilter
方法,这里可以解答我们前面的一些疑问。
private CorsFilter getCorsFilter(ApplicationContext context) {
if (this.configurationSource != null) {
return new CorsFilter(this.configurationSource);
}
boolean containsCorsFilter = context
.containsBeanDefinition(CORS_FILTER_BEAN_NAME);
if (containsCorsFilter) {
return context.getBean(CORS_FILTER_BEAN_NAME, CorsFilter.class);
}
boolean containsCorsSource = context
.containsBean(CORS_CONFIGURATION_SOURCE_BEAN_NAME);
if (containsCorsSource) {
CorsConfigurationSource configurationSource = context.getBean(
CORS_CONFIGURATION_SOURCE_BEAN_NAME, CorsConfigurationSource.class);
return new CorsFilter(configurationSource);
}
boolean mvcPresent = ClassUtils.isPresent(HANDLER_MAPPING_INTROSPECTOR,
context.getClassLoader());
if (mvcPresent) {
return MvcCorsFilter.getMvcCorsFilter(context);
}
return null;
}
这是CorsConfigurer
寻找CorsFilter
的全部逻辑,我们用人话来说就是:
-
CorsConfigurer
自己是否有配置CorsConfigurationSource
,如果有的话,就用它创建一个CorsFilter
。 - 在当前的上下文中,是否存在一个名为
corsFilter
的实例,如果有的话,就把他当作一个CorsFilter
来用。 - 在当前的上下文中,是否存在一个名为
corsConfigurationSource
的CorsConfigurationSource
实例,如果有的话,就用它创建一个CorsFilter
。 - 在当前上下文的类加载器中,是否存在类
HandlerMappingIntrospector
,如果有的话,则通过MvcCorsFilter
这个内部类创建一个CorsFilter
。 - 如果没有找到,那就返回一个
null
,调用的地方最后会抛出异常,阻止 Spring 初始化。
上面的第 2、3、4 步能解答我们前面的配置为什么生效,以及它们的区别。
注册CorsFilter
的方式,这个Filter
最终会被直接注册到 Servlet container 中被使用到。
注册CorsConfigurationSource
的方式,会用这个source
创建一个CorsFiltet
然后注册到 Servlet container 中被使用到。
而第四步的情况比较复杂。HandlerMappingIntrospector
是Spring Web
提供的一个类,实现了CorsConfigurationSource
接口,所以在MvcCorsFilter
中,它被直接用于创建CorsFilter
。它实现的getCorsConfiguration
方法,会经历:
- 遍历
HandlerMapping
- 调用
getHandler
方法得到HandlerExecutionChain
- 从中找到
CorsConfigurationSource
的实例 - 调用这个实例的
getCorsConfiguration
方法,返回得到的CorsConfiguration
所以得到的CorsConfigurationSource
实例,实际上就是前面讲到的CorsInterceptor
或者PreFlightHandler
。
所以第四步实际上匹配的是实现WebMvcConfigurer.addCorsMappings
方法的方式。
由于在CorsFilter
中每次处理请求时都会调用CorsConfigurationSource.getCorsConfiguration
方法,而DispatcherServlet
中也会每次调用HandlerMapping.getHandler
方法,再加上这时的HandlerExecutionChain
中还有CorsInterceptor
,所以使用这个方式相对于其他方式,做了很多重复的工作。所以WebMvcConfigurer.addCorsMappings
+HttpSecurity.cors
的方式降低了我们代码的效率,也许微乎其微,但能避免的情况下,还是不要使用。
HttpSecurity 中的 filters 属性
在CorsConfigurer.configure
方法中调用的HttpSecurity.addFilter
方法,由它的父类HttpSecurityBuilder
声明,并约定了很多Filter
的顺序。然而CorsFilter
并不在其中。不过在Spring Security
中,目前还只有HttpSecurity
这一个实现,所以我们来看看这里的代码实现就知道CorsFilter
会排在什么地方了。
public HttpSecurity addFilter(Filter filter) {
Class<? extends Filter> filterClass = filter.getClass();
if (!comparator.isRegistered(filterClass)) {
throw new IllegalArgumentException("...");
}
this.filters.add(filter);
return this;
}
我们可以看到,Filter
会被直接加到List
中,而不是按照一定的顺序来加入的。但同时,我们也发现了一个comparator
对象,并且只有被注册到了该类的Filter
才能被加入到filters
属性中。这个comparator
又是用来做什么的呢?
在 Spring Security 创建过程中,会调用到HttpSeciryt.performBuild
方法,在这里我们可以看到filters
和comparator
是如何被使用到的。
protected DefaultSecurityFilterChain performBuild() throws Exception {
Collections.sort(filters, comparator);
return new DefaultSecurityFilterChain(requestMatcher, filters);
}
可以看到,Spring Security 使用了这个comparator
在获取SecurityFilterChain
的时候来保证filters
的顺序,所以,研究这个comparator
就能知道在SecurityFilterChain
中的那些Filter
的顺序是如何的了。
这个comparator
的类型是FilterComparator
,从名字就能看出来是专用于Filter
比较的类,它的实现也并不神秘,从构造函数就能猜到是如何实现的:
FilterComparator() {
Step order = new Step(INITIAL_ORDER, ORDER_STEP);
put(ChannelProcessingFilter.class, order.next());
put(ConcurrentSessionFilter.class, order.next());
put(WebAsyncManagerIntegrationFilter.class, order.next());
put(SecurityContextPersistenceFilter.class, order.next());
put(HeaderWriterFilter.class, order.next());
put(CorsFilter.class, order.next());
// 省略代码
}
可以看到CorsFilter
排在了第六位,在所有的 Security Filter 之前,由此便解决了preflight request
没有携带认证信息的问题。
小结
引入Spring Security
之后,我们的CORS
验证实际上是依然运行着的,只是因为preflight request
不会携带认证信息,所以无法通过身份验证。使用HttpSecurity.cors
方法会帮助我们在当前的 Spring Context 中找到或创建一个CorsFilter
并安排在身份验证的Filter
之前,以保证能对preflight request
正确处理。
总结
研究了 Spring 中 CORS 的代码,我们了解到了这样一些知识:
- 实现
WebMvcConfigurer.addCorsMappings
方法来进行的CORS
配置,最后会在 Spring 的Interceptor
或Handler
中生效 - 注入
CorsFilter
的方式会让CORS
验证在Filter
中生效 - 引入
Spring Security
后,需要调用HttpSecurity.cors
方法以保证CorsFilter
会在身份验证相关的Filter
之前执行 -
HttpSecurity.cors
+WebMvcConfigurer.addCorsMappings
是一种相对低效的方式,会导致跨域请求分别在Filter
和Interceptor
层各经历一次CORS
验证 -
HttpSecurity.cors
+ 注册CorsFilter
与HttpSecurity.cors
+ 注册CorsConfigurationSource
在运行的时候是等效的 - 在 Spring 中,没有通过
CORS
验证的请求会得到状态码为 403 的响应
喜欢 (4)or分享 (0)
- php常见的判断函数
- BZOJ 1179: [Apio2009]Atm(tarjan+SPFA)
- angularjs promise详解
- svn 文件状态标记含义
- php mail函数发送html邮件不解析,linux+postfix
- POJ 1523 SPF(tarjan求割点)
- css渲染(三)颜色与背景
- file_get_contents()函数超时处理方法
- tarjan系列算法代码小结
- JQuery处理json与ajax返回JSON实例
- angularjs 表单验证
- CSS布局(一) 盒子模型基础
- PHP基于数组的分页函数(核心函数array_slice())
- CSS布局(二) 盒子模型属性
- java教程
- Java快速入门
- Java 开发环境配置
- Java基本语法
- Java 对象和类
- Java 基本数据类型
- Java 变量类型
- Java 修饰符
- Java 运算符
- Java 循环结构
- Java 分支结构
- Java Number类
- Java Character类
- Java String类
- Java StringBuffer和StringBuilder类
- Java 数组
- Java 日期时间
- Java 正则表达式
- Java 方法
- Java 流(Stream)、文件(File)和IO
- Java 异常处理
- Java 继承
- Java 重写(Override)与重载(Overload)
- Java 多态
- Java 抽象类
- Java 封装
- Java 接口
- Java 包(package)
- Java 数据结构
- Java 集合框架
- Java 泛型
- Java 序列化
- Java 网络编程
- Java 发送邮件
- Java 多线程编程
- Java Applet基础
- Java 文档注释
- R语言Kaggle泰坦尼克号性别阶级模型数据分析案例
- 以图搜图系统概述
- GitHub Pages 配置 letsencrypt 开启HTTPS
- R语言中ARMA,ARIMA(Box-Jenkins),SARIMA和ARIMAX模型用于预测时间序列数据
- 以图搜图系统工程实践
- R语言线性判别分析(LDA),二次判别分析(QDA)和正则判别分析(RDA)
- 用R语言实现神经网络预测股票实例
- R语言社区主题检测算法应用案例
- C++ vector学习笔记
- 锂电池充电慢?手把手教你制作锂电池快充充电器
- c++ cin, get学习笔记
- c++ sort 学习笔记
- CSS3 引入方式 注释 颜色属性 学习笔记
- 使用 Makefile 构建指令集
- 安卓 App 逆向课程之四 frida 注入 Okhttp 抓包中篇