请求头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 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. 创建一个随机的 <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
@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 "用户未登录,请到 <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()
}
}