JavaWeb-SpringSecurity自定义登陆配置
系列博文
项目已上传至guthub 传送门
JavaWeb-SpringSecurity初认识 传送门
JavaWeb-SpringSecurity在数据库中查询登陆用户 传送门
JavaWeb-SpringSecurity自定义登陆页面 传送门
JavaWeb-SpringSecurity实现需求-判断请求是否以html结尾 传送门
JavaWeb-SpringSecurity自定义登陆配置 传送门
JavaWeb-SpringSecurity图片验证ImageCode 传送门
JavaWeb-SpringSecurity记住我功能 传送门
JavaWeb-SpringSecurity使用短信验证码登陆 传送门
使用Restful自定义登陆配置
自定义登陆成功后的Handler
添加hhandler类库,创建LoginSuccessHandler.class,实现用户成功登陆Handler
@Override
//登陆成功之后会调用的函数
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
//封装了我们的认证信息(发起的认证请求(ip,session),认证成功后的用户信息)
Authentication authentication) throws IOException, ServletException {
// TODO Auto-generated method stub System.out.println("登陆成功"); response.setContentType("application/json;charset=UTF-8"); //将我们authentication转换为json通过response对象以application/json写到页面
response.getWriter().write(objectMapper.writeValueAsString(authentication)); }
在SecurityConfig.java中配置configure()方法
protected void configure(HttpSecurity http) throws Exception{
//表单验证(身份认证)
http.formLogin()
//自定义登陆页面
.loginPage("/require")
//如果URL为loginPage,则用SpringSecurity中自带的过滤器去处理该请求
.loginProcessingUrl("/loginPage")
//配置登陆成功调用loginSuccessHandler
.successHandler(loginSuccessHandler)
.and()
//请求授权
.authorizeRequests()
//在访问我们的URL时,我们是不需要省份认证,可以立即访问
.antMatchers("/login.html","/require").permitAll()
//所有请求都被拦截,跳转到(/login请求中)
.anyRequest()
//都需要我们身份认证
.authenticated()
//SpringSecurity保护机制
.and().csrf().disable();
}
package com.Gary.GaryRESTful.handler; import java.io.IOException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; @Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler{ //将我们的authentication转换为json所需要的类
@Autowired
private ObjectMapper objectMapper; @Override
//登陆成功之后会调用的函数
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
//封装了我们的认证信息(发起的认证请求(ip,session),认证成功后的用户信息)
Authentication authentication) throws IOException, ServletException {
// TODO Auto-generated method stub System.out.println("登陆成功"); response.setContentType("application/json;charset=UTF-8"); //将我们authentication转换为json通过response对象以application/json写到页面
response.getWriter().write(objectMapper.writeValueAsString(authentication)); } }
LoginSuccessHandler.java
package com.Gary.GaryRESTful.config; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import com.Gary.GaryRESTful.handler.LoginSuccessHandler; //Web应用安全适配器
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{ //告诉SpringSecurity密码用什么加密的
@Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
} @Autowired
private LoginSuccessHandler loginSuccessHandler; protected void configure(HttpSecurity http) throws Exception{
//表单验证(身份认证)
http.formLogin()
//自定义登陆页面
.loginPage("/require")
//如果URL为loginPage,则用SpringSecurity中自带的过滤器去处理该请求
.loginProcessingUrl("/loginPage")
//配置登陆成功调用loginSuccessHandler
.successHandler(loginSuccessHandler)
.and()
//请求授权
.authorizeRequests()
//在访问我们的URL时,我们是不需要省份认证,可以立即访问
.antMatchers("/login.html","/require").permitAll()
//所有请求都被拦截,跳转到(/login请求中)
.anyRequest()
//都需要我们身份认证
.authenticated()
//SpringSecurity保护机制
.and().csrf().disable();
} }
SecurityConfig.java
//用户权限
authorities: //认证请求的信息(ip,session)
details //用户是否已经通过了我们的身份认证
authenticated //UserDetails
principal //用户输入的密码
credentials //用户名
name
用户登陆失败后的Handler
@Override
//登陆不成功产生的错误
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException { System.out.println("登陆失败"); //设置返回的状态码 500
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR); response.setContentType("application/json;charset=UTF-8"); //将我们authentication转换为json通过response对象以application/json写到页面
response.getWriter().write(objectMapper.writeValueAsString(exception)); }
在SecurityConfig.java中配置configure()方法
protected void configure(HttpSecurity http) throws Exception{
//表单验证(身份认证)
http.formLogin()
//自定义登陆页面
.loginPage("/require")
//如果URL为loginPage,则用SpringSecurity中自带的过滤器去处理该请求
.loginProcessingUrl("/loginPage")
//配置登陆成功调用loginSuccessHandler
.successHandler(loginSuccessHandler)
//配置登陆失败调用loginFailureHandler
.failureHandler(loginFailureHandler)
.and()
//请求授权
.authorizeRequests()
//在访问我们的URL时,我们是不需要省份认证,可以立即访问
.antMatchers("/login.html","/require").permitAll()
//所有请求都被拦截,跳转到(/login请求中)
.anyRequest()
//都需要我们身份认证
.authenticated()
//SpringSecurity保护机制
.and().csrf().disable();
}
package com.Gary.GaryRESTful.handler; import java.io.IOException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import org.apache.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; @Component
public class LoginFailureHandler implements AuthenticationFailureHandler{ //将我们的authentication转换为json所需要的类
@Autowired
private ObjectMapper objectMapper; @Override
//登陆不成功产生的错误
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException { System.out.println("登陆失败"); //设置返回的状态码 500
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR); response.setContentType("application/json;charset=UTF-8"); //将我们authentication转换为json通过response对象以application/json写到页面
response.getWriter().write(objectMapper.writeValueAsString(exception)); } }
LoginFailureHandler.java
package com.Gary.GaryRESTful.config; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.WebSecurityConfigurerAdapter;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder; import com.Gary.GaryRESTful.handler.LoginFailureHandler;
import com.Gary.GaryRESTful.handler.LoginSuccessHandler; //Web应用安全适配器
@Configuration
public class SecurityConfig extends WebSecurityConfigurerAdapter{ //告诉SpringSecurity密码用什么加密的
@Bean
public PasswordEncoder passwordEncoder()
{
return new BCryptPasswordEncoder();
} @Autowired
private LoginSuccessHandler loginSuccessHandler; @Autowired
private LoginFailureHandler loginFailureHandler; protected void configure(HttpSecurity http) throws Exception{
//表单验证(身份认证)
http.formLogin()
//自定义登陆页面
.loginPage("/require")
//如果URL为loginPage,则用SpringSecurity中自带的过滤器去处理该请求
.loginProcessingUrl("/loginPage")
//配置登陆成功调用loginSuccessHandler
.successHandler(loginSuccessHandler)
//配置登陆失败调用loginFailureHandler
.failureHandler(loginFailureHandler)
.and()
//请求授权
.authorizeRequests()
//在访问我们的URL时,我们是不需要省份认证,可以立即访问
.antMatchers("/login.html","/require").permitAll()
//所有请求都被拦截,跳转到(/login请求中)
.anyRequest()
//都需要我们身份认证
.authenticated()
//SpringSecurity保护机制
.and().csrf().disable();
} }
SecurityConfig.java
用户自定义登陆配置
在application.properties中配置gary.security.loginType为JSON
当用户登陆成功时,当用户打印出登陆成功信息(JSON格式)
@Override
//登陆成功之后会调用的函数
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
//封装了我们的认证信息(发起的认证请求(ip,session),认证成功后的用户信息)
Authentication authentication) throws IOException, ServletException {
// TODO Auto-generated method stub System.out.println("登陆成功");
System.out.println(garySecurityProperties.getLoginType()); response.setContentType("application/json;charset=UTF-8"); //将我们authentication转换为json通过response对象以application/json写到页面
response.getWriter().write(objectMapper.writeValueAsString(authentication)); }
#datasource
spring.datasource.url=jdbc:mysql:///springsecurity?serverTimezone=UTC&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.dricer-class-name=com.mysql.jdbc.Driver #jpa
#打印出数据库语句
spring.jpa.show-sql=true
#更新数据库表
spring.jpa.hibernate.ddl-auto=update gary.security.loginType = JSON
application.properties
package com.Gary.GaryRESTful.properties; import org.springframework.boot.context.properties.ConfigurationProperties; @ConfigurationProperties(prefix = "gary.security")
public class GarySecurityProperties { //LoginType登陆的方式,默认为JSON(restful设计风格)
private LoginType loginType = LoginType.JSON; public LoginType getLoginType() {
return loginType;
} public void setLoginType(LoginType loginType) {
this.loginType = loginType;
} }
GarySecurityProperties.java
package com.Gary.GaryRESTful.properties; //登陆的方式
public enum LoginType { JSON, REDIRECT }
LoginType.java
package com.Gary.GaryRESTful.properties; import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.context.annotation.Configuration; @Configuration
//让我们的配置生效
@EnableConfigurationProperties(GarySecurityProperties.class)
public class GarySecurityConfig { }
GarySecurityConfig.java
package com.Gary.GaryRESTful.handler; import java.io.IOException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import org.apache.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component; import com.fasterxml.jackson.databind.ObjectMapper; @Component
public class LoginFailureHandler implements AuthenticationFailureHandler{ //将我们的authentication转换为json所需要的类
@Autowired
private ObjectMapper objectMapper; @Override
//登陆不成功产生的错误
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException { System.out.println("登陆失败"); //设置返回的状态码 500 SC_INTERNAL_SERVER_ERROR
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR); response.setContentType("application/json;charset=UTF-8"); //将我们authentication转换为json通过response对象以application/json写到页面
response.getWriter().write(objectMapper.writeValueAsString(exception)); } }
LoginFailureHandler.java
package com.Gary.GaryRESTful.handler; import java.io.IOException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.stereotype.Component; import com.Gary.GaryRESTful.properties.GarySecurityProperties;
import com.fasterxml.jackson.databind.ObjectMapper; @Component
public class LoginSuccessHandler implements AuthenticationSuccessHandler{ //将我们的authentication转换为json所需要的类
@Autowired
private ObjectMapper objectMapper; @Autowired
private GarySecurityProperties garySecurityProperties; @Override
//登陆成功之后会调用的函数
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
//封装了我们的认证信息(发起的认证请求(ip,session),认证成功后的用户信息)
Authentication authentication) throws IOException, ServletException {
// TODO Auto-generated method stub System.out.println("登陆成功");
System.out.println(garySecurityProperties.getLoginType()); response.setContentType("application/json;charset=UTF-8"); //将我们authentication转换为json通过response对象以application/json写到页面
response.getWriter().write(objectMapper.writeValueAsString(authentication)); } }
LoginSuccessHandler.java
为提高软件通用性
在application.properties中配置gary.security.loginType为REDIRECT(重定向)
当用户登陆成功时,LoginSuccessHandler重定向到default.jsp继承SavedRequestAwareAuthenticationSuccessHandler,SavedRequestAwareAuthenticationSuccessHandler为SpringSecurity默认处理机制
@Override
//登陆成功之后会调用的函数
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
//封装了我们的认证信息(发起的认证请求(ip,session),认证成功后的用户信息)
Authentication authentication) throws IOException, ServletException {
// TODO Auto-generated method stub System.out.println("登陆成功"); if(LoginType.JSON.equals(garySecurityProperties.getLoginType()))
{
response.setContentType("application/json;charset=UTF-8"); //将我们authentication转换为json通过response对象以application/json写到页面
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
else
{
//调用父类中的方法,跳转到其它页面
super.onAuthenticationSuccess(request, response, authentication);
} }
当用户登陆失败时,springsecurity进行对请求的拦截
//登陆不成功产生的错误
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException { System.out.println("登陆失败"); if(LoginType.JSON.equals(garySecurityProperties.getLoginType()))
{
//设置返回的状态码 500 SC_INTERNAL_SERVER_ERROR
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR); response.setContentType("application/json;charset=UTF-8");
//将我们authentication转换为json通过response对象以application/json写到页面
response.getWriter().write(objectMapper.writeValueAsString(exception)); }
else
{
//调用父类中的方法,跳转到其它页面
super.onAuthenticationFailure(request, response, exception);
}
#datasource
spring.datasource.url=jdbc:mysql:///springsecurity?serverTimezone=UTC&characterEncoding=utf-8
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.dricer-class-name=com.mysql.jdbc.Driver #jpa
#打印出数据库语句
spring.jpa.show-sql=true
#更新数据库表
spring.jpa.hibernate.ddl-auto=update gary.security.loginType = REDIRECT
application.properties
package com.Gary.GaryRESTful.handler; import java.io.IOException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
import org.springframework.security.web.authentication.SavedRequestAwareAuthenticationSuccessHandler;
import org.springframework.stereotype.Component; import com.Gary.GaryRESTful.properties.GarySecurityProperties;
import com.Gary.GaryRESTful.properties.LoginType;
import com.fasterxml.jackson.databind.ObjectMapper; @Component
//SavedRequestAwareAuthenticationSuccessHandler为SpringSecurity默认处理机制
public class LoginSuccessHandler extends SavedRequestAwareAuthenticationSuccessHandler{ //将我们的authentication转换为json所需要的类
@Autowired
private ObjectMapper objectMapper; @Autowired
private GarySecurityProperties garySecurityProperties; @Override
//登陆成功之后会调用的函数
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
//封装了我们的认证信息(发起的认证请求(ip,session),认证成功后的用户信息)
Authentication authentication) throws IOException, ServletException {
// TODO Auto-generated method stub System.out.println("登陆成功"); if(LoginType.JSON.equals(garySecurityProperties.getLoginType()))
{
response.setContentType("application/json;charset=UTF-8"); //将我们authentication转换为json通过response对象以application/json写到页面
response.getWriter().write(objectMapper.writeValueAsString(authentication));
}
else
{
//调用父类中的方法,跳转到其它页面
super.onAuthenticationSuccess(request, response, authentication);
} } }
LoginSuccessHandler.java
package com.Gary.GaryRESTful.handler; import java.io.IOException; import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse; import org.apache.http.HttpStatus;
import org.springframework.beans.factory.annotation.Autowired; import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationFailureHandler;
import org.springframework.stereotype.Component; import com.Gary.GaryRESTful.properties.GarySecurityProperties;
import com.Gary.GaryRESTful.properties.LoginType;
import com.fasterxml.jackson.databind.ObjectMapper; @Component
//springsecurity默认处理器
public class LoginFailureHandler extends SimpleUrlAuthenticationFailureHandler{ //将我们的authentication转换为json所需要的类
@Autowired
private ObjectMapper objectMapper; @Autowired
//我们自己的配置
private GarySecurityProperties garySecurityProperties; @Override
//登陆不成功产生的错误
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException { System.out.println("登陆失败"); if(LoginType.JSON.equals(garySecurityProperties.getLoginType()))
{
//设置返回的状态码 500 SC_INTERNAL_SERVER_ERROR
response.setStatus(HttpStatus.SC_INTERNAL_SERVER_ERROR); response.setContentType("application/json;charset=UTF-8");
//将我们authentication转换为json通过response对象以application/json写到页面
response.getWriter().write(objectMapper.writeValueAsString(exception)); }
else
{
//调用父类中的方法,跳转到其它页面
super.onAuthenticationFailure(request, response, exception);
} } }
LoginFailureHandler.java
JavaWeb-SpringSecurity自定义登陆配置的更多相关文章
- SpringSecurity自定义登陆页面和跳转页面
如果我们不用form-login说明登陆界面,springsecurity框架将自动为我们生成登陆界面 现在我们不想用自动生成的登陆界面了,而想使用自定义的漂亮的登陆界面 则需要使用<secur ...
- springSecurity自定义认证配置
上一篇讲了springSecurity的简单入门的小demo,认证用户是在xml中写死的.今天来说一下自定义认证,读取数据库来实现认证.当然,也是非常简单的,因为仅仅是读取数据库,权限是写死的,因为相 ...
- JavaWeb-SpringSecurity自定义登陆页面
系列博文 项目已上传至guthub 传送门 JavaWeb-SpringSecurity初认识 传送门 JavaWeb-SpringSecurity在数据库中查询登陆用户 传送门 JavaWeb-Sp ...
- SharePoint 2013混合模式登陆中 使用 自定义登陆页
接前一篇博客<SharePoint 2013自定义Providers在基于表单的身份验证(Forms-Based-Authentication)中的应用>,当实现混合模式登陆后,接着我们就 ...
- Shiro 自定义登陆、授权、拦截器
Shiro 登陆.授权.拦截 按钮权限控制 一.目标 Maven+Spring+shiro 自定义登陆.授权 自定义拦截器 加载数据库资源构建拦截链 使用总结: 1.需要设计的数据库:用户.角色.权限 ...
- SpringSecurity 自定义用户 角色 资源权限控制
SpringSecurity 自定义用户 角色 资源权限控制 package com.joyen.learning.security; import java.sql.ResultSet; impor ...
- SpringSecurity 自定义表单登录
SpringSecurity 自定义表单登录 本篇主要讲解 在SpringSecurity中 如何 自定义表单登录 , SpringSecurity默认提供了一个表单登录,但是实际项目里肯定无法使用的 ...
- 【.net 深呼吸】自定义缓存配置(非Web项目)
在前一篇烂文中,老周简单讲述了非Web应用的缓存技术的基本用法.其实嘛,使用系统默认方案已经满足我们的需求了,不过,如果你真想自己来配置缓存,也是可以的. 缓存的自定义配置可以有两种方案,一种是用代码 ...
- phpstorm 自定义函数配置
phpstorm 自定义函数配置 打开设置->活动模板->
随机推荐
- centos 配置mysql主从复制
mysql+centos7+主从复制 MYSQL(mariadb) MariaDB数据库管理系统是MySQL的一个分支,主要由开源社区在维护,采用GPL授权许可.开发这个分支的原因之一是:甲骨文公 ...
- Java 串口通信 Ubuntu
说一下我的操作过程吧 在Windows上先用阿猫串口网络调试助手,进行调试: 在网上找Java代码,我选择的是RXTXcomm,网上代码很多,基本都一样. 在Windows电脑上把rxtx压缩包中的r ...
- arcgisJs之featureLayer中feature的获取
arcgisJs之featureLayer中feature的获取 在featureLayer中source可以获取到一个Graphic数组,但是这个数组属于原数据数组.当使用 applyEdits修改 ...
- MySQL通过 LOAD DATA INFILE 批量导入数据
LOAD DATA INFILE 语句用法 参考手册 本文语句参数使用默认值 PHP: TP框架环境 // 定义文件路径$file_path = 'LOAD_DATA_LOCAL_INFILE.tx ...
- 制作linux云主机镜像
目录 制作linux云主机镜像 1.物理机环境准备 2.安装kvm虚拟机 3.操作虚拟机 4.在物理机上处理镜像 5.拷贝制作好的raw格式的镜像 6.发布镜像到云平台 制作linux云主机镜像 1. ...
- 利用协程和socket实现并发
服务端代码 from gevent import monkey monkey.patch_all() from gevent import spawn import socket def commun ...
- gyp ERR! stack Error: EACCES: permission denied, mkdir问题解决方案
sudo npm i --unsafe-perm 原因还是权限问题 就是说 npm 出于安全考虑不支持以 root 用户运行,即使你用 root 用户身份运行了,npm 会自动转成一个叫 nobody ...
- contos7 yum 安装golang
一.安装 [root@localhost golang]# yum install golang 安装默认目录为/usr/lib/golang/ 二.配置环境变量 echo "export ...
- C++虚函数作用原理(一)——虚函数如何在C++语言逻辑中存在
C++多态,接触其实也没太长的时间.上课的时候老师总是不停的讲,多态可以实现利用一个基类对象调用不同继承类的成员函数.我就会觉得很伤脑筋,这个的原理到底是什么?是什么呢? 开始的时候我觉得自己应该能够 ...
- 网络协议相关面试问题-http协议相关面试问题
HTTP协议简介: 一些基本概念: 协议:指计算机通信网络中两台计算机之间进行通信所必须共同遵守的规定或规则. HTTP协议:超文本传输协议(HTTP)是一种通信协议,它允许将超文本标记语言(HTML ...