一个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" }
发表回复