美文网首页
iOS Developer的全栈之路 - cookie

iOS Developer的全栈之路 - cookie

作者: 西西的一天 | 来源:发表于2019-11-18 20:49 被阅读0次

由于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中的,为了更好的理解,我们先来看看客户端和服务器的通信,这个过程分两步完成:

  1. 与服务器建立Socket连接
  2. 生成请求数据并通过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,需要首先获得HttpServletRequestHttpServletResponse对象,我们看一段简单的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方法没有给我提供获取HttpServletRequestHttpServletResponse对象的机会。贴心的@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啦:

chrome.png
之后在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发来请求

相关文章

网友评论

      本文标题:iOS Developer的全栈之路 - cookie

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