但是,我们又需要在不同请求-响应之间,来区分请求-响应是不是同一个用户发起的。
比如:用户购买越多,折扣越多的活动,就需要我们分辨出不同请求-响应之间的逻辑关系
// 专门用来发凭证(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 MapcountMap = new HashMap<>(); // 这个 map 就是就是房子 private final Map> 模拟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.Entryentry : 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 中保存的数据也可以视为name-value
一般利用Cookie设置session-id,这样,客户端和服务器之间仍然使用cookie机制,只是cookie 只传递id即可。主要数据全部保存在服务器。
Cookie和Session在实际场景中最常见的应用——在线用户管理(如何在一次HTTP请求-响应中判断用户是否登录,以及登录用户是谁)
用户管理:用户注册(建档)、用户登录(当前用户在线)、用户退出(当前用户下线)、用户注销(销档)、
获取当前登录用户(currentUser):判断用户是否在线;如果在线,得到当前用户是谁。和Cookie、Session有关系的,其实是在线用户状态的维护:用户登录、用户退出、获取当前登录用户信息;当前用户保存Session中。
用户注册:为用户建档,为用户信息做持久化保存——在数据库中维护一条用户信息,
记录用户基本信息。用户登录时通过验证这份基本信息来证明“你”是“你”用户名+密码的方案——设计一个最简单的数据库表:
用户表(users){该用户的唯一标识符(uid) 、
该用户的用名——登录时唯一验证,需要保持记录的唯一性(username)、
密码(暂时明文)}
- // 1. 支持 POST
- // 2. 资源路径是 /register.do
- @PostMapping("/register.do")
- // 3. 希望结果是重定向,所以,不需要使用 @ResponseBody 注解修饰方法
- // 4. 方法的返回值类型是 String,到时候会返回 "redirect:..."
- // 5. 需要读取用户提交的 和
- // 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 request public 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. 创建一个随机的 // 2. 为这个分配一个 HttpSession 对象(Map 结构) // 3. 响应中设置 Cookie(Set-Cookie):JSESSIONID= // 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 @ResponseBody public 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 "用户未登录,请到 登录页 进行登录"; } else { System.out.println("拿到 uid 了"); // return String.format("当前登录用户为: uid = %d, username = %s, password = %s", uid, username, password); return String.format("当前登录用户为: %s", currentUser); // 这里虽然没写,但实际就是调用的 currnetUser.toString() } }