<?xml version="1.0" encoding="UTF-8"?><rss version="2.0"
	xmlns:content="http://purl.org/rss/1.0/modules/content/"
	xmlns:wfw="http://wellformedweb.org/CommentAPI/"
	xmlns:dc="http://purl.org/dc/elements/1.1/"
	xmlns:atom="http://www.w3.org/2005/Atom"
	xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
	xmlns:slash="http://purl.org/rss/1.0/modules/slash/"
	xmlns:series="https://publishpress.com/"
	>

<channel>
	<title>Nguyen Luan, Author at Tomoshare</title>
	<atom:link href="https://blog.tomosia.com.vn/author/nguyen-van-luan/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.tomosia.com.vn/author/nguyen-van-luan/</link>
	<description>Kênh chia sẻ kiến thức Tomosia Việt Nam</description>
	<lastBuildDate>Fri, 30 Jan 2026 09:56:11 +0000</lastBuildDate>
	<language>en-US</language>
	<sy:updatePeriod>
	hourly	</sy:updatePeriod>
	<sy:updateFrequency>
	1	</sy:updateFrequency>
	<generator>https://wordpress.org/?v=6.9.4</generator>

<image>
	<url>https://blog.tomosia.com.vn/wp-content/uploads/2023/09/cropped-icon-32x32.png</url>
	<title>Nguyen Luan, Author at Tomoshare</title>
	<link>https://blog.tomosia.com.vn/author/nguyen-van-luan/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>IDOR &#8211; Lỗ hổng &#8220;ngây thơ&#8221; nhưng chí mạng trong kiến trúc phần mềm?</title>
		<link>https://blog.tomosia.com.vn/idor-lo-hong-ngay-tho-nhung-chi-mang-trong-kien-truc-phan-mem/</link>
					<comments>https://blog.tomosia.com.vn/idor-lo-hong-ngay-tho-nhung-chi-mang-trong-kien-truc-phan-mem/#respond</comments>
		
		<dc:creator><![CDATA[Nguyen Luan]]></dc:creator>
		<pubDate>Fri, 30 Jan 2026 09:56:08 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[Web Server]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[Python]]></category>
		<guid isPermaLink="false">https://blog.tomosia.com.vn/?p=4177</guid>

					<description><![CDATA[<p>Hãy tưởng tượng bạn check-in vào một khách sạn 5 sao sang trọng. Hệ thống an ninh tối&#8230;</p>
<p>The post <a href="https://blog.tomosia.com.vn/idor-lo-hong-ngay-tho-nhung-chi-mang-trong-kien-truc-phan-mem/">IDOR &#8211; Lỗ hổng &#8220;ngây thơ&#8221; nhưng chí mạng trong kiến trúc phần mềm?</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Hãy tưởng tượng bạn check-in vào một khách sạn 5 sao sang trọng. Hệ thống an ninh tối tân, camera khắp nơi, thẻ từ thang máy hiện đại. Lễ tân đưa cho bạn chìa khóa phòng 101. Bạn lên phòng, mở cửa và nghỉ ngơi. Nhưng sau đó, vì tò mò, bạn thử cạo sửa con số trên chìa khóa thành 102 và cắm vào ổ khóa phòng bên cạnh. <em>Cạch!</em> Cửa mở toang.</p>



<p>Trong thế giới thực, điều này thật nực cười. Nhưng trong thế giới lập trình Web và API, tình huống này xảy ra thường xuyên đến mức nó trở thành &#8220;cơn đau đầu&#8221; kinh niên của giới bảo mật. Nó được gọi là <strong>IDOR</strong> (Insecure Direct Object Reference) hay <strong>BOLA</strong> (Broken Object Level Authorization).</p>



<p>Nếu bạn là một Backend Developer, và bạn tin rằng chỉ cần có <mark style="background-color:#e9ecef" class="has-inline-color"><code>Authentication</code> </mark>(Đăng nhập) là hệ thống đã an toàn, thì bài viết này sẽ thay đổi tư duy lập trình của bạn. Chúng ta sẽ không chỉ nói về bề nổi, mà sẽ đi sâu vào bản chất kiến trúc của lỗi này.</p>



<h3 id="1-ban-chat-ky-thuat-cua-idor" class="wp-block-heading">1. Bản chất kỹ thuật của IDOR</h3>



<p><strong>IDOR</strong> (Insecure Direct Object Reference) không chỉ là một lỗi code, nó là một <strong>lỗi về thiết kế Logic (Business Logic Flaw)</strong>.</p>



<p>Trong danh sách <strong>OWASP API Security Top 10</strong>, IDOR chính là bản chất của lỗ hổng số 1: <strong><a href="https://owasp.org/API-Security/editions/2023/en/0xa1-broken-object-level-authorization/">API1: 2023 Broken Object Level Authorization (BOLA)</a></strong>.</p>



<p>Hiểu sâu hơn, IDOR xảy ra khi 3 điều kiện sau hội tụ:</p>



<ol class="wp-block-list">
<li><strong>Direct Reference:</strong> Ứng dụng để lộ mã định danh nội bộ (Internal Identifier) của đối tượng (như Database Primary Key, Filename) trực tiếp ra ngoài URL hoặc Body request.</li>



<li><strong>User Input:</strong> Ứng dụng tin tưởng và sử dụng trực tiếp đầu vào này để truy vấn dữ liệu.</li>



<li><strong>Missing Authorization:</strong> Ứng dụng bỏ qua bước kiểm tra quyền truy cập <em>tại thời điểm</em> truy xuất đối tượng.</li>
</ol>



<p><strong>Sai lầm phổ biến:</strong> Nhiều Dev nhầm lẫn giữa <em>Authentication</em> (Bạn là ai?) và <em>Authorization</em> (Bạn được làm gì?). IDOR xảy ra khi bạn đã qua cửa Authentication, nhưng hệ thống quên kiểm tra Authorization ở mức độ đối tượng (Object-level).</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="559" src="https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_eohbxheohbxheohb-1-1024x559.png" alt="" class="wp-image-4187" srcset="https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_eohbxheohbxheohb-1-1024x559.png 1024w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_eohbxheohbxheohb-1-300x164.png 300w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_eohbxheohbxheohb-1-768x419.png 768w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_eohbxheohbxheohb-1-380x207.png 380w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_eohbxheohbxheohb-1-800x436.png 800w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_eohbxheohbxheohb-1-1160x633.png 1160w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_eohbxheohbxheohb-1.png 1408w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h3 id="2-giai-phau-co-che-tan-cong" class="wp-block-heading">2. Giải phẫu cơ chế tấn công</h3>



<p>Hãy mổ xẻ một HTTP Request để thấy IDOR có thể lẩn trốn ở đâu. Không chỉ ở URL Parameter, nó có thể nằm ở Header hoặc Body.</p>



<p><strong>Kịch bản:</strong> Ứng dụng Chat cho phép người dùng tải lại lịch sử chat.</p>



<p><strong>Request bình thường:</strong></p>



<pre class="wp-block-code"><code>GET /api/v1/messages/history HTTP/1.1
Host: example.com
Cookie: session_id=abc123xyz...
X-User-ID: 1005  &lt;-- IDOR tiềm ẩn ở Header</code></pre>



<ol class="wp-block-list">
<li><strong>Hacker quan sát:</strong> Thấy Header <code><mark style="background-color:#e9ecef" class="has-inline-color">X-User-ID: 1005</mark></code> được gửi kèm mỗi request.</li>



<li><strong>Thử nghiệm:</strong> Hacker dùng Postman hoặc Burp Suite để chặn request, sửa <code><mark style="background-color:#e9ecef" class="has-inline-color">X-User-ID</mark></code> thành <code>1006</code>.</li>



<li><strong>Kết quả:</strong> Server, thay vì lấy ID từ session an toàn (Server-side), lại tin tưởng lấy ID từ Header (Client-side) để query database <code><mark style="background-color:#e9ecef" class="has-inline-color">SELECT * FROM messages WHERE user_id = 1006</mark></code>.</li>



<li><strong>Hậu quả:</strong> Hacker đọc được toàn bộ tin nhắn của người dùng 1006.</li>
</ol>



<p><em><strong>IDOR có thể xuất hiện ở:</strong></em></p>



<ul class="wp-block-list">
<li><strong>URL Path:</strong> <code><mark style="background-color:#e9ecef" class="has-inline-color">/users/100/profile</mark></code></li>



<li><strong>Query Params:</strong> <code><mark style="background-color:#e9ecef" class="has-inline-color">/invoices?id=100</mark></code></li>



<li><strong>Body (JSON/XML):</strong> <mark style="background-color:#e9ecef" class="has-inline-color"><code>POST /api/change-password</code> với body <code>{"user_id": 100, "new_pass": "..."}</code></mark></li>
</ul>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="572" src="https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_t2k171t2k171t2k1-1-1024x572.png" alt="" class="wp-image-4190" srcset="https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_t2k171t2k171t2k1-1-1024x572.png 1024w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_t2k171t2k171t2k1-1-300x167.png 300w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_t2k171t2k171t2k1-1-768x429.png 768w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_t2k171t2k171t2k1-1-380x212.png 380w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_t2k171t2k171t2k1-1-800x447.png 800w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_t2k171t2k171t2k1-1-1160x647.png 1160w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_t2k171t2k171t2k1-1.png 1376w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h3 id="3-tai-sao-idor-lai-la-sat-thu-tham-lang" class="wp-block-heading">3. Tại sao IDOR lại là &#8220;Sát thủ thầm lặng&#8221;?</h3>



<p>IDOR nguy hiểm vì tính &#8220;hợp lệ&#8221; về mặt cú pháp của nó.</p>



<ul class="wp-block-list">
<li><strong>Vượt mặt WAF (Web Application Firewall):</strong> Các tường lửa thường chặn các ký tự lạ như <code><mark style="background-color:#e9ecef" class="has-inline-color">' OR 1=1 --</mark></code> (SQL Injection) hay <code><mark style="background-color:#e9ecef" class="has-inline-color">&lt;script&gt;</mark></code> (XSS). Nhưng với IDOR, payload chỉ là một con số: <code>id=1006</code>. Đối với WAF, đây là một con số hoàn toàn sạch và hợp lệ.</li>



<li><strong>Khó phát hiện bằng Automation Tool:</strong> Các máy quét (Scanner) không hiểu ngữ cảnh nghiệp vụ. Nếu máy quét gửi <code><mark style="background-color:#e9ecef" class="has-inline-color">id=1006</mark></code> và nhận về <code>200 OK</code>, nó đánh dấu là &#8220;Thành công&#8221;. Nó không biết rằng dữ liệu trả về đó <em>không thuộc về</em> người đang gửi request. Chỉ có con người (Pentesters) hoặc Unit Test được viết kỹ lưỡng mới phát hiện ra.</li>



<li><strong>Hệ quả dây chuyền:</strong> IDOR thường là bước đệm để leo thang đặc quyền (Privilege Escalation), dẫn đến việc chiếm đoạt toàn bộ hệ thống (Account Takeover).</li>
</ul>



<p>Sự nguy hiểm của IDOR nằm ở chỗ nó là một lỗ hổng logic, không phải lỗ hổng cú pháp. Các công cụ quét bảo mật tĩnh (SAST) truyền thống thường thất bại trong việc phát hiện IDOR vì chúng không thể hiểu ngữ cảnh &#8220;ai sở hữu cái gì&#8221; trong một ứng dụng cụ thể. Điều này dẫn đến một nghịch lý: trong khi các lỗ hổng kỹ thuật như <strong>SQL Injection</strong> đang giảm dần nhờ các framework hiện đại, IDOR vẫn tồn tại dai dẳng và chiếm tỷ lệ cao trong các chương trình săn lỗi nhận thưởng (Bug Bounty), đặc biệt là trong các lĩnh vực y tế, chính phủ và dịch vụ chuyên nghiệp.</p>



<h3 id="4-phan-loai-idor-chuyen-sau" class="wp-block-heading">4. Phân loại IDOR chuyên sâu.</h3>



<h4 id="4-1-leo-thang-dac-quyen-ngang-horizontal-escalation" class="wp-block-heading">4.1. Leo thang đặc quyền ngang (Horizontal Escalation)</h4>



<p>Đây là dạng phổ biến nhất. Người dùng A truy cập dữ liệu của Người dùng B có cùng vai trò.</p>



<ul class="wp-block-list">
<li><strong>Ví dụ:</strong> Xem đơn hàng, tải hóa đơn, xem ảnh riêng tư.</li>



<li><strong>Rủi ro:</strong> Rò rỉ thông tin cá nhân (PII Breach), vi phạm GDPR/CCPA.</li>
</ul>



<h4 id="4-2-leo-thang-dac-quyen-doc-vertical-escalation" class="wp-block-heading">4.2. Leo thang đặc quyền dọc (Vertical Escalation)</h4>



<p>Người dùng thường (User) tìm cách gọi các API dành riêng cho Quản trị viên (Admin/Manager).</p>



<ul class="wp-block-list">
<li><strong>Ví dụ:</strong>
<ul class="wp-block-list">
<li>Hacker tìm thấy API: <mark style="background-color:#e9ecef" class="has-inline-color"><code>POST /api/users/1001/roles</code>.</mark></li>



<li>Hacker gửi request với body: <code><mark style="background-color:#e9ecef" class="has-inline-color">{"role": "ADMIN"}</mark></code>.</li>



<li>Nếu Backend không check xem &#8220;người gọi API này có phải là Super Admin không&#8221;, Hacker lập tức trở thành Admin.</li>
</ul>
</li>
</ul>



<h4 id="4-3-idor-tren-file-tinh-static-file-idor" class="wp-block-heading">4.3. IDOR trên File tĩnh (Static File IDOR)</h4>



<p>Tên file thường dễ đoán.</p>



<ul class="wp-block-list">
<li>Ví dụ: <code><mark style="background-color:#e9ecef" class="has-inline-color">https://example.com/uploads/receipts/receipt_001.pdf.</mark></code></li>



<li>Hacker viết script lặp từ <code>001</code> đến <code>9999</code> để tải toàn bộ hóa đơn của hệ thống.</li>
</ul>



<h3 id="5-dan-chung-thuc-te-khi-ong-lon-cung-mac-sai-lam" class="wp-block-heading">5. Dẫn chứng thực tế: Khi &#8220;Ông lớn&#8221; cũng mắc sai lầm</h3>



<h4 id="5-1-mcdonalds-su-co-mchire-7-2025" class="wp-block-heading">5.1. McDonald&#8217;s &amp; sự cố McHire (7/2025)</h4>



<p>Link: <a href="https://research.cgu.edu/icdc/2025/07/01/mcdonalds-july-2025-breach/">https://research.cgu.edu/icdc/2025/07/01/mcdonalds-july-2025-breach/</a></p>



<p><strong>Bối cảnh</strong>: Hệ thống bị ảnh hưởng là&nbsp;<strong>McHire</strong>, nền tảng tuyển dụng toàn cầu của McDonald&#8217;s, được vận hành bởi đối tác công nghệ bên thứ ba là&nbsp;<strong>Paradox.ai</strong>. Paradox.ai cung cấp giải pháp chatbot AI tên là &#8220;Olivia&#8221; để tự động hóa quy trình phỏng vấn, thu thập thông tin ứng viên và lên lịch làm việc. Sự phụ thuộc vào bên thứ ba này đã mở rộng bề mặt tấn công của McDonald&#8217;s ra ngoài hạ tầng kiểm soát trực tiếp của họ.<sup></sup>&nbsp;&nbsp;&nbsp;</p>



<p><strong>Nguyên nhân chính</strong><br>Sự cố xảy ra do kết hợp 2 lỗi nghiêm trọng:</p>



<ol class="wp-block-list">
<li><strong>Xác thực yếu (Weak Authentication)</strong>
<ul class="wp-block-list">
<li>Một tài khoản admin thử nghiệm của Paradox.ai dùng mật khẩu <strong>“123456”</strong>, không có MFA.</li>



<li>Tài khoản này tồn tại từ <strong>2019</strong>, không bị vô hiệu hóa dù có quyền truy cập production.</li>
</ul>
</li>



<li><strong>Lỗ hổng IDOR (Missing Object Level Authorization)</strong>
<ul class="wp-block-list">
<li>API backend cho phép truy xuất hồ sơ ứng viên bằng <code><mark style="background-color:#e9ecef" class="has-inline-color">applicant_id</mark></code>.</li>



<li>Không kiểm tra quyền truy cập → chỉ cần đổi ID là đọc được dữ liệu của bất kỳ ứng viên nào.</li>



<li>Từ 1 tài khoản → mở rộng ra <strong>64 triệu hồ sơ toàn cầu</strong>.</li>
</ul>
</li>
</ol>



<p><strong>Tác động</strong></p>



<ul class="wp-block-list">
<li>Rò rỉ <strong>PII</strong>: tên, email, số điện thoại, địa chỉ nhà.</li>



<li>Lộ dữ liệu nhạy cảm: log chat với AI (tính cách, sở thích, thông tin cá nhân).</li>



<li>Lộ token phiên → có thể giả danh ứng viên.</li>



<li>Nguy cơ cao cho <strong>social engineering, lừa đảo, chiếm tài khoản</strong>.</li>
</ul>



<p>Sự kết hợp giữa <strong>quản lý tài khoản kém</strong> (tài khoản ma, không MFA) và <strong>thiếu kiểm soát quyền API</strong> (IDOR) tạo ra “cơn bão hoàn hảo” cho rò rỉ dữ liệu quy mô toàn cầu – một xu hướng cực kỳ đáng báo động trong năm 2025</p>



<h4 id="5-2-mozilla-firefox-accounts-lo-hong-xoa-tai-khoan-5-2025" class="wp-block-heading">5.2. Mozilla Firefox Accounts – Lỗ hổng xóa tài khoản (5/2025)</h4>



<p>Link: <a href="https://hackerone.com/reports/3154983">https://hackerone.com/reports/3154983</a></p>



<p><strong>Bản chất sự cố</strong></p>



<ul class="wp-block-list">
<li>Lỗ hổng IDOR nghiêm trọng trong API xóa tài khoản (<code><mark style="background-color:#e9ecef" class="has-inline-color">/v1/account/destroy</mark></code>).</li>



<li>Cho phép <strong>xóa vĩnh viễn tài khoản người khác</strong> mà không cần tương tác từ nạn nhân (zero-click).</li>



<li>Ảnh hưởng trực tiếp đến Integrity (toàn vẹn) và Availability (sẵn sàng) của dữ liệu.</li>
</ul>



<p><strong>Nguyên nhân kỹ thuật</strong></p>



<ul class="wp-block-list">
<li>Lỗi Session Misbinding: backend không kiểm tra phiên làm việc có khớp với tài khoản bị xóa hay không.</li>



<li>API tin vào payload JSON (email, authPW) thay vì session của người gửi request.</li>



<li>Đây là dạng IDOR ghi/xóa, nguy hiểm hơn IDOR đọc dữ liệu.</li>
</ul>



<p><strong>Kịch bản khai thác</strong></p>



<ol class="wp-block-list">
<li>Kẻ tấn công tạo tài khoản Firefox, đăng nhập bằng SSO (Google).</li>



<li>Chặn request xóa tài khoản của chính mình.</li>



<li>Đổi email trong payload thành email nạn nhân (cũng dùng SSO, chưa set mật khẩu).</li>



<li>Gửi request → hệ thống xóa tài khoản nạn nhân.</li>
</ol>



<p><strong>Tác động</strong></p>



<ul class="wp-block-list">
<li>Mất vĩnh viễn dữ liệu: mật khẩu đã lưu, lịch sử duyệt web, tab, sync data.</li>



<li>Không thể khôi phục, không cần nạn nhân click hay xác nhận.</li>



<li>Được đánh giá <mark style="background-color:rgba(0, 0, 0, 0)" class="has-inline-color has-vivid-red-color"><strong>Critical </strong></mark>trên HackerOne.</li>
</ul>



<p><strong>Bài học cốt lõi</strong>: IDOR không chỉ gây rò rỉ dữ liệu (read) mà còn có thể phá hủy dữ liệu (write/delete) nếu backend không ràng buộc chặt chẽ giữa session và đối tượng tài nguyên.</p>



<h4 id="5-3-gitlab-lo-hong-machine-learning-model-registry-2024-2025" class="wp-block-heading">5.3. GitLab – Lỗ hổng Machine Learning Model Registry (2024–2025)</h4>



<p>Link: <a href="https://hackerone.com/reports/2528293">https://hackerone.com/reports/2528293</a></p>



<p><strong>Bối cảnh</strong></p>



<ul class="wp-block-list">
<li>GitLab triển khai Machine Learning Model Registry để quản lý phiên bản mô hình AI.</li>



<li>Lỗ hổng được báo cáo từ 5/2024, vá và công bố kéo dài sang <strong>2025</strong>, cho thấy độ phức tạp của bảo mật trên nền tảng lớn.</li>



<li>Sự cố ảnh hưởng trực tiếp đến tài sản trí tuệ AI (model weights, biases).</li>
</ul>



<p><strong>Nguyên nhân kỹ thuật</strong></p>



<ul class="wp-block-list">
<li>GitLab dùng <strong>ID tịnh tiến (1,2,3,&#8230;)</strong> cho <mark style="background-color:#e9ecef" class="has-inline-color"><code>model_id</code> </mark>thay vì UUID ngẫu nhiên.</li>



<li>API tải/xem mô hình không kiểm tra quyền truy cập dự án (Missing Object Level Authorization).</li>



<li>Đây là IDOR điển hình + thiết kế API kém an toàn.</li>
</ul>



<p><strong>Cách khai thác</strong></p>



<ul class="wp-block-list">
<li>Kẻ tấn công chỉ cần tài khoản GitLab miễn phí.</li>



<li>Lặp ID từ 1 → hàng triệu để liệt kê và tải mô hình của dự án khác.</li>



<li>Không cần brute-force, không cần quyền dự án.</li>
</ul>



<p><strong>Tác động</strong></p>



<ul class="wp-block-list">
<li>Rò rỉ mô hình ML độc quyền của doanh nghiệp.</li>



<li>Nguy cơ mất bí mật thương mại, thiệt hại kinh tế lớn trong bối cảnh AI là tài sản cốt lõi.</li>



<li>Lỗ hổng có thể bị khai thác tự động, quy mô lớn (enumeration).</li>
</ul>



<p><strong>Bài học cốt lõi</strong>: Không dùng ID tịnh tiến cho API công khai. Nó biến IDOR từ lỗ hổng tiềm ẩn thành thảm họa có thể khai thác hàng loạt, đặc biệt nguy hiểm với tài sản AI.</p>



<h3 id="6-ngan-chan-idor-hieu-qua-best-practices" class="wp-block-heading">6. Ngăn chặn IDOR hiệu quả (Best Practices)</h3>



<p>Khi một tài khoản test có thể mở khóa 64 triệu hồ sơ, một email có thể xóa tài khoản người khác, và một con số tăng dần có thể làm bay cả mô hình AI, thì rõ ràng vấn đề không nằm ở “hacker quá giỏi” — mà ở chỗ chúng ta cần chặn IDOR cho đúng cách. </p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="572" src="https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_61mhmz61mhmz61mh1-1024x572.png" alt="" class="wp-image-4197" srcset="https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_61mhmz61mhmz61mh1-1024x572.png 1024w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_61mhmz61mhmz61mh1-300x167.png 300w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_61mhmz61mhmz61mh1-768x429.png 768w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_61mhmz61mhmz61mh1-380x212.png 380w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_61mhmz61mhmz61mh1-800x447.png 800w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_61mhmz61mhmz61mh1-1160x647.png 1160w, https://blog.tomosia.com.vn/wp-content/uploads/2026/01/Image_61mhmz61mhmz61mh1.png 1376w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p><em>&#8220;Đừng bao giờ tin tưởng Client&#8221;</em>. Dưới đây là chiến lược phòng thủ đa lớp <strong>(Defense in Depth)</strong>.</p>



<p><strong>Lớp 1: Kiểm soát truy cập tại tầng Data (Data-Layer Authorization)</strong></p>



<p>Đây là chốt chặn cuối cùng và quan trọng nhất. Mọi câu lệnh truy vấn liên quan đến dữ liệu người dùng <strong>phải</strong> kèm theo điều kiện về chủ sở hữu.</p>



<p><strong>❌ Code rủi ro (Spring Data JPA):</strong></p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:7.703125px;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#2b2b2b;color:#c7c7c7">Java</span><span role="button" tabindex="0" data-code="// Controller
public Order getOrder(@PathVariable Long id) {
    return orderRepo.findById(id); // Chỉ tìm theo ID đơn hàng
}" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #6A9955">// Controller</span></span>
<span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">getOrder</span><span style="color: #D4D4D4">(@</span><span style="color: #4EC9B0">PathVariable</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Long</span><span style="color: #D4D4D4"> id) {</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">orderRepo</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">findById</span><span style="color: #D4D4D4">(id); </span><span style="color: #6A9955">// Chỉ tìm theo ID đơn hàng</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre></div>



<p><strong>✅ Code an toàn (Repository Pattern):</strong></p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:15.40625px;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#2b2b2b;color:#c7c7c7">Java</span><span role="button" tabindex="0" data-code="// Repository
// Luôn gắn kèm userId vào câu query
@Query(&quot;SELECT o FROM Order o WHERE o.id = :id AND o.owner.id = :currentUserId&quot;)
Optional&lt;Order&gt; findByIdAndOwner(@Param(&quot;id&quot;) Long id, @Param(&quot;currentUserId&quot;) Long userId);

// Service Layer
public Order getOrder(Long id, User currentUser) {
    return orderRepo.findByIdAndOwner(id, currentUser.getId())
        .orElseThrow(() -&gt; new ResourceNotFoundException(&quot;Order not found or access denied&quot;));
}" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #6A9955">// Repository</span></span>
<span class="line"><span style="color: #6A9955">// Luôn gắn kèm userId vào câu query</span></span>
<span class="line"><span style="color: #D4D4D4">@</span><span style="color: #4EC9B0">Query</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;SELECT o FROM Order o WHERE o.id = :id AND o.owner.id = :currentUserId&quot;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #4EC9B0">Optional</span><span style="color: #D4D4D4">&lt;Order&gt; </span><span style="color: #DCDCAA">findByIdAndOwner</span><span style="color: #D4D4D4">(@</span><span style="color: #4EC9B0">Param</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;id&quot;</span><span style="color: #D4D4D4">) </span><span style="color: #4EC9B0">Long</span><span style="color: #D4D4D4"> id, @</span><span style="color: #4EC9B0">Param</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;currentUserId&quot;</span><span style="color: #D4D4D4">) </span><span style="color: #4EC9B0">Long</span><span style="color: #D4D4D4"> userId);</span></span>
<span class="line"></span>
<span class="line"><span style="color: #6A9955">// Service Layer</span></span>
<span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Order</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">getOrder</span><span style="color: #D4D4D4">(</span><span style="color: #4EC9B0">Long</span><span style="color: #D4D4D4"> id, </span><span style="color: #4EC9B0">User</span><span style="color: #D4D4D4"> currentUser) {</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">orderRepo</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">findByIdAndOwner</span><span style="color: #D4D4D4">(id, </span><span style="color: #9CDCFE">currentUser</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">getId</span><span style="color: #D4D4D4">())</span></span>
<span class="line"><span style="color: #D4D4D4">        .</span><span style="color: #DCDCAA">orElseThrow</span><span style="color: #D4D4D4">(() </span><span style="color: #569CD6">-&gt;</span><span style="color: #D4D4D4"> </span><span style="color: #C586C0">new</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">ResourceNotFoundException</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;Order not found or access denied&quot;</span><span style="color: #D4D4D4">));</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre></div>



<p><strong>Lớp 2: Kiểm soát truy cập tại tầng Controller (Method-Level Security)</strong></p>



<p>Sử dụng các Annotation bảo mật của Framework (như Spring Security) để chặn ngay từ cửa vào.</p>



<p><strong>✅ Ví dụ với Spring Security (<code><mark style="background-color:#e9ecef" class="has-inline-color">@PreAuthorize</mark></code>):</strong></p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#D4D4D4;--cbp-line-number-width:7.703125px;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#2b2b2b;color:#c7c7c7">Java</span><span role="button" tabindex="0" data-code="@GetMapping(&quot;/invoices/{id}&quot;)
// SpEL: Chỉ cho phép nếu user hiện tại là chủ sở hữu của invoice có id này
@PreAuthorize(&quot;@securityService.isOwner(authentication, #id)&quot;) 
public Invoice getInvoice(@PathVariable Long id) {
    return invoiceRepo.findById(id);
}" style="color:#D4D4D4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dark-plus" style="background-color: #1E1E1E" tabindex="0"><code><span class="line"><span style="color: #D4D4D4">@</span><span style="color: #4EC9B0">GetMapping</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;/invoices/{id}&quot;</span><span style="color: #D4D4D4">)</span></span>
<span class="line"><span style="color: #6A9955">// SpEL: Chỉ cho phép nếu user hiện tại là chủ sở hữu của invoice có id này</span></span>
<span class="line"><span style="color: #D4D4D4">@</span><span style="color: #4EC9B0">PreAuthorize</span><span style="color: #D4D4D4">(</span><span style="color: #CE9178">&quot;@securityService.isOwner(authentication, #id)&quot;</span><span style="color: #D4D4D4">) </span></span>
<span class="line"><span style="color: #569CD6">public</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Invoice</span><span style="color: #D4D4D4"> </span><span style="color: #DCDCAA">getInvoice</span><span style="color: #D4D4D4">(@</span><span style="color: #4EC9B0">PathVariable</span><span style="color: #D4D4D4"> </span><span style="color: #4EC9B0">Long</span><span style="color: #D4D4D4"> id) {</span></span>
<span class="line"><span style="color: #D4D4D4">    </span><span style="color: #C586C0">return</span><span style="color: #D4D4D4"> </span><span style="color: #9CDCFE">invoiceRepo</span><span style="color: #D4D4D4">.</span><span style="color: #DCDCAA">findById</span><span style="color: #D4D4D4">(id);</span></span>
<span class="line"><span style="color: #D4D4D4">}</span></span></code></pre></div>



<p><em>Cách này giúp code business logic sạch hơn, tách biệt logic bảo mật ra khỏi logic nghiệp vụ.</em></p>



<p><strong>Lớp 3: Làm khó Hacker bằng UUID (Obfuscation)</strong></p>



<p>Thay vì dùng Auto-Increment Integer (<code>1, 2, 3</code>), hãy dùng <strong>UUID</strong> (Random String: <code><mark style="background-color:#e9ecef" class="has-inline-color">a0eebc99-9c0b...</mark></code>) hoặc các thuật toán băm ID (như Hashids).</p>



<ul class="wp-block-list">
<li><strong>Lợi ích:</strong> Chặn đứng tấn công kiểu liệt kê (Enumeration Attack). Hacker không thể viết vòng lặp <code><mark style="background-color:#e9ecef" class="has-inline-color">for (i=0; i&lt;1000; i++)</mark></code> để quét dữ liệu.</li>



<li><strong>Cảnh báo:</strong> UUID <strong>KHÔNG THỂ THAY THẾ</strong> được <em>Lớp 1</em> và <em>Lớp 2</em>. Nếu hacker bằng cách nào đó biết được UUID của nạn nhân, IDOR vẫn xảy ra nếu thiếu Authorization check. UUID chỉ là &#8220;lớp sương mù&#8221;, không phải &#8220;bức tường thép&#8221;.</li>
</ul>



<p><strong>Lớp 4: Kiểm thử tự động (Automated Security Testing)</strong></p>



<p>Đừng đợi Pentester tìm lỗi. Hãy viết Test Case cho IDOR.</p>



<ul class="wp-block-list">
<li><strong>Unit Test:</strong> Tạo 2 user (Alice, Bob). Đăng nhập bằng Alice, cố gắng request dữ liệu ID của Bob. Assert rằng kết quả trả về phải là <code><mark style="background-color:#e9ecef" class="has-inline-color">403 Forbidden</mark></code> hoặc <code><mark style="background-color:#e9ecef" class="has-inline-color">404 Not Found</mark></code>.</li>
</ul>



<h3 id="7-ket-luan" class="wp-block-heading">7. Kết luận</h3>



<p>IDOR không phải là một lỗi kỹ thuật phức tạp, nhưng nó phản ánh sự thiếu sót trong tư duy thiết kế hệ thống.</p>



<p>Hãy nhớ nguyên tắc vàng: <strong>Mọi request truy cập dữ liệu đều phải trả lời được hai câu hỏi:</strong></p>



<ol class="wp-block-list">
<li><em>Người dùng này là ai? (Authentication)</em></li>



<li><em>Dữ liệu này có phải của họ không? (Authorization)</em></li>
</ol>



<p>Nếu thiếu câu hỏi số 2, chúng ta đang mở toang cánh cửa &#8220;phòng 102&#8221; cho bất kỳ ai cầm chìa khóa &#8220;phòng 101&#8221;.</p>



<p></p>
<p>The post <a href="https://blog.tomosia.com.vn/idor-lo-hong-ngay-tho-nhung-chi-mang-trong-kien-truc-phan-mem/">IDOR &#8211; Lỗ hổng &#8220;ngây thơ&#8221; nhưng chí mạng trong kiến trúc phần mềm?</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.tomosia.com.vn/idor-lo-hong-ngay-tho-nhung-chi-mang-trong-kien-truc-phan-mem/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Keycloak: Giải pháp IAM toàn diện cho ứng dụng hiện đại</title>
		<link>https://blog.tomosia.com.vn/keycloak-giai-phap-iam-toan-dien-cho-ung-dung-hien-dai/</link>
					<comments>https://blog.tomosia.com.vn/keycloak-giai-phap-iam-toan-dien-cho-ung-dung-hien-dai/#comments</comments>
		
		<dc:creator><![CDATA[Nguyen Luan]]></dc:creator>
		<pubDate>Tue, 09 Dec 2025 03:26:45 +0000</pubDate>
				<category><![CDATA[Java]]></category>
		<category><![CDATA[Solution]]></category>
		<guid isPermaLink="false">https://blog.tomosia.com.vn/?p=3519</guid>

					<description><![CDATA[<p>Keycloak là một giải pháp quản lý danh tính và truy cập (IAM – Identity and Access Management)&#8230;</p>
<p>The post <a href="https://blog.tomosia.com.vn/keycloak-giai-phap-iam-toan-dien-cho-ung-dung-hien-dai/">Keycloak: Giải pháp IAM toàn diện cho ứng dụng hiện đại</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p><em>Keycloak là <strong>một giải pháp quản lý danh tính và truy cập (IAM – Identity and Access Management)</strong> mã nguồn mở, giúp bạn triển khai đăng nhập, phân quyền và bảo mật cho ứng dụng mà không cần tự xây từ đầu.</em><br></p>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="1024" height="538" src="https://blog.tomosia.com.vn/wp-content/uploads/2025/11/image-1-1024x538.png" alt="" class="wp-image-3520" srcset="https://blog.tomosia.com.vn/wp-content/uploads/2025/11/image-1-1024x538.png 1024w, https://blog.tomosia.com.vn/wp-content/uploads/2025/11/image-1-300x158.png 300w, https://blog.tomosia.com.vn/wp-content/uploads/2025/11/image-1-768x403.png 768w, https://blog.tomosia.com.vn/wp-content/uploads/2025/11/image-1-380x200.png 380w, https://blog.tomosia.com.vn/wp-content/uploads/2025/11/image-1-800x420.png 800w, https://blog.tomosia.com.vn/wp-content/uploads/2025/11/image-1.png 1034w" sizes="auto, (max-width: 1024px) 100vw, 1024px" /></figure>



<p>Bảo mật luôn là yếu tố cốt lõi trong quá trình phát triển ứng dụng, đặc biệt khi bạn phải xử lý việc xác thực và phân quyền cho nhiều người dùng khác nhau.</p>



<p>Dù các developer hoàn toàn có thể tự xây dựng hệ thống quản lý danh tính từ con số 0, nhưng việc này tốn rất nhiều thời gian, công sức và dễ phát sinh lỗi &#8211; nhất là khi phải hỗ trợ các giao thức tiêu chuẩn như OAuth2 hay OpenID Connect.</p>



<p>Keycloak xuất hiện như một giải pháp giúp đơn giản hóa toàn bộ quá trình này. Nó không chỉ giảm tải việc triển khai xác thực mà còn mang đến sự đồng nhất, bảo mật cao và khả năng tùy biến mạnh mẽ. Bạn có thể tích hợp đăng nhập bằng Google, Facebook hoặc thiết lập SSO chỉ trong vài bước cấu hình mà không cần viết lại cả hệ thống.</p>



<h2 id="1-what-keycloak-la-gi" class="wp-block-heading"><strong>1.</strong> <strong>What? Keycloak là gì?</strong></h2>



<p><strong>Keycloak</strong> là một nền tảng mã nguồn mở mạnh mẽ, cung cấp giải pháp đăng nhập một lần (SSO) cùng hệ thống quản lý danh tính và quyền truy cập cho các ứng dụng và dịch vụ.<br><br>Nền tảng này được thiết kế nhằm đơn giản hóa toàn bộ bài toán bảo mật, giúp developer dễ dàng tích hợp cơ chế đăng nhập, phân quyền và xác thực vào ứng dụng mà không phải xây dựng mọi thứ từ đầu. Tất cả đều được Keycloak cung cấp sẵn dưới dạng các tính năng linh hoạt, dễ tùy chỉnh để phù hợp với nhu cầu của từng doanh nghiệp hay tổ chức.<br></p>



<figure class="wp-block-image size-large"><img decoding="async" src="https://mintlify.s3.us-west-1.amazonaws.com/infisical/images/sso/keycloak-oidc/create-client-login-settings.png" alt=""/></figure>



<p><br>Keycloak cho phép bạn tùy chỉnh giao diện người dùng cho các trang đăng nhập, đăng ký, quản lý tài khoản hay trang quản trị. Bên cạnh đó, nó còn hỗ trợ tích hợp với các hệ thống quản lý danh tính sẵn có như LDAP hoặc Active Directory, và cho phép xác thực thông qua các nhà cung cấp bên thứ ba như Google hay Facebook.<br><br><strong>Tóm lại</strong>: đối với developer, Keycloak giống như một “bộ công cụ all-in-one” để giải quyết toàn bộ vấn đề liên quan đến đăng nhập và phân quyền. Thay vì tự xây các tính năng như login, role-based access hay kết nối với các nền tảng bên ngoài, bạn chỉ cần cấu hình và sử dụng những gì Keycloak đã cung cấp.</p>



<p><strong>Luồng hoạt động:</strong><br>+ Spring Boot kiểm tra Token và cho phép truy cập nếu hợp lệ.<br>+ User đăng nhập vào Keycloak.<br>+ Keycloak trả về Token.<br>+ User gửi request kèm Token đến Spring Boot.<br></p>



<h2 id="2-why-tai-sao-lai-la-keycloak" class="wp-block-heading"><strong>2.</strong> <strong>Why? Tại sao lại là Keycloak?</strong></h2>



<figure class="wp-block-image size-large is-resized"><img decoding="async" src="https://www.voiceatthetable.com/wp-content/uploads/why-1780726_640.png" alt="" style="width:680px;height:auto"/></figure>



<p>Thay vì tự xây dựng module đăng nhập (Login) trong Spring Boot, việc sử dụng Keycloak mang lại các lợi ích:</p>



<p><strong>a</strong>. <strong>Quản lý tập trung:</strong> Admin có thể khóa user, đổi password, cấp quyền ngay trên giao diện Keycloak mà không cần sửa code hay database của ứng dụng.</p>



<p><strong>b</strong>. <strong>Bảo mật chuyên sâu (Delegated Authentication):</strong> Việc xử lý mật khẩu, mã hóa, 2FA (xác thực 2 bước) được Keycloak đảm nhận. Spring Boot không cần lo về lộ mật khẩu.</p>



<p><strong>c. Single Sign-On (SSO):</strong> Nếu bạn có 10 ứng dụng (Microservices), user chỉ cần đăng nhập 1 lần tại Keycloak là vào được cả 10 ứng dụng.</p>



<p><strong>d. Chuẩn hóa (Standardization):</strong> Sử dụng giao thức OAuth2 và OpenID Connect chuẩn quốc tế. Dễ dàng tích hợp với Frontend (React, Angular, Mobile App).</p>



<h2 id="3-cac-thanh-phan-chinh-trong-keycloak" class="wp-block-heading"><strong>3.</strong> <strong>Các thành phần chính trong Keycloak</strong></h2>



<h3 id="3-1-realm-khong-gian-quan-ly" class="wp-block-heading"><em><strong>3.1. Realm (Không gian quản lý)</strong></em></h3>



<p><strong>Định nghĩa:</strong> Realm là một không gian quản lý định danh hoàn toàn độc lập. Dữ liệu (user, role, client) của Realm A hoàn toàn tách biệt với Realm B.<br><br><strong>Master Realm:</strong> Khi cài đặt xong, Keycloak có sẵn realm tên là <strong><em>Master</em></strong>. Đây là realm dùng để quản trị hệ thống Keycloak. Không nên dùng realm này để quản lý user cho ứng dụng của bạn.<br><br><strong>Ví dụ:</strong> Bạn có thể tạo Realm <strong><em>Users </em></strong>cho người dùng cuối và Realm <strong><em>Admins </em></strong>cho site quản trị.</p>



<h3 id="3-2-client-ung-dung-khach" class="wp-block-heading"><em><strong>3.2. Client (Ứng dụng khách)</strong></em></h3>



<p><strong>Định nghĩa:</strong> Client là các ứng dụng hoặc dịch vụ muốn sử dụng Keycloak để xác thực. Client đại diện cho các ứng dụng hoặc dịch vụ được bảo mật bởi Keycloak. Mỗi client có cấu hình riêng, bao gồm giao thức (OAuth2, OpenID Connect), URL callback, và thông tin bảo mật như client secret.</p>



<h3 id="3-3-user-nguoi-dung" class="wp-block-heading"><strong><em>3.3. User (Người dùng)</em></strong></h3>



<p><strong>Định nghĩa:</strong> Thực thể có thể đăng nhập vào hệ thống.</p>



<p><strong>Thuộc tính:</strong> User có Username, Password, Email, và các thuộc tính tùy chỉnh (Attributes) như số điện thoại, mã nhân viên, phòng ban&#8230;</p>



<p>Keycloak hỗ trợ các phương thức xác thực đa dạng:<br>&#8211; Đăng nhập bằng email/mật khẩu.<br>&#8211; Đăng nhập qua các nhà cung cấp bên thứ ba (Google, Facebook).<br>&#8211; Xác thực hai yếu tố (2FA).</p>



<h3 id="3-4-role-vai-tro" class="wp-block-heading"><strong><em>3.4. Role (Vai trò)</em></strong></h3>



<p>Role dùng để phân quyền (Authorization). Có 2 loại Role trong Keycloak:<br>&#8211; <strong>Realm Role:</strong> Role toàn cục. Ví dụ: <code>Global_Admin</code>, <code>User</code>. Có hiệu lực trên toàn bộ Realm.<br>&#8211; <strong>Client Role:</strong> Role gắn liền với một Client cụ thể. Ví dụ: Client <code>Inventory-App</code> có role <code>stock-keeper</code>. Role này chỉ có ý nghĩa với app kho, không có ý nghĩa với app nhân sự.</p>



<h3 id="3-5-group-nhom" class="wp-block-heading"><strong><em>3.5. Group (Nhóm)</em></strong></h3>



<p><strong>Định nghĩa:</strong> Dùng để gom nhóm các User lại.<br><br><strong>Đặc điểm:</strong> Group có thể lồng nhau (Parent &#8211; Child). Ví dụ: <code>IT Department</code> -> <code>Dev Team</code>.<br><br><strong>Tác dụng:</strong> Bạn có thể gán Role cho Group, tất cả User trong Group đó sẽ tự động kế thừa Role. Giúp quản lý quyền hạn dễ dàng hơn gán lẻ tẻ.</p>



<h2 id="4-how-lam-the-nao-de-tich-hop-keycloak-vao-1-ung-dung-spring-boot-chi-trong-vai-buoc-don-gian" class="wp-block-heading"><strong>4. How? Làm thế nào để tích hợp Keycloak vào 1 ứng dụng Spring Boot chỉ trong vài bước đơn giản.</strong></h2>



<figure class="wp-block-image size-large"><img loading="lazy" decoding="async" width="842" height="1024" src="https://blog.tomosia.com.vn/wp-content/uploads/2025/11/image-16-842x1024.png" alt="" class="wp-image-3591" srcset="https://blog.tomosia.com.vn/wp-content/uploads/2025/11/image-16-842x1024.png 842w, https://blog.tomosia.com.vn/wp-content/uploads/2025/11/image-16-247x300.png 247w, https://blog.tomosia.com.vn/wp-content/uploads/2025/11/image-16-scaled.png 2104w" sizes="auto, (max-width: 842px) 100vw, 842px" /></figure>



<p><strong>Phần A: Cấu hình Keycloak (Docker)</strong><br><strong>Bước 1: Khởi chạy Keycloak</strong> Mở terminal và chạy lệnh Docker (bản developer):</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:7.703125px;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#333545;color:#ebebe6">ShellScript</span><span role="button" tabindex="0" data-code="docker run -p 8080:8080 -e KEYCLOAK_ADMIN=admin -e KEYCLOAK_ADMIN_PASSWORD=admin quay.io/keycloak/keycloak:24.0.1 start-dev" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #62E884">docker</span><span style="color: #F6F6F4"> </span><span style="color: #E7EE98">run</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">-p</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">8080</span><span style="color: #E7EE98">:8080</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">-e</span><span style="color: #F6F6F4"> </span><span style="color: #E7EE98">KEYCLOAK_ADMIN=admin</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">-e</span><span style="color: #F6F6F4"> </span><span style="color: #E7EE98">KEYCLOAK_ADMIN_PASSWORD=admin</span><span style="color: #F6F6F4"> </span><span style="color: #E7EE98">quay.io/keycloak/keycloak:24.0.1</span><span style="color: #F6F6F4"> </span><span style="color: #E7EE98">start-dev</span></span></code></pre></div>



<p><strong>Bước 2: Thiết lập Realm và Client</strong></p>



<ol class="wp-block-list">
<li>Truy cập <code>http://localhost:8080</code> -&gt; Vào <strong>Administration Console</strong> (Login: admin/admin).</li>



<li><strong>Tạo Realm:</strong> Bấm vào dropdown menu góc trái -&gt; <strong>Create Realm</strong> -&gt; Tên: <code>springboot-demo</code>.</li>



<li><strong>Tạo Client:</strong><br>&#8211; Vào menu <strong>Clients</strong> -&gt; <strong>Create client</strong>.<br>&#8211; Client ID: <code>my-backend-api</code>.<br>&#8211; Capability config: Bật <strong>Client authentication</strong> (để có Client Secret) và <strong>Authorization</strong>.<br>&#8211; Lưu lại.</li>



<li><strong>Tạo Roles:</strong><br>&#8211; Vào menu <strong>Realm roles</strong> -&gt; <strong>Create role</strong>.<br>&#8211; Tạo 2 role: <code>USER</code> và <code>ADMIN</code>.</li>
</ol>



<p><strong>Phần B: Lập trình Spring Boot (Resource Server)</strong><br><strong>Bước 1: Dependencies (<code>pom.xml</code>)</strong> Cần Java 17+ và Spring Boot 3.x.</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:15.40625px;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#333545;color:#ebebe6">XML</span><span role="button" tabindex="0" data-code="&lt;dependencies&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-web&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;!-- Thư viện quan trọng nhất để biến App thành Resource Server --&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-oauth2-resource-server&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.springframework.boot&lt;/groupId&gt;
        &lt;artifactId&gt;spring-boot-starter-security&lt;/artifactId&gt;
    &lt;/dependency&gt;
    &lt;dependency&gt;
        &lt;groupId&gt;org.projectlombok&lt;/groupId&gt;
        &lt;artifactId&gt;lombok&lt;/artifactId&gt;
        &lt;optional&gt;true&lt;/optional&gt;
    &lt;/dependency&gt;
&lt;/dependencies&gt;" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F6F6F4">&lt;</span><span style="color: #F286C4">dependencies</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">    &lt;</span><span style="color: #F286C4">dependency</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">        &lt;</span><span style="color: #F286C4">groupId</span><span style="color: #F6F6F4">&gt;org.springframework.boot&lt;/</span><span style="color: #F286C4">groupId</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">        &lt;</span><span style="color: #F286C4">artifactId</span><span style="color: #F6F6F4">&gt;spring-boot-starter-web&lt;/</span><span style="color: #F286C4">artifactId</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">    &lt;/</span><span style="color: #F286C4">dependency</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">&lt;!-- Thư viện quan trọng nhất để biến App thành Resource Server --&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">    &lt;</span><span style="color: #F286C4">dependency</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">        &lt;</span><span style="color: #F286C4">groupId</span><span style="color: #F6F6F4">&gt;org.springframework.boot&lt;/</span><span style="color: #F286C4">groupId</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">        &lt;</span><span style="color: #F286C4">artifactId</span><span style="color: #F6F6F4">&gt;spring-boot-starter-oauth2-resource-server&lt;/</span><span style="color: #F286C4">artifactId</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">    &lt;/</span><span style="color: #F286C4">dependency</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">    &lt;</span><span style="color: #F286C4">dependency</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">        &lt;</span><span style="color: #F286C4">groupId</span><span style="color: #F6F6F4">&gt;org.springframework.boot&lt;/</span><span style="color: #F286C4">groupId</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">        &lt;</span><span style="color: #F286C4">artifactId</span><span style="color: #F6F6F4">&gt;spring-boot-starter-security&lt;/</span><span style="color: #F286C4">artifactId</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">    &lt;/</span><span style="color: #F286C4">dependency</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">    &lt;</span><span style="color: #F286C4">dependency</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">        &lt;</span><span style="color: #F286C4">groupId</span><span style="color: #F6F6F4">&gt;org.projectlombok&lt;/</span><span style="color: #F286C4">groupId</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">        &lt;</span><span style="color: #F286C4">artifactId</span><span style="color: #F6F6F4">&gt;lombok&lt;/</span><span style="color: #F286C4">artifactId</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">        &lt;</span><span style="color: #F286C4">optional</span><span style="color: #F6F6F4">&gt;true&lt;/</span><span style="color: #F286C4">optional</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">    &lt;/</span><span style="color: #F286C4">dependency</span><span style="color: #F6F6F4">&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">&lt;/</span><span style="color: #F286C4">dependencies</span><span style="color: #F6F6F4">&gt;</span></span></code></pre></div>



<p><strong>Bước 2: Cấu hình (<code>application.yml</code>)</strong> Khai báo địa chỉ của Keycloak để Spring Boot biết nơi xác thực token.</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:15.40625px;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#333545;color:#ebebe6">YAML</span><span role="button" tabindex="0" data-code="server:
  port: 5000 # Chạy port khác Keycloak

spring:
  application:
    name: keycloak-integration-demo
  security:
    oauth2:
      resourceserver:
        jwt:
          # Đường dẫn Issuer của Realm (Quan trọng)
          # Cấu trúc: http://&lt;host&gt;:&lt;port&gt;/realms/&lt;realm-name&gt;
          issuer-uri: http://localhost:8080/realms/springboot-demo
          # jwk-set-uri sẽ được tự động cấu hình từ issuer-uri" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #97E1F1">server</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">  </span><span style="color: #97E1F1">port</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">5000</span><span style="color: #F6F6F4"> </span><span style="color: #7B7F8B"># Chạy port khác Keycloak</span></span>
<span class="line"></span>
<span class="line"><span style="color: #97E1F1">spring</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">  </span><span style="color: #97E1F1">application</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">name</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #E7EE98">keycloak-integration-demo</span></span>
<span class="line"><span style="color: #F6F6F4">  </span><span style="color: #97E1F1">security</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #97E1F1">oauth2</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">      </span><span style="color: #97E1F1">resourceserver</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1">jwt</span><span style="color: #F286C4">:</span></span>
<span class="line"><span style="color: #F6F6F4">          </span><span style="color: #7B7F8B"># Đường dẫn Issuer của Realm (Quan trọng)</span></span>
<span class="line"><span style="color: #F6F6F4">          </span><span style="color: #7B7F8B"># Cấu trúc: http://&lt;host&gt;:&lt;port&gt;/realms/&lt;realm-name&gt;</span></span>
<span class="line"><span style="color: #F6F6F4">          </span><span style="color: #97E1F1">issuer-uri</span><span style="color: #F286C4">:</span><span style="color: #F6F6F4"> </span><span style="color: #E7EE98">http://localhost:8080/realms/springboot-demo</span></span>
<span class="line"><span style="color: #F6F6F4">          </span><span style="color: #7B7F8B"># jwk-set-uri sẽ được tự động cấu hình từ issuer-uri</span></span></code></pre></div>



<p><strong>Bước 3: Class <code>SecurityConfig.java</code> (Quan trọng nhất)</strong> Mặc định Keycloak trả về role trong JSON phức tạp, ta cần convert nó về dạng <code>ROLE_ABC</code> để Spring Security hiểu.</p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:15.40625px;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#333545;color:#ebebe6">Java</span><span role="button" tabindex="0" data-code="package com.example.demo.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import 
org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;
import org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;
import org.springframework.security.web.SecurityFilterChain;

import java.util.Collection;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Configuration
@EnableWebSecurity
@EnableMethodSecurity // Cho phép dùng @PreAuthorize ở Controller
public class SecurityConfig {

    @Bean
    public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
        http
            .csrf(csrf -&gt; csrf.disable()) // Tắt CSRF cho API
            .authorizeHttpRequests(auth -&gt; auth
                .requestMatchers(&quot;/public/**&quot;).permitAll() // API public không cần token
                .anyRequest().authenticated() // Các API còn lại yêu cầu phải login
            )
            .oauth2ResourceServer(oauth2 -&gt; oauth2
                .jwt(jwt -&gt; jwt.jwtAuthenticationConverter(jwtAuthenticationConverter()))
            );
        
        return http.build();
    }

    // Converter: Biến đổi thông tin từ JWT Keycloak sang Spring Security Authority
    @Bean
    public JwtAuthenticationConverter jwtAuthenticationConverter() {
        JwtAuthenticationConverter converter = new JwtAuthenticationConverter();
        converter.setJwtGrantedAuthoritiesConverter(source -&gt; {
            // Lấy map roles từ claim &quot;realm_access&quot; của Keycloak
            Map&lt;String, Object&gt; realmAccess = source.getClaimAsMap(&quot;realm_access&quot;);
            
            if (realmAccess == null || !realmAccess.containsKey(&quot;roles&quot;)) {
                return List.of();
            }

            List&lt;String&gt; roles = (List&lt;String&gt;) realmAccess.get(&quot;roles&quot;);
            
            // Convert: [USER, ADMIN] -&gt; [ROLE_USER, ROLE_ADMIN]
            return roles.stream()
                    .map(role -&gt; new org.springframework.security.core.authority.SimpleGrantedAuthority(&quot;ROLE_&quot; + role))
                    .collect(Collectors.toList());
        });
        return converter;
    }
}
" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">package</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">com.example.demo.config</span><span style="color: #F6F6F4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> org.springframework.context.annotation.Bean;</span></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> org.springframework.context.annotation.Configuration;</span></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> </span></span>
<span class="line"><span style="color: #F6F6F4">org.springframework.security.config.annotation.method.configuration.EnableMethodSecurity;</span></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> org.springframework.security.config.annotation.web.builders.HttpSecurity;</span></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;</span></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> org.springframework.security.oauth2.server.resource.authentication.JwtAuthenticationConverter;</span></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter;</span></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> org.springframework.security.web.SecurityFilterChain;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> java.util.Collection;</span></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> java.util.List;</span></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> java.util.Map;</span></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> java.util.stream.Collectors;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">@</span><span style="color: #97E1F1; font-style: italic">Configuration</span></span>
<span class="line"><span style="color: #F6F6F4">@</span><span style="color: #97E1F1; font-style: italic">EnableWebSecurity</span></span>
<span class="line"><span style="color: #F6F6F4">@</span><span style="color: #97E1F1; font-style: italic">EnableMethodSecurity</span><span style="color: #F6F6F4"> </span><span style="color: #7B7F8B">// Cho phép dùng @PreAuthorize ở Controller</span></span>
<span class="line"><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">class</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">SecurityConfig</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    @</span><span style="color: #97E1F1; font-style: italic">Bean</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">SecurityFilterChain</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">filterChain</span><span style="color: #F6F6F4">(</span><span style="color: #97E1F1; font-style: italic">HttpSecurity</span><span style="color: #F6F6F4"> </span><span style="color: #FFB86C; font-style: italic">http</span><span style="color: #F6F6F4">) </span><span style="color: #F286C4">throws</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">Exception</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">        http</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #62E884">csrf</span><span style="color: #F6F6F4">(csrf </span><span style="color: #97E1F1; font-style: italic">-&gt;</span><span style="color: #F6F6F4"> csrf.</span><span style="color: #62E884">disable</span><span style="color: #F6F6F4">()) </span><span style="color: #7B7F8B">// Tắt CSRF cho API</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #62E884">authorizeHttpRequests</span><span style="color: #F6F6F4">(auth </span><span style="color: #97E1F1; font-style: italic">-&gt;</span><span style="color: #F6F6F4"> auth</span></span>
<span class="line"><span style="color: #F6F6F4">                .</span><span style="color: #62E884">requestMatchers</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">/public/**</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">).</span><span style="color: #62E884">permitAll</span><span style="color: #F6F6F4">() </span><span style="color: #7B7F8B">// API public không cần token</span></span>
<span class="line"><span style="color: #F6F6F4">                .</span><span style="color: #62E884">anyRequest</span><span style="color: #F6F6F4">().</span><span style="color: #62E884">authenticated</span><span style="color: #F6F6F4">() </span><span style="color: #7B7F8B">// Các API còn lại yêu cầu phải login</span></span>
<span class="line"><span style="color: #F6F6F4">            )</span></span>
<span class="line"><span style="color: #F6F6F4">            .</span><span style="color: #62E884">oauth2ResourceServer</span><span style="color: #F6F6F4">(oauth2 </span><span style="color: #97E1F1; font-style: italic">-&gt;</span><span style="color: #F6F6F4"> oauth2</span></span>
<span class="line"><span style="color: #F6F6F4">                .</span><span style="color: #62E884">jwt</span><span style="color: #F6F6F4">(jwt </span><span style="color: #97E1F1; font-style: italic">-&gt;</span><span style="color: #F6F6F4"> jwt.</span><span style="color: #62E884">jwtAuthenticationConverter</span><span style="color: #F6F6F4">(</span><span style="color: #62E884">jwtAuthenticationConverter</span><span style="color: #F6F6F4">()))</span></span>
<span class="line"><span style="color: #F6F6F4">            );</span></span>
<span class="line"><span style="color: #F6F6F4">        </span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> http.</span><span style="color: #62E884">build</span><span style="color: #F6F6F4">();</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #7B7F8B">// Converter: Biến đổi thông tin từ JWT Keycloak sang Spring Security Authority</span></span>
<span class="line"><span style="color: #F6F6F4">    @</span><span style="color: #97E1F1; font-style: italic">Bean</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">JwtAuthenticationConverter</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">jwtAuthenticationConverter</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #97E1F1; font-style: italic">JwtAuthenticationConverter</span><span style="color: #F6F6F4"> converter </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4; font-weight: bold">new</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">JwtAuthenticationConverter</span><span style="color: #F6F6F4">();</span></span>
<span class="line"><span style="color: #F6F6F4">        converter.</span><span style="color: #62E884">setJwtGrantedAuthoritiesConverter</span><span style="color: #F6F6F4">(source </span><span style="color: #97E1F1; font-style: italic">-&gt;</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #7B7F8B">// Lấy map roles từ claim &quot;realm_access&quot; của Keycloak</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1; font-style: italic">Map</span><span style="color: #F6F6F4">&lt;String, Object&gt; realmAccess </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> source.</span><span style="color: #62E884">getClaimAsMap</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">realm_access</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">);</span></span>
<span class="line"><span style="color: #F6F6F4">            </span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #F286C4">if</span><span style="color: #F6F6F4"> (realmAccess </span><span style="color: #F286C4">==</span><span style="color: #F6F6F4"> </span><span style="color: #BF9EEE">null</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">||</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">!</span><span style="color: #F6F6F4">realmAccess.</span><span style="color: #62E884">containsKey</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">roles</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)) {</span></span>
<span class="line"><span style="color: #F6F6F4">                </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> List.</span><span style="color: #62E884">of</span><span style="color: #F6F6F4">();</span></span>
<span class="line"><span style="color: #F6F6F4">            }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #97E1F1; font-style: italic">List</span><span style="color: #F6F6F4">&lt;String&gt; roles </span><span style="color: #F286C4">=</span><span style="color: #F6F6F4"> (</span><span style="color: #97E1F1; font-style: italic">List</span><span style="color: #F286C4">&lt;</span><span style="color: #F6F6F4">String</span><span style="color: #F286C4">&gt;</span><span style="color: #F6F6F4">) realmAccess.</span><span style="color: #62E884">get</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">roles</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">);</span></span>
<span class="line"><span style="color: #F6F6F4">            </span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #7B7F8B">// Convert: [USER, ADMIN] -&gt; [ROLE_USER, ROLE_ADMIN]</span></span>
<span class="line"><span style="color: #F6F6F4">            </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> roles.</span><span style="color: #62E884">stream</span><span style="color: #F6F6F4">()</span></span>
<span class="line"><span style="color: #F6F6F4">                    .</span><span style="color: #62E884">map</span><span style="color: #F6F6F4">(role </span><span style="color: #97E1F1; font-style: italic">-&gt;</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4; font-weight: bold">new</span><span style="color: #F6F6F4"> org.springframework.security.core.authority.</span><span style="color: #62E884">SimpleGrantedAuthority</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">ROLE_</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">+</span><span style="color: #F6F6F4"> role))</span></span>
<span class="line"><span style="color: #F6F6F4">                    .</span><span style="color: #62E884">collect</span><span style="color: #F6F6F4">(Collectors.</span><span style="color: #62E884">toList</span><span style="color: #F6F6F4">());</span></span>
<span class="line"><span style="color: #F6F6F4">        });</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> converter;</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span>
<span class="line"></span></code></pre></div>



<p><strong>Bước 4: Class <code>DemoController.java</code></strong></p>



<div class="wp-block-kevinbatdorf-code-block-pro cbp-has-line-numbers" data-code-block-pro-font-family="Code-Pro-JetBrains-Mono" style="font-size:.875rem;font-family:Code-Pro-JetBrains-Mono,ui-monospace,SFMono-Regular,Menlo,Monaco,Consolas,monospace;--cbp-line-number-color:#f6f6f4;--cbp-line-number-width:15.40625px;line-height:1.25rem;--cbp-tab-width:2;tab-size:var(--cbp-tab-width, 2)"><span style="display:flex;align-items:center;padding:10px 0px 10px 16px;margin-bottom:-2px;width:100%;text-align:left;background-color:#333545;color:#ebebe6">Java</span><span role="button" tabindex="0" data-code="package com.example.demo.controller;

import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
public class DemoController {

    @GetMapping(&quot;/public/hello&quot;)
    public String hello() {
        return &quot;Public: Xin chào, ai cũng xem được!&quot;;
    }

    @GetMapping(&quot;/user/profile&quot;)
    @PreAuthorize(&quot;hasRole('USER')&quot;)
    public String userProfile() {
        return &quot;Secured: Đây là trang dành cho User có role USER.&quot;;
    }

    @GetMapping(&quot;/admin/dashboard&quot;)
    @PreAuthorize(&quot;hasRole('ADMIN')&quot;)
    public String adminDashboard() {
        return &quot;Secured: Đây là trang quản trị (ADMIN ONLY).&quot;;
    }
}" style="color:#f6f6f4;display:none" aria-label="Copy" class="code-block-pro-copy-button"><svg xmlns="http://www.w3.org/2000/svg" style="width:24px;height:24px" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2"><path class="with-check" stroke-linecap="round" stroke-linejoin="round" d="M4.5 12.75l6 6 9-13.5"></path><path class="without-check" stroke-linecap="round" stroke-linejoin="round" d="M16.5 8.25V6a2.25 2.25 0 00-2.25-2.25H6A2.25 2.25 0 003.75 6v8.25A2.25 2.25 0 006 16.5h2.25m8.25-8.25H18a2.25 2.25 0 012.25 2.25V18A2.25 2.25 0 0118 20.25h-7.5A2.25 2.25 0 018.25 18v-1.5m8.25-8.25h-6a2.25 2.25 0 00-2.25 2.25v6"></path></svg></span><pre class="shiki dracula-soft" style="background-color: #282A36" tabindex="0"><code><span class="line"><span style="color: #F286C4">package</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">com.example.demo.controller</span><span style="color: #F6F6F4">;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> org.springframework.security.access.prepost.PreAuthorize;</span></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> org.springframework.web.bind.annotation.GetMapping;</span></span>
<span class="line"><span style="color: #F286C4">import</span><span style="color: #F6F6F4"> org.springframework.web.bind.annotation.RestController;</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">@</span><span style="color: #97E1F1; font-style: italic">RestController</span></span>
<span class="line"><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #F286C4">class</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1">DemoController</span><span style="color: #F6F6F4"> {</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    @</span><span style="color: #97E1F1; font-style: italic">GetMapping</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">/public/hello</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">hello</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Public: Xin chào, ai cũng xem được!</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">;</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    @</span><span style="color: #97E1F1; font-style: italic">GetMapping</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">/user/profile</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    @</span><span style="color: #97E1F1; font-style: italic">PreAuthorize</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">hasRole(&#39;USER&#39;)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">userProfile</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Secured: Đây là trang dành cho User có role USER.</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">;</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"></span>
<span class="line"><span style="color: #F6F6F4">    @</span><span style="color: #97E1F1; font-style: italic">GetMapping</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">/admin/dashboard</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    @</span><span style="color: #97E1F1; font-style: italic">PreAuthorize</span><span style="color: #F6F6F4">(</span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">hasRole(&#39;ADMIN&#39;)</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">)</span></span>
<span class="line"><span style="color: #F6F6F4">    </span><span style="color: #F286C4">public</span><span style="color: #F6F6F4"> </span><span style="color: #97E1F1; font-style: italic">String</span><span style="color: #F6F6F4"> </span><span style="color: #62E884">adminDashboard</span><span style="color: #F6F6F4">() {</span></span>
<span class="line"><span style="color: #F6F6F4">        </span><span style="color: #F286C4">return</span><span style="color: #F6F6F4"> </span><span style="color: #DEE492">&quot;</span><span style="color: #E7EE98">Secured: Đây là trang quản trị (ADMIN ONLY).</span><span style="color: #DEE492">&quot;</span><span style="color: #F6F6F4">;</span></span>
<span class="line"><span style="color: #F6F6F4">    }</span></span>
<span class="line"><span style="color: #F6F6F4">}</span></span></code></pre></div>



<p><strong>4. RESULT: Kiểm thử hoạt động</strong></p>



<p>Sử dụng <strong>Postman</strong> để demo luồng OAuth2.</p>



<p>Bước 1: Lấy Token từ Keycloak (Login)<br>&#8211; <strong>URL:</strong> <code>http://localhost:8080/realms/springboot-demo/protocol/openid-connect/token</code><br>&#8211; <strong>Body (x-www-form-urlencoded):</strong> <br>   + <code>client_id</code>: <code>my-backend-api</code><br>   + <code>client_secret</code>: <em>(Lấy trong Keycloak: Clients -&gt; my-backend-api -&gt; Credentials)</em><br>   + <code>grant_type</code>: <code>password</code><br>   + <code>username</code>: <code>testuser</code><br>   + <code>password</code>: <code>123</code><br>&#8211; <strong>Kết quả:</strong> Bạn nhận được JSON chứa <code>access_token</code>. Copy chuỗi này.</p>



<p>Bước 2: Gọi API bảo mật<br>&#8211; <strong>URL:</strong> <code>http://localhost:8081/user/profile</code><br>&#8211; <strong>Authorization Tab:</strong> Chọn type <strong>Bearer Token</strong> và dán access token vào.<br>&#8211; <strong>Kết quả:</strong><br>   + Nếu token hợp lệ: Trả về <code>Secured: Đây là trang dành cho User...</code> (Status 200).<br>   + Nếu không gửi token: <code>401 Unauthorized</code>.</p>



<p><strong>Kết luận:</strong> Bạn đã xây dựng thành công một hệ thống bảo mật hiện đại, tách biệt hoàn toàn Resource Server và Authorization Server, tuân thủ chuẩn OAuth2.</p>
<p>The post <a href="https://blog.tomosia.com.vn/keycloak-giai-phap-iam-toan-dien-cho-ung-dung-hien-dai/">Keycloak: Giải pháp IAM toàn diện cho ứng dụng hiện đại</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.tomosia.com.vn/keycloak-giai-phap-iam-toan-dien-cho-ung-dung-hien-dai/feed/</wfw:commentRss>
			<slash:comments>18</slash:comments>
		
		
			</item>
	</channel>
</rss>
