请求头Cookie、响应头Set-Cookie
但是,我们又需要在不同请求-响应之间,来区分请求-响应是不是同一个用户发起的。
比如:用户购买越多,折扣越多的活动,就需要我们分辨出不同请求-响应之间的逻辑关系
// 专门用来发凭证(cookie)的@GetMapping("/set-cookie")public void setCookie(HttpServletRequest request, HttpServletResponse response) throws Exception {// servlet 里的处理方式// 不要用中文,因为 cookie 的设置和携带是放在请求响应头中的,字符集编码是西文格式,不支持中文Cookie cookie1 = new Cookie("name", "chen");Cookie cookie2 = new Cookie("learning", "java");Cookie cookie3 = new Cookie("time", "2022");response.addCookie(cookie1);response.addCookie(cookie2);response.addCookie(cookie3);// 设置响应体的字符集编码response.setCharacterEncoding("utf-8");// 设置响应体的格式 —— 纯文本response.setContentType("text/plain");// 写响应内容response.getWriter().println("cookie 设置成功,请观察开发者工具的 网络面板 和 应用面板");}// 记录用户访问次数private final Map<String, Integer> countMap = new HashMap<>(); // 这个 map 就是就是房子private final Map<String, Map<String, Object>> 模拟session的结构 = new HashMap<>();// 模拟一个 session@GetMapping("/use-cookie")public void useCookie(HttpServletRequest request, HttpServletResponse response) throws Exception {String id = null;Cookie[] cookies = request.getCookies();if (cookies != null) {for (Cookie cookie : cookies) {if (cookie.getName().equals("id")) {id = cookie.getValue();}}}Integer count = null;if (id == null) {// cookie 中没有携带 id,认为是用户第一次访问// 将当前时间戳当作这次用户的访问标识long ts = System.currentTimeMillis();System.out.println("是第一次访问");id = String.valueOf(ts);Cookie cookie = new Cookie("id", id);response.addCookie(cookie);count = 1;countMap.put(id, count);} else {// cookie 中携带了 id,说明之前是访问过的System.out.println("不是第一次访问");count = countMap.get(id);if (count == null) {// 由于程序重启,内存中的 countMap 的数据丢了// 所以当成第一次吧count = 1;} else {count += 1;}countMap.put(id, count);}System.out.println("现在统计的所有人的访问次数:");for (Map.Entry<String, Integer> entry : countMap.entrySet()) {System.out.printf("%s => %d\r\n", entry.getKey(), entry.getValue());}// 设置响应体的字符集编码response.setCharacterEncoding("utf-8");// 设置响应体的格式 —— 纯文本response.setContentType("text/plain");// 写响应内容if (count == 1) {response.getWriter().println("第 1 次访问,您的 id 是: " + id);} else {response.getWriter().printf("欢迎 %s 的第 %d 次访问\r\n", id, count);}}
Session会话
![]()
Session是保存在服务器的,专属某次会话的,一组数据。可以跨请求访问到(生命周期是跨请求存在的)。通常: Session 中保存的数据也可以视为name-value
一般利用Cookie设置session-id,这样,客户端和服务器之间仍然使用cookie机制,只是cookie 只传递id即可。主要数据全部保存在服务器。
Cookie和Session在实际场景应用
Cookie和Session在实际场景中最常见的应用——在线用户管理(如何在一次HTTP请求-响应中判断用户是否登录,以及登录用户是谁)
用户管理:用户注册(建档)、用户登录(当前用户在线)、用户退出(当前用户下线)、用户注销(销档)、
获取当前登录用户(currentUser):判断用户是否在线;如果在线,得到当前用户是谁。和Cookie、Session有关系的,其实是在线用户状态的维护:用户登录、用户退出、获取当前登录用户信息;当前用户保存Session中。
用户注册:为用户建档,为用户信息做持久化保存——在数据库中维护一条用户信息,
记录用户基本信息。用户登录时通过验证这份基本信息来证明“你”是“你”
用户名+密码的方案——设计一个最简单的数据库表:
用户表(users){该用户的唯一标识符(uid) 、
该用户的用名——登录时唯一验证,需要保持记录的唯一性(username)、
密码(暂时明文)}
用户注册示例
数据库准备
注册场景
// 1. 支持 POST// 2. 资源路径是 /register.do@PostMapping("/register.do")// 3. 希望结果是重定向,所以,不需要使用 @ResponseBody 注解修饰方法// 4. 方法的返回值类型是 String,到时候会返回 "redirect:..."// 5. 需要读取用户提交的 <input type="text" name="username" placeholder="用户名"> 和 <input type="text" name="password" placeholder="密码">// 5. 所以,方法的形参有两个,分别使用 @RequestParam 修饰public String register(@RequestParam("username") String username, @RequestParam("password") String password) {System.out.println("用户注册:username = " + username + ", password = " + password);// 1. TODO: 本来应该要去完成的参数合法性校验// 2. 执行 SQL 语句String sql = "insert into users (username, password) values (?, ?)";try (Connection c = DBUtil.connection()) {try (PreparedStatement ps = c.prepareStatement(sql)) {ps.setString(1, username);ps.setString(2, password);ps.executeUpdate();}} catch (SQLException exc) {System.out.println("用户注册失败");exc.printStackTrace(System.out);return "redirect:/register.html"; // 失败后重定向到 注册页}System.out.println("用户注册成功");return "redirect:/login.html"; // 目前用户最终看到 404}
登录场景
// 准备 POST /login.do 动态资源@PostMapping("/login.do")// 要重定向,不需要 @ResponseBody 注解修饰方法// 返回值类型是 String// 要读取用户输入的用户名 + 密码信息,和注册一样// 要创建 session 对象,所以,形参中有 HttpServletRequest requestpublic String login(@RequestParam("username") String usernameInput,@RequestParam("password") String passwordInput,HttpServletRequest request) {System.out.println("用户登录:username = " + usernameInput + ", password = " + passwordInput);// TODO: 参数合法性校验// 通过数据库查询该用户的信息 // Integer uid = null; // 使用 Integer 而不是 int,是 Integer 可以保存 null 这种特殊值 // String username = null; // String password = null;User user = null;try (Connection c = DBUtil.connection()) {String sql = "select uid, username, password from users where username = ? and password = ?";try (PreparedStatement ps = c.prepareStatement(sql)) {ps.setString(1, usernameInput); // 把用户填写的用户名添加到 SQL 参数ps.setString(2, passwordInput); // 同理,密码// 带结果的,并且结果只有 1 条 或者 0 条try (ResultSet rs = ps.executeQuery()) {if (rs.next()) {// 查询到了 1 条记录,说明用户名 + 密码正确int uid = rs.getInt("uid");String username = rs.getString("username"); // 实际上,这两个字段可以不查询String password = rs.getString("password"); // 实际上,这两个字段可以不查询user = new User(uid, username, password);} else {// 一条记录都没有查询到,这里什么都不干// 这里加上 else 的目的是为了写这个注释,以后我就不写了}}}} catch (SQLException exc) {System.out.println("登录失败 by 数据库 SQL 执行失败,如果符合 HTTP 语义,应该是 500 错误");exc.printStackTrace(System.out);return "redirect:/login.html";}// 根据 uid 或者 username 或者 password 值来判断用户是否登录成功了,其中一个就行了if (user == null) {// 登录失败了System.out.println("登录失败 by 用户输入的参数有错误,如果符合 HTTP 语义,应该是 4XX 错误");return "redirect:/login.html";}// 登录成功// 获取 session 对象HttpSession session = request.getSession(); // 不带参数,默认是 create = true// 由于之前很大可能是没有 session 的// 所以内部所作的工作是// 1. 创建一个随机的 <session-id>// 2. 为这个 <session-id> 分配一个 HttpSession 对象(Map<String, Object> 结构)// 3. 响应中设置 Cookie(Set-Cookie):JSESSIONID=<session-id>// 4. 所以,应该能在前端看到浏览器中有个 JSESSIONID 的 cookie// 把当前用户的信息(uid、username、password)保存到 session 中,key 我们随意指定// currentUser.uid、currentUser.username、currentUser.password // session.setAttribute("currentUser.uid", uid); // session.setAttribute("currentUser.username", username); // session.setAttribute("currentUser.password", password);session.setAttribute("currentUser", user);System.out.println("登录成功");return "redirect:/"; // 404}
// GET /@GetMapping("/")// 在响应体中输入内容,所以 @ResponseBody 注解修饰方法 + 方法返回值类型是 String// 响应的 Content-Type 是 HTML,返回值直接当成 HTML 内容在对待// 由于需要获取 HttpSession 对象,所以,形参中有 HttpServletRequest request@ResponseBodypublic String getCurrentUser(HttpServletRequest request) { // Integer uid = null; // String username = null; // String password = null;User currentUser = null;HttpSession session = request.getSession(false);if (session != null) {System.out.println("有 session 对象");// 说明是有 Session 的// 获取的时候,必须保证 key 和 登录场景下 setAttribute 时的 key 一致// getAttribute(...) 的返回值类型是 Object// Object 是 Integer 的上级类型// Object -> Integer 是向下转型// 向下转型是不自然的,是有风险的,所以程序员(我们)明确告诉编译器是我要转的,职责我承担(有异常我自己弄)// 需要通过类型强制转换 (Integer) // uid = (Integer) session.getAttribute("currentUser.uid"); // username = (String) session.getAttribute("currentUser.username"); // password = (String) session.getAttribute("currentUser.password");currentUser = (User) session.getAttribute("currentUser");// 如果当时没有设置,getAttribute 的返回值是 null} else {System.out.println("没有 session 对象");// 反之没有 session,什么都不需要做}if (currentUser == null) {System.out.println("没有拿到 uid");// session 可能是 null 或者 session.getAttribute("currentUser.uid") 是 null// 不管是哪种可能,都代表本次请求-响应的用户,没有经历过登录的流程,也就是用户未登录return "用户未登录,请到 <a href='/login.html'>登录页</a> 进行登录";} else {System.out.println("拿到 uid 了"); // return String.format("当前登录用户为: uid = %d, username = %s, password = %s", uid, username, password);return String.format("当前登录用户为: %s", currentUser); // 这里虽然没写,但实际就是调用的 currnetUser.toString()}}