Spring Security实现原理剖析(一):filter的构造和初始化

栏目: Java · 发布时间: 7年前

内容简介:我们知道Spring Security的核心实现原理都是从filter开始的,Spring Security通过构造层层filter来实现登录跳转、权限验证,角色管理等功能。本章通过剖析Spring Security的核心源码来说明Spring Security的filter是如何开始构造并运行的。往往我们定义一个Spring Security程序都是通过配置一个WebSecurityConfig类开始的,简单代码如下:通过以上代码一个简单的Spring Security应用程序就能成功执行了,该程序能拦截

前言

我们知道Spring Security的核心实现原理都是从filter开始的,Spring Security通过构造层层filter来实现登录跳转、权限验证,角色管理等功能。本章通过剖析Spring Security的核心源码来说明Spring Security的filter是如何开始构造并运行的。

从最初开始

往往我们定义一个Spring Security程序都是通过配置一个WebSecurityConfig类开始的,简单代码如下:

@Configuration
@EnableWebSecurity
public class WebSecurityConfig extends WebSecurityConfigurerAdapter {

    @Override
    protected void configure(final HttpSecurity http) throws Exception {
        http.authorizeRequests()
                .antMatchers("/login").permitAll()
                .anyRequest().authenticated()
                .and().formLogin();
    }
    
}

通过以上代码一个简单的Spring Security应用程序就能成功执行了,该程序能拦截除了/login路径以外的所有请求到登录页面。

我们可以看到以上代码并没有任何显示声明filter的语句,那么Spring Security是如何通过上述代码生成filter的呢?下面就由我来一层层解剖Spring Security的源码来说明。

@EnableWebSecurity注解

我们注意到如上代码有个@EnableWebSecurity注解,进入该注解查看

@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE})
@Documented
#注意这里!
@Import({WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class})
@EnableGlobalAuthentication
@Configuration
public @interface EnableWebSecurity {
    boolean debug() default false;
}

WebSecurityConfiguration类

我们可以看到如上该注解导入了WebSecurityConfiguration类,进入该类查看:

@Configuration
public class WebSecurityConfiguration implements ImportAware, BeanClassLoaderAware {
    private WebSecurity webSecurity;
    private Boolean debugEnabled;
    private List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers;
    private ClassLoader beanClassLoader;
    @Autowired(
        required = false
    )
    private ObjectPostProcessor<Object> objectObjectPostProcessor;

    public WebSecurityConfiguration() {
    }

    @Bean
    public static DelegatingApplicationListener delegatingApplicationListener() {
        return new DelegatingApplicationListener();
    }
    ........
}

WebSecurityConfiguration类是作为一个Spring配置源,同时定义了许多bean,这里重点看如下这个方法:

@Autowired(
        required = false
    )
    public void setFilterChainProxySecurityConfigurer(ObjectPostProcessor<Object> objectPostProcessor, 
    @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") 
    List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers) throws Exception {
    
        #这段代码初始化webSecurity
        this.webSecurity = (WebSecurity)objectPostProcessor.postProcess(new WebSecurity(objectPostProcessor));
        if (this.debugEnabled != null) {
            this.webSecurity.debug(this.debugEnabled);
        }
        
        #webSecurityConfigurers该属性是通过@Value注解注入的
        Collections.sort(webSecurityConfigurers, WebSecurityConfiguration.AnnotationAwareOrderComparator.INSTANCE);
        Integer previousOrder = null;
        Object previousConfig = null;

        Iterator var5;
        SecurityConfigurer config;
        for(var5 = webSecurityConfigurers.iterator(); var5.hasNext(); previousConfig = config) {
            config = (SecurityConfigurer)var5.next();
            Integer order = WebSecurityConfiguration.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;
        }
        
        #将webSecurityConfigurers依次放入webSecurity
        var5 = webSecurityConfigurers.iterator();
        while(var5.hasNext()) {
            config = (SecurityConfigurer)var5.next();
            this.webSecurity.apply(config);
        }

        this.webSecurityConfigurers = webSecurityConfigurers;
    }

总结下该方法所做的主要操作:

  1. 首先初始化了webSecurity属性,该属性对应WebSecurity类
  2. 注入了webSecurityConfigurers属性,该属性是一个List<SecurityConfigurer>集合
  3. 遍历webSecurityConfigurers集合,调用webSecurity的apply方法,该方法参数为SecurityConfigurer接口

这里有一个重要的接口SecurityConfigurer接口,该接口代码如下:

public interface SecurityConfigurer<O, B extends SecurityBuilder<O>> {
    void init(B var1) throws Exception;

    void configure(B var1) throws Exception;
}

回顾上面我们编写的WebSecurityConfig配置类,也有一个configure方法,那么我们猜测WebSecurityConfig类是不是也实现了SecurityConfigurer接口呢?答案是是的,我们可以看WebSecurityConfig类的类图

Spring Security实现原理剖析(一):filter的构造和初始化

可以看到WebSecurityConfig类实现了SecurityConfigurer接口。

因此webSecurityConfigurers属性通过依赖注入包含了WebSecurityConfig类,通过上述第3条操作将我们配置的WebSecurityConfig类和WebSecurity类关联起来。

WebSecurity类

到这里我们知道了WebSecurityConfiguration类调用上述方法将我们配置的WebSecurityConfig类用WebSecurity类的apply方法关联起来,那么我们详细看看WebSecurity类的apply方法:

public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer) throws Exception {
        configurer.addObjectPostProcessor(this.objectPostProcessor);
        configurer.setBuilder(this);
        #继续调用该类的add方法
        this.add(configurer);
        return configurer;
    }
    
private <C extends SecurityConfigurer<O, B>> void add(C configurer) throws Exception {
        Assert.notNull(configurer, "configurer cannot be null");
        #获取class属性
        Class<? extends SecurityConfigurer<O, B>> clazz = configurer.getClass();
        #获取LinkedHashMap
        LinkedHashMap var3 = this.configurers;
        synchronized(this.configurers) {
            if (this.buildState.isConfigured()) {
                throw new IllegalStateException("Cannot apply " + configurer + " to already built object");
            } else {
                List<SecurityConfigurer<O, B>> configs = this.allowConfigurersOfSameType ? (List)this.configurers.get(clazz) : null;
                if (configs == null) {
                    configs = new ArrayList(1);
                }

                ((List)configs).add(configurer);
                #将configurer放入一个LinkedHashMap中
                this.configurers.put(clazz, configs);
                if (this.buildState.isInitializing()) {
                    this.configurersAddedInInitializing.add(configurer);
                }

            }
        }
    }

从上述代码可知,实际上就是将WebSecurityConfig类放入了WebSecurity类的一个LinkedHashMap中,该LinkedHashMap在WebSecurity中属性名为configurers。

我们继续回到WebSecurityConfiguration类,查看它的另外一个重要的方法:

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

        return (Filter)this.webSecurity.build();
    }

该方法即为Spring Security构建Filter的核心方法,通过webSecurity的build方法构建了Spring Security的Filter。

我们继续查看WebSecurity类的build方法:

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

实际上调用了上层的doBuild:

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

这里主要看WebSecurity的init方法和performBuild方法,首先看init方法

private void init() throws Exception {
        #this.getConfigurers()该方法实际上获取WebSecurity中LinkedHashMap中的Value值集合
        Collection<SecurityConfigurer<O, B>> configurers = this.getConfigurers();
        Iterator var2 = configurers.iterator();

        SecurityConfigurer configurer;
        while(var2.hasNext()) {
            configurer = (SecurityConfigurer)var2.next();
            #调用SecurityConfigurer的init方法
            configurer.init(this);
        }

        var2 = this.configurersAddedInInitializing.iterator();

        while(var2.hasNext()) {
            configurer = (SecurityConfigurer)var2.next();
            configurer.init(this);
        }

    }

通过该代码可知,该方法首先获取WebSecurity中的LinkedHashMap中的Value值集合,再对Value值进行遍历并执行其中的init方法,从上面的代码分析我们知道WebSecurity中的LinkedHashMap实际存的就是WebSecurityConfig,这段代码将会调用WebSecurityConfig的init方法,而WebSecurityConfig的init方法来自于它的父类WebSecurityConfigurerAdapter,该init方法代码如下:

public abstract class WebSecurityConfigurerAdapter implements WebSecurityConfigurer<WebSecurity> {
        public void init(final WebSecurity web) throws Exception {
        #获取HttpSecurity
        final HttpSecurity http = this.getHttp();
        #将HttpSecurity放入WebSecurity中
        web.addSecurityFilterChainBuilder(http).postBuildAction(new Runnable() {
            public void run() {
                FilterSecurityInterceptor securityInterceptor =
                 (FilterSecurityInterceptor)http.getSharedObject(FilterSecurityInterceptor.class);
                web.securityInterceptor(securityInterceptor);
            }
        });
       }
       
       protected final HttpSecurity getHttp() throws Exception {
        if (this.http != null) {
            return this.http;
        } else {
            DefaultAuthenticationEventPublisher eventPublisher = (DefaultAuthenticationEventPublisher)this.objectPostProcessor.postProcess(new DefaultAuthenticationEventPublisher());
            this.localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
            AuthenticationManager authenticationManager = this.authenticationManager();
            this.authenticationBuilder.parentAuthenticationManager(authenticationManager);
            this.authenticationBuilder.authenticationEventPublisher(eventPublisher);
            Map<Class<? extends Object>, Object> sharedObjects = this.createSharedObjects();
            this.http = new HttpSecurity(this.objectPostProcessor, this.authenticationBuilder, sharedObjects);
            if (!this.disableDefaults) {
                ((HttpSecurity)((DefaultLoginPageConfigurer)((HttpSecurity)((HttpSecurity)((HttpSecurity)((HttpSecurity)((HttpSecurity)((HttpSecurity)((HttpSecurity)((HttpSecurity)this.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();
                ClassLoader classLoader = this.context.getClassLoader();
                List<AbstractHttpConfigurer> defaultHttpConfigurers = SpringFactoriesLoader.loadFactories(AbstractHttpConfigurer.class, classLoader);
                Iterator var6 = defaultHttpConfigurers.iterator();

                while(var6.hasNext()) {
                    AbstractHttpConfigurer configurer = (AbstractHttpConfigurer)var6.next();
                    this.http.apply(configurer);
                }
            }
            #调用本类的configure方法
            this.configure(this.http);
            return this.http;
        }
    }
    
    #模板方法设计模式,子类WebSecurityConfig将会覆盖该方法
    protected void configure(HttpSecurity http) throws Exception {
        this.logger.debug("Using default configure(HttpSecurity). If subclassed this will potentially override subclass configure(HttpSecurity).");
        ((HttpSecurity)((HttpSecurity)((AuthorizedUrl)http.authorizeRequests().anyRequest()).authenticated().and()).formLogin().and()).httpBasic();
    }
       
}

以上代码最终还是实际调用了我们写的WebSecurityConfig类的configure方法。

仔细观察以上代码,我们发现有一条语句web.addSecurityFilterChainBuilder(http),该语句将构建的HttpSecurity放入WebSecurity类中,以下是该方法源码:

public WebSecurity addSecurityFilterChainBuilder(SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder) {
        this.securityFilterChainBuilders.add(securityFilterChainBuilder);
        return this;
    }

实际上就是将HttpSecurity放入了WebSecurity的一个list集合里,该list集合属性名为securityFilterChainBuilders。

到目前为止,我们终于知道我们编写的WebSecurityConfig类的configure方法是如何被调用的了,但是仍有许多疑问没解开,比如HttpSecurity类的作用,Spring Security是如何通过HttpSecurity类构建一条拦截器链等。

这里我们先不分析HttpSecurity类的具体实现,再来看看WebSecurity的init方法执行后所执行的performBuild方法,该方法源码如下:

protected Filter performBuild() throws Exception {
        Assert.state(!this.securityFilterChainBuilders.isEmpty(), () -> {
            return "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 = this.ignoredRequests.size() + this.securityFilterChainBuilders.size();
        List<SecurityFilterChain> securityFilterChains = new ArrayList(chainSize);
        Iterator var3 = this.ignoredRequests.iterator();

        while(var3.hasNext()) {
            RequestMatcher ignoredRequest = (RequestMatcher)var3.next();
            securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest, new Filter[0]));
        }
        
        #遍历securityFilterChainBuilders集合
        var3 = this.securityFilterChainBuilders.iterator();

        while(var3.hasNext()) {
            SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder = (SecurityBuilder)var3.next();
            #执行securityFilterChainBuilders集合单位的build方法,返回一个SecurityFilterChain类,并加入List<SecurityFilterChain>中
            securityFilterChains.add(securityFilterChainBuilder.build());
        }
        
        #将List<SecurityFilterChain>类构建成一个FilterChainProxy代理类
        FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
        if (this.httpFirewall != null) {
            filterChainProxy.setFirewall(this.httpFirewall);
        }

        filterChainProxy.afterPropertiesSet();
        Filter result = filterChainProxy;
        if (this.debugEnabled) {
            this.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);
        }

        this.postBuildAction.run();
        #返回FilterChainProxy代理类
        return (Filter)result;
    }

该方法执行的操作主要如下:

  1. 遍历securityFilterChainBuilders集合,并执行其中的build方法,从上面代码分析可知,securityFilterChainBuilders集合里存储了HttpSecurity,所以这里执行了HttpSecurity的build方法构建SecurityFilterChain类
  2. 将List<SecurityFilterChain>集合构建成一个FilterChainProxy代理类
  3. 返回这个FilterChainProxy代理类

到这里总的过程就非常明了了,实际上Spring Security的顶层filter就是一个FilterChainProxy类,而HttpSecurity主要用于注册和实例化各种filter

到这里就分成了两路,一路是HttpSecurity的build方法构建SecurityFilterChain类的原理,一路是FilterChainProxy类的作用,我们先从FilterChainProxy类开始

FilterChainProxy类

当请求到达的时候,FilterChainProxy会调用dofilter()方法,会遍历所有的SecurityFilterChain,对匹配到的url,则一一调用SecurityFilterChain中的filter做认证授权。FilterChainProxy的dofilter()中调用了doFilterInternal()方法,如下:

private void doFilterInternal(ServletRequest request, ServletResponse response,
            FilterChain chain) throws IOException, ServletException {

    FirewalledRequest fwRequest = firewall
            .getFirewalledRequest((HttpServletRequest) request);
    HttpServletResponse fwResponse = firewall
            .getFirewalledResponse((HttpServletResponse) response);
    # 获取请求对应的filter列表
    List<Filter> filters = getFilters(fwRequest);

    if (filters == null || filters.size() == 0) {
        if (logger.isDebugEnabled()) {
            logger.debug(UrlUtils.buildRequestUrl(fwRequest)
                    + (filters == null ? " has no matching filters"
                            : " has an empty filter list"));
        }

        fwRequest.reset();

        chain.doFilter(fwRequest, fwResponse);

        return;
    }

    VirtualFilterChain vfc = new VirtualFilterChain(fwRequest, chain, filters);
    # 执行每个filter
    vfc.doFilter(fwRequest, fwResponse);
}

# 通过遍历filterChains,调用SecurityFilterChain的matches方法,判断当前的请求对应哪些filter,返回匹配的filter列表
private List<Filter> getFilters(HttpServletRequest request) {
    for (SecurityFilterChain chain : filterChains) {
        if (chain.matches(request)) {
            return chain.getFilters();
        }
    }

    return null;
}

我们理清了FilterChainProxy类的作用,那么这些SecurityFilterChain是从哪来的呢?从上节可知SecurityFilterChain是由HttpSecurity的build方法生成的,下面我们分析下HttpSecurity类

HttpSecurity

HttpSecurity与WebSecurity一样,都继承了AbstractConfiguredSecurityBuilder类,而WebSecurity的build和doBuild方法和LinkedHashMap属性,均来自AbstractConfiguredSecurityBuilder,故HttpSecurity的build方法代码与WebSecurity的相同,区别在于LinkedHashMap存储的东西不同,HttpSecurity正是通过如此来生成SecurityFilterChain类的。

下面我们来看HttpSecurity构建filter的几个常见方法:

public ExpressionUrlAuthorizationConfigurer<HttpSecurity>.ExpressionInterceptUrlRegistry authorizeRequests() throws Exception {
        ApplicationContext context = this.getContext();
        return ((ExpressionUrlAuthorizationConfigurer)this.getOrApply(new ExpressionUrlAuthorizationConfigurer(context))).getRegistry();
    }


public FormLoginConfigurer<HttpSecurity> formLogin() throws Exception {
        return (FormLoginConfigurer)this.getOrApply(new FormLoginConfigurer());
    }

都调用了getOrApply方法,再来看getOrApply方法,又调用了其中的apply方法

private <C extends SecurityConfigurerAdapter<DefaultSecurityFilterChain, HttpSecurity>> C getOrApply(C configurer) throws Exception {
        C existingConfig = (SecurityConfigurerAdapter)this.getConfigurer(configurer.getClass());
        return existingConfig != null ? existingConfig : this.apply(configurer);
    }
    
public <C extends SecurityConfigurerAdapter<O, B>> C apply(C configurer) throws Exception {
        configurer.addObjectPostProcessor(this.objectPostProcessor);
        configurer.setBuilder(this);
        this.add(configurer);
        return configurer;
    }

apply方法又调用了add方法,这里的add方法最终还是将该configurer加入了linkedHashMap中

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

                ((List)configs).add(configurer);
                this.configurers.put(clazz, configs);
                if (this.buildState.isInitializing()) {
                    this.configurersAddedInInitializing.add(configurer);
                }

            }
        }
    }

故HttpSecurity在构建filter的过程中,本质还是将形如ExpressionUrlAuthorizationConfigurer、FormLoginConfigurer等类加入了它的LinkedHashMap中。

那么将这些Configurer类存入LinkedHashMap的作用又是什么?我们回忆上面WebSecurity类的doBuild方法,我们知道HttpSecurity类调用的doBuild方法与WebSecurity类一样,而通过观察WebSecurity类doBuild方法里this.init();this.configure();这些语句的具体实现,实际就是调用其LinkedHashMap中的元素的init方法和configure方法。

我们现在来查看其中一个ExpressionUrlAuthorizationConfigurer类的configure方法的详细代码:

public void configure(H http) throws Exception {
        FilterInvocationSecurityMetadataSource metadataSource = this.createMetadataSource(http);
        if (metadataSource != null) {
            FilterSecurityInterceptor securityInterceptor = this.createFilterSecurityInterceptor(http, metadataSource, (AuthenticationManager)http.getSharedObject(AuthenticationManager.class));
            if (this.filterSecurityInterceptorOncePerRequest != null) {
                securityInterceptor.setObserveOncePerRequest(this.filterSecurityInterceptorOncePerRequest);
            }

            securityInterceptor = (FilterSecurityInterceptor)this.postProcess(securityInterceptor);
            #将Filter加入了HttpSecurity的Filters集合中
            http.addFilter(securityInterceptor);
            http.setSharedObject(FilterSecurityInterceptor.class, securityInterceptor);
        }
    }

最后来看看HttpSecruity的performBuild()方法:

protected DefaultSecurityFilterChain performBuild() throws Exception {
        Collections.sort(this.filters, this.comparator);
        return new DefaultSecurityFilterChain(this.requestMatcher, this.filters);
    }

实际上就是通过Filters集合构建了SecurityFilterChain。

从上面代码可总结出,HttpSecurity内部维护一个Filter列表,而HttpSecurity调用形如authorizeRequests(),formLogin()等方法实际上就是将filter添加入它的列表当中,最后通过performBuild()方法构建出SecurityFilterChain,至此HttpSecurity构建filter的总过程就完成了。

总结

到目前为止,我们终于知道Spring Security是如何一步步的构建和初始化filter的了,我们最后再来简单总结下构建过程:

  1. Spring Security启动过程中通过WebSecurityConfiguration实例化WebSecurity
  2. WebSecurityConfiguration会将使用者编写的WebSecurityConfig类放入WebSecurity中的LinkedHashMap中
  3. 在构建WebSecurity的时候,会调用WebSecurity的doBuild()方法,这个方法是一个核心方法。
  4. doBuild中的init方法将会调用LinkedHashMap中元素的init方法(这里的元素是WebSecurityConfig),然后WebSecurityConfig的init方法会调用configure方法,调用configure方法后,将会初始化HttpSecurity构建各种Filter,这时HttpSecurity将会加入WebSecurity中。
  5. doBuild中的init方法调用完后将会调用下一个performBuild()方法,该方法会获取到HttpSecurity调用其doBuild方法构造SecurityFilterChain
  6. 将获取到的SecurityFilterChain构建成一个FilterChainProxy类,作为Spring Security的顶层filter
  7. 至此Spring Security的Filter构建完成

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

查看所有标签

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

The Art of Computer Programming, Volumes 1-3 Boxed Set

The Art of Computer Programming, Volumes 1-3 Boxed Set

Donald E. Knuth / Addison-Wesley Professional / 1998-10-15 / USD 199.99

This multivolume work is widely recognized as the definitive description of classical computer science. The first three volumes have for decades been an invaluable resource in programming theory and p......一起来看看 《The Art of Computer Programming, Volumes 1-3 Boxed Set》 这本书的介绍吧!

JSON 在线解析
JSON 在线解析

在线 JSON 格式化工具

MD5 加密
MD5 加密

MD5 加密工具

Markdown 在线编辑器
Markdown 在线编辑器

Markdown 在线编辑器