URL学习笔记

2025/10/15

# 基本结构

https://example.com/path/to/resource?param1=value1&param2=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

# 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
        9
      • IP绑定验证

        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
    18

    TypeScript实现

    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

# 版本控制

/api/v1/users
/api/v2/users
1
2

# 分页和过滤

/products?page=2&limit=20&category=electronics&sort=price_asc
1

# 层级关系

/users/123/orders/456/items
/companies/abc/departments/hr/employees
1
2