Bộ Phân Tích & Giải Mã URL
Phân tích bất kỳ URL nào thành các thành phần của nó · giao thức, host, port, đường dẫn, tham số truy vấn và fragment.
Cấu trúc của một URL
Một URL được phân tích thành sáu phần khái niệm: scheme://userinfo@host:port/path?query#fragment. scheme báo cho client biết giao thức nào dùng (https, http, ftp, mailto, file, data) và là phần duy nhất luôn có. Phần userinfo (username:password@) hiếm trong dùng hiện đại; trình duyệt thường gỡ nó khỏi URL hiển thị vì nó là vector phishing từ thập niên 1990. host là vị trí mạng, một tên miền đã đăng ký, một địa chỉ IP (IPv4 dạng bốn-số-có-dấu-chấm hoặc IPv6 trong dấu ngoặc vuông) hoặc một tên đặc biệt như localhost. port là cổng TCP/UDP (mặc định 80 cho HTTP, 443 cho HTTPS, vân vân); nếu bỏ qua, mặc định của scheme áp dụng. path là phân cấp ngăn cách bằng dấu sổ chéo nhận diện tài nguyên trong host. query string (mọi thứ sau ?) mang các cặp khóa-giá trị ngăn cách bằng &, dùng cho lọc, phân trang, theo dõi, gửi biểu mẫu. fragment (mọi thứ sau #) là phần duy nhất của URL không bao giờ được gửi đến server, nó được trình duyệt xử lý hoàn toàn ở phía client để cuộn đến phần cụ thể hoặc, trong single-page app, để chỉ ra trạng thái route.
Định dạng query string tự nó có ngã rẽ: ?key=value&key2=value2 truyền thống với giá trị percent-encoded theo RFC 3986, vs quy ước form-encoded cũ application/x-www-form-urlencoded nơi + nghĩa là khoảng trắng (ban đầu cho gửi biểu mẫu HTML). Phần lớn parser xử lý cả hai, nhưng chuyển đổi không đối xứng: %20 luôn giải mã thành khoảng trắng; + chỉ giải mã thành khoảng trắng bên trong query string, không bao giờ trong path. Đó là một trong các bug parse URL phổ biến nhất trong production.
Lược sử ngắn của URL
URL, ban đầu là "Universal Document Identifier", rồi "Universal Resource Locator", do Tim Berners-Lee phát minh giữa memo "Information Management: A Proposal" tháng 3 năm 1989 tại CERN (cái mà sếp ông Mike Sendall ghi chú "Vague but exciting") và các trang web công khai xem được đầu tiên tháng 8 năm 1991. URL kinh điển đầu tiên là http://info.cern.ch/hypertext/WWW/TheProject.html, công bố ngày 6 tháng 8 năm 1991. Thảo luận IETF năm 1992 đổi tên UDI thành URL để tránh trận chiến từ vựng. RFC 1738 ("Uniform Resource Locators"), do Berners-Lee, Masinter và McCahill soạn, được công bố vào tháng 12 năm 1994 như cú pháp URL chính thức đầu tiên. RFC 2396 theo sau vào tháng 8 năm 1998, tổng quát hóa URL trong khái niệm rộng hơn URI. Đặc tả kinh điển hiện hành là RFC 3986 ("URI Generic Syntax"), công bố tháng 1 năm 2005, do Berners-Lee, Roy Fielding và Larry Masinter biên tập, một Internet Standard STD 66, mức trưởng thành cao nhất của IETF. RFC 3986 là cái mọi parser URL nhắm về danh nghĩa. Trong thực tế, các trình duyệt hiện đại lệch khỏi RFC 3986 ở nhiều trường hợp đặc biệt, lý do WHATWG bảo trì URL Living Standard riêng tại url.spec.whatwg.org; đặc tả WHATWG nêu rõ mục tiêu làm RFC 3986 và RFC 3987 trở nên lỗi thời theo thời gian, và hai bên vẫn lệch nhau ở các điểm như xử lý khoảng trắng dẫn đầu, các tập percent-encoding và chuẩn hóa Unicode.
Ký tự không-dành-riêng, dành riêng và percent-encoded
RFC 3986 §2.3 định nghĩa ký tự không-dành-riêng, các ký tự duy nhất được đảm bảo an toàn trong mọi thành phần URI mà không cần percent-encoding: A-Z, a-z, 0-9, gạch nối (-), dấu chấm (.), gạch dưới (_) và dấu ngã (~). 66 ký tự tổng cộng. Mọi cái khác hoặc là ký tự dành riêng có ý nghĩa cấu trúc trong một thành phần nào đó, gen-delims (:/?#[]@) và sub-delims (!$&'()*+,;=), hoặc là "khác" và phải được percent-encode nếu xuất hiện trong URI. Percent-encoding (RFC 3986 §2.1) lấy chuỗi byte của một ký tự (ở UTF-8 trừ khi scheme nói khác) và thay mỗi byte bằng %HH với HH là giá trị hex hai chữ số của byte. Vậy một é mã hóa UTF-8 (byte 0xC3 0xA9) trở thành %C3%A9; từ tiếng Nga привет trở thành %D0%BF%D1%80%D0%B8%D0%B2%D0%B5%D1%82, hai byte mỗi ký tự, sáu bộ ba %XX và 36 ký tự percent-encoded của URL cho sáu chữ Cyrillic.
Trình duyệt hiển thị path percent-encoded theo hai cách: phần lớn trình duyệt hiện đại (Chrome, Firefox, Safari) giải mã và render glyph Unicode gốc trong thanh địa chỉ khi mã hóa là UTF-8 hợp lệ, nhưng sao chép dạng percent-encoded nguyên văn khi người dùng sao URL. Trình duyệt cũ và nhiều log web chỉ hiển thị dạng percent-encoded, đó là lý do "URL Unicode đẹp" có thể đánh lừa: chúng trông đẹp trong thanh địa chỉ và xấu trong bất kỳ văn bản nào chúng được chia sẻ. RFC 3987 ("Internationalized Resource Identifiers", IRI), công bố tháng 1 năm 2005, chính thức hóa URL Unicode ở dạng không mã hóa; Punycode (RFC 3492, tháng 3 năm 2003) định nghĩa cách tên miền quốc tế hóa được mã hóa thành ASCII cho DNS, từng nhãn một, nhãn cấp cao tiếng Trung 中国 trở thành xn--fiqs8s, do đó example.中国 phân giải ở mức DNS thành example.xn--fiqs8s. Minh chứng kinh điển là URL IRI Wikipedia: https://ja.wikipedia.org/wiki/東京 hoạt động trong mọi trình duyệt hiện đại dù request bên dưới mã hóa path là /wiki/%E6%9D%B1%E4%BA%AC.
WHATWG URL Standard, điều trình duyệt thực sự làm
RFC 3986 của IETF nói một thứ; trình duyệt làm hơi khác. WHATWG (cơ quan chuẩn hóa của các nhà phát hành trình duyệt) bảo trì URL Living Standard riêng tại url.spec.whatwg.org mô tả máy trạng thái thuật toán mà trình duyệt thực sự thực thi, gồm xử lý khoảng trắng dẫn đầu, ký tự điều khiển, các tập percent-encoding thay đổi theo thành phần, và chuẩn hóa Unicode. Đặc tả WHATWG là cái constructor URL của trình duyệt (new URL(input)) triển khai, và là cái Node.js, Deno và Bun đều hội tụ về cho việc parse URL tích hợp. Parser Ada, viết bằng C++ bởi Yagiz Nizipli, Daniel Lemire và những người khác, đã trở thành parser tuân theo WHATWG cấp năng lượng cho việc parse URL của Node.js từ Node.js 18.16.0 (tháng 4 năm 2023), thay đường url.parse() cũ; nó đo được nhanh hơn mọi triển khai trước đó và là chuẩn de facto cho parse URL hiệu năng cao trong năm 2026. RFC 3986 và đặc tả WHATWG vẫn chưa hoàn toàn hòa giải, và lệch lịch sử vẫn xuất hiện trong các đường mã legacy và phiên bản runtime cũ hơn.
Query string và API URLSearchParams
Query string về kỹ thuật chỉ là "mọi thứ sau ? và trước #", đặc tả không thực sự định nghĩa cách diễn giải nó. Quy ước ?key=value&key=value với phân cách & là quy ước, không phải yêu cầu. Trong thực tế, hai định dạng query string thống trị: application/x-www-form-urlencoded (định dạng gửi biểu mẫu HTML mặc định, nơi + nghĩa là khoảng trắng) và quy ước query URI chuẩn (nơi khoảng trắng luôn là %20). API URLSearchParams của trình duyệt (phần của WHATWG URL Living Standard, baseline trong mọi trình duyệt hiện đại từ khoảng 2017) xử lý cả hai định dạng trong suốt cho parse và phát ra biến thể form-encoded khi stringify. Khóa lặp hợp pháp: ?tag=red&tag=blue&tag=green hợp lệ, và URLSearchParams.getAll('tag') trả về ['red', 'blue', 'green']. Các framework web khác xử lý trường hợp khóa lặp khác nhau, Rails và Express thu thập khóa lặp vào mảng, trong khi PHP ghi đè giá trị trước bằng giá trị sau trừ khi khóa dùng quy ước dấu ngoặc name[], đây là nguồn liên tục của bug cross-framework trong tích hợp API.
Cạm bẫy parse URL thường gặp
- Mã hóa kép. Mã hóa một URL đã mã hóa tạo ra
%2520(chính%được mã hóa thành%25). Điều này xuất hiện khi URL đi qua nhiều lớp (frontend → backend → analytics) và mỗi lớp "nhiệt tình" mã hóa thêm một lần. Cách giải là giải mã hết xuống đáy trước khi mã hóa lại đúng một lần. - + vs %20 trong query string.
+nghĩa là khoảng trắng trong query string form-encoded nhưng nghĩa là+chữ trong path hoặc fragment. Trộn quy ước tạo bug khó debug nơi "John+Doe" thành "John Doe" trong query nhưng vẫn là "John+Doe" trong path. - Phân biệt hoa thường. Host không phân biệt hoa thường (
EXAMPLE.comvàexample.comlà cùng host); path phân biệt hoa thường trên phần lớn server (Linux/Unix) nhưng không phân biệt trên cái khác (Windows IIS theo mặc định). Nghĩa là cùng URL có thể phân giải đến nội dung khác tùy server. - IPv6 trong URL. Địa chỉ IPv6 chứa dấu hai chấm xung đột với phân cách host:port. Cách giải là bao địa chỉ IPv6 trong dấu ngoặc vuông:
http://[2001:db8::1]:8080/path. Nhiều parser URL trong lịch sử đã thất bại với điều này; trình duyệt hiện đại và parser WHATWG xử lý đúng. - Token OAuth trong fragment. Luồng OAuth 2.0 implicit grant trả access token trong fragment URL (
#access_token=...) để chúng không xuất hiện trong log server. Hướng dẫn OAuth hiện đại không khuyến nghị luồng này, ưu tiên authorisation code với PKCE, nhưng hệ thống legacy vẫn phát token trong fragment. - Không đồng nhất round-trip. Parse một URL và stringify lại không luôn tạo ra chuỗi gốc, parser chuẩn hóa (giải mã ký tự không-dành-riêng percent-encoded, đưa host về chữ thường, sắp xếp tham số query trong một số triển khai). Đừng giả định
parse(url).toString() === url.
Trường hợp dùng phổ biến
- Debug request API. Một endpoint REST có query string dài khó đọc; parser hiển thị mỗi tham số trên dòng riêng.
- Soi callback OAuth. URL luồng xác thực mang state, code, scope và access token đã mã hóa cần được giải mã để debug mà không phơi chúng cho server.
- Theo dõi chuỗi chuyển hướng. Khi một URL chuyển hướng qua nhiều URL trung gian, parse mỗi cái giúp theo chuỗi và xác định nơi chuyển hướng vỡ.
- Audit thẻ UTM. URL analytics (
?utm_source=...&utm_medium=...&utm_campaign=...) dễ đọc hơn theo từng tham số so với một bức tường query string. - Audit bảo mật. Tìm khuôn mẫu SQL injection hoặc chuỗi path traversal trong tham số URL; parse phơi mỗi giá trị riêng để xem xét.
- Parse body form-encoded. Cùng định dạng dùng trong query string URL được dùng trong body POST
application/x-www-form-urlencoded. - Soi deep link. Deep link app di động và route app web mã hóa trạng thái phức tạp trong path hoặc query; parse làm cấu trúc hiện rõ.
- Xem liên kết affiliate / chia sẻ. Liên kết theo dõi chiến dịch email hoặc chương trình affiliate mang URL chuyển hướng đã mã hóa và ID theo dõi có lợi từ giải mã.
Quyền riêng tư: URL mang bí mật thật
URL thường không được xem là bí mật, nhưng chúng thường mang dữ liệu mà có. URL callback OAuth bao gồm access token. URL đăng nhập magic-link bao gồm token xác thực một lần. Liên kết đặt lại mật khẩu bao gồm token reset. URL API nội bộ bao gồm tên host nội bộ và đường dẫn route tiết lộ hạ tầng. Ngay cả URL ứng dụng thông thường cũng tiết lộ hành vi người dùng qua tham số query, từ tìm kiếm, chọn lọc, ID hồ sơ, định danh phiên. Header Referer rò rỉ URL trước đến mỗi site được liên kết, được giảm nhẹ bởi header Referrer-Policy được giới thiệu là W3C Candidate Recommendation năm 2017 (mặc định trình duyệt vẫn khác nhau). URL kết thúc trong access log server, trong lịch sử trình duyệt, trong bookmark, trong log CDN, trong pipeline analytics, trong xem trước liên kết của app chat. Một parser URL phía server thấy mỗi URL được dán vào; một parser chỉ phía trình duyệt thì không. Cho URL API nội bộ, callback OAuth có token, liên kết đặt lại mật khẩu, hoặc bất kỳ URL nào bạn không muốn bị sao vào ổ cứng của người lạ, parser chỉ trình duyệt là kiến trúc đúng. Hãy kiểm chứng trong tab Network của DevTools khi bạn parse, hoặc đặt trang offline (chế độ máy bay) sau khi đã tải xong.
Câu hỏi thường gặp
URL chứa những phần nào?
Sáu phần khái niệm: scheme (https, http, ftp, mailto), userinfo (hiếm trong dùng hiện đại, hầu hết bị trình duyệt gỡ như giảm nhẹ phishing), host (miền hoặc IP), port (mặc định 80 cho HTTP, 443 cho HTTPS), path (phân cấp ngăn cách bằng dấu sổ), query (cặp khóa-giá trị sau ?) và fragment (sau #, không bao giờ gửi đến server). Ngữ pháp đầy đủ trong RFC 3986 §3 (tháng 1 năm 2005, STD 66) và WHATWG URL Living Standard.
Làm sao giải mã ký tự URL-encoded?
Percent-encoding thay ký tự nguy hiểm bằng % theo sau là mã hex của byte: khoảng trắng là %20, hai chấm là %3A, dấu sổ là %2F, dấu & là %26, dấu @ là %40. Ký tự đa byte UTF-8 được mã hóa từng byte, nên é trở thành %C3%A9 (hai byte). Parser tự động giải mã mọi ký tự percent-encoded trong đầu ra hiển thị. Hàm JavaScript chuẩn là encodeURIComponent() để mã hóa giá trị riêng và decodeURIComponent() để giải mã.
Fragment URL là gì?
Fragment (mọi thứ sau #) là phần duy nhất của URL được xử lý hoàn toàn ở phía client, nó không bao giờ được gửi đến server web trong request HTTP. Mục đích ban đầu: làm trình duyệt cuộn đến phần tử neo có ID đó. Cách dùng hiện đại bao gồm trạng thái route của single-page application (#/dashboard/profile), token của luồng implicit OAuth (giờ được khuyến không nên dùng, ưu tiên authorisation code với PKCE), và điều hướng theo trang của PDF (file.pdf#page=5). Vì fragment không đến server, đó là chỗ để giấu giá trị không nên xuất hiện trong log server.
Vì sao + đôi khi nghĩa là khoảng trắng và đôi khi là +?
Hai quy ước mã hóa tồn tại. application/x-www-form-urlencoded (định dạng gửi biểu mẫu HTML mặc định) mã hóa khoảng trắng thành +; percent-encoding chuẩn (theo RFC 3986) mã hóa khoảng trắng thành %20. Cả hai hợp lệ trong query string; chỉ %20 hợp lệ trong path và fragment. URLSearchParams xử lý cả hai trong suốt. Bug cross-context xảy ra khi mã dùng encodeURIComponent (mã hóa khoảng trắng thành %20) cho tham số query mà server mong form-encoded, hoặc ngược lại.
Có xử lý URL tương đối không?
Parser mong URL đầy đủ có scheme. Cho path tương đối như /api/users, hãy thêm tiền tố URL gốc (https://example.com/api/users) cho parser. Parse URL tương đối (phân giải so với URL gốc như cách trình duyệt làm cho thuộc tính href) đang trong roadmap, dạng hai-tham-số của constructor URL của WHATWG (new URL(relative, base)) xử lý điều đó và là cái mã production nên dùng.
URL của tôi có được gửi đi đâu không?
Không. Việc parse chạy hoàn toàn trong trình duyệt qua constructor URL của WHATWG, URL bạn dán không bao giờ rời khỏi thiết bị của bạn. Hãy kiểm chứng trong tab Network của DevTools khi bạn bấm Parse, hoặc đặt trang offline (chế độ máy bay) sau khi đã tải xong. An toàn cho URL callback OAuth chứa access token, liên kết đặt lại mật khẩu chứa token một lần, URL API nội bộ tiết lộ hạ tầng, hoặc bất kỳ URL nào bạn không muốn bị sao vào ổ cứng của người lạ.