由于Http协议是无状态的,也就是说来一个客户端发送两次请求,服务器是无法辨别这两次请求是否为同一个客户端发出的。为了让服务器知道请求的状态,有两种常见的方式:
cookie
服务器在response中可以设置cookie,客户端在收到response后,会读取其中的cookie,并在后序的请求中都带着这个cookie。cookie是存在于request/response的header中的。
cookie是将状态存在于客户端,对于浏览器来说,如果cookie中带有一些敏感信息,便存在安全隐患。
session
Session则是将状态存在服务器端,而在cookie中仅存储一个sessionID。Tomcat 的 Session 管理器提供了多种持久化方案来存储 Session,通常会采用Redis作为高性能存储方案。并将Redis进行集群部署,提高可用性。
在本小结中,我们先来看看cookie的使用方式,cookie在http的报文中是存在于header中的,为了更好的理解,我们先来看看客户端和服务器的通信,这个过程分两步完成:
- 与服务器建立Socket连接
- 生成请求数据并通过Socket发送出去
建立socket连接的意思是,客户端向服务器发出 TCP 连接请求,经过TCP三次握手,建立TCP连接。此后便可以通过此连接发送数据、接收数据。HTTP是基于TCP/IP协议的,HTTP协议就是发送/接收数据格式的一种定义。
下图便是http request的数据格式:
request.png
当接收的数据后便会对请求进行处理,生成返回的数据包,http response 的数据格式如下图所示
response.png
那么在SpringBoot中如何使用cookie呢?SpringBoot内嵌了Tomcat服务器,在接收到请求数据后,便将数据封装成HttpServletRequest对象,处理后再生成HttpServletResponse对象,它们帮我们处理了socket连接,以及将原始报文转换为Java对象的操作。
那么,想要操作cookie,需要首先获得HttpServletRequest
和HttpServletResponse
对象,我们看一段简单的controller,它提供了一个login的endpoint:
@RestController
@RequestMapping("/user")
@Slf4j
public class PassportController {
@Autowired
private PassportService passportService;
@PostMapping("/login")
public JSONResult login(@Valid @RequestBody LoginRequest loginRequest, BindingResult bindingResult) {
if (bindingResult.hasErrors()) {
List<ObjectError> errors = bindingResult.getAllErrors();
log.info("errors: {}", errors);
return JSONResult.error("parameter error");
}
Users user = passportService.login(loginRequest);
if (user == null) {
return JSONResult.error("username or password is invalid");
}
return JSONResult.success(user);
}
}
通过postman就可以访问这个endpoint
post http://localhost:8088/user/login
body { "username": "xiaoming", "password": "123456" }
我们发现login方法没有给我提供获取HttpServletRequest
和HttpServletResponse
对象的机会。贴心的@PostMapping为我们给方法自动注入了这两个参数,我们只需要在方法入参里添加这两个参数即可:
@PostMapping("/register")
public JSONResult register(@Valid @RequestBody RegisterRequest registerRequest, BindingResult bindingResult, HttpServletRequest request, HttpServletResponse response) { ... }
cookie通常是服务端首先生成,并设置于response中,可以回看上面的第二张response的截图,在header中有Set-Cookie的字段,那么现在我们来在response中设置一下,HttpServletResponse这个Interface提供了一个方法名为addCookie
,出入一个cookie对象即可。javax.servlet.http
包中定义了Cookie
类,它提供了一个构造方法:
public Cookie(String name, String value) {
validation.validate(name);
this.name = name;
this.value = value;
}
通过代码中的注释,可以看出name,value就是一个键值对,name需要符合RFC 2109
协议,除了这个核心键值对,还可通过其他的set方法添加一些其他的属性,比如:
public void setPath() { ... }
public void setMaxAge(int expiry) { ... }
public void setDomain(String pattern) { ... }
OK,我们可以开始给response添加cookie啦,这里使用了一个工具类来实现,也只是对上面各个属性设置的一个封装
@RestController
@RequestMapping("/user")
@Slf4j
public class PassportController {
@Autowired
private PassportService passportService;
@PostMapping("/login")
public JSONResult login(@Valid @RequestBody LoginRequest loginRequest, BindingResult bindingResult, HttpServletRequest request, HttpServletResponse response) {
if (bindingResult.hasErrors()) {
List<ObjectError> errors = bindingResult.getAllErrors();
log.info("errors: {}", errors);
return JSONResult.error("parameter error");
}
Users user = passportService.login(loginRequest);
if (user == null) {
return JSONResult.error("username or password is invalid");
}
CookieUtils.setCookie(request, response, "user", JsonUtils.objectToJson(user), true);
return JSONResult.success(user);
}
}
这里通过CookieUtils.setCookie来设置这个cookie,为了演示客户端的确拿到这个cookie,写了一个简单的React App,通过npx create-react-app foodie-frontend
创建一个React项目,再使用npm install axios --save
安装一个http三方库,通过React Hooks添加一个启动发请求的功能,在App.js中添加如下代码
function App() {
useEffect(() => {
const login = async () => {
try {
axios.defaults.withCredentials = true;
const result = await axios.post("http://localhost:8088/user/login", {username: "xiaoming", password: "123456"})
console.log(result.data);
} catch (e) {
console.log(e);
}
}
login()
});
...
}
使用npm run start
来启动这个前端工程,打开chrome的控制台,就可以看到这个cookie啦:
之后在client每次的请求中都会带着这个cookie,验证也很容易,在其他方法中添加如下代码即可看到客户端发来的cookie
String userCookie = CookieUtils.getCookieValue(request, "user", "utf-8");
Users cookie = JsonUtils.jsonToPojo(userCookie, Users.class);
log.info("user cookie: {}", cookie);
Tips
通过浏览器发送给服务端会遇到跨域问题,跨域是什么可以百度一下,如何解决呢,在服务端添加如下代码:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.cors.CorsConfiguration;
import org.springframework.web.cors.UrlBasedCorsConfigurationSource;
import org.springframework.web.filter.CorsFilter;
@Configuration
public class CorsConfig {
public CorsConfig() { }
@Bean
public CorsFilter corsFilter() {
CorsConfiguration config = new CorsConfiguration();
config.addAllowedOrigin("http://localhost:3000");
config.addAllowedOrigin("*");
config.setAllowCredentials(true);
config.addAllowedMethod("*");
config.addAllowedHeader("*");
UrlBasedCorsConfigurationSource corsSource = new UrlBasedCorsConfigurationSource();
corsSource.registerCorsConfiguration("/**", config);
return new CorsFilter(corsSource);
}
}
其中config.addAllowedOrigin
便用于设置允许那些client发来请求
网友评论