URL学习笔记
maiaimei 2025/10/15
# 基本结构
https://example.com/path/to/resource?param1=value1¶m2=value2#fragment
1
# 命名规范
# 1. 域名
- 使用小写字母
- 避免下划线,使用连字符分隔
- 保持简短易记
# 2. 路径设计
- 使用小写字母:
/users/profile
而非/Users/Profile
- 用连字符分隔单词:
/user-settings
而非/user_settings
- 避免文件扩展名:
/about
而非/about.html
- 使用复数形式:
/users/123
而非/user/123
# 3. RESTful API规范
GET /users # 获取用户列表
GET /users/123 # 获取特定用户
POST /users # 创建用户
PUT /users/123 # 更新用户
DELETE /users/123 # 删除用户
1
2
3
4
5
2
3
4
5
# 4. 查询参数
- 使用小写字母和下划线:
?sort_by=name&page_size=20
- 布尔值使用
true/false
:?is_active=true
- 日期使用ISO格式:
?created_date=2024-01-01
# 字符规范
# 允许的字符
- 字母:a-z, A-Z
- 数字:0-9
- 特殊字符:
- . _ ~ : / ? # [ ] @ ! $ & ' ( ) * + , ; =
# 需要编码的字符
- 空格:
%20
- 中文:UTF-8编码
- 特殊符号:按需进行百分号编码
# 安全考虑
避免敏感信息:
核心原则
- 不要在URL中直接传递敏感信息,如密码、token等
- 使用临时令牌替代敏感数据
- 实施时间限制和单次使用策略
推荐方案
方案一:JWT Token(推荐)
// 生成短期token const generateSecureToken = (payload: any): string => { return jwt.sign(payload, SECRET_KEY, { expiresIn: '15m', issuer: 'your-app' }); }; // 使用 const token = generateSecureToken({ userId, action: 'view' }); const url = `https://example.com/page?token=${token}`;
1
2
3
4
5
6
7
8
9
10
11
12方案二:临时存储 + 随机ID
// 服务端存储 const storeTemporary = (data: any): string => { const id = crypto.randomUUID(); redis.setex(id, 900, JSON.stringify(data)); // 15分钟过期 return id; }; // 使用 const tempId = storeTemporary({ userId, permissions }); const url = `https://example.com/page?ref=${tempId}`;
1
2
3
4
5
6
7
8
9
10
安全增强措施
添加签名验证
const createSignedUrl = (params: Record<string, string>): string => { const timestamp = Date.now().toString(); const nonce = crypto.randomUUID(); const payload = { ...params, timestamp, nonce }; const signature = hmacSha256(JSON.stringify(payload), SECRET_KEY); return `https://example.com/page?data=${btoa(JSON.stringify(payload))}&sig=${signature}`; };
1
2
3
4
5
6
7
8
9IP绑定验证
const generateIpBoundToken = (data: any, clientIp: string): string => { return jwt.sign({ ...data, ip: clientIp }, SECRET_KEY, { expiresIn: '10m' }); };
1
2
3
实施检查清单
✅ 必须做的
使用HTTPS协议
设置token过期时间(≤30分钟)
实施单次使用策略
一、Redis黑名单方式(推荐)
// 生成单次使用token const generateOneTimeToken = (payload: any): string => { const jti = crypto.randomUUID(); // JWT ID const token = jwt.sign({ ...payload, jti }, SECRET_KEY, { expiresIn: '15m' }); return token; }; // 验证并标记已使用 const verifyOneTimeToken = async (token: string): Promise<any> => { const decoded = jwt.verify(token, SECRET_KEY) as any; const key = `used_token:${decoded.jti}`; // 检查是否已使用 const isUsed = await redis.get(key); if (isUsed) { throw new Error('Token already used'); } // 标记为已使用 await redis.setex(key, decoded.exp - Math.floor(Date.now() / 1000), 'used'); return decoded; };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23二、数据库方式
// 生成token并存储 const createOneTimeToken = async (payload: any): Promise<string> => { const tokenId = crypto.randomUUID(); const expiresAt = new Date(Date.now() + 15 * 60 * 1000); await db.tokens.create({ id: tokenId, payload: JSON.stringify(payload), used: false, expiresAt }); return jwt.sign({ tokenId }, SECRET_KEY); }; // 验证并消费token const consumeOneTimeToken = async (token: string): Promise<any> => { const { tokenId } = jwt.verify(token, SECRET_KEY) as any; const tokenRecord = await db.tokens.findUnique({ where: { id: tokenId, used: false, expiresAt: { gt: new Date() } } }); if (!tokenRecord) { throw new Error('Invalid or used token'); } // 标记为已使用 await db.tokens.update({ where: { id: tokenId }, data: { used: true, usedAt: new Date() } }); return JSON.parse(tokenRecord.payload); };
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三、内存缓存方式(简单场景)
const usedTokens = new Set<string>(); const generateNonce = (): string => crypto.randomUUID(); const verifyAndConsumeNonce = (nonce: string): boolean => { if (usedTokens.has(nonce)) { return false; // 已使用 } usedTokens.add(nonce); // 定期清理过期nonce setTimeout(() => usedTokens.delete(nonce), 15 * 60 * 1000); return true; };
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16最佳实践
- 使用Redis的原子操作避免竞态条件
- 设置合理的过期时间(5-15分钟)
- 记录使用日志便于审计
- 定期清理过期的token记录
- 添加IP绑定增强安全性
这种方式确保每个URL只能被访问一次,有效防止重放攻击。
添加请求频率限制
记录访问日志
❌ 禁止做的
- 在URL中放置密码、API密钥
- 使用可预测的ID
- 忽略token过期验证
- 在客户端存储敏感密钥
参数验证:对所有参数进行验证和过滤
长度限制:URL总长度不超过2048字符
编码处理:正确处理特殊字符的URL编码
Java实现
import java.net.URLEncoder; import java.net.URLDecoder; import java.nio.charset.StandardCharsets; public class UrlUtils { public static String encode(String data) { return URLEncoder.encode(data, StandardCharsets.UTF_8); } public static String decode(String encodedData) { return URLDecoder.decode(encodedData, StandardCharsets.UTF_8); } } // 使用示例 String encoded = UrlUtils.encode("hello world & special chars"); String decoded = UrlUtils.decode(encoded);
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18TypeScript实现
export const urlEncode = (data: string): string => encodeURIComponent(data); export const urlDecode = (encodedData: string): string => decodeURIComponent(encodedData); // 使用示例 const encoded = urlEncode("hello world & special chars"); const decoded = urlDecode(encoded);
1
2
3
4
5
6
7
8
9
# 最佳实践
# 语义化路径
✅ /products/electronics/smartphones
✅ /blog/2024/01/article-title
✅ /api/v1/users/profile
❌ /prod/elec/phone
❌ /blog/art123
❌ /getUserProfile
1
2
3
4
5
6
7
2
3
4
5
6
7
# 版本控制
/api/v1/users
/api/v2/users
1
2
2
# 分页和过滤
/products?page=2&limit=20&category=electronics&sort=price_asc
1
# 层级关系
/users/123/orders/456/items
/companies/abc/departments/hr/employees
1
2
2