• spring-security源码-初始化(九)


    说明

    使用spring-boot 我们引入security的包 就可以自动实现简单的登录,是怎么做到的呢?

    知道spring-security源码,我们的可以通过打断点方式,找到各个核心源码处,知道各个配置原理,和扩展点 完成业务定制化逻辑

    security自动化配置

    1.在spring-boot-autoconfigure的spring.factories引入了security的自动化配置。我们主要看最核心的org.springframework.boot.autoconfigure.security.servlet.SecurityAutoConfiguration

    spriing自动化配置原理可以参考《Spring Boot-Starter(九)》

    2.SecurityAutoConfiguration实现

    Import导入原理可以参考《spring源码阅读(五)-Spring Import注解使用》《Spring源码阅读(六)-ConfigurationClassPostProcessor》

    @Configuration(
            proxyBeanMethods = false
    )
    @ConditionalOnClass({DefaultAuthenticationEventPublisher.class})//class path有此类加载
    @EnableConfigurationProperties({SecurityProperties.class})
    //Import导入 我们主要看WebSecurityEnablerConfiguration
    @Import({SpringBootWebSecurityConfiguration.class, WebSecurityEnablerConfiguration.class, SecurityDataConfiguration.class})
    public class SecurityAutoConfiguration {
        public SecurityAutoConfiguration() {
        }
    
        @Bean
        @ConditionalOnMissingBean({AuthenticationEventPublisher.class})//容器没有这个bean触发加载
        public DefaultAuthenticationEventPublisher authenticationEventPublisher(ApplicationEventPublisher publisher) {
            return new DefaultAuthenticationEventPublisher(publisher);
        }
    }

    3.我们继续看EnableWebSecurity

    @Configuration(
            proxyBeanMethods = false//不需要代理
    )
    @ConditionalOnBean({WebSecurityConfigurerAdapter.class})//容器中有WebSecurityConfigurerAdapter对象触发自动加载
    @ConditionalOnMissingBean(//容器中不能出现springSecurityFilterChain的实例
            name = {"springSecurityFilterChain"}
    )
    @ConditionalOnWebApplication(
            type = ConditionalOnWebApplication.Type.SERVLET
    )
    @EnableWebSecurity//组合注解
    public class WebSecurityEnablerConfiguration {
        public WebSecurityEnablerConfiguration() {
        }
    }

    4.

    这里又用到了Import导入 我们主要看WebSecurityConfiguration

    @Retention(RetentionPolicy.RUNTIME)
    @Target(ElementType.TYPE)
    @Documented
    @Import({ WebSecurityConfiguration.class, SpringWebMvcImportSelector.class, OAuth2ImportSelector.class,
            HttpSecurityConfiguration.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;
    
    }

    5.WebSecurityConfiguration首先会初始化一个webSecurity管理我们定义的WebSecurityConfigurerAdapter子类

    org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration#setFilterChainProxySecurityConfigurer

      /**
         *autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()
         * 这个类里面就是根据beanFactory获取WebSecurityConfigurer的实现 也就是我们的配置的WebSecurityConfigurerAdapter子类
         */
        @Autowired(required = false)
        public void setFilterChainProxySecurityConfigurer(ObjectPostProcessor<Object> objectPostProcessor,
                                                          @Value("#{@autowiredWebSecurityConfigurersIgnoreParents.getWebSecurityConfigurers()}") List<SecurityConfigurer<Filter, WebSecurity>> webSecurityConfigurers)
                throws Exception {
            //这里是通过new创建WebSecurity  同时通过objectPostProcessor 从容器中找如果有的话就依赖注入
            //我们可以阅读里面成员变量原理 通过容器注入对应对象完成初始化复制
            this.webSecurity = objectPostProcessor.postProcess(new WebSecurity(objectPostProcessor));
            if (this.debugEnabled != null) {
                this.webSecurity.debug(this.debugEnabled);
            }
            //排序
            webSecurityConfigurers.sort(WebSecurityConfiguration.AnnotationAwareOrderComparator.INSTANCE);
            Integer previousOrder = null;
            Object previousConfig = null;
            for (SecurityConfigurer<Filter, WebSecurity> config : webSecurityConfigurers) {
                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;
                previousConfig = config;
            }
            //循环遍历设置到 add 到webSecurity成员变量configurers webSecurityConfigures为我们自定义继承的WebSecurityConfigureAdapter配置类
            for (SecurityConfigurer<Filter, WebSecurity> webSecurityConfigurer : webSecurityConfigurers) {
                this.webSecurity.apply(webSecurityConfigurer);
            }
            this.webSecurityConfigurers = webSecurityConfigurers;
        }

    6.

    public static final String DEFAULT_FILTER_NAME = "springSecurityFilterChain"; 最后通过org.springframework.boot.autoconfigure.security.servlet.SecurityFilterAutoConfiguration 注入到Servlet

    org.springframework.security.config.annotation.web.configuration.WebSecurityConfiguration#springSecurityFilterChain

     @Bean(name = AbstractSecurityWebApplicationInitializer.DEFAULT_FILTER_NAME)
        public Filter springSecurityFilterChain() throws Exception {
            boolean hasConfigurers = this.webSecurityConfigurers != null && !this.webSecurityConfigurers.isEmpty();
            boolean hasFilterChain = !this.securityFilterChains.isEmpty();
            Assert.state(!(hasConfigurers && hasFilterChain),
                    "Found WebSecurityConfigurerAdapter as well as SecurityFilterChain. Please select just one.");
            //我们没有配置 这里应该是配置一个默认的
            if (!hasConfigurers && !hasFilterChain) {
                WebSecurityConfigurerAdapter adapter = this.objectObjectPostProcessor
                        .postProcess(new WebSecurityConfigurerAdapter() {
                        });
                this.webSecurity.apply(adapter);
            }
    
            /**
             * securityFilterChains 是通过容器获取 通过@Autowired set方法注入
             * 这里也是一个扩展点 我们可以增加手动增加securityFilterChain
             */
            for (SecurityFilterChain securityFilterChain : this.securityFilterChains) {
                this.webSecurity.addSecurityFilterChainBuilder(() -> securityFilterChain);
                for (Filter filter : securityFilterChain.getFilters()) {
                    //如果这个filter是FilterSecurityInterceptor 则加入到securityInterceptor
                    if (filter instanceof FilterSecurityInterceptor) {
                        this.webSecurity.securityInterceptor((FilterSecurityInterceptor) filter);
                        break;
                    }
                }
            }
            /**
             * webSecurityCustomizers也是从容器获取
             * 也是一个扩展点。我们可以自定义 在build前对webSecurity做一些定制化操作
             * @通过@Autowired set方法注入
             */
            for (WebSecurityCustomizer customizer : this.webSecurityCustomizers) {
                customizer.customize(this.webSecurity);
            }
            /*
            *<1>执行build
             */
            return this.webSecurity.build();
        }

    <1>

    模板模式

    org.springframework.security.config.annotation.AbstractSecurityBuilder#build

        @Override
        public final O build() throws Exception {
            if (this.building.compareAndSet(false, true)) {
                //<2>模板模式
                this.object = doBuild();
                return this.object;
            }
            throw new AlreadyBuiltException("This object has already been built");
        }

    <2>

    所有配置都继承这个类

    org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder#doBuild

     /**
         * @return
         * @throws Exception
         */
        @Override
        protected final O doBuild() throws Exception {
            synchronized (this.configurers) {
                this.buildState = AbstractConfiguredSecurityBuilder.BuildState.INITIALIZING;
                //空实现
                beforeInit();
                //<3>初始化config
                init();
                this.buildState = AbstractConfiguredSecurityBuilder.BuildState.CONFIGURING;
                beforeConfigure();
                //<4>
                configure();
                this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILDING;
    //模板方法 抽象的子类必须实现 真正的build方法 O result
    = performBuild(); this.buildState = AbstractConfiguredSecurityBuilder.BuildState.BUILT; return result; } }

    针对不同的build会调用不同的performBuild方法

    如webSecurity则是调用<10>

    如HttpSecurity则调用<13>

    针对DefaultPasswordEncoderAuthenticationManagerBuilder 则调用<16>

    <3>

    org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder#init

    @SuppressWarnings("unchecked")
        private void init() throws Exception {
            //获得configures调用configures的init 注意不同的配置类 就是调用不同配置的init方法
            Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
            for (SecurityConfigurer<O, B> configurer : configurers) {
                configurer.init((B) this);
            }
            for (SecurityConfigurer<O, B> configurer : this.configurersAddedInInitializing) {
                configurer.init((B) this);
            }
        }

    针对不同配置 Configurers不一样,如果是WebSecurity则getConfigures是 WebSecurityConfigurerAdapter 所以调用的WebSecurityConfigurerAdapter的init方法<6>

    <4>

    org.springframework.security.config.annotation.AbstractConfiguredSecurityBuilder#configure

        @SuppressWarnings("unchecked")
        private void configure() throws Exception {
            Collection<SecurityConfigurer<O, B>> configurers = getConfigurers();
            for (SecurityConfigurer<O, B> configurer : configurers) {
                //不同的配置类就是调用不同的 configure方法初始化
                configurer.configure((B) this);
            }
        }

    针对不同配置 Configurers不一样

    1.如果是WebSecurity则getConfigures是 WebSecurityConfigurerAdapter 所以调用的WebSecurityConfigurerAdapter的configure方法<5>

    2.如果是HttpSecurity则是<11>处配置的各种config如 如果有需要可以研究各个config如何初始化的比如我们参考HeaderConfigure的实现<12>

    3.针对DefaultPasswordEncoderAuthenticationManagerBuilder 的confgure请看<15>

    <15>

    <5>

    一般我们都会重写

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

    <6>

    org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#init

     @Override
        public void init(WebSecurity web) throws Exception {
            //<7>初始化HttpSecurity 本事也是一个build
            HttpSecurity http = getHttp();
            //将HttpSecurity add 到WebSecurity 后续会使用securityFilterChainBuilders 
            web.addSecurityFilterChainBuilder(http).postBuildAction(() -> {
                FilterSecurityInterceptor securityInterceptor = http.getSharedObject(FilterSecurityInterceptor.class);
                web.securityInterceptor(securityInterceptor);
            });
        }

    <7>

    org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#getHttp

        @SuppressWarnings({ "rawtypes", "unchecked" })
        protected final HttpSecurity getHttp() throws Exception {
            if (this.http != null) {
                return this.http;
            }
            //从容器获取AuthenticationEventPublisher
            AuthenticationEventPublisher eventPublisher = getAuthenticationEventPublisher();
            //设置到localConfigureAuthenticationBldr build
            this.localConfigureAuthenticationBldr.authenticationEventPublisher(eventPublisher);
            //<8>通过localConfigureAuthenticationBldr build初始化AuthenticationManager 这里是org.springframework.security.authentication.ProviderManager
            AuthenticationManager authenticationManager = authenticationManager();
            //给authenticationBuilder 设置authenticationManager
            this.authenticationBuilder.parentAuthenticationManager(authenticationManager);
            Map<Class<?>, Object> sharedObjects = createSharedObjects();
            //初始化HttpSecurity
            this.http = new HttpSecurity(this.objectPostProcessor, this.authenticationBuilder, sharedObjects);
            if (!this.disableDefaults) {
                //<11>进行默认配置
                applyDefaultConfiguration(this.http);
                ClassLoader classLoader = this.context.getClassLoader();
                List<AbstractHttpConfigurer> defaultHttpConfigurers = SpringFactoriesLoader
                        .loadFactories(AbstractHttpConfigurer.class, classLoader);
                for (AbstractHttpConfigurer configurer : defaultHttpConfigurers) {
                    this.http.apply(configurer);
                }
            }
            /**
             * <9>传入我们的http build 让我们可以做定制化配置
             *  @Override
             *     protected void configure(HttpSecurity http) throws Exception{
             *       ....
             *     }
             */
            configure(this.http);
            return this.http;
        }

    <8>

    org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#authenticationManager

        protected AuthenticationManager authenticationManager() throws Exception {
            //避免重复初始化
            if (!this.authenticationManagerInitialized) {
                /**
                 * 这里就是调用自定义继承WebSecurityConfigurerAdapter重写的configure(AuthenticationManagerBuilder auth)
                 * 传入build 让我们可以自定义一些参数配置 比如配置用户信息是基于应用 还是内存
                 *  auth.inMemoryAuthentication()
                 *                 .withUser("liqiang").password("liqiang").roles("admin")
                 *                 .and()
                 *                 .withUser("admin").password("admin").roles("admin");
                 */
                configure(this.localConfigureAuthenticationBldr);
                //
                if (this.disableLocalConfigureAuthenticationBldr) {
                    //这里是一个扩展点我们可以直接 authenticationConfiguration是根据spring容器初始化的 根据authenticationConfiguration而不是通过build
                    this.authenticationManager = this.authenticationConfiguration.getAuthenticationManager();
                }
                else {
                    //正常是走得这个build方法 build authenticationManager  这里会调用<1> 为何到1请看下面说明
                    //默认 localConfigureAuthenticationBldr是DefaultPasswordEncoderAuthenticationManagerBuilder
                    //初始化处 详看:<14>
                    this.authenticationManager = this.localConfigureAuthenticationBldr.build();
                }
                this.authenticationManagerInitialized = true;
            }
            return this.authenticationManager;
        }

    configure(this.localConfigureAuthenticationBldr);

    这里需要强调的一点是调用我们继承的WebSecurityConfigurerAdapter 我们可以定义用户管理器

    如基于应用内存

    内部创建inMemoryAuthentication方法 创建InMemoryUserDetailsManagerConfigurer 到build confgures

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            /**
             *  inMemoryAuthentication 开启在内存中定义用户
             *  多个用户通过and隔开
             */
            auth.inMemoryAuthentication()
                    .withUser("liqiang").password("liqiang").roles("admin")
                    .and()
                    .withUser("admin").password("admin").roles("admin");
        }

    自定义userDetail

    内部创建DaoAuthenticationConfigurer 到build confgures

    @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            /**
             *  inMemoryAuthentication 开启在内存中定义用户
             *  多个用户通过and隔开
             */
            auth.userDetailsService(new UserDetailsService() {
                @Override
                public UserDetails loadUserByUsername(String s) throws UsernameNotFoundException {
                    return null;
                }
            });
        }

    基于封装的jdbc查询jdbcAuthentication方法 创建JdbcUserDetailsManagerConfigurer 到build confgures

        @Override
        protected void configure(AuthenticationManagerBuilder auth) throws Exception {
            /**
             *  inMemoryAuthentication 开启在内存中定义用户
             *  多个用户通过and隔开
             */
            auth.jdbcAuthentication().dataSource(null).usersByUsernameQuery("");
        }

    他们本质都是根据auth.创建不同的config对象 设置到build的configures  

    <9>

        /**
         * 对于不需要授权的静态文件放行
         * @param web
         * @throws Exception
         */
        @Override
        public void configure(WebSecurity web) throws Exception {
            web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
        }

    <10>

    org.springframework.security.config.annotation.web.builders.WebSecurity#performBuild

    securityFilterChains为什么是列表

    因为不同的url可以有不同的处理逻辑

      @Override
        protected Filter performBuild() throws Exception {
            Assert.state(!this.securityFilterChainBuilders.isEmpty(),
                    () -> "At least one SecurityBuilder<? extends SecurityFilterChain> needs to be specified. "
                            + "Typically this is done by exposing a SecurityFilterChain bean "
                            + "or by adding a @Configuration that extends WebSecurityConfigurerAdapter. "
                            + "More advanced users can invoke " + WebSecurity.class.getSimpleName()
                            + ".addSecurityFilterChainBuilder directly");
            /**
             * 得我们设置的忽略检查为他们添加一个 这里会添加3个chains 根据匹配做不通过处理
             *  public void configure(WebSecurity web) throws Exception {
             *         web.ignoring().antMatchers("/js/**", "/css/**", "/images/**");
             *     }
             */
            int chainSize = this.ignoredRequests.size() + this.securityFilterChainBuilders.size();
            List<SecurityFilterChain> securityFilterChains = new ArrayList<>(chainSize);
            for (RequestMatcher ignoredRequest : this.ignoredRequests) {
                securityFilterChains.add(new DefaultSecurityFilterChain(ignoredRequest));
            }
            //securityFilterChainBuilders为HttpSecurity<6>处初始化
            for (SecurityBuilder<? extends SecurityFilterChain> securityFilterChainBuilder : this.securityFilterChainBuilders) {
                //执行build<1> 最终会构建成<13>
                securityFilterChains.add(securityFilterChainBuilder.build());
            }
            //通过FilterChainProxy 代理管理 它实现了ServletFilter 通过FilterChainProxy为Servlet入口 进入security的自己的filter
            FilterChainProxy filterChainProxy = new FilterChainProxy(securityFilterChains);
            if (this.httpFirewall != null) {
                filterChainProxy.setFirewall(this.httpFirewall);
            }
            if (this.requestRejectedHandler != null) {
                filterChainProxy.setRequestRejectedHandler(this.requestRejectedHandler);
            }
            filterChainProxy.afterPropertiesSet();
    
            Filter result = filterChainProxy;
            if (this.debugEnabled) {
               
                result = new DebugFilter(filterChainProxy);
            }
            this.postBuildAction.run();
            //返回filter 我们请求都会到filterChainProxy  通过他调用security的filter实现securityfilter 注入逻辑
            return result;
        }

    <11>

    org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#applyDefaultConfiguration

      private void applyDefaultConfiguration(HttpSecurity http) throws Exception {
            //http本质也是build 这里都是配置默认的config configure add CsrfConfigurer
            http.csrf();
            //默认增加一个WebAsyncManagerIntegrationFilter
            http.addFilter(new WebAsyncManagerIntegrationFilter());
            //configures add ExceptionHandlingConfigurer
            http.exceptionHandling();
            //configures add HeadersConfigurer
            http.headers();
            //configures add SessionManagementConfigurer
            http.sessionManagement();
            //configure add SecurityContextConfigurer
            http.securityContext();
            //configure add RequestCacheConfigurer
            http.requestCache();
            ///configure add AnonymousConfigurer
            http.anonymous();
            ///configure add ServletApiConfigurer
            http.servletApi();
            //自定义默认config
            http.apply(new DefaultLoginPageConfigurer<>());
            //configure LogoutConfigurer
            http.logout();
        }

    <12>

    org.springframework.security.config.annotation.web.configurers.HeadersConfigurer#configure

       @Override
        public void configure(H http) {
            //创建一个HeaderFilter
            HeaderWriterFilter headersFilter = createHeaderWriterFilter();
            //添加到HttpSecurityFilter
            http.addFilter(headersFilter);
        }

    <13>

    org.springframework.security.config.annotation.web.builders.HttpSecurity#performBuild

    @Override
        protected DefaultSecurityFilterChain performBuild() {
            //将httpSecurity filter排序
            this.filters.sort(OrderComparator.INSTANCE);
            List<Filter> sortedFilters = new ArrayList<>(this.filters.size());
            for (Filter filter : this.filters) {
                sortedFilters.add(((OrderedFilter) filter).filter);
            }
            //requestMatcher 为匹配条件  DefaultSecurityFilterChain 包装起来 管理所有Filter
            return new DefaultSecurityFilterChain(this.requestMatcher, sortedFilters);
        }

    <14>

    org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter#setApplicationContext

    @Autowired
        public void setApplicationContext(ApplicationContext context) {
            this.context = context;
            ObjectPostProcessor<Object> objectPostProcessor = context.getBean(ObjectPostProcessor.class);
            LazyPasswordEncoder passwordEncoder = new LazyPasswordEncoder(context);
            this.authenticationBuilder = new DefaultPasswordEncoderAuthenticationManagerBuilder(objectPostProcessor,
                    passwordEncoder);
            this.localConfigureAuthenticationBldr = new DefaultPasswordEncoderAuthenticationManagerBuilder(
                    objectPostProcessor, passwordEncoder) {
    
                @Override
                public AuthenticationManagerBuilder eraseCredentials(boolean eraseCredentials) {
                    WebSecurityConfigurerAdapter.this.authenticationBuilder.eraseCredentials(eraseCredentials);
                    return super.eraseCredentials(eraseCredentials);
                }
    
                @Override
                public AuthenticationManagerBuilder authenticationEventPublisher(
                        AuthenticationEventPublisher eventPublisher) {
                    WebSecurityConfigurerAdapter.this.authenticationBuilder.authenticationEventPublisher(eventPublisher);
                    return super.authenticationEventPublisher(eventPublisher);
                }
    
            };
        }

    <15>

    跟WebSecurityConfigurerAdapter 一样 以下3个方法都是add不同的config 执行不同的初始化逻辑

    static class DefaultPasswordEncoderAuthenticationManagerBuilder extends AuthenticationManagerBuilder {
    
            private PasswordEncoder defaultPasswordEncoder;
    
            /**
             * Creates a new instance
             * @param objectPostProcessor the {@link ObjectPostProcessor} instance to use.
             */
            DefaultPasswordEncoderAuthenticationManagerBuilder(ObjectPostProcessor<Object> objectPostProcessor,
                    PasswordEncoder defaultPasswordEncoder) {
                super(objectPostProcessor);
                this.defaultPasswordEncoder = defaultPasswordEncoder;
            }
    
            @Override
            public InMemoryUserDetailsManagerConfigurer<AuthenticationManagerBuilder> inMemoryAuthentication()
                    throws Exception {
                return super.inMemoryAuthentication().passwordEncoder(this.defaultPasswordEncoder);
            }
    
            @Override
            public JdbcUserDetailsManagerConfigurer<AuthenticationManagerBuilder> jdbcAuthentication() throws Exception {
                return super.jdbcAuthentication().passwordEncoder(this.defaultPasswordEncoder);
            }
    
            @Override
            public <T extends UserDetailsService> DaoAuthenticationConfigurer<AuthenticationManagerBuilder, T> userDetailsService(
                    T userDetailsService) throws Exception {
                return super.userDetailsService(userDetailsService).passwordEncoder(this.defaultPasswordEncoder);
            }
    
        }

    <16>

    org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder#performBuild

        @Override
        protected ProviderManager performBuild() throws Exception {
            if (!isConfigured()) {
                this.logger.debug("No authenticationProviders and no parentAuthenticationManager defined. Returning null.");
                return null;
            }
            //通过ProviderManager 统一管理providers authenticationProviders 都可以定制
            ProviderManager providerManager = new ProviderManager(this.authenticationProviders,
                    this.parentAuthenticationManager);
            if (this.eraseCredentials != null) {
                providerManager.setEraseCredentialsAfterAuthentication(this.eraseCredentials);
            }
            if (this.eventPublisher != null) {
                providerManager.setAuthenticationEventPublisher(this.eventPublisher);
            }
            //依赖注入
            providerManager = postProcess(providerManager);
            return providerManager;
        }
  • 相关阅读:
    SQLite 与 SqlCE 比较
    window.showModalDialog以及window.open用法简介
    Flex 3D Engine演示 帅呆了。
    MySQLFront
    数据库复制相同表语句
    org.jboss.web.jsf.integration.config.JBossJSFConfigureListener
    PHP中全局变量$_SERVER的详细用法
    PHP date函数使用说明
    如何学习Flex Framework
    richfaces a4j标签帮助文档 地址
  • 原文地址:https://www.cnblogs.com/LQBlog/p/15508248.html
Copyright © 2020-2023  润新知