美文网首页
Spring Security实现原理剖析(一):filter的

Spring Security实现原理剖析(一):filter的

作者: Morphing0527 | 来源:发表于2020-11-27 17:05 被阅读0次

    前言

    我们知道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;
        }
    

    总结下该方法所做的主要操作:
    首先初始化了webSecurity属性,该属性对应WebSecurity类
    注入了webSecurityConfigurers属性,该属性是一个List<SecurityConfigurer>集合
    遍历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类的类图


    图片.png

    可以看到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;
        }
    

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

    遍历securityFilterChainBuilders集合,并执行其中的build方法,从上面代码分析可知,securityFilterChainBuilders集合里存储了HttpSecurity,所以这里执行了HttpSecurity的build方法构建SecurityFilterChain类。将List<SecurityFilterChain>集合构建成一个FilterChainProxy代理类,返回这个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的了,我们最后再来简单总结下构建过程:
    Spring Security启动过程中通过WebSecurityConfiguration实例化WebSecurity。

    WebSecurityConfiguration会将使用者编写的WebSecurityConfig类放入WebSecurity中的LinkedHashMap中。

    在构建WebSecurity的时候,会调用WebSecurity的doBuild()方法,这个方法是一个核心方法。

    doBuild中的init方法将会调用LinkedHashMap中元素的init方法(这里的元素是WebSecurityConfig),然后WebSecurityConfig的init方法会调用configure方法,调用configure方法后,将会初始化HttpSecurity构建各种Filter,这时HttpSecurity将会加入WebSecurity中。

    doBuild中的init方法调用完后将会调用下一个performBuild()方法,该方法会获取到HttpSecurity调用其doBuild方法构造SecurityFilterChain。

    将获取到的SecurityFilterChain构建成一个FilterChainProxy类,作为Spring Security的顶层filter。至此Spring Security的Filter构建完成。

    原文地址:https://www.ucloud.cn/yun/77713.html

    相关文章

      网友评论

          本文标题:Spring Security实现原理剖析(一):filter的

          本文链接:https://www.haomeiwen.com/subject/qzwxwktx.html