【现代网页开发】前后端分离 之 API

一个API的标准是什么?这个定位每个人心中,对每个项目都不同,但是有一个确实是需要考虑的,API接口的认证。

一般来说,除了前端认证,如果API需要认证(重要),可以从几个方面下手呢

API 不同应用场景

下面是API的一些场景

  • 开放 – 一般用于开放不敏感数据,如天气预告,新闻等等。

  • API Key – 一般用于服务器内部沟通,或者对开放不敏感数据添加一些访问限制

  • Session – 一般用于登录状态、权限、购物车维持。有状态,无需在每次请求中传递这些信息。

  • JWT – 一般用于用户登录认证,无状态,可以携带很多信息(速度会慢),所以可以按照用户(组)不同,给他们不同的访问策略。

  • OAuth 2.0 – 标准化流程,授权细粒度,安全性高。其实就是第三方授权。实现复杂,适配成本高。

提前具体说说重要的 Session 和 JWT 认证:

Session:

优点:

  • 比较安全,因为Session(依赖Cookie)存储在服务器。
    • 适合前后端同源的应用 – Session 依赖于 Cookie,而 Cookie 的自动传递特性使其非常适合传统的 Web 应用(同源系统)。(如PHP前后端混写等情况..)

    • 支持复杂的用户场景 – 适合需要长期会话、复杂状态管理的场景,例如电商购物车、用户权限控制等。

缺点:

  • 不适合分布式系统 – 因为 Session 是存储在服务器上的,因此在分布式架构中(多个服务器或容器),需要额外的机制(如共享存储或 Session 集群)来同步 Session 数据。

  • 不适用于无状态 API – RESTful API 通常是无状态的,Session 的状态依赖性与 REST 的设计原则冲突。

    • 对服务器压力较大 – 果有大量用户访问,服务器需要存储大量的 Session 数据。

    • 跨域问题 – Session 依赖于 Cookie,而 Cookie 默认不支持跨域(需要设置 CORS 和 SameSite 属性)。

JWT:

优点:

  • 无状态 – JWT 是自包含的,所有认证数据(如用户信息、权限等)都存储在 Token 中,服务器不需要维护会话状态。

  • 适合分布式系统(前后端分离在不同服务器/容器),多个服务器或微服务可以共享同一个 JWT,无需额外的存储同步。

  • 服务器压力低,而且JWT扩展性好,可以一次携带如用户角色(role)、权限(scope)、过期时间(exp)等。

  • 可以跨域,而且安全性也好(和Session一样,它们安全性都好)

缺点:

  • JWT 是无状态的,因此服务器无法强制使已经发放的 Token 失效。如果 JWT 被泄露,攻击者可以在有效期内冒充用户

  • JWT 的 Payload 是 Base64 编码的,而不是加密的,任何人都可以解码并查看其中的内容。所以如果 Payload 中包含敏感信息(如密码、个人数据),可能会导致泄露。(如果必须在Payload传递敏感数据,可以对JWT进行加密)

  • Token 大小可能较大,因为 JWT 通常包含用户信息和签名,体积可能比简单的 Session ID 或 API Key 大很多。如果 Payload 数据较多,Token 长度会显著增长,增加网络传输的开销。

  • 不要泄露源代码的”JWT密码”,否则骇客可以通过密钥去伪造JWT。如果密钥需要更新,所有旧的 JWT 都会失效。

  • 不支持会话上下文,如用户登录后权限变化(如被管理员禁用),JWT 无法反映这些实时变化,除非重新签发新的 Token。可以在 JWT 中加入版本号(version),每次用户权限变更时增加版本号,验证时检查版本号是否匹配。


Session & API Key & JWT 对比

特性 Session API Key JWT
状态性 有状态(服务器存储用户数据) 无状态(仅通过 Key 验证) 无状态(Token 中包含用户数据)
存储位置 服务器端存储 Session 数据 无需存储(Key 由客户端携带) 无需存储(Token 自包含信息)
安全性 较高,敏感数据存储在服务器端 较低,API Key 曝露后可被滥用 较高,Token 签名保证完整性
适用场景 前后端同源系统,复杂状态管理 简单的服务间通信或公开 API RESTful API,分布式认证,用户授权
性能 服务器压力较大(需存储 Session 数据) 性能高,无需服务器存储 性能高,无需服务器存储
实现复杂性 简单,传统 Web 应用常用 简单,适合快速实现 较复杂,需要签名、解析和 Token 管理
跨域支持 较差(Cookie 默认限制跨域) 较好(适用于多来源的客户端) 较好(Token 可用于任何来源)
会话过期 服务器端控制(主动销毁或过期) 不支持,需要手动更新 Key 支持过期时间,Token 自动失效

实现

Nodejs + Express

API Key

// app.js
const express = require('express');
const app = express();

app.use(express.json());

// 模拟存储的 API Key 数据库
const apiKeys = ['key1', 'key2', 'key3'];

// 中间件:校验 API Key
function checkApiKey(req, res, next) {
  const apiKey = req.headers['x-api-key']; // 从请求头获取 API Key

  if (!apiKey || !apiKeys.includes(apiKey)) {
    return res.status(403).json({ message: 'Invalid API Key' });
  }

  next(); // 验证通过,继续处理请求
}

// 示例 API 路由
app.get('/api/data', checkApiKey, (req, res) => {
  res.json({ data: 'This is protected data.' });
});

// 启动服务
app.listen(3000, () => console.log('Server running on http://localhost:3000'));

测试
1.请求 URL:http://localhost:3000/api/data
2.请求头:x-api-key: key1
3.响应:
– 成功:

{ "data": "This is protected data." }
  • 失败(无效 Key):
{ "message": "Invalid API Key" }

Session

const express = require('express');
const session = require('express-session');
const app = express();

app.use(express.json());

// 配置 Session
app.use(
  session({
    secret: 'your_secret_key', // 用于加密 Session ID 的密钥
    resave: false,             // 是否强制保存会话
    saveUninitialized: false,  // 是否为未初始化的会话存储 Session
    cookie: { httpOnly: true, maxAge: 3600000 }, // 1小时过期
  })
);

// 模拟用户数据库
const users = [{ id: 1, username: 'user1', password: 'password1' }];

// 登录路由:创建 Session
app.post('/login', (req, res) => {
  const { username, password } = req.body;

  const user = users.find(u => u.username === username && u.password === password);
  if (!user) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  // 将用户 ID 存储到 Session
  req.session.userId = user.id;
  res.json({ message: 'Login successful' });
});

// 受保护路由:验证 Session
app.get('/protected', (req, res) => {
  if (!req.session.userId) {
    return res.status(401).json({ message: 'Unauthorized' });
  }

  res.json({ message: 'Welcome to the protected route' });
});

// 启动服务
app.listen(3000, () => console.log('Server running on http://localhost:3000'));

测试
1.登录:

  • 请求 URL:http://localhost:3000/login

  • 请求体:

{ "username": "user1", "password": "password1" }
  • 响应:
{ "message": "Login successful" }
  • 返回的 Cookie 将包含 Session ID。

2.访问受保护资源:

  • 请求 URL:http://localhost:3000/protected

  • 请求头:携带之前的 Cookie。

  • 响应:

{ "message": "Welcome to the protected route" }

JWT

const express = require('express');
const jwt = require('jsonwebtoken');
const app = express();

app.use(express.json());

// 模拟用户数据库
const users = [{ id: 1, username: 'user1', password: 'password1' }];

// 登录路由:生成 JWT
app.post('/login', (req, res) => {
  const { username, password } = req.body;

  const user = users.find(u => u.username === username && u.password === password);
  if (!user) {
    return res.status(401).json({ message: 'Invalid credentials' });
  }

  // 生成 JWT
  const token = jwt.sign(
    { id: user.id, username: user.username }, // Payload
    'your_secret_key',                        // Secret Key
    { expiresIn: '1h' }                       // Token 有效期
  );

  res.json({ token });
});

// 受保护路由:验证 JWT
app.get('/protected', (req, res) => {
  const authHeader = req.headers['authorization'];
  const token = authHeader && authHeader.split(' ')[1]; // Bearer <Token>

  if (!token) {
    return res.status(401).json({ message: 'Unauthorized' });
  }

  // 验证 Token
  jwt.verify(token, 'your_secret_key', (err, user) => {
    if (err) {
      return res.status(403).json({ message: 'Invalid or expired token' });
    }

    res.json({ message: 'Welcome to the protected route', user });
  });
});

// 启动服务
app.listen(3000, () => console.log('Server running on http://localhost:3000'));

测试
1.登录:

  • 请求 URL:http://localhost:3000/login

  • 请求体:

{ "username": "user1", "password": "password1" }
  • 响应:
{ "token": "<JWT_TOKEN>" }

2.访问受保护资源:

  • 请求 URL:http://localhost:3000/protected

  • 请求头:Authorization: Bearer <JWT_TOKEN>

  • 响应:

{ "message": "Welcome to the protected route", "user": { "id": 1, "username": "user1" } }

Python + FastAPI

API Key

from fastapi import FastAPI, Header, HTTPException

app = FastAPI()

# 模拟存储的 API Key 数据库
API_KEYS = {"key1", "key2", "key3"}

# 校验 API Key 的依赖
def verify_api_key(x_api_key: str = Header(None)):
    if x_api_key not in API_KEYS:
        raise HTTPException(status_code=403, detail="Invalid API Key")

# 示例受保护路由
@app.get("/protected", dependencies=[verify_api_key])
async def protected_data():
    return {"message": "This is protected data."}

# 示例公开路由
@app.get("/")
async def public_data():
    return {"message": "Welcome to the public route."}

测试
1.访问受保护路由:

  • URL:http://127.0.0.1:8000/protected

  • 请求头:x-api-key: key1

  • 响应:

{ "message": "This is protected data." }

2.如果 API Key 无效:

  • 返回:
{ "detail": "Invalid API Key" }

Session

from fastapi import FastAPI, Depends, HTTPException
from fastapi.middleware.cors import CORSMiddleware
from starlette.requests import Request
from starlette.responses import JSONResponse
from fastapi.session import FastAPISessionMiddleware
from fastapi_session import MemorySessionBackend

app = FastAPI()

# 设置 Session 后端(内存存储)
app.add_middleware(
    FastAPISessionMiddleware,
    backend=MemorySessionBackend(),
    secret_key="your_secret_key",
)

# 模拟用户数据库
USERS = {"user1": "password1"}

# 登录接口:创建 Session
@app.post("/login")
async def login(request: Request, username: str, password: str):
    if USERS.get(username) != password:
        raise HTTPException(status_code=401, detail="Invalid credentials")

    # 创建 Session 数据
    session = await request.session()
    session["user"] = username

    return {"message": "Login successful"}

# 受保护路由:验证 Session
@app.get("/protected")
async def protected_route(request: Request):
    session = await request.session()
    if "user" not in session:
        raise HTTPException(status_code=401, detail="Unauthorized")

    return {"message": f"Welcome {session['user']} to the protected route"}

测试
1.登录:

  • URL:http://127.0.0.1:8000/login

  • 请求体:

{ "username": "user1", "password": "password1" }
  • 响应:
{ "message": "Login successful" }
  • 返回的响应 Cookie 包含 Session ID。

2.访问受保护资源:

  • URL:http://127.0.0.1:8000/protected

  • 请求头:自动携带之前返回的 Cookie。

  • 响应:

{ "message": "Welcome user1 to the protected route" }

3.未登录访问受保护资源:
– 返回:

{ "detail": "Unauthorized" }

JWT

from fastapi import FastAPI, Depends, HTTPException
from fastapi.security import OAuth2PasswordBearer, OAuth2PasswordRequestForm
from jose import JWTError, jwt
from datetime import datetime, timedelta

app = FastAPI()

# 密钥配置
SECRET_KEY = "your_secret_key"
ALGORITHM = "HS256"
ACCESS_TOKEN_EXPIRE_MINUTES = 30

# 模拟用户数据库
USERS = {"user1": {"username": "user1", "password": "password1"}}

# 用于 OAuth2 的 Token URL
oauth2_scheme = OAuth2PasswordBearer(tokenUrl="login")

# 生成 JWT Token
def create_access_token(data: dict, expires_delta: timedelta = None):
    to_encode = data.copy()
    if expires_delta:
        expire = datetime.utcnow() + expires_delta
    else:
        expire = datetime.utcnow() + timedelta(minutes=15)
    to_encode.update({"exp": expire})
    encoded_jwt = jwt.encode(to_encode, SECRET_KEY, algorithm=ALGORITHM)
    return encoded_jwt

# 登录接口:生成 JWT
@app.post("/login")
async def login(form_data: OAuth2PasswordRequestForm = Depends()):
    user = USERS.get(form_data.username)
    if not user or user["password"] != form_data.password:
        raise HTTPException(status_code=401, detail="Invalid credentials")

    access_token = create_access_token(data={"sub": form_data.username})
    return {"access_token": access_token, "token_type": "bearer"}

# 验证 JWT 的依赖
async def get_current_user(token: str = Depends(oauth2_scheme)):
    try:
        payload = jwt.decode(token, SECRET_KEY, algorithms=[ALGORITHM])
        username: str = payload.get("sub")
        if username is None:
            raise HTTPException(status_code=401, detail="Invalid token")
    except JWTError:
        raise HTTPException(status_code=401, detail="Invalid token")
    return username

# 受保护路由
@app.get("/protected")
async def protected_route(current_user: str = Depends(get_current_user)):
    return {"message": f"Welcome {current_user} to the protected route"}

测试
1.登录:

  • URL:http://127.0.0.1:8000/login

  • 请求体:username=user1&password=password1

  • 响应:

{ "access_token": "<JWT_TOKEN>", "token_type": "bearer" }

2.访问受保护资源:

  • URL:http://127.0.0.1:8000/protected

  • 请求头:Authorization: Bearer <JWT_TOKEN>

  • 响应:

{ "message": "Welcome user1 to the protected route" }

3.Token 无效或过期:

  • 返回:
{ "detail": "Invalid token" }

发表回复

您的邮箱地址不会被公开。 必填项已用 * 标注