Spring Security 源码分析九:Java config - WebSecurity & @EnableWebSecurity

栏目: 后端 · 发布时间: 5年前

内容简介:本文是对 Spring Security Core 4.0.4 Release 进行源码分析的系列文章之一;本系列开始,将讲解有关 Spring Security 的配置相关的内容;本文为作者的原创作品,转载需注明出处;

本文是对 Spring Security Core 4.0.4 Release 进行源码分析的系列文章之一;

本系列开始,将讲解有关 Spring Security 的配置相关的内容;

本文为作者的原创作品,转载需注明出处;

简介

本博文将继续使用 Spring Security 源码分析八:Spring Security 过滤链二 - Demo 例子 中所使用到的例子,来讲解,基于 Spring Boot 的 Java Config 的方式;

@EnableWebSecurity 是用户自定义 Spring Security 过滤链 的入口,是核心,任何相关的认证操作,都将从这里开始;所以,笔者首先从这里入手,开启讲解 Spring Security 配置的相关内容;

配置

重温一下之前在DemoApplication 中所介绍的一个例子,可以看到该例子继承自 WebSecurityConfigurerAdapter 并且添加了 @EnableWebSecurity 注解;

@Configuration
@EnableWebSecurity
@Order(1)
static class WebSecurityConfig extends WebSecurityConfigurerAdapter {

   @Autowired
   public void configureGlobal(AuthenticationManagerBuilder auth) throws Exception {
      
      auth
         .inMemoryAuthentication()
            .withUser("user").password("password").roles("USER").and()  
            .withUser("manager").password("password").roles("MANAGER");
      
   }

   @Override
   protected void configure(HttpSecurity http) throws Exception {

      http.antMatcher("/web/**") // the filter chain defined for web request
          .authorizeRequests()
          .antMatchers("/web/report/**").hasRole("MANAGER")
          .anyRequest().authenticated()
          .and()
          .formLogin()
            // login 的相对路径必须与 security chain 的的相对路径吻合,这里是 /web/**;注意 login 分两步,一步是 Getter 会到 login.html,另外一步是从 login.html -> post -> /web/login/
            .loginPage("/web/login")
            // 允许访问
            .permitAll(); 
      
   }    
}

@EnableWebSecurity

首先来看一下该 annotation 的注解,

Add this annotation to an @Configuration class to have the Spring Security configuration defined in any WebSecurityConfigurer or more likely by extending the WebSecurityConfigurerAdapter base class and overriding individual methods:

可以在任何通过 @Configuration 注解的 WebSecurityConfigurerWebSecurityConfigurerAdapter 类中进行 Spring Security 相关的配置;这个也正是我们上面的例子中所做的那样,但是,问题是,@EnableWebSecurity 的核心功能是什么?它的底层运作机制是什么?先来看看它的源码,

@Retention(value = java.lang.annotation.RetentionPolicy.RUNTIME)
@Target(value = { java.lang.annotation.ElementType.TYPE })
@Documented
@Import({ WebSecurityConfiguration.class, ObjectPostProcessorConfiguration.class, SpringWebMvcImportSelector.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;
}

可以看到,该注解通过 @Import 导入了其它三个配置类, WebSecurityConfiguration.class 、ObjectPostProcessorConfiguration.class 以及 SpringWebMvcImportSelector.class;这里最重要的是 WebSecurityConfiguration.class ,那么它的内部运行机制是什么呢?

WebSecurityConfiguration.class

先看看该类的说明,

Uses a WebSecurity to create the FilterChainProxy that performs the web based security for Spring Security.

It then exports the necessary beans. Customizations can be made to WebSecurity by extending WebSecurityConfigurerAdapter and exposing it as a Configuration or implementing WebSecurityConfigurer and exposing it as a Configuration. This configuration is imported when using EnableWebSecurity.

先总结一下它的功能,一句话,根据用户所配置的 Spring Security 配置通过创建出对应FilterChainProxy 并生成相应的 Spring Security 过滤链;那么看看它是如何一步一步做到的呢,在讲解之前,要知道 WebSecurityConfiguration 是通过 @Configuration 进行注解的;

@Configuration
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {

   ...
}

加载用户自定义配置类

webSecurityConfigurers

如何加载用户的自定义配置呢?比如上面所介绍的以及DemoApplication 中所介绍的RestSecurityConfig 的呢?答案就在 WebSecurityConfiguration 的成员变量 webSecurityConfigurers 上;

Spring Security 源码分析九:Java config - WebSecurity & @EnableWebSecurity
  • 首先,该方法是通过 @Autowired 注解的,也就是说在 Spring 容器初始化的时候,该方法即可被加载;在该方法的加载过程当中,通过 @Value 注解从 Spring 容器中取得 autowiredWebSecurityConfigurersIgnoreParents Spring bean 实例,并从该实例中通过方法getWebSecurityConfigurers()获取得到 webSecurityConfigurers 参数,该参数保存的既是WebSecurityConfig和RestSecurityConfig两个配置类,既是用户通过注解自定义实现的两个有关 Spring Security 配置类,非常之 关键 ;Wow,这时一个神奇的参数,是的,它的确是;但问题是,该参数是如何获取得到如此重要的配置类信息的呢?关键就在于 autowiredWebSecurityConfigurersIgnoreParents 所对应的 Spring Bean,来看下面这个方法;

    @Bean
    public AutowiredWebSecurityConfigurersIgnoreParents autowiredWebSecurityConfigurersIgnoreParents(
          ConfigurableListableBeanFactory beanFactory) {
       return new AutowiredWebSecurityConfigurersIgnoreParents(beanFactory);
    }
    

    该方法通过注解 @Bean 初始化得到一个名为 autowiredWebSecurityConfigurersIgnoreParents 由 Spring 容器所管理的 bean;该 bean 是通过初始化 AutowiredWebSecurityConfigurersIgnoreParents 所的到的,

    final class AutowiredWebSecurityConfigurersIgnoreParents {
    
       private final ConfigurableListableBeanFactory beanFactory;
    
       public AutowiredWebSecurityConfigurersIgnoreParents(
             ConfigurableListableBeanFactory beanFactory) {
          Assert.notNull(beanFactory, "beanFactory cannot be null");
          this.beanFactory = beanFactory;
       }
    
       @SuppressWarnings({ "rawtypes", "unchecked" })
       public List<SecurityConfigurer<Filter, WebSecurity>> getWebSecurityConfigurers() {
          List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers = new ArrayList<SecurityConfigurer<Filter, WebSecurity>>();
          Map<String, WebSecurityConfigurer> beansOfType = beanFactory
                .getBeansOfType(WebSecurityConfigurer.class);
          for (Entry<String, WebSecurityConfigurer> entry : beansOfType.entrySet()) {
             webSecurityConfigurers.add(entry.getValue());
          }
          return webSecurityConfigurers;
       }
    }
    

    该类非常的简单,核心既是上面的方法getWebSecurityConfigurers(),直接从当前的 Spring 容器中去加载类型为 WebSecurityConfigurer 类的实例;回头看看DemoApplication 中所实现的WebSecurityConfig和RestSecurityConfig正好继承自 WebSecurityConfigurer ,所以这两个由用户自定义的 Spring Security 配置类都将在这里被加载;

  • 然后依次遍历 webSecurityConfigurer 配置并将其通过 WebSecurity 的 apply() 方法加载入 WebSecurity 对象实例中;为后续的构建进行准备;

创建 FilterChainProxy 实例 ( named “springSecurityFilterChain” )

由前面的系列文章分析可知,FilterChainProxy 在 Spring 容器中的 bean 的名字为 springSecurityFilterChain ,该实例包含 1 个或者多个SecurityFilterChain,并且该实例被 DelegatingFilterProxy 实例所代理,接收并处理由其所转发的请求;那么本章节将探讨的既是 FilterChainProxy 实例是如何被创建的?

答案就在中;

WebSecurity.build

首选,看看 WebSecurity 相关的注解,

The WebSecurity is created by WebSecurityConfiguration to create the FilterChainProxy known as the Spring Security Filter Chain ( springSecurityFilterChain ). The springSecurityFilterChain is the Filter that the DelegatingFilterProxy delegates to.

Customizations to the WebSecurity can be made by creating a WebSecurityConfigurer or more likely by overriding WebSecurityConfigurerAdapter .

从其注解可知,WebSecurity 的核心功能既是去创建 FilterChainProxy ;下面看一下 WebSecurity 构建的入口,

Spring Security 源码分析九:Java config - WebSecurity & @EnableWebSecurity

可见通过方法上的注解 @Bean 来构建一个名为 springSecurityFilterChain 的 Spring Bean,该 bean 就是 FilterChainProxy

由上一小节的第二点分析可知,当通过webSecurityConfigurers获取得到用户自定义配置类以后,将依次的通过调用 WebSecurityapply() 将其加载;其目的其实就是为了后续的构建动作,下面,笔者就来分析一下 WebSecurity 的构建行为;

Spring Security 源码分析九:Java config - WebSecurity & @EnableWebSecurity

上面这段代码显示了其核心的构建步骤,其构建的步骤由这样三个流程、以及所构成;将 debug 断点打在 WebSecurityConfig.configure 的方法中,来分析下面这三种情况;

init process

在开始执行该流程以前,build state 将会被置为 INITIALIZING 的状态;

Spring Security 源码分析九:Java config - WebSecurity & @EnableWebSecurity

从上述的调用流程中可以清晰的看到, WebSecurityConfig 既用户自定义的 Spring Security 配置类的configure(HttpSecurity)方法将会被调用,同样,用户自定义的 RestSecurityConfig 的configure(HttpSecurity)方法同样会被调用;可以看到这两个类都被 Cglib 生成了相应的代理类,然后,调用 WebSecurityConfigurerAdapter . getHttp() 方法,该方法中最终调用到 WebSecurityConfig .configure(HttpSecurity) 方法加载用户自定义的 HttpSecurity 的配置;

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);
  http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
        localConfigureAuthenticationBldr.getSharedObjects());
  http.setSharedObject(UserDetailsService.class, userDetailsService());
  http.setSharedObject(ApplicationContext.class, context);
  http.setSharedObject(ContentNegotiationStrategy.class, contentNegotiationStrategy);
  http.setSharedObject(AuthenticationTrustResolver.class, trustResolver);
  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<HttpSecurity>()).and()
        .logout();
     // @formatter:on
  }
  configure(http);
  return http;
}

可见,该方法中会首先初始化一个 HttpSecurity 对象实例,然后通过模板方法 configure(HttpSecurity) 去加载用户自定义的 Java Config Security 相关的配置;所以,这里比较迷惑人的是,照理说,init 应该只取调用 init 相关的内容,但这里确实却 调用了用户的 configure 相关的内容

因此,加载用户通过 WebSecurityConfig 和 RestSecurityConfig 自定义的有关 HttpSecurity 的配置,这里的配置包括为 HttpSecurity 配置相关的 SecurityConfigurers,以及 intercept-url 等;

configure process

执行之前,build state 将会被置为 configuring;该方法调用的是 abstract 方法 WebSecurityConfigurerAdapter . configure(WebSecurity web) ,也就是说,如果 WebSecurityConfigRestSecurityConfig 实现了 configure(WebSecurity web) 模板方法,将会在这里被调用;当然,显而易见,这里的目的是给用户一个机会去 重载 WebSecurity 的机会;

perform build process

因为 Cglib 代理类的原因,因此 debug 断点需要打在 WebSecurity.performBuild() 方法中,否则断点不会进行;

Spring Security 源码分析九:Java config - WebSecurity & @EnableWebSecurity

该过程最核心的两个地方就是由箭头所指向的地方,下面笔者将来分别就这两个关键地方进行分析,

第一个地方,

securityFilterChainBuilder 实现了 SecurityBuilder 接口,该接口提供了一个 build() 接口方法,便于执行 Security 构建相关操作;

public interface SecurityBuilder<O> {

  /**
   - Builds the object and returns it or null.
   *
   - @return the Object to be built or null if the implementation allows it.
   - @throws Exception if an error occurred when building the Object
   */
  O build() throws Exception;
}

从 debug 的过程中可以看到,该 securityFilterChainBuilder 分别对应的是 WebSecurityCofnigRestSecurityConfig 中被重载 HttpSecurity 对象,可见,这里通过调用 securityFilterChainBuilder.build() 对其执行构建动作,该构建将会生成一个关键的SecurityFilterChain 对象,而 FilterChainProxy 对象正式由多个 SecurityFilterChain 对象实例所构成,具体详情参考中的子流程;

第二个地方,

然后另外一个重要的就是,通过一个 SecurityFilterChains 队列来构造 FilterChainProxy 对象,并将其作为 Filter 接口对象返回;从前面的系列文章总我们可以知道,FilterChainProxy 实际上是 DelegatingFilterProxy 的一个代理……;

HttpSecurity.build

从第 3 点中可以知道,通过 HttpSecurity.build() 方法将会返回一个 SecurityFilterChain 对象,那么这个过程是怎样的呢?首先,HttpSecurity 同样实现了 SecurityBuilder 接口,并且其调用流程同样是通过 AbstractSecurityBuilder.build() 方法开始的,

public abstract class AbstractSecurityBuilder<O> implements SecurityBuilder<O> {

   ......

   public final O build() throws Exception {
      if (building.compareAndSet(false, true)) {
         object = doBuild();
         return object;
      }
      throw new AlreadyBuiltException("This object has already been built");
   }

   ......

}

通过上述代码第 7 行,执行 doBuild() 逻辑,该方法将会调用,AbstractConfiguredSecurityBuilder.doBuild() 方法

public abstract class AbstractConfiguredSecurityBuilder<O, B extends SecurityBuilder<O>> extends AbstractSecurityBuilder<O> {

   ......

   @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、configure 以及 perform build 三个流程所构成的;后续打算新开一个系列来专门讲解 HttpSecurity 对象逻辑,所以这里不打算对 HttpSecurity 的构建流程做深入的分析;直接跳转到最后一步,HttpSecurity.performBuild()

public final class HttpSecurity extends
      AbstractConfiguredSecurityBuilder<DefaultSecurityFilterChain, HttpSecurity>
      implements SecurityBuilder<DefaultSecurityFilterChain>,
      HttpSecurityBuilder<HttpSecurity> {

   .......      

   @Override
   protected DefaultSecurityFilterChain performBuild() throws Exception {
      Collections.sort(filters, comparitor);
      return new DefaultSecurityFilterChain(requestMatcher, filters);
   }

   .......

}

可见这里将会初始化一个 DefaultSecurityFilterChain 并返回;

设计

从上述的分析后,笔者构建了相关的设计图来总结其相关的流程和逻辑;

类图

Spring Security 源码分析九:Java config - WebSecurity & @EnableWebSecurity

整个上述流程中所涉及的类如上图所示,让整个 Spring Security 流转起来的最核心的类是WebSecurityConfiguration;

Sequence

相关业务流程图如下,

Spring Security 源码分析九:Java config - WebSecurity & @EnableWebSecurity

看这张图的时候,还需要注意,WebSecurity 与 HttpSecurity 是一对多的关系;另外需要关注的是,从步骤 1.1.1.1.4.1.1.1.1 getHttp() 开始,初始化 HttpSecurity 实例,并为每一个 HttpSecurity 实例进行配置( 通过 SecurityConfigurer 进行配置 ),要注意的是 HttpSecurity 与用户自定义的 WebSecurityConfigRestSecurityConfig 是一对一的关系;当 HttpSecurity 初始化和初始配置结束以后,会将该实例通过 Step 1.1.1.1.4.1.1.1.2 webSecurity.addSecurityFilterChainBuilder(http) 步骤将其注入到 WebSecurity 实例中;

WebSecurity

与 WebSecurity 相互交织,有关联的概念主要有三个个方面;1、,既 WebSecurity 本身是一个 Security Builder 这样一个角色;2、WebSecurityConfigurer,这里主要是用来扩展用户自定义安全链的规则;3、WebSecurity 通过apply() 方法加载用户自定义 WebSecurityConfigurers,也就是说 WebSecurity 包含一个或者多个 WebSecurityConfigurers 对象;下面笔者分别就这三个方面来进行描述;

SecurityBuilder

这部分内容是后续补记的内容,由类图可知,WebSecurity 本身是一个 SecurityBuilder,因此它支持三步构建的方式,这部分参考;

Spring Security 源码分析九:Java config - WebSecurity & @EnableWebSecurity

WebSecurityConfigurer

Spring Security 源码分析九:Java config - WebSecurity & @EnableWebSecurity

这部分描述了用户自定义的 WebConfigSecurityRestConfigSecurity 的类的形态;他们分别继承自WebSecurityConfigurerAdapter抽象类;

更多有关 SecurityConfigurer 的内容参考 Spring Security 源码分析九:Java config - HttpSecurity & SecurityConfigurer 小节SecurityConfigurer 部分;

WebSecurityConfigurerAdapter

WebSecurityConfigurerAdapter是非常核心的类,用来扩展用户自定义的 Security Chain 的规则,那么下面,笔者就相关的核心方法进行依次介绍

  1. configure(HttpSecurity http)

    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(WebSecurity web)

    /**
     * Override this method to configure {@link WebSecurity}. For example, if you wish to
     * ignore certain requests.
     */
    public void configure(WebSecurity web) throws Exception {
    }
    

    默认提供的是一个空的模板方法,目的是让用户来重载该方法并定制 WebSecurity 的相关属性;该方法是在的 1.1.1.1.3.1 步骤中被调用;

  3. HttpSecurity getHttp()

    /**
     - Creates the {@link HttpSecurity} or returns the current instance
     *
     - ] * @return the {@link HttpSecurity}
     - @throws Exception
     */
    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);
      http = new HttpSecurity(objectPostProcessor, authenticationBuilder,
          localConfigureAuthenticationBldr.getSharedObjects());
      http.setSharedObject(UserDetailsService.class, userDetailsService());
      http.setSharedObject(ApplicationContext.class, context);
      http.setSharedObject(ContentNegotiationStrategy.class, contentNegotiationStrategy);
      http.setSharedObject(AuthenticationTrustResolver.class, trustResolver);
      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<HttpSecurity>()).and()
          .logout();
        // @formatter:on
      }
      configure(http);
      return http;
    }
    

    在前面的小节的第 #1 步中,既是在 init build process 中,上述的 getHttp() 将会被调用,并且通过模板方法 configure(http) 去加载用户对 HttpSecurity 的相关的配置;

WebSecurityConfiguration

WebSecurityConfiguration 在WebSecurityConfiguration.class章节中有过细致的介绍;这里的讲解重心是 WebSecurity;

构造 WebSecurity 并加载 WebSecurityConfigurers

WebSecurityConfiguration.class

@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));

  Collections.sort(webSecurityConfigurers, AnnotationAwareOrderComparator.INSTANCE);

  Integer previousOrder = 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, so it cannot be used on "
              + config + " too.");
    }
    previousOrder = order;
  }
  for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
    webSecurity.apply(webSecurityConfigurer);
  }
  this.webSecurityConfigurers = webSecurityConfigurers;
}
  • 代码第 7 行构造了 WebSecurity 对象;
  • 代码第 23 到 25 行,将用户自定义的 web configurers,WebSecurityConfig 和 RestSecurityConfig,载入 WebSecurity 对象实例中;

执行 WebSecurity 构建操作

WebSecurityConfiguration.class

@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();
}

代码第 11 行,开始执行 WebSecurity 实例的构建操作,该构建过程在小节中有过非常详细的介绍;

Sequence

总结(关联关系总结)

搞清楚以下的逻辑,也就搞清楚了 WebSecurity 的作用和地位了;

  • WebSecurity 与 FilterChainProxy 和 DelegatingFilterProxy 是一对一的关系;

    FilterChainProxy 对象是由 WebSecurity 对象所构建出来的;

  • WebSecurity 与 HttpSecurity 是一对多的关系;

    FilterChainProxy 对象包含多个安全链既 SecurityFilterChain 对象,HttpSecurity 将会负责构建出 SecurityFilterChain 对象;所以,WebSecuirty 包含多个 HttpSecurity;

  • HttpSecurity 与 SecurityFilterChain 是一对一的关系;


以上就是本文的全部内容,希望对大家的学习有所帮助,也希望大家多多支持 码农网

查看所有标签

猜你喜欢:

本站部分资源来源于网络,本站转载出于传递更多信息之目的,版权归原作者或者来源机构所有,如转载稿涉及版权问题,请联系我们

Vim实用技巧

Vim实用技巧

[英] Drew Neil / 杨源、车文隆 / 人民邮电出版社 / 2014-5-1 / 59.00元

vim是一款功能丰富而强大的文本编辑器,其代码补全、编译及错误跳转等方便编程的功能特别丰富,在程序员中得到非常广泛的使用。vim能够大大提高程序员的工作效率。对于vim高手来说,vim能以与思考同步的速度编辑文本。同时,学习和熟练使用vim又有一定的难度。 《vim实用技巧》为那些想要提升自己的程序员编写,阅读本书是熟练地掌握高超的vim技巧的必由之路。全书共21章,包括121个技巧。每一章......一起来看看 《Vim实用技巧》 这本书的介绍吧!

JS 压缩/解压工具
JS 压缩/解压工具

在线压缩/解压 JS 代码

UNIX 时间戳转换
UNIX 时间戳转换

UNIX 时间戳转换

RGB CMYK 转换工具
RGB CMYK 转换工具

RGB CMYK 互转工具