SpringSecurity 初始化流程源码

本篇主要讲解 SpringSecurity初始化流程的源码部分,包括核心的 springSecurityFilterChain 是如何创建的,以及在介绍哪里可以扩展个性化的配置,SpringSecurity源码其实是蛮难得 各种Builder Configure 看得真的头疼!

 1.简单介绍

 SpringSecurity 的核心功能主要包括:

 认证 (你是谁)

 授权 (你能干什么)

 攻击防护 (防止伪造身份)

 其核心就是一组过滤器链,项目启动后将会自动配置,本篇也会涉及过滤器链是如何自动初始化的。

SecurityContextPersistenceFilter 是最前面的一个filter

 请求到它时候会去检查 根据sessionId找到session 判断session 中是否存在 SecurityContext 在 则将 SecurityContext 存入当前的线程中去

 响应的时候,看当前线程是否有SecurityContext ,如果有 放入到session中去 这样不同的请求都能拿到相同的 用户认证信息。

UsernamePasswordAuthenticationFilter 该过滤器是处理表单登录的,通过表单登录提交的认证都会经过它处理

SocialAuthenticationFilter 比如这个就是社交登录使用的Filter

 详细可以看我另外一篇 SpringSocial 实现第三方QQ登录SpringSocial 实现第三方QQ登录

绿色的过滤器都是可配置的,其他颜色的都不行!

 2.SecurityAutoConfiguration

 如果是SpringBoot项目只要你依赖了SpringSecurity相关依赖依然会有自动配置类

SecurityAutoConfiguration 生效 它会导入 WebSecurityEnableConfiguration

 @EnableWebSecurity将会是我们本篇的主要切入点

 3.@EnableWebSecurity注解介绍

 该注解 它是初始化Spring Security的入口 .

 打开@EnableWebSecurity注解

@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class,
SpringWebMvcImportSelector.class,
OAuth2ImportSelector.class })
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity { /**
* Controls debugging support for Spring Security. Default is false.
* @return if true, enables debug support with Spring Security
*/
boolean debug() default false;
}

 该注解类通过@Configuration和@Import配合使用引入了一个配置类(WebSecurityConfiguration)和两个ImportSelector(SpringWebMvcImportSelector,OAuth2ImportSelector),我们重点关注下WebSecurityConfiguration,它是Spring Security的核心

 4.springSecurityFilterChain初始化流程及源码

 打开WebSecurityConfiguration 它是一个配置类,主要看 springSecurityFilterChain()方法,它就是初始化

springSecurityFilterChain的核心方法

/**
* Creates the Spring Security Filter Chain
* @return the {@link Filter} that represents the security filter chain
* @throws Exception
*/
@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build();
}

@Bean注解name属性值AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME就是XML中定义的springSecurityFilterChain

 从源码中知道过滤器通过最后的 webSecurity.build()创建,webSecurity的类型为:WebSecurity,它在 setFilterChainProxySecurityConfigurer方法中优先被创建了:

@Autowired(required = false)
public void setFilterChainProxySecurityConfigurer(
ObjectPostProcessor<Object> objectPostProcessor,
@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
throws Exception {
webSecurity = objectPostProcessor
.postProcess(new WebSecurity(objectPostProcessor));
if (debugEnabled != null) {
webSecurity.debug(debugEnabled);
} webSecurityConfigurers.sort(AnnotationAwareOrderComparator.INSTANCE); Integer previousOrder = null;
Object previousConfig = null;
for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
Integer order = AnnotationAwareOrderComparator.lookupOrder(config);
if (previousOrder != null && previousOrder.equals(order)) {
throw new IllegalStateException(
"@Order on WebSecurityConfigurers must be unique. Order of "
+ order + " was already used on " + previousConfig + ", so it cannot be used on "
+ config + " too.");
}
previousOrder = order;
previousConfig = config;
}
for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
webSecurity.apply(webSecurityConfigurer);
}
this.webSecurityConfigurers = webSecurityConfigurers;
}

 从代码中可以看到,它是直接被new出来的:

webSecurity = objectPostProcessor
.postProcess(new WebSecurity(objectPostProcessor));

setFilterChainProxySecurityConfigurer 该方法的webSecurityConfigurers 参数是通过@Value注入的

@Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}")

AutowiredWebSecurityConfigurersIgnoreParents的 getWebSecurityConfigurers()

 如下,就是获取所有的 WebSecurityConfigurer的类型的配置类

 而通常 我们通过继承 WebSecurityConfigurerAdapter 来自定义WebSecurityConfigurer

public List<SecurityConfigurer<Filter, WebSecurity>> getWebSecurityConfigurers() {
List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers = new ArrayList<>();
Map<String, WebSecurityConfigurer> beansOfType = beanFactory
.getBeansOfType(WebSecurityConfigurer.class);
for (Entry<String, WebSecurityConfigurer> entry : beansOfType.entrySet()) {
webSecurityConfigurers.add(entry.getValue());
}
return webSecurityConfigurers;
}

再回到setFilterChainProxySecurityConfigurer方法 下面有一段这样的代码 ,对于上面获取的所有的WebSecurityConfigurer类型 循环执行 webSecurity的apply方法

for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
webSecurity.apply(webSecurityConfigurer);
}

webSecurity集成AbstractConfiguredSecurityBuilder 它提供apply方法 再其内部调用add方法

public <C extends SecurityConfigurer<O, B>> C apply(C configurer) throws Exception {
add(configurer);
return configurer;
}

add(configurer),主要就是将其传入的WebSecurityConfigurer存入到 LinkedHashMap configures中,

主要代码 this.configurers.put(clazz, configs);

private <C extends SecurityConfigurer<O, B>> void add(C configurer) {
Assert.notNull(configurer, "configurer cannot be null"); Class<? extends SecurityConfigurer<O, B>> clazz = (Class<? extends SecurityConfigurer<O, B>>) configurer
.getClass();
synchronized (configurers) {
if (buildState.isConfigured()) {
throw new IllegalStateException("Cannot apply " + configurer
+ " to already built object");
}
List<SecurityConfigurer<O, B>> configs = allowConfigurersOfSameType ? this.configurers
.get(clazz) : null;
if (configs == null) {
configs = new ArrayList<>(1);
}
configs.add(configurer);
this.configurers.put(clazz, configs);
if (buildState.isInitializing()) {
this.configurersAddedInInitializing.add(configurer);
}
}
}

当所有的 WebSecurityConfigurer 类型的配置 全部应用到 WebSecurity中去后 setFilterChainProxySecurityConfigurer方法也就结束了


回到创建过滤器链的方法 springSecurityFilterChain()

 它会判断我们刚刚的webSecurityConfigurers是否存在,不存在就新建一个,然后执行 webSecurity.build() 重要!

@Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
public Filter springSecurityFilterChain() throws Exception {
boolean hasConfigurers = webSecurityConfigurers != null
&& !webSecurityConfigurers.isEmpty();
if (!hasConfigurers) {
WebSecurityConfigurerAdapter adapter = objectObjectPostProcessor
.postProcess(new WebSecurityConfigurerAdapter() {
});
webSecurity.apply(adapter);
}
return webSecurity.build();
}

最终内部会有下面这段代码, 主要关注 init() configure() 和 performBuild() 这三个方法

@Override
protected final O doBuild() throws Exception {
synchronized (configurers) {
buildState = BuildState.INITIALIZING; beforeInit();
init(); buildState = BuildState.CONFIGURING; beforeConfigure();
configure(); buildState = BuildState.BUILDING; O result = performBuild(); buildState = BuildState.BUILT; return result;
}
}

init() 内部循环遍历 所有的 WebSecurityConfigurer ,它会执行到 WebSecurityConfigurerAdapter的

private void init() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = getConfigurers(); for (SecurityConfigurer<O, B> configurer : configurers) {
configurer.init((B) this);
} for (SecurityConfigurer<O, B> configurer : configurersAddedInInitializing) {
configurer.init((B) this);
}
}

configurer.init((B) this)

它只要完成两件重要的事情

初始化HttpSecurity对象(注意它和WebSecurity不一样 );

设置HttpSecurity对象添加至WebSecurity的securityFilterChainBuilders列表中;

public void init(final WebSecurity web) throws Exception {
final HttpSecurity http = getHttp();
web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
FilterSecurityInterceptor securityInterceptor = http
.getSharedObject(FilterSecurityInterceptor.class);
web.securityInterceptor(securityInterceptor);
});
}

初始化HttpSecurity对象在getHttp()方法中实现:

protected final HttpSecurity getHttp() throws Exception {
if (http != null) {
return http;
} DefaultAuthenticationEventPublisher eventPublisher = objectPostProcessor
.postProcess(new DefaultAuthenticationEventPublisher());
localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher); AuthenticationManager authenticationManager = authenticationManager();
authenticationBuilder.parentAuthenticationManager(authenticationManager);
authenticationBuilder.authenticationEventPublisher(eventPublisher);
Map<Class<?>, Object> sharedObjects = createSharedObjects(); http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
sharedObjects);
if (!disableDefaults) {
// @formatter:off
http
.csrf().and()
.addFilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and()
.headers().and()
.sessionManagement().and()
.securityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi().and()
.apply(new DefaultLoginPageConfigurer<>()).and()
.logout();
// @formatter:on
ClassLoader classLoader = this.context.getClassLoader();
List<AbstractHttpConfigurer> defaultHttpConfigurers =
SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader); for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
http.apply(configurer);
}
}
configure(http);
return http;
}

 从代码中可以了解,HttpSecurity是直接被new出来的,在创建HttpSecurity之前,首先初始化了AuthenticationManagerBuilder对象,这里有段代码很重要就是: AuthenticationManager authenticationManager = authenticationManager();,它创建AuthenticationManager实例,打开authenticationManager()方法:

 默认实现是在 WebSecurityConfigurerAdapter 中

protected void configure(AuthenticationManagerBuilder auth) throws Exception {
this.disableLocalConfigureAuthenticationBldr = true;
}

1、个性化配置入口之configure(AuthenticationManagerBuilder auth)

 我们可以通过继承WebSecurityConfigurerAdapter并重写该方法来个性化配置AuthenticationManagerBuilder。

如下是自己继承WebSecurityConfigurerAdapter 重写 configure(AuthenticationManagerBuilder auth),实现个性化的第一个配置入口

/**
* @author johnny
* @create 2020-01-18 下午6:40
**/
@Configuration
@Slf4j
public class MyWebSecurityConfig extends WebSecurityConfigurerAdapter { @Override
protected void configure(AuthenticationManagerBuilder auth) throws Exception {
super.configure(auth);
log.info("【测试 定制化入口 configure(AuthenticationManagerBuilder auth) 的执行 】");
}
}

 构建完HttpSecurity实例后,默认情况下会添加默认的拦截其配置:

        http
.csrf().and()
.addFilter(new WebAsyncManagerIntegrationFilter())
.exceptionHandling().and()
.headers().and()
.sessionManagement().and()
.securityContext().and()
.requestCache().and()
.anonymous().and()
.servletApi().and()
.apply(new DefaultLoginPageConfigurer<>()).and()
.logout();

 我挑一个默认的方法展开看一下比如 会话管理的sessionManagement(),内部就是去创建SessionManagementConfigurer并应用它

public SessionManagementConfigurer<HttpSecurity> sessionManagement() throws Exception {
return getOrApply(new SessionManagementConfigurer<>());
}

 getOrApply 最有一句代码 return apply(configurer);

private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply(
C configurer) throws Exception {
C existingConfig = (C) getConfigurer(configurer.getClass());
if (existingConfig != null) {
return existingConfig;
}
return apply(configurer);
}

 apply(configurer) 注意这里的 configurer传入的是SessionManagementConfigurer

public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer)
throws Exception {
configurer.addObjectPostProcessor(objectPostProcessor);
configurer.setBuilder((B) this);
add(configurer);
return configurer;
}

最终又调用了 add(configurer); 这不过这里是给 HttpSecurity的 configurers 配置初始的,上面是配置的WebSecurity的configurers, 不要混淆,最终这些configurers会被一个个创建成 对应的过滤器Filter的 详细在后面有说明

private <C extends SecurityConfigurer<O, B>> void add(C configurer) {
Assert.notNull(configurer, "configurer cannot be null"); Class<? extends SecurityConfigurer<O, B>> clazz = (Class<? extends SecurityConfigurer<O, B>>) configurer
.getClass();
synchronized (configurers) {
if (buildState.isConfigured()) {
throw new IllegalStateException("Cannot apply " + configurer
+ " to already built object");
}
List<SecurityConfigurer<O, B>> configs = allowConfigurersOfSameType ? this.configurers
.get(clazz) : null;
if (configs == null) {
configs = new ArrayList<>(1);
}
configs.add(configurer);
this.configurers.put(clazz, configs);
if (buildState.isInitializing()) {
this.configurersAddedInInitializing.add(configurer);
}
}
}

 如下图:为HttpSecurity添加了很多默认的配置

 回到 getHttp()方法

 最后调用configure(http);,这又是一个可个性化的配置入口,它的默认实现是:WebSecurityConfigurerAdapter提供的

 默认的配置是拦截所有的请求需要认证之后才能访问,如果没有认证,会自动生成一个认证表单要求输入用户名和密码。

protected void configure(HttpSecurity http) throws Exception {
logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity)."); http
.authorizeRequests()
.anyRequest().authenticated()
.and()
.formLogin().and()
.httpBasic();
}

2、个性化配置入口之configure(HttpSecurity http)

 我们可以通过继承WebSecurityConfigurerAdapter并重写该方法来个性化配置HttpSecurity。

 OK,目前为止HttpSecurity已经被初始化,接下去需要设置HttpSecurity对象添加至WebSecurity的securityFilterChainBuilders列表中:

public void init(final WebSecurity web) throws Exception {
final HttpSecurity http = getHttp();
web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
FilterSecurityInterceptor securityInterceptor = http
.getSharedObject(FilterSecurityInterceptor.class);
web.securityInterceptor(securityInterceptor);
});
}

 当所有的WebSecurityConfigurer的init方法被调用之后,webSecurity.init()工作就结束了

 接下去调用了webSecurity.configure(),该方法同样是在AbstractConfiguredSecurityBuilder中实现的:

private void configure() throws Exception {
Collection<SecurityConfigurer<O, B>> configurers = getConfigurers(); for (SecurityConfigurer<O, B> configurer : configurers) {
configurer.configure((B) this);
}
}

 它的主要工作是迭代调用所有WebSecurityConfigurer的configurer方法,参数是WebSeucrity本身,这又是另外一个重要的个性化入口:

3、个性化配置入口之configure(WebSecurity web)

 我们可以通过继承WebSecurityConfigurerAdapter并重写该方法来个性化配置WebSecurity。

 至此,三个重要的个性化入口都已经被调用,即在实现WebSecurityConfigurerAdapter经常需要重写的:

1、configure(AuthenticationManagerBuilder auth);

2、configure(WebSecurity web);

3、configure(HttpSecurity http);

 回到webSecurity构建过程,接下去重要的的调用:

O result = performBuild();

performBuild() 非常重要!!

@Override
protected Filter performBuild() throws Exception {
Assert.state(
!securityFilterChainBuilders.isEmpty(),
() -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
+ "Typically this done by adding a @Configuration that extends WebSecurityConfigurerAdapter. "
+ "More advanced users can invoke "
+ WebSecurity.class.getSimpleName()
+ ".addSecurityFilterChainBuilder directly");
int chainSize = ignoredRequests.size() + securityFilterChainBuilders.size();
List<SecurityFilterChain> securityFilterChains = new ArrayList<>(
chainSize);
for (RequestMatcher ignoredRequest : ignoredRequests) {
securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
}
for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : securityFilterChainBuilders) {
securityFilterChains.add(securityFilterChainBuilder.build());
}
FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
if (httpFirewall != null) {
filterChainProxy.setFirewall(httpFirewall);
}
filterChainProxy.afterPropertiesSet(); Filter result = filterChainProxy;
if (debugEnabled) {
logger.warn("\n\n"
+ "********************************************************************\n"
+ "********** Security debugging is enabled. *************\n"
+ "********** This may include sensitive information. *************\n"
+ "********** Do not use in a production system! *************\n"
+ "********************************************************************\n\n");
result = new DebugFilter(filterChainProxy);
}
postBuildAction.run();
return result; }

 首先计算出chainSize,也就是ignoredRequests.size() + securityFilterChainBuilders.size();,如果你不配置ignoredRequests,那就是securityFilterChainBuilders.size(),也就是HttpSecurity的个数,其本质上就是你一共配置几个WebSecurityConfigurerAdapter,因为每个WebSecurityConfigurerAdapter对应一个HttpSecurity,而所谓的ignoredRequests就是FilterChainProxy的请求,默认是没有的,如果你需要条跳过某些请求不需要认证或授权,可以如下配置:

@Override
public void configure(WebSecurity web) throws Exception {
web.ignoring().antMatchers("/statics/**");
}

 在上面配置中,所有以/statics开头请求都将被FilterChainProxy忽略。

 securityFilterChains.add(securityFilterChainBuilder.build()); 这一行就是初始化所有的过滤器,记得上面有段代码如下,将HttpSecurity设置到WebSecurity的 securityFilterChainBuilder中,上面就是调用HttpSecurity.build()方法,初始化所有的 HttpSecurity的过滤器链

public void init(final WebSecurity web) throws Exception {
final HttpSecurity http = getHttp();
web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
FilterSecurityInterceptor securityInterceptor = http
.getSharedObject(FilterSecurityInterceptor.class);
web.securityInterceptor(securityInterceptor);
});
}

 依然来到 doBuild()方法,只不过这次是执行的 HttpSecurity的

@Override
protected final O doBuild() throws Exception {
synchronized (configurers) {
buildState = BuildState.INITIALIZING; beforeInit();
init(); buildState = BuildState.CONFIGURING; beforeConfigure();
configure(); buildState = BuildState.BUILDING; O result = performBuild(); buildState = BuildState.BUILT; return result;
}
}

 重点查看 configure()该方法 会调用对应的 过滤器配置的configure()

如 再内部创建 SessionManagementFilter 最后添加到HttpSecurity中,也就是拿 HttpSecurity的configures 一个个创建出对应的过滤器

@Override
public void configure(H http) {
SecurityContextRepository securityContextRepository = http
.getSharedObject(SecurityContextRepository.class);
SessionManagementFilter sessionManagementFilter = new SessionManagementFilter(
securityContextRepository, getSessionAuthenticationStrategy(http));
if (this.sessionAuthenticationErrorUrl != null) {
sessionManagementFilter.setAuthenticationFailureHandler(
new SimpleUrlAuthenticationFailureHandler(
this.sessionAuthenticationErrorUrl));
}
InvalidSessionStrategy strategy = getInvalidSessionStrategy();
if (strategy != null) {
sessionManagementFilter.setInvalidSessionStrategy(strategy);
}
AuthenticationFailureHandler failureHandler = getSessionAuthenticationFailureHandler();
if (failureHandler != null) {
sessionManagementFilter.setAuthenticationFailureHandler(failureHandler);
}
AuthenticationTrustResolver trustResolver = http
.getSharedObject(AuthenticationTrustResolver.class);
if (trustResolver != null) {
sessionManagementFilter.setTrustResolver(trustResolver);
}
sessionManagementFilter = postProcess(sessionManagementFilter); http.addFilter(sessionManagementFilter);
if (isConcurrentSessionControlEnabled()) {
ConcurrentSessionFilter concurrentSessionFilter = createConcurrencyFilter(http); concurrentSessionFilter = postProcess(concurrentSessionFilter);
http.addFilter(concurrentSessionFilter);
}
}

 当doBuild()中的 configure();执行完毕后 的会得到如下HttpSecurity可以看到它内部的filters已经全部创建完毕

 回到doBuild()方法 该方中有 performBuild() 调用HttpSecurity的 performBuild(),默认实现如下,先对上面所有的过滤器进行排序,使用的是 FilterComparator() 进行排序的,这里不展开了,反正就是会排序成文章开始的那张图上面的顺序

@Override
protected DefaultSecurityFilterChain performBuild() {
filters.sort(comparator);
return new DefaultSecurityFilterChain(requestMatcher, filters);
}

 最后返回的是SecurityFilterChain的默认实现DefaultSecurityFilterChain。

 构建完所有SecurityFilterChain后,创建最为重要的FilterChainProxy实例,

FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);

 至此Spring Security 初始化完成,包括springSecurityFilterChain初始化,我们通过继承WebSecurityConfigurerAdapter来代达到个性化配置目的,文中提到了三个重要的个性化入口,并且WebSecurityConfigurerAdapter是可以配置多个的,其对应的接口就是会存在多个SecurityFilterChain实例,但是它们人仍然在同一个FilterChainProxy中,通过RequestMatcher来匹配并传入到对应的SecurityFilterChain中执行请求。

 5.个性化入口配置(扩展WebSecurityConfigurerAdapter)

 重要的个性化入口都是哪里调用的 已经在上面初始化 springSecurityFilterChain 源码中讲解了,这里知识总结一下

1、个性化配置入口之configure(AuthenticationManagerBuilder auth)

 我们可以通过继承WebSecurityConfigurerAdapter并重写该方法来个性化配置AuthenticationManagerBuilder。

2、个性化配置入口之configure(HttpSecurity http)

 我们可以通过继承WebSecurityConfigurerAdapter并重写该方法来个性化配置HttpSecurity。

3、个性化配置入口之configure(WebSecurity web)

 我们可以通过继承WebSecurityConfigurerAdapter并重写该方法来个性化配置WebSecurity。

 实现WebSecurityConfigurerAdapter经常需要重写的:

1、configure(AuthenticationManagerBuilder auth);

2、configure(WebSecurity web);

3、configure(HttpSecurity http);

 6.总结

本篇主要讲解了

 1.SpringBoot对于SpringSecurity的自动配置的支持类SecurityAutoConfiguration,

 2.核心注解@EnableWebSecurity

 3. SpringSecurity的核心过滤器链 springSecurityFilterChain 的初始化流程的源码

源码部分还是定下心来多看 加油!

个人博客地址: https://www.askajohnny.com 欢迎访问!

本文由博客一文多发平台 OpenWrite 发布!

最新文章

  1. ci重定向
  2. 使用boost的asio,io_service无法初始化
  3. springboot
  4. mvc3在window 7 iis7下以及 xp iis 5.1下的部署 ,asp.net MVC3无法打开项目文件E:/我们的项目/Project/HeatingMIS.Web/HeatingMIS.Web.csproj”。此安装不支持该项目类型。
  5. jmeter 建立一个JMS主题测试计划
  6. Objective-C中的const ,extern,static
  7. htaccess URL重写rewrite与重定向redirect(转)
  8. 用自己的话表达出来-Servlet
  9. SimpleDateFormat使用和线程安全问题
  10. Android studio,第一个生成,调用成功的jni(说多了都是泪)
  11. Grafana的基本使用
  12. 修改CentOS的IP地址
  13. Eureka的自我保护模式
  14. CentOS RabbitMQ 高可用(Mirrored)
  15. SpringMVC+SPring+Maven+Mybaits+Shiro+Mybaits基础开发项目
  16. Spring.NET学习笔记6——依赖注入(应用篇)
  17. 【Android】自己定义控件实现可滑动的开关(switch)
  18. LeetCode: Median of Two Sorted Arrays 解题报告
  19. Linux 精确获取指定目录对应的块的剩余空间
  20. BluePrint和ORM

热门文章

  1. Nuget 通过 dotnet 命令行发布
  2. css 百分比继承关系的探讨
  3. P1065 汪老师的烟
  4. WPF 从零开始开发 dotnet Remoting 程序
  5. Vue基础练习之Todo List
  6. The Preliminary Contest for ICPC Asia Nanjing 2019ICPC南京网络赛
  7. 【土旦】vue 解决ios H5底部输入框 获取焦点时弹出虚拟键盘挡住输入框 以及监听键盘收起事件
  8. Cisco DNA-C POC环境配置
  9. git之github下载篇(ssh需要配置密钥)
  10. 【python测试开发栈】—帮你总结Python os模块高频使用的方法