HTML进阶
咱们书接上回,继续我们的HTML学习,上文我们了解了表单的具体格式。
<form action="服务器地址" method="请求方式" enctype="数据格式">
<!--表单项-->
<input type="submit" value="提交按钮">
</form>
好像比较陌生的还是method
和enctype
这两个属性,我们想要去深入了解method
和enctype
这两个元素,我们就要去知道http
请求.
HTTP请求
http
请求可以说是整个web开发的基础,而method
正是http
请求的一个组成。
对于表单来讲,我们的method
请求方式有两种
- get(默认)提交时,数据跟在URL地址之后
- post提交时,数据在请求体内
请求方式的不同导致了数据在请求中的位置不一样,这里捏我们要去了解http
请求的结构
http
请求的组成
分为三部分
- 请求行包含了请求方式
get
orpost
,请求的URL地址(对应了服务器中方法的URL路径),协议的版本,ps:这里并没有主机ip和端口号,因为捏发送请求的前提是建立了链接,你已经建立了链接主机ip和端口号也就确定下来了,即使不写也没关系了 - 请求头请求头可以有多行,请求行只有一行格式:头名: 头值
- 请求体不是所有请求都要有的
由于网页无法设计http请求的原始格式,这里可以用telnet程序测试,下面的HTTP指令都需要用到telnet程序哦,不会的兄弟们可以自行学习配置哦。
我们通过get
和post
请求来看一下http
请求的组成
such as 如果服务器需要一个name数据,我们怎么把name传过去捏
get
GET /test?name=li HTTP/1.1
<!-- 上面是请求行 -->
Host: localhost
<!-- 上面是请求头 -->
<!-- Host指明访问服务器中哪一个虚拟主机 -->
get
请求中跟在URL后有一个?
,?
后面有个name=li
,这是把数据跟在了URL后面,跟在URL后面的叫查询参数,这样服务器就可以获得这个查询参数的值,所以get
请求也就是把数据直接携带在了URL里面
post
Post /test HTTP/1.1
<!-- 上面是请求行 -->
Host: localhost
Content-type:application/x-wwwwform-urlencoded
<!-- 决定请求体的类型 -->
Content-Length:10
<!-- 决定请求体的长度 -->
<!-- 上面三行代码是请求头 -->
name=zhang
<!-- 这是请求体 -->
post
要完成相同的工作,则是把数据放在了请求体当中。
enctype
在post请求时,指定请求体的数据格式
application/x-www-form-urlencoded
(默认)即 名字=值multipart/form-data
我们来细嗦一下咱们的默认格式
如果又多个参数我们的格式时什么样捏
name=zhang&age=18
多个参数之间用&
链接捏,这里的请求体增加了, 我们的请求体长度自然也变化了,我们总不能一个一个算帕,小编也懒得算捏,这里,让我们随便打开一个网页,按F12
进入控制台,用一点JavaScript的小手段
输入下面这串代码
"name=zhang&age=18".length

一秒出结果捏,我们再请求头把数据一改即可哦。
那么我们用get
请求请求时该怎么办捏,实际上?
后面的格式就是默认格式捏,只要再后面将不同数据用&
连接起来就好。
有一个小细节捏,这里规避了汉字作为数据捏,在这里汉字是作为特殊字符出现的捏,要传输特殊字符就需要先用URI对特殊字符进行编码,再发送才不会报错捏,但是怎么编码呢,我们又需要一点JavaScript的小手段了
依旧那个控制台,输入
encodeURIComponent("张")

编码get,我们把引号里面的编码替代汉字就好了捏
name=%E5%BC%A0
再求一下请求体的长度,改一下长度,这样就,不会报错啦。
对编码感兴趣的小伙伴,我把编码方式放到文末了捏。
另外两种常见请求
json
格式
POST /test3 HTTP/1.1
Host:localhost
Content-Type:application/json
Content-length:25
{"name":"zhang","age":18}
注意这里用小手段求长度时候我们需要用''
来包含整个请求体的内容

json就是用字符串来表示对象和数组
{"属性名":属性值}
[数组元素1,元素2······]
属性值和元素值可以有的类型:字符串,数字布尔值,null
,也可以嵌套其他对象或数组
我们json类型的请求要在服务器怎么接收捏
@RequestMApping("/test3")
@ResponseBody
public Req test3(@RequestBody Req req){
System.out,println("req:"+ req);
return req;
}
static class Req {
private String name;
private int age;
}
public String getRname() {
return name;
}
public void setRname(String rname) {
this.name = rname;
}
public int getAge() {
return age;
}
public void setAge(int age) {
this.age = age;
}
@Override
public String toString() {
return "Req{" +
"rname='" + name + '\'' +
", age=" + age +
'}';
}
一个小技巧
jdk16以上的可以简化一下代码,因为新增了record类捏
@RequestMApping("/test3")
@ResponseBody
public Req test3(@RequestBody Req req){
System.out,println("req:"+ req);
return req;
}
record Req(String name, int age){}
注意:这里要接收json类型的数据必须要在方法参数上添加@RequstBody
。
@RequestBody
注解的作用就是req
这个对象转换为json
对象的格式作为响应。
json的优越性:可以直接发送汉字而不需要编码哦
但是你在计算数据长度时候,每有一个汉字需要+2哦,因为一个汉字是三个字节捏,计算长度时候JavaScript是按照一个字节来算的捏。
multipartq
格式
POST /test2 HTTP/1.1
Host: localhost
Content-Type:multipart/form-data;boundary=123
Content-Length:125
--123
Content-Disposition: form-data;name="name"
lisi
---123
Content-Disposition: form-data; name="age"
30
--123--
- boundary=123用来定义分隔符
- 起始分隔符是
---分隔符
- 结束分隔符是
--分隔符--
- 其中lisi和30分别为name和age的内容哦
我们该怎么接受捏
@RequestMapping("/test2")
@ResponnseBody
public String test2(String name, Integer age){
System.out.println(name + " " + age);
return "收到:" + name + " " + age;
}
当然我们了解完之后,当然要知道这种请求优越在哪里啦
URI编码的请求而multipart
方式的请求可以上传文件。
一个小坑
multipart
请求体的长度是怎么求出来的捏,这里不能用双引号也不能用单引号了,因为multipart
的请求体里面有换行而单引号双引号均不支持换行,所以我们需要用反引号(键盘是最上面数字键盘最左面的那个)来包裹请求体

这时候你会发现输入116还是会报错的,而上面代码的长度填写的是125,少的九个字节差在哪里捏,这是因为JavaScript的换行符是/n,而http
请求的换行符有两个,一个是/r(回车)另一个是/n,所以我们需要加上九个回车符,所以是116+9=125
数据格式小结
客户端发送
- 编码
application/x-www-form-urlencoded
:url编码application/json
:utf-8编码multipart/form-data
:每部分编码可以不同
- 表单只支持
application/x-www-form-urlencoded
和multipart/form-data
格式发送数据 - 文件上传需要用
multipart/form-data
格式 - JavaScript代码可以支持任意格式发送数据
服务端接收
- 对
application/x-www-form-urlencoded
和multipart/form-data
格式的数据,Spring接收方式是统一的,只需要用java bean的属性名对应请求参数名即可 - 对于
application/json
格式的数据,Spring接收需要使用@RequestBody
注解+java bean的方式
session原理
http
无状态,有会话
- 无状态是指,请求之间相互独立,第一次请求的数据,第二次请求不能重用第一次请求用到
name=“zhang”
在第二次请求时是不能再用的 - 有会话是指,客户端和服务端都有相应的技术,可以暂存数据,让数据在请求间共享

我们用一张图来形象理解一下喵,当我们客户端1的用户想要发送请求去寻找数据的时候,并不知道要去那个session对象里面寻找捏,但实际上捏在第一次提交之后,session不仅仅是存储了数据,更重要的是给客户端反馈了一个响应,返回session对象的编号即jsessionid

这时客户端会记住这个编号,同时在第二次会把jssionid连同它的值再次发送给服务器,这样第二次请求一定会去找session对象1,这就是session的工作原理
来看个例子
存
GET /s1?name=zhang HTTP/1.1
Host: localhost

会获得一段特有的cookie
服务器端
@RequestMApping("/s1")
@ResponseBody
public String s1(Httpsession session,String name){
session.setAttribute("name",name);
return "数据已存储";
}
取
GET /s2 HTTP/1.1
Host: localhost
Cookie:JSESSIONID=560FAB45D02AE09B7E1BC5D9816A5D
服务器端
@RequestMApping("/s2")
@ResponseBody
public String s2(Httpsession session){
return "取出数据" + session.getAttribute("name");
}
应用
实现身份认证

这里介绍另一种身份验证技术——jwt技术

不同点在于这里返回了一个token的令牌,把令牌返回给客户端,发送其他请求的时候校验token即可
这里我们来看一下代码实现
生成token
GET /ji?name=zhang&pass=123 HTTP/1.1
Host: localhost

服务器端
@RequestMApping("/j1")
@ResponseBody
public String j1(String name,String pass){
if("zhang".equals(name)&&"123".equals(pass)){
String token = Jwts.builder().setSubject("name").signWith(key).compact();
return "身份验证通过:" + token;
}else{
return "身份验证失败";
}
}
校验token
GET /j2 HTTP/1.1
Host: localhost
Authorization:
eyJhbGci0iJIUzI1NiJ9.eyJzdWIi0iJ6aGFUZyJ9._1-P_TLIZQPb1_1CyGWplMZaKQ8MCw_plBbYPZ30X28
服务器端
@RequestMApping("/j2")
@ResponseBody
public String j2(@RequesHeader string authorization){
try{
System.out.println(authorization);
Jws<Claims> jws = Jwts.parserBuilder().setSigningKey(key).build().parseClaimsJws(authorization);
return "校验通过,你是" + jws.getBody().getSubject();
}catch (Exception e){
return "校验失败";
}
}
有些同学又要对token的组成好奇了,那么我们来看一下token
实际上这一段token分为三部分用.
分隔开
eyJhbGci0iJIUzI1NiJ9
表示的是header(签名算法)eyJzdWIi0iJ6aGFUZyJ9
表示的时payload(数据)_1-P_TLIZQPb1_1CyGWplMZaKQ8MCw_plBbYPZ30X28
表示的是所携带的签名
注意前两段都是没有加密的,都是一个json
对象只不过时用了base64的编码变成了这个样子,当然我们可以尝试还原一下
@Test
public void test(){
String token= "eyJhbGci0iJIUzI1Ni19.eyJzdWIi0iJ6aGFUZyJ9._1-P_TLIZQPb1_1CyGWplMZaKQ8MCw_plBbYPZ30X28";
System.out.println(new String(Base64.getDecoder().decode("eyJhbGci0iJIUzI1NiJ9")));
System.out.println(new String(Base64.getDecoder().decode("eyJzdWIiOiJ6aGFuZyJ9")));
}

可以发现这里用的是HS256的算法,而且可以反映出数据并没有加密,所以不要把敏感数据放进去哦。
但是既然没被加密为什么token还具有安全性捏,这个安全性来自于签名,签名能保证token的数据不被篡改。也就是说如果我们把数据的base64加密字符串改为管理员的base64加密字符串后再向服务器发送数据替换后的数据就会返回校验失败

关键在于签名,签名的生成来自于前面两部分外加密钥,虽然签名算法和数据是客户端已知的,但是并不知道密钥,如果恶意篡改了数据,则会导致签名变化,就会导致校验失败
特殊字符编码
有些友友对怎么编码很感兴趣捏,那我们就来了解一下吧
UTF-8
是最常见的一种字符编码,我们就先来了解一下UTF-8
吧
这里我们用Java代码来实现编码过程
package java2;
import org.junit.jupiter.api.Test;
import java.nio.charset.StandardCharsets;
public class bianma {
@Test
public void Test() throws Exception {
byte[] bytes = "张".getBytes(StandardCharsets.UTF_8);
/*这里返回的是一个字节数组,我们用一个字节数组来接收*/
System.out.println(bytes);
}
}
让我们开始debug,并观察数组内容,将其转化为十六进制

欸好像和编码后的数据很像欸,这里URI编码的秘密也揭开了哦,就是保留了特殊字符UTF-8
的十六进制编码欸