不要直接存储密码

image-20241210222801763

直接存储密码容易泄露

解决方法一:使用Hash

image-20241210222902464

解决方法二:使用Google登录

image-20241210222941978

Sessions, Tokens, Cookie

image-20241210223055166

流程

客户端通过发送 Cookie,其中包含了 Session ID,服务器利用该 ID 来识别用户的会话信息。具体来说,流程如下:


1. 客户端发送请求

  • 用户访问网站并进行登录操作(例如 /login)。
  • 服务器验证用户凭证(如用户名和密码)后,为用户创建一个会话(Session)。

2. 服务器生成 Session

  • 生成 Session ID:服务器生成一个唯一的 Token(如 mZIYWQyNTkyMz),并将其作为该用户会话的标识。

  • 存储会话数据:服务器会将用户的相关数据(如用户名 username 和用户 ID user_id)与该 Session ID 关联,存储在会话存储(如内存、数据库、Redis 等)中。

    • 例如:

      1
      2
      3
      4
      5
      6
      7
      {
      "Session ID": "mZIYWQyNTkyMz",
      "User Info": {
      "username": "abbychou",
      "user_id": "1627201"
      }
      }

3. 将 Session ID 返回客户端

  • 服务器将生成的 Session ID 通过 HTTP 响应头中的 Set-Cookie 字段发送到客户端。

  • 浏览器会将 Session ID 保存到 Cookie 中。例如:

    1
    Set-Cookie: session_id=mZIYWQyNTkyMz; Path=/; HttpOnly

  • 在后续的每次请求中,浏览器会自动将存储的 Cookie(包括 Session ID)附加到 HTTP 请求头中发送给服务器:

    1
    Cookie: session_id=mZIYWQyNTkyMz

5. 服务器识别会话

  • 服务器接收到请求后,提取请求头中的 Session ID。
  • 在会话存储中查找与该 Session ID 对应的会话数据(例如用户信息)。
  • 如果找到有效的会话,则可以识别用户并执行后续操作(如返回用户的私人数据或授权操作)。

6. 会话管理

  • 会话的超时与销毁:

    • 如果用户长时间不活动,服务器可能会将该会话标记为过期。
    • 当用户主动注销(Logout)时,服务器会删除该会话记录。
  • Token 的更新:

    • 在某些场景下,服务器可能会定期更新 Session Token 以增强安全性(例如防止 Session 固定攻击)。

总结

客户端的 Cookie 是存储 Session Token 的载体,而服务器通过 Session Token 来查找并管理用户的会话数据。两者配合使用,实现了在无状态的 HTTP 协议中维护用户状态的功能。

Session

Session(会话) 是 Web 开发中的一个概念,表示服务器和客户端之间的一次对话或交互状态。它是用于在无状态的 HTTP 协议上实现状态保持的机制,使服务器能够在多个请求之间记住用户的信息。以下是对 Session 的详细解释:


Session 的定义

  • Session 是一种状态管理机制,用来跟踪用户在服务器上的活动。它通常在用户登录、访问受保护资源或进行多步操作(如购物车、表单填写等)时使用。
  • 在 Web 应用中,HTTP 是无状态的协议,这意味着每次 HTTP 请求都是独立的,没有记忆任何之前的请求。Session 通过在服务器端保存用户的会话数据,为每个用户创建一个“会话”,从而实现状态保持。

Session 的工作原理

  1. 会话的开始
    • 当用户首次访问某个网站时,服务器为该用户创建一个唯一的 Session。
    • 服务器生成一个唯一的标识符(通常称为 Session ID),用来标识这个会话。
  2. 会话的标识
    • 服务器会将生成的 Session ID 发送到客户端,通常通过 Cookie 的方式。
    • 客户端的浏览器会保存这个 Session ID,并在后续请求中自动将其附加到 HTTP 请求中。
  3. 会话的数据存储
    • 服务器端维护一个数据结构(如内存、数据库或缓存),用于存储与会话相关的数据。例如:
      • 用户的身份信息
      • 购物车内容
      • 表单数据等
  4. 会话的延续
    • 用户在后续的每次请求中,浏览器会将存储的 Session ID 发送给服务器。
    • 服务器通过 Session ID 检索用户的会话数据,从而了解用户的身份或当前状态。
  5. 会话的结束
    • 会话会在以下情况之一终止:
      • 用户主动注销(Logout)。
      • 会话超时(Inactivity Timeout),即用户长时间未与服务器交互。
      • 服务器重启或清理会话数据。

Session 的实现方式

Session 通常由以下几个部分组成:

  1. Session ID
    • Session 的唯一标识符。
    • 一般是随机生成的字符串,通常使用加密技术生成,确保难以预测和篡改。
  2. Session 数据存储
    • Session 数据存储在服务器端,可以存储在以下位置:
      • 内存:适用于小规模应用,但不适合分布式系统。
      • 数据库:适用于需要持久化会话的场景。
      • 缓存系统(如 Redis):适用于高并发和分布式系统。
  3. Session ID 的传递方式
    • Cookie:最常用的方式,浏览器自动处理。
    • URL 参数:通过在 URL 中附加 Session ID,但不安全。
    • 隐藏字段:在表单提交时附加。

Session 的特点

  1. 服务器端状态
    • Session 数据存储在服务器端,客户端仅保存 Session ID。
    • 相较于 Cookie,Session 更安全,因为敏感数据不会存储在客户端。
  2. 短期有效
    • Session 的生命周期通常与会话时间一致。会话结束后,Session 数据会被清除。
  3. 依赖于 Session ID
    • 客户端每次请求时,必须携带 Session ID,服务器才能识别并恢复会话。

特性 Session Cookie
存储位置 服务器端 客户端(浏览器)
数据大小 较大(取决于服务器存储能力) 较小(一般限制为 4KB)
安全性 更安全,数据不暴露给客户端 较不安全,数据可能被篡改或盗取
生命周期 通常短期有效 可设置长期有效
依赖性 依赖 Session ID 无需依赖服务器

举例说明

场景:购物车功能

  1. 用户访问电商网站,添加商品到购物车。
  2. 服务器为用户创建一个 Session,生成一个 Session ID,并通过 Cookie 发送给用户。
  3. 服务器在 Session 中保存购物车数据(如商品 ID 和数量)。
  4. 用户刷新页面或继续购物时,浏览器发送 Cookie 中的 Session ID 给服务器。
  5. 服务器通过 Session ID 检索购物车数据,并返回给用户。

Session 的安全性

  1. 防止 Session ID 被盗用
    • 使用 HTTPS 加密传输数据,防止 Session ID 在网络中被窃取。
    • 设置 Cookie 的 HttpOnlySecure 属性。
  2. 防止会话劫持
    • 定期更换 Session ID。
    • 使用 IP 绑定、用户代理验证等措施。
  3. 设置会话过期时间
    • 避免长期未使用的 Session 占用服务器资源。

Token,Cookie

Session TokenCookie 是 Web 开发中用于实现用户会话管理的两个重要概念,它们分别在客户端和服务器端有不同的作用。让我们详细解释这两个概念:

1. Session Token(会话令牌)

Session Token 是一个由服务器生成的唯一标识符,用于标识用户的会话状态。它通常是一个随机生成的字符串,用于与服务器端的会话数据进行关联,确保服务器能够识别一个特定的用户,并维持该用户的会话状态。

  • 生成:当用户登录时,服务器会根据用户的身份生成一个 Session Token。这个 Token 通常是一个随机的字符串,可以是任何一组字符(例如:e4f08d8bf5f9e34a34f1c6d6ed567b9c)。
  • 存储:Session Token 在服务器端通常存储在一个会话管理系统中,通常是一个数据库、内存存储或者分布式缓存(如 Redis)。服务器通过这个 Token 查找对应的会话数据,例如用户信息、访问权限等。
  • 作用:Session Token 的作用是保持用户的身份状态。例如,用户登录后,服务器通过 Session Token 识别用户,避免用户每次请求都重新进行身份验证。
  • 生命周期:Session Token 的生命周期通常与用户的会话时间一致。会话结束或超时后,Session Token 会失效。

2. Cookie(浏览器中的小文件)

Cookie 是浏览器用于存储小量数据(如 Session Token)的一种机制,它由服务器发送到客户端,并由客户端的浏览器在随后的请求中自动携带返回服务器。

  • 存储:Cookie 被存储在客户端的浏览器中,是一些小文件,通常以键值对的形式存在。例如,一个 Session Token 的 Cookie 可能是这样:

    1
    2
    3
    4
    5
    Name: session_id
    Value: e4f08d8bf5f9e34a34f1c6d6ed567b9c
    Domain: example.com
    Path: /
    Expiry: Session-based (或指定时间)
  • 作用:Cookie 主要用于在客户端存储信息(如 Session Token),并在用户与服务器的交互中自动携带这些信息。每次用户发起请求时,浏览器会自动将 Cookie 中的 Session Token 附加到请求头中,服务器利用这个 Token 查找会话信息并识别用户身份。

  • 生命周期

    • Session Cookie:没有设置明确过期时间的 Cookie,通常在浏览器关闭时被删除。
    • Persistent Cookie(持久化 Cookie):有明确过期时间的 Cookie,会在指定的时间点过期。
  • 作用范围:Cookie 受限于其设置的 DomainPath。例如,Cookie 可能只对 example.comsub.example.com 生效。

二者的关系和配合

  • Session Token 是服务器端生成的标识符,它用于跟踪和识别用户的会话。当用户登录时,服务器会生成 Session Token,并将其存储在服务器端。
  • Cookie 是客户端浏览器用来存储 Session Token 的地方。当服务器生成 Session Token 后,它会通过 HTTP 响应将 Token 作为一个 Cookie 发送到客户端。客户端的浏览器会在后续的请求中自动将这个 Cookie 和其中的 Session Token 发送回服务器。
  • 会话管理
    • 客户端的浏览器会存储一个 Session Cookie,它的内容包含了 Session Token
    • 每当客户端发送请求时,浏览器会自动将 Session Token(存储在 Cookie 中)附加到 HTTP 请求头部。
    • 服务器接收到请求后,使用这个 Session Token 查找相应的会话数据,识别用户身份,维持会话状态。

总结

  • Session Token 是一个由服务器生成的唯一标识符,用于标识用户会话,它存储在服务器端,通常在登录后由服务器生成并通过 Cookie 传递给客户端。
  • Cookie 是存储在客户端浏览器中的小文件,它存储用户相关的信息(如 Session Token)。每次用户向服务器发送请求时,浏览器会自动将 Cookie 中的信息带回服务器,服务器通过 Cookie 中的 Session Token 识别用户。

通过这种机制,Session Token 和 Cookie 配合使用,能够保持用户会话的状态,免去每次请求都进行身份验证的麻烦。

在CatBook中添加登入功能

  • 分离认证服务器和资源服务器

    • 存储密码很困难,所以我们让 Google 来处理认证。
  • 初次登录:通过 Google 登录。

    • 使用令牌(如 JWT)。
  • 保持登录状态:使用 Express.js 的会话(Sessions)。

image-20241210224310570

image-20241210224234705

这样子很不安全,我们需要Goole返回一个Token 这样子我们才能安全的识别是不是真正的用户

image-20241210224358844

image-20241210224407381

如何获得Google Auth 注册

API 和服务 – API 和服务 – My First Project – Google Cloud Console

将 Google 登录功能集成到您的 Web 应用中 | Authentication | Google for Developers

工作步骤

  • Step0: Add Login/Logout Button
  • Step1: Add API routes for login/logout (connect frontend with backend)
  • Step2: Add User database schema
  • Step3: Add whoami endpoint

Navbar

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
const NavBar = (props) => {
const [userId, setUserId] = useState(null);

useEffect(() => {
get("/api/whoami").then((user) => {
if (user._id) {
// they are registed in the database, and currently logged in.
setUserId(user._id);
}
});
}, []);

const handleLogin = (res) => {
// 'res' contains the response from Google's authentication servers
console.log(res);

const userToken = res.tokenObj.id_token;
post("/api/login", { token: userToken }).then((user) => {
// the server knows we're logged in now
setUserId(user._id);
console.log(user);
});
};

const handleLogout = () => {
console.log("Logged out successfully!");
post("/api/logout");
setUserId(null);
};

return (
<nav className="NavBar-container">
<div className="NavBar-title u-inlineBlock">Catbook</div>
<div className="NavBar-linkContainer u-inlineBlock">
<Link to="/" className="NavBar-link">
Home
</Link>
{userId && (
<Link to={`/profile/${userId}`} className="NavBar-link">
Profile
</Link>
)}
{userId ? (
<GoogleLogout
clientId={GOOGLE_CLIENT_ID}
buttonText="Logout"
onLogoutSuccess={handleLogout}
onFailure={(err) => console.log(err)}
className="NavBar-link NavBar-login"
/>
) : (
<GoogleLogin
clientId={GOOGLE_CLIENT_ID}
buttonText="Login"
onSuccess={handleLogin}
onFailure={(err) => console.log(err)}
className="NavBar-link NavBar-login"
/>
)}
</div>
</nav>
);
};

api.js

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
router.post("/story", (req, res) => {
const newStory = new Story({
creator_id: req.user._id,
creator_name: req.user.name,
content: req.body.content,
});

newStory.save().then((story) => res.send(story));
});

router.post("/comment", (req, res) => {
const newComment = new Comment({
creator_id: req.user._id,
creator_name: req.user.name,
parent: req.body.parent,
content: req.body.content,
});

newComment.save().then((comment) => res.send(comment));
});

router.post("/login", auth.login);
router.post("/logout", auth.logout);

router.get("/whoami", (req, res) => {
if (req.user) {
res.send(req.user);
} else {
// user is not logged in
res.send({});
}
});