# 使用 JWT 验证

URL: https://caijiao.org/express/chapter8/3-jwt
Source: docs/express/chapter8/3-jwt.md
Description: 本章节将介绍如何使用 JWT（JSON Web Token）在 Express 应用中实现用户认证。通过 JWT，我们可以在客户端和服务器之间传递加密的身份验证信息，增强安全性和灵活性。

JSON Web Token (JWT) 是一种开放标准（RFC 7519），用于在网络应用环境中以一种简短、安全的方式传递信息。JWT 在实现用户认证时比传统的基于 Session 的验证方式更加灵活，尤其适用于分布式架构、微服务等场景。在本章节中，我们将展示如何在 Express 中使用 JWT 进行用户认证。

## 1. 安装依赖

首先，我们需要安装 `jsonwebtoken` 库来生成和验证 JWT：

```bash
npm install jsonwebtoken
````

我们还将使用 `bcryptjs` 来加密用户密码，提高系统的安全性：

```bash
npm install bcryptjs
```

## 2. 配置 Express 项目

接着，创建一个基本的 Express 应用，并设置必要的中间件：

```js
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const bodyParser = require('body-parser');

const app = express();
const port = 3000;
const SECRET_KEY = 'your-secret-key';  // 用于加密和解密 JWT 的密钥

// 使用 body-parser 解析 POST 请求的表单数据
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

// 用户数据库模拟
const users = [
  { username: 'john_doe', password: bcrypt.hashSync('password123', 10) },
  { username: 'jane_doe', password: bcrypt.hashSync('12345', 10) }
];

app.listen(port, () => {
  console.log(`Server is running at http://localhost:${port}`);
});
```

在上面的代码中，`bcrypt.hashSync` 用于加密密码，这样即使用户密码暴露，数据也更为安全。

## 3. 用户注册与登录

### 3.1 用户注册

用户注册时，我们将密码通过 `bcryptjs` 进行加密，并将用户信息存储在我们的“数据库”中。

```js
app.post('/register', (req, res) => {
  const { username, password } = req.body;

  // 检查用户名是否已经存在
  const userExists = users.some(user => user.username === username);
  if (userExists) {
    return res.status(400).send('用户名已存在');
  }

  // 加密用户密码
  const hashedPassword = bcrypt.hashSync(password, 10);

  // 将新用户添加到 "数据库" 中
  users.push({ username, password: hashedPassword });
  res.send('注册成功');
});
```

### 3.2 用户登录

在用户登录时，我们首先验证用户名和密码，如果正确，我们将为用户生成一个 JWT 并返回给客户端，客户端可以在后续请求中通过 HTTP 请求头携带这个 JWT 进行身份验证。

```js
app.post('/login', (req, res) => {
  const { username, password } = req.body;

  // 查找匹配的用户
  const user = users.find(u => u.username === username);
  if (!user) {
    return res.status(401).send('用户名或密码错误');
  }

  // 验证密码是否匹配
  const isPasswordValid = bcrypt.compareSync(password, user.password);
  if (!isPasswordValid) {
    return res.status(401).send('用户名或密码错误');
  }

  // 生成 JWT
  const token = jwt.sign({ username: user.username }, SECRET_KEY, { expiresIn: '1h' });

  res.json({ token });
});
```

在这段代码中，我们通过 `jwt.sign()` 生成一个包含用户名的 JWT，并设置了过期时间为 1 小时。生成的 JWT 将被返回给客户端。

## 4. 保护需要认证的路由

在需要用户认证的路由上，我们需要验证客户端传递的 JWT 是否有效。我们可以通过 `Authorization` 请求头获取 JWT，然后使用 `jsonwebtoken` 库进行解密和验证。

### 4.1 创建认证中间件

```js
function authenticateToken(req, res, next) {
  const token = req.header('Authorization') && req.header('Authorization').split(' ')[1];  // 获取请求头中的 token

  if (!token) {
    return res.status(403).send('访问被拒绝，未提供 token');
  }

  // 验证 token
  jwt.verify(token, SECRET_KEY, (err, user) => {
    if (err) {
      return res.status(403).send('无效的 token');
    }
    req.user = user;  // 将解码后的用户信息附加到请求对象上
    next();  // 继续执行后续的路由处理
  });
}
```

在这个中间件中，我们首先检查请求头中是否包含 `Authorization` 字段，如果存在则获取并验证 JWT。如果验证成功，解码后的用户信息将被存储在 `req.user` 中，继续执行后续的路由处理；如果验证失败，则返回 403 错误。

### 4.2 保护路由

现在，我们可以保护需要认证的路由。只有在用户提供有效的 JWT 时，才能访问这些路由。

```js
app.get('/dashboard', authenticateToken, (req, res) => {
  res.send(`欢迎回来，${req.user.username}`);
});
```

在上面的代码中，`/dashboard` 路由需要用户认证才能访问。如果请求中没有有效的 JWT，`authenticateToken` 中间件将返回 403 错误。

## 5. 退出登录

使用 JWT 时，用户退出登录的操作相对简单。JWT 不会存储在服务器端，因此只需要客户端删除存储的 JWT 即可实现退出。

```js
app.get('/logout', (req, res) => {
  res.send('已退出登录');
});
```

客户端只需要删除浏览器中存储的 JWT（例如，在 `localStorage` 或 `sessionStorage` 中），即可实现登出。

## 6. 完整代码示例

以下是完整的 Express 应用代码，展示了如何使用 JWT 来实现用户认证、保护路由以及退出登录的功能：

```js
const express = require('express');
const jwt = require('jsonwebtoken');
const bcrypt = require('bcryptjs');
const bodyParser = require('body-parser');

const app = express();
const port = 3000;
const SECRET_KEY = 'your-secret-key';

// 使用 body-parser 解析 POST 请求的表单数据
app.use(bodyParser.urlencoded({ extended: false }));
app.use(bodyParser.json());

// 用户数据库模拟
const users = [
  { username: 'john_doe', password: bcrypt.hashSync('password123', 10) },
  { username: 'jane_doe', password: bcrypt.hashSync('12345', 10) }
];

// 用户注册路由
app.post('/register', (req, res) => {
  const { username, password } = req.body;
  const userExists = users.some(user => user.username === username);
  if (userExists) {
    return res.status(400).send('用户名已存在');
  }
  const hashedPassword = bcrypt.hashSync(password, 10);
  users.push({ username, password: hashedPassword });
  res.send('注册成功');
});

// 用户登录路由
app.post('/login', (req, res) => {
  const { username, password } = req.body;
  const user = users.find(u => u.username === username);
  if (!user) {
    return res.status(401).send('用户名或密码错误');
  }
  const isPasswordValid = bcrypt.compareSync(password, user.password);
  if (!isPasswordValid) {
    return res.status(401).send('用户名或密码错误');
  }
  const token = jwt.sign({ username: user.username }, SECRET_KEY, { expiresIn: '1h' });
  res.json({ token });
});

// 认证中间件
function authenticateToken(req, res, next) {
  const token = req.header('Authorization') && req.header('Authorization').split(' ')[1];
  if (!token) {
    return res.status(403).send('访问被拒绝，未提供 token');
  }
  jwt.verify(token, SECRET_KEY, (err, user) => {
    if (err) {
      return res.status(403).send('无效的 token');
    }
    req.user = user;
    next();
  });
}

// 保护路由
app.get('/dashboard', authenticateToken, (req, res) => {
  res.send(`欢迎回来，${req.user.username}`);
});

// 退出登录
app.get('/logout', (req, res) => {
  res.send('已退出登录');
});

app.listen(port, () => {
  console.log(`Server is running at http://localhost:${port}`);
});
```

## 总结

本章节介绍了如何在 Express 中使用 JWT 实现用户认证：

1. **用户注册与登录**：通过 `jsonwebtoken` 库生成和验证 JWT，客户端存储 JWT 以便后续请求使用。
2. **保护路由**：使用中间件验证 JWT，确保只有持有有效 JWT 的用户才能访问受保护的路由。
3. **退出登录**：JWT 不存储在服务器端，客户端
