<?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>Tomoshare</title>
	<atom:link href="https://blog.tomosia.com.vn/feed/" rel="self" type="application/rss+xml" />
	<link>https://blog.tomosia.com.vn/</link>
	<description>Kênh chia sẻ kiến thức Tomosia Việt Nam</description>
	<lastBuildDate>Tue, 28 Apr 2026 09:36:05 +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>Tomoshare</title>
	<link>https://blog.tomosia.com.vn/</link>
	<width>32</width>
	<height>32</height>
</image> 
	<item>
		<title>Tối ưu Slow Query PostgreSQL: Khi một dòng code cứu cả hệ thống</title>
		<link>https://blog.tomosia.com.vn/toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong/</link>
					<comments>https://blog.tomosia.com.vn/toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong/#respond</comments>
		
		<dc:creator><![CDATA[hoa nguyen]]></dc:creator>
		<pubDate>Fri, 17 Apr 2026 07:07:57 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[database]]></category>
		<category><![CDATA[Laravel]]></category>
		<guid isPermaLink="false">https://blog.tomosia.com.vn/?p=4257</guid>

					<description><![CDATA[<p>Tối ưu Slow Query PostgreSQL: Khi một dòng code cứu cả hệ thống Bối cảnh Trong một dự&#8230;</p>
<p>The post <a href="https://blog.tomosia.com.vn/toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong/">Tối ưu Slow Query PostgreSQL: Khi một dòng code cứu cả hệ thống</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h1 id="toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong" class="wp-block-heading">Tối ưu Slow Query PostgreSQL: Khi một dòng code cứu cả hệ thống</h1>



<h2 id="boi-canh" class="wp-block-heading">Bối cảnh</h2>



<p>Trong một dự án e-commerce gần đây, tôi gặp một bài toán rất quen thuộc:<br>trên trang chi tiết đơn hàng, cần có nút:</p>



<ul class="wp-block-list">
<li>“Đơn tiếp theo” (Next)</li>



<li>“Đơn trước đó” (Previous)</li>
</ul>



<p>Yêu cầu tưởng đơn giản, nhưng có một constraint quan trọng:<br><strong>không được load toàn bộ danh sách lên RAM</strong>.</p>



<p>Giải pháp hiển nhiên là dùng <strong>Keyset Pagination</strong> (cursor-based), thay vì offset pagination.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="cach-trien-khai-ban-dau" class="wp-block-heading">Cách triển khai ban đầu</h2>



<p>Dữ liệu được sort theo:</p>



<ul class="wp-block-list">
<li><code>paid_at</code> (thời điểm thanh toán)</li>



<li><code>id</code> (để break tie)</li>
</ul>



<p>Code Laravel Eloquent ban đầu của tôi cho “Next Order” trông như sau:</p>



<pre class="wp-block-preformatted">-&gt;where(function (Builder $q) use ($order) {<br>    $q-&gt;where('paid_at', '&gt;', $order-&gt;paid_at)<br>        -&gt;orWhere(function (Builder $q1) use ($order) {<br>            $q1-&gt;where('paid_at', $order-&gt;paid_at)<br>                -&gt;where('id', '&gt;', $order-&gt;id);<br>        });<br>})<br>-&gt;orderBy('paid_at')<br>-&gt;orderBy('id')</pre>



<p>Nếu đọc bằng “tiếng người”, logic này hoàn toàn đúng:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Lấy các đơn có <code>paid_at</code> lớn hơn,<br>hoặc nếu cùng thời điểm thì <code>id</code> phải lớn hơn.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="van-de-thuc-te-xay-ra" class="wp-block-heading">Vấn đề thực tế xảy ra</h2>



<p>Khi dữ liệu tăng lên vài triệu bản ghi, hệ thống bắt đầu:</p>



<ul class="wp-block-list">
<li>xuất hiện <strong>slow query</strong></li>



<li>query plan chuyển sang:
<ul class="wp-block-list">
<li><code>Bitmap Heap Scan</code></li>



<li>hoặc tệ hơn là <code>Seq Scan</code></li>
</ul>
</li>
</ul>



<p>Trong khi đó, database đã có <strong>composite index</strong>:</p>



<pre class="wp-block-preformatted">(paid_at, id)</pre>



<p>=&gt; Về lý thuyết phải chạy rất nhanh.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="nguyen-nhan-cot-loi" class="wp-block-heading">Nguyên nhân cốt lõi</h2>



<p>Thủ phạm nằm ở <strong>mệnh đề <code>OR</code></strong>.</p>



<p>PostgreSQL Query Planner khi gặp <code>OR</code> thường:</p>



<ul class="wp-block-list">
<li>tách điều kiện thành nhiều nhánh</li>



<li>không tận dụng được composite index một cách hiệu quả</li>



<li>dẫn tới scan nhiều hơn cần thiết</li>
</ul>



<p>Nói đơn giản:<br><strong>Index có, nhưng không được dùng đúng cách.</strong></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="cach-giai-quyet-va-la-diem-aha-moment" class="wp-block-heading">Cách giải quyết (và là điểm “aha moment”)</h2>



<p>Thay vì viết điều kiện dạng boolean phức tạp,<br>ta sử dụng <strong>Tuple Comparison (Row Value Syntax)</strong> của PostgreSQL.</p>



<h3 id="thay-doi-duy-nhat" class="wp-block-heading">Thay đổi duy nhất:</h3>



<pre class="wp-block-preformatted">-&gt;whereRaw('(paid_at, id) &gt; (?, ?)', [$order-&gt;paid_at, $order-&gt;id])</pre>



<p>Với “Previous”:</p>



<pre class="wp-block-preformatted">-&gt;whereRaw('(paid_at, id) &lt; (?, ?)', [$order-&gt;paid_at, $order-&gt;id])</pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="vi-sao-cach-nay-hieu-qua" class="wp-block-heading">Vì sao cách này hiệu quả?</h2>



<p>Composite index <code>(paid_at, id)</code> trong PostgreSQL được sắp xếp theo kiểu:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>từ điển (lexicographical order)</p>
</blockquote>



<p>Tức là:</p>



<ol class="wp-block-list">
<li>So sánh <code>paid_at</code></li>



<li>Nếu bằng nhau → so sánh <code>id</code></li>
</ol>



<p>Khi bạn viết:</p>



<pre class="wp-block-preformatted">(paid_at, id) &gt; (x, y)</pre>



<p>PostgreSQL có thể:</p>



<ul class="wp-block-list">
<li><strong>map trực tiếp vào B-Tree index</strong></li>



<li>nhảy đúng vị trí <code>(x, y)</code></li>



<li>thực hiện <strong>Index Scan tuyến tính</strong></li>
</ul>



<p>=&gt; Không cần phân nhánh logic như <code>OR</code> nữa.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="ket-qua" class="wp-block-heading">Kết quả</h2>



<p>Sau khi thay đổi:</p>



<ul class="wp-block-list">
<li>Query Plan chuyển thành <strong>Index Scan</strong></li>



<li>Slow query biến mất</li>



<li>Thời gian response ổn định lại</li>
</ul>



<p>Ngoài ra còn một lợi ích “không ngờ”:</p>



<h3 id="code-gon-hon-rat-nhieu" class="wp-block-heading">Code gọn hơn rất nhiều</h3>



<p>Từ:</p>



<ul class="wp-block-list">
<li>nhiều closure lồng nhau</li>



<li>logic khó đọc</li>
</ul>



<p>=&gt; còn:</p>



<pre class="wp-block-preformatted">-&gt;whereRaw('(paid_at, id) &gt; (?, ?)', [...])</pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="bai-hoc-rut-ra" class="wp-block-heading">Bài học rút ra</h2>



<h3 id="1-tranh-or-khi-lam-keyset-pagination" class="wp-block-heading">1. Tránh <code>OR</code> khi làm keyset pagination</h3>



<p>Đặc biệt khi:</p>



<ul class="wp-block-list">
<li>sort theo nhiều cột</li>



<li>đã có composite index</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 id="2-hieu-cach-database-to-chuc-index-quan-trong-hon-orm" class="wp-block-heading">2. Hiểu cách database tổ chức index quan trọng hơn ORM</h3>



<p>ORM giúp code “đẹp”, nhưng:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>không đảm bảo query tối ưu</p>
</blockquote>



<p>Đôi khi, quay về <strong>RAW SQL đúng chỗ</strong> lại là lựa chọn tốt hơn.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h3 id="3-tan-dung-tuple-comparison-trong-postgresql" class="wp-block-heading">3. Tận dụng Tuple Comparison trong PostgreSQL</h3>



<p>Đây là một feature rất mạnh nhưng ít được dùng:</p>



<pre class="wp-block-preformatted">(col1, col2) &gt; (val1, val2)</pre>



<p>Rất phù hợp cho:</p>



<ul class="wp-block-list">
<li>keyset pagination</li>



<li>multi-column sorting</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



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



<p>Một thay đổi nhỏ:</p>



<ul class="wp-block-list">
<li>bỏ <code>OR</code></li>



<li>dùng tuple comparison</li>
</ul>



<p>=&gt; có thể:</p>



<ul class="wp-block-list">
<li>giảm load database</li>



<li>tránh slow query</li>



<li>đơn giản hoá code</li>
</ul>



<p>Đây là một ví dụ điển hình cho việc:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>tối ưu performance không phải lúc nào cũng cần rewrite lớn — đôi khi chỉ cần hiểu đúng cách database hoạt động.</strong></p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p></p>
<p>The post <a href="https://blog.tomosia.com.vn/toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong/">Tối ưu Slow Query PostgreSQL: Khi một dòng code cứu cả hệ thống</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.tomosia.com.vn/toi-uu-slow-query-postgresql-khi-mot-dong-code-cuu-ca-he-thong/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Tối ưu SEO cho Laravel Inertia.js với Vue 3 + Docker + Nginx (Phần 2)</title>
		<link>https://blog.tomosia.com.vn/toi-uu-seo-cho-laravel-inertia-js-voi-vue-3-docker-nginx-phan-2/</link>
					<comments>https://blog.tomosia.com.vn/toi-uu-seo-cho-laravel-inertia-js-voi-vue-3-docker-nginx-phan-2/#respond</comments>
		
		<dc:creator><![CDATA[dang cao]]></dc:creator>
		<pubDate>Thu, 12 Mar 2026 07:35:09 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<guid isPermaLink="false">https://blog.tomosia.com.vn/?p=3981</guid>

					<description><![CDATA[<p>Một hướng dẫn thực tế dành cho các dự án Laravel + Inertia.js + Vue 3 chạy trong&#8230;</p>
<p>The post <a href="https://blog.tomosia.com.vn/toi-uu-seo-cho-laravel-inertia-js-voi-vue-3-docker-nginx-phan-2/">Tối ưu SEO cho Laravel Inertia.js với Vue 3 + Docker + Nginx (Phần 2)</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Một hướng dẫn thực tế dành cho các dự án Laravel + Inertia.js + Vue 3 chạy trong Docker với Nginx, tập trung vào SEO kỹ thuật (technical SEO) và hiệu năng.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="1-tong-quan-kien-truc" class="wp-block-heading">1. Tổng quan kiến trúc</h2>



<p>Một stack phổ biến hiện nay:</p>



<ul class="wp-block-list">
<li><strong>Backend</strong>: Laravel</li>



<li><strong>Frontend</strong>: Vue 3 thông qua Inertia.js (SPA-like)</li>



<li><strong>Web server</strong>: Nginx</li>



<li><strong>Môi trường</strong>: Docker / Docker Compose</li>
</ul>



<p>Vấn đề lớn nhất với SEO trong mô hình này:</p>



<ul class="wp-block-list">
<li>Nội dung render phía client (CSR)</li>



<li>Bot Google có thể crawl được JS nhưng <strong>chậm</strong> và <strong>không ổn định</strong></li>



<li>Thiếu metadata động</li>



<li>TTFB cao nếu cấu hình Docker/Nginx chưa tối ưu</li>
</ul>



<p>Mục tiêu:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Làm cho website <strong>crawl được – index đúng – load nhanh – metadata đầy đủ</strong>.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="2-hieu-dung-ve-seo-voi-inertia-js" class="wp-block-heading">2. Hiểu đúng về SEO với Inertia.js</h2>



<p>Inertia.js không phải SPA thuần, mà là:</p>



<ul class="wp-block-list">
<li>Server vẫn trả HTML gốc</li>



<li>Vue render phía client</li>
</ul>



<p>Tuy nhiên:</p>



<ul class="wp-block-list">
<li>Nội dung chính vẫn xuất hiện sau khi JS chạy</li>



<li>SEO <strong>chấp nhận được</strong>, nhưng <strong>chưa tối ưu</strong> nếu không cấu hình thêm</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="3-thiet-lap-metadata-dong-cuc-ky-quan-trong" class="wp-block-heading">3. Thiết lập metadata động (cực kỳ quan trọng)</h2>



<h3 id="3-1-dung-head-api-cua-inertia" class="wp-block-heading">3.1 Dùng Head API của Inertia</h3>



<pre class="wp-block-code"><code>import { Head } from '@inertiajs/vue3';</code></pre>



<pre class="wp-block-code"><code>&lt;template>
  &lt;Head>
    &lt;title>{{ title }}&lt;/title>
    &lt;meta name="description" :content="description" />
    &lt;meta property="og:title" :content="title" />
    &lt;meta property="og:description" :content="description" />
  &lt;/Head>
&lt;/template></code></pre>



<h3 id="3-2-truyen-du-lieu-tu-laravel-controller" class="wp-block-heading">3.2 Truyền dữ liệu từ Laravel Controller</h3>



<pre class="wp-block-code"><code>return Inertia::render('Post/Show', &#91;
    'title' => $post->title,
    'description' => Str::limit($post->content, 160),
]);</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="4-render-html-ban-dau-day-du-hon-preload-data" class="wp-block-heading">4. Render HTML ban đầu đầy đủ hơn (Preload data)</h2>



<h3 id="kich-hoat-ssr-server-side-rendering-cho-inertia" class="wp-block-heading">Kích hoạt SSR (Server Side Rendering) cho Inertia</h3>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>Đây là bước giúp SEO <strong>nhảy vọt</strong>.</p>
</blockquote>



<h4 id="cai-ssr-tham-khao-chi-tiet-o-phan-1" class="wp-block-heading">Cài SSR (tham khảo chi tiết ở phần 1)</h4>



<pre class="wp-block-code"><code>npm install @inertiajs/server</code></pre>



<pre class="wp-block-code"><code>php artisan inertia:start-ssr</code></pre>



<p>Laravel sẽ:</p>



<ul class="wp-block-list">
<li>Render HTML phía server</li>



<li>Bot Google thấy ngay nội dung</li>
</ul>



<h4 id="loi-ich" class="wp-block-heading">Lợi ích</h4>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Tiêu chí</th><th>Không SSR</th><th>Có SSR</th></tr></thead><tbody><tr><td>Index</td><td>Chậm</td><td>Nhanh</td></tr><tr><td>Nội dung</td><td>Phụ thuộc JS</td><td>Có sẵn</td></tr><tr><td>SEO score</td><td>Trung bình</td><td>Cao</td></tr></tbody></table></figure>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="5-cau-hinh-nginx-chuan-seo-hieu-nang" class="wp-block-heading">5. Cấu hình Nginx chuẩn SEO + hiệu năng</h2>



<h3 id="5-1-bat-gzip-brotli" class="wp-block-heading">5.1 Bật gzip / brotli</h3>



<pre class="wp-block-code"><code>gzip on;
gzip_types text/plain text/css application/json application/javascript text/xml application/xml application/xml+rss text/javascript;</code></pre>



<h3 id="5-2-cache-static-files" class="wp-block-heading">5.2 Cache static files</h3>



<pre class="wp-block-code"><code>location ~* \.(js|css|png|jpg|jpeg|gif|svg|webp)$ {
    expires 30d;
    access_log off;
}</code></pre>



<h3 id="5-3-http-headers-seo" class="wp-block-heading">5.3 HTTP Headers SEO</h3>



<pre class="wp-block-code"><code>add_header X-Content-Type-Options nosniff;
add_header X-Frame-Options SAMEORIGIN;
add_header X-XSS-Protection "1; mode=block";</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="6-docker-toi-uu-cho-seo-ttfb-thap" class="wp-block-heading">6. Docker tối ưu cho SEO (TTFB thấp)</h2>



<h3 id="6-1-su-dung-php-fpm-production" class="wp-block-heading">6.1 Sử dụng PHP-FPM production</h3>



<pre class="wp-block-code"><code>FROM php:8.2-fpm-alpine</code></pre>



<h3 id="6-2-opcache" class="wp-block-heading">6.2 OPcache</h3>



<pre class="wp-block-code"><code>opcache.enable=1
opcache.memory_consumption=256
opcache.max_accelerated_files=20000</code></pre>



<h3 id="6-3-docker-compose-network-noi-bo" class="wp-block-heading">6.3 Docker Compose network nội bộ</h3>



<p>Tránh expose không cần thiết, giảm latency.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="7-sitemap-robots-txt-tu-dong-trong-laravel" class="wp-block-heading">7. Sitemap &amp; robots.txt tự động trong Laravel</h2>



<h3 id="7-1-cai-package" class="wp-block-heading">7.1 Cài package</h3>



<pre class="wp-block-code"><code>composer require spatie/laravel-sitemap</code></pre>



<h3 id="7-2-tao-sitemap" class="wp-block-heading">7.2 Tạo sitemap</h3>



<pre class="wp-block-code"><code>SitemapGenerator::create(config('app.url'))
    ->writeToFile(public_path('sitemap.xml'));</code></pre>



<h3 id="7-3-robots-txt" class="wp-block-heading">7.3 robots.txt</h3>



<pre class="wp-block-code"><code>User-agent: *
Allow: /
Sitemap: https://domain.com/sitemap.xml</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="8-schema-org-rich-snippets" class="wp-block-heading">8. Schema.org (Rich snippets)</h2>



<p>Thêm JSON-LD trong Inertia Head:</p>



<pre class="wp-block-code"><code>&lt;script type="application/ld+json">
{
  "@context": "https://schema.org",
  "@type": "Article",
  "headline": "{{ title }}"
}
&lt;/script></code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="9-kiem-tra-seo" class="wp-block-heading">9. Kiểm tra SEO</h2>



<h3 id="cong-cu-nen-dung" class="wp-block-heading">Công cụ nên dùng</h3>



<ul class="wp-block-list">
<li>Google Search Console</li>



<li>PageSpeed Insights</li>



<li>Lighthouse</li>



<li>Screaming Frog</li>
</ul>



<h3 id="kpi-nen-theo-doi" class="wp-block-heading">KPI nên theo dõi</h3>



<ul class="wp-block-list">
<li>LCP &lt; 2.5s</li>



<li>CLS &lt; 0.1</li>



<li>TTFB &lt; 500ms</li>



<li>Index coverage</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="10-checklist-nhanh" class="wp-block-heading">10. Checklist nhanh</h2>



<p>✅ Inertia Head metadata</p>



<p>✅ SSR enabled</p>



<p>✅ Gzip + cache static</p>



<p>✅ Sitemap.xml</p>



<p>✅ robots.txt</p>



<p>✅ JSON-LD</p>



<p>✅ Docker optimized</p>



<p>✅ OPcache</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



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



<p>Laravel + Inertia.js + Vue 3 hoàn toàn <strong>SEO tốt</strong> nếu:</p>



<ul class="wp-block-list">
<li>Có SSR</li>



<li>Metadata đúng</li>



<li>Server nhanh</li>



<li>Cấu hình Nginx &amp; Docker chuẩn</li>
</ul>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p>SEO không chỉ là content – mà là <strong>kiến trúc hệ thống</strong>.</p>
</blockquote>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Nếu bạn đang chạy dự án Laravel Inertia trên Docker và muốn audit SEO chuyên sâu (TTFB, crawlability, SSR setup), bạn có thể mở rộng thêm:</p>



<ul class="wp-block-list">
<li>Redis cache</li>



<li>Full page cache cho bot</li>



<li>Edge caching (Cloudflare)</li>



<li>Prerender.io cho bot</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p>Chúc bạn tối ưu SEO thành công </p>
<p>The post <a href="https://blog.tomosia.com.vn/toi-uu-seo-cho-laravel-inertia-js-voi-vue-3-docker-nginx-phan-2/">Tối ưu SEO cho Laravel Inertia.js với Vue 3 + Docker + Nginx (Phần 2)</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.tomosia.com.vn/toi-uu-seo-cho-laravel-inertia-js-voi-vue-3-docker-nginx-phan-2/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>SQL Partitioning: &#8220;Chia Để Trị&#8221; Dữ Liệu Khổng Lồ</title>
		<link>https://blog.tomosia.com.vn/sql-partitioning-chia-de-tri-du-lieu-khong-lo/</link>
					<comments>https://blog.tomosia.com.vn/sql-partitioning-chia-de-tri-du-lieu-khong-lo/#respond</comments>
		
		<dc:creator><![CDATA[Hoang Nam]]></dc:creator>
		<pubDate>Fri, 06 Feb 2026 08:34:06 +0000</pubDate>
				<category><![CDATA[Kinh nghiệm]]></category>
		<category><![CDATA[MySQL]]></category>
		<category><![CDATA[Database]]></category>
		<category><![CDATA[Sql]]></category>
		<category><![CDATA[partition]]></category>
		<category><![CDATA[mysql]]></category>
		<category><![CDATA[postgres]]></category>
		<category><![CDATA[db]]></category>
		<guid isPermaLink="false">https://blog.tomosia.com.vn/?p=4215</guid>

					<description><![CDATA[<p>Database Của Bạn Đang &#8220;Kêu Cứu&#8221; Vì Chậm? Đã Đến Lúc &#8220;Chia Để Trị&#8221;! Bạn có đang &#8220;đau&#8230;</p>
<p>The post <a href="https://blog.tomosia.com.vn/sql-partitioning-chia-de-tri-du-lieu-khong-lo/">SQL Partitioning: &#8220;Chia Để Trị&#8221; Dữ Liệu Khổng Lồ</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h2 id="database-cua-ban-dang-keu-cuu-vi-cham-da-den-luc-chia-de-tri" class="wp-block-heading">Database Của Bạn Đang &#8220;Kêu Cứu&#8221; Vì Chậm? Đã Đến Lúc &#8220;Chia Để Trị&#8221;!</h2>



<p>Bạn có đang &#8220;đau đầu&#8221; vì một câu query SQL, dù rất đơn giản, lại chạy mãi không xong trên một bảng dữ liệu ngày càng &#8220;béo phì&#8221;? Bạn là dev đang săn lùng từng mili giây hiệu suất, là tester đang vật lộn với những bài test performance, hay đơn giản là người mới tò mò về thế giới dữ liệu?</p>



<p>Nếu câu trả lời là &#8220;có&#8221;, thì đây chính là &#8220;liều thuốc&#8221; bạn cần.</p>



<p>Hãy quên đi những định nghĩa khô khan. Hôm nay, chúng ta sẽ khám phá <strong>SQL Partitioning</strong> – một kỹ thuật &#8220;chia để trị&#8221; đầy quyền năng – qua những ví dụ đời thường và những bí kíp thực chiến mà bạn sẽ không tìm thấy trong sách vở.</p>



<h2 id="1-sql-partitioning-la-gi" class="wp-block-heading">1. SQL Partitioning Là Gì? </h2>



<p>Trước hết, Hãy tưởng tượng database của bạn là một cái nhà kho khổng lồ, chứa hàng triệu món đồ (dữ liệu). Khi cần tìm một món đồ, bạn phải chạy khắp cái nhà kho đó. Rất mất thời gian!</p>



<p>Tuy nhiên, nếu bạn chia nhà kho thành các <strong>gian nhỏ có nhãn rõ ràng</strong>, mọi thứ sẽ khác.</p>



<p><strong>SQL Partitioning</strong> chính là việc bạn thông minh sắp xếp nhà kho đó thành các <strong>gian hàng nhỏ hơn, được dán nhãn rõ ràng</strong> (gọi là các <em>partition</em> &#8211; phân vùng):</p>



<ul class="wp-block-list">
<li>Gian hàng &#8220;Hóa đơn năm 2023&#8221;</li>



<li>Gian hàng &#8220;Hóa đơn năm 2024&#8221;</li>



<li>Gian hàng &#8220;Hóa đơn năm 2025&#8221;</li>
</ul>



<div class="wp-block-kevinbatdorf-code-block-pro" 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;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:#2f363c;color:#d3d7dd">Code mẫu:</span><span role="button" tabindex="0" data-code="CREATE TABLE invoice (
    invoice_id INT,
    invoice_date DATE,
    amount DECIMAL(10,2)
)
PARTITION BY RANGE (YEAR(invoice_date)) (
    PARTITION p2022 VALUES LESS THAN (2023),
    PARTITION p2023 VALUES LESS THAN (2024),
    PARTITION p2024 VALUES LESS THAN (2025),
    PARTITION p_future VALUES LESS THAN MAXVALUE
);" style="color:#e1e4e8;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 github-dark" style="background-color: #24292e" tabindex="0"><code><span class="line"><span style="color: #F97583">CREATE</span><span style="color: #E1E4E8"> </span><span style="color: #F97583">TABLE</span><span style="color: #E1E4E8"> </span><span style="color: #B392F0">invoice</span><span style="color: #E1E4E8"> (</span></span>
<span class="line"><span style="color: #E1E4E8">    invoice_id </span><span style="color: #F97583">INT</span><span style="color: #E1E4E8">,</span></span>
<span class="line"><span style="color: #E1E4E8">    invoice_date </span><span style="color: #F97583">DATE</span><span style="color: #E1E4E8">,</span></span>
<span class="line"><span style="color: #E1E4E8">    amount </span><span style="color: #F97583">DECIMAL</span><span style="color: #E1E4E8">(</span><span style="color: #79B8FF">10</span><span style="color: #E1E4E8">,</span><span style="color: #79B8FF">2</span><span style="color: #E1E4E8">)</span></span>
<span class="line"><span style="color: #E1E4E8">)</span></span>
<span class="line"><span style="color: #F97583">PARTITION</span><span style="color: #E1E4E8"> </span><span style="color: #F97583">BY</span><span style="color: #E1E4E8"> </span><span style="color: #F97583">RANGE</span><span style="color: #E1E4E8"> (</span><span style="color: #79B8FF">YEAR</span><span style="color: #E1E4E8">(invoice_date)) (</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #F97583">PARTITION</span><span style="color: #E1E4E8"> p2022 </span><span style="color: #F97583">VALUES</span><span style="color: #E1E4E8"> LESS THAN (</span><span style="color: #79B8FF">2023</span><span style="color: #E1E4E8">),</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #F97583">PARTITION</span><span style="color: #E1E4E8"> p2023 </span><span style="color: #F97583">VALUES</span><span style="color: #E1E4E8"> LESS THAN (</span><span style="color: #79B8FF">2024</span><span style="color: #E1E4E8">),</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #F97583">PARTITION</span><span style="color: #E1E4E8"> p2024 </span><span style="color: #F97583">VALUES</span><span style="color: #E1E4E8"> LESS THAN (</span><span style="color: #79B8FF">2025</span><span style="color: #E1E4E8">),</span></span>
<span class="line"><span style="color: #E1E4E8">    </span><span style="color: #F97583">PARTITION</span><span style="color: #E1E4E8"> p_future </span><span style="color: #F97583">VALUES</span><span style="color: #E1E4E8"> LESS THAN MAXVALUE</span></span>
<span class="line"><span style="color: #E1E4E8">);</span></span></code></pre></div>



<p>Khi sếp yêu cầu bạn tìm hóa đơn tháng 12/2024, bạn chỉ việc đi thẳng đến gian hàng &#8220;Hóa đơn năm 2024&#8221; để tìm. Nhanh hơn gấp bội!</p>



<p>Quan trọng hơn, dù dữ liệu được chia nhỏ ra bên dưới, đối với ứng dụng của bạn nó vẫn là <strong>một bảng duy nhất</strong>. Đó chính là sự “thần kỳ” của partition.</p>



<h3 id="loi-ich-vang-cua-partitioning" class="wp-block-heading">Lợi ích VÀNG của Partitioning:</h3>



<ul class="wp-block-list">
<li><strong>Tăng tốc độ truy vấn &#8220;tên lửa&#8221;:</strong> Khi bạn lọc dữ liệu theo &#8220;nhãn&#8221; của gian hàng (ví dụ: <code>WHERE OrderDate BETWEEN '2024-01-01' AND '2024-12-31'</code>), hệ thống sẽ chỉ quét gian hàng &#8220;2024&#8221;, bỏ qua toàn bộ các gian hàng khác. Đây gọi là kỹ thuật <em>Partition Pruning</em> (cắt tỉa phân vùng).</li>



<li><strong>Quản lý dữ liệu dễ như trở bàn tay:</strong> Muốn xóa toàn bộ dữ liệu năm 2020? Thay vì chạy lệnh <code>DELETE</code> hàng triệu dòng và chờ &#8220;dài cổ&#8221;, bạn chỉ cần &#8220;dỡ&#8221; cả gian hàng 2020 đi. Xong!</li>



<li><strong>Bảo trì nhẹ nhàng hơn:</strong> Việc bảo trì (như rebuild index) trên một gian hàng nhỏ sẽ nhanh và ít ảnh hưởng đến hệ thống hơn là làm trên cả cái nhà kho.</li>
</ul>



<h2 id="2-cac-loai-vu-khi-phan-vung-pho-bien-chon-dung-thang-to" class="wp-block-heading">2. Các &#8220;Loại Vũ Khí&#8221; Phân Vùng Phổ Biến: Chọn Đúng, Thắng To</h2>



<p>Có 3 loại partitioning chính. Việc chọn đúng loại sẽ quyết định sự thành bại của bạn.</p>



<figure class="wp-block-image size-large"><img fetchpriority="high" decoding="async" width="1024" height="683" src="https://blog.tomosia.com.vn/wp-content/uploads/2026/02/type-partition-1024x683.png" alt="" class="wp-image-4232" srcset="https://blog.tomosia.com.vn/wp-content/uploads/2026/02/type-partition-1024x683.png 1024w, https://blog.tomosia.com.vn/wp-content/uploads/2026/02/type-partition-300x200.png 300w, https://blog.tomosia.com.vn/wp-content/uploads/2026/02/type-partition-768x512.png 768w, https://blog.tomosia.com.vn/wp-content/uploads/2026/02/type-partition-380x253.png 380w, https://blog.tomosia.com.vn/wp-content/uploads/2026/02/type-partition-800x533.png 800w, https://blog.tomosia.com.vn/wp-content/uploads/2026/02/type-partition-1160x773.png 1160w, https://blog.tomosia.com.vn/wp-content/uploads/2026/02/type-partition.png 1536w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<h3 id="range-partitioning-phan-vung-theo-khoang" class="wp-block-heading">RANGE Partitioning (Phân vùng theo Khoảng)</h3>



<ul class="wp-block-list">
<li><strong>Ý tưởng:</strong> Chia dữ liệu theo một khoảng giá trị liên tục.</li>



<li><strong>Ví dụ Kinh điển:</strong> Phân vùng bảng <code>Sales</code> theo cột <code>OrderDate</code>. Mỗi phân vùng chứa dữ liệu của một tháng hoặc một quý.</li>



<li><strong>Khi nào dùng:</strong> Cực kỳ hoàn hảo cho dữ liệu chuỗi thời gian (time-series) như logs, giao dịch, dữ liệu cảm biến IoT.</li>
</ul>



<h3 id="list-partitioning-phan-vung-theo-danh-sach" class="wp-block-heading">LIST Partitioning (Phân vùng theo Danh sách)</h3>



<ul class="wp-block-list">
<li><strong>Ý tưởng:</strong> Chia dữ liệu theo một danh sách các giá trị cụ thể.</li>



<li><strong>Ví dụ Kinh điển:</strong> Phân vùng bảng <code>Customers</code> theo cột <code>Region</code> (Vùng miền). Một phân vùng cho &#8216;Miền Bắc&#8217;, một cho &#8216;Miền Trung&#8217;, một cho &#8216;Miền Nam&#8217;.</li>



<li><strong>Khi nào dùng:</strong> Phù hợp khi cột phân vùng có một tập hợp các giá trị hữu hạn và rõ ràng (như Tỉnh/Thành, Trạng thái, Loại sản phẩm).</li>
</ul>



<h3 id="hash-partitioning-phan-vung-theo-ham-bam" class="wp-block-heading">HASH Partitioning (Phân vùng theo Hàm băm)</h3>



<ul class="wp-block-list">
<li><strong>Ý tưởng:</strong> Tự động phân bổ đều dữ liệu vào một số lượng phân vùng đã định trước.</li>



<li><strong>Ví dụ Kinh điển:</strong> Phân vùng bảng <code>Users</code> theo <code>user_id</code>. Hệ thống sẽ dùng một thuật toán để đảm bảo dữ liệu người dùng được chia đều vào các phân vùng.</li>



<li><strong>Khi nào dùng:</strong> Dùng khi bạn không có cột nào phù hợp cho RANGE hay LIST, và mục tiêu chính là phân tán đều dữ liệu để tránh &#8220;điểm nóng&#8221;.</li>
</ul>



<h2 id="3-bi-kip-thuc-chien-nhung-cu-lua-can-tranh" class="wp-block-heading">3. Bí Kíp Thực Chiến &amp; Những &#8220;Cú Lừa&#8221; Cần Tránh</h2>



<p>Lý thuyết màu hồng, nhưng thực tế thì&#8230; không phải lúc nào cũng vậy. Đây là những kinh nghiệm xương máu giúp bạn tránh đi vào &#8220;vết xe đổ&#8221;.</p>



<h3 id="hoi-dap-nhanh-cac-loi-sai-kinh-dien-khi-dung-partitioning" class="wp-block-heading">Hỏi &amp; Đáp Nhanh: Các Lỗi Sai Kinh Điển Khi Dùng Partitioning</h3>



<p><strong>Câu hỏi:</strong> <em>Tôi đã phân vùng rồi mà query vẫn chậm như rùa. Tôi đã làm sai ở đâu?</em></p>



<p><strong>Trả lời:</strong> Rất có thể bạn đã mắc phải một trong những lỗi sau:</p>



<ul class="wp-block-list">
<li><strong>Chọn sai &#8220;Chìa Khóa&#8221; (Partition Key):</strong> Đây là sai lầm chết người nhất. Nếu các query của bạn không lọc theo cột mà bạn dùng để phân vùng (<code>partition key</code>), thì việc phân vùng hoàn toàn vô dụng. Hệ thống vẫn phải quét tất cả các &#8220;gian hàng&#8221;.</li>



<li><strong>&#8220;Tham Lam&#8221; Chia Quá Nhiều Phân Vùng:</strong> Chia nhỏ quá không tốt! Quá nhiều phân vùng sẽ làm tăng gánh nặng quản lý cho database, khiến việc lên kế hoạch truy vấn chậm đi. Vài chục đến vài trăm phân vùng thường là con số hợp lý.</li>



<li><strong>Dữ Liệu Phân Bổ Không Đều:</strong> Nếu 90% dữ liệu của bạn bị dồn vào chỉ một phân vùng, thì lợi ích gần như bằng không. Hãy phân tích dữ liệu trước khi chọn cột phân vùng.</li>



<li><strong>&#8220;Bỏ quên&#8221; Index:</strong> Partitioning và Indexing là &#8220;cặp bài trùng&#8221; chứ không phải kẻ thù. Phân vùng giúp giới hạn số lượng dữ liệu cần quét, còn index giúp tìm kiếm nhanh chóng bên trong phân vùng đó.</li>



<li><strong>Đừng chia quá nhỏ (Over-partitioning)</strong><br>Đừng chia mỗi ngày 1 partition nếu bạn có dữ liệu 10 năm (sẽ ra 3650 partitions). Khi partitions quá nhiều:
<ul class="wp-block-list">
<li>Hệ điều hành quá tải vì mở quá nhiều file.</li>



<li>Database tốn tài nguyên để quản lý metadata.</li>



<li>Query có thể chậm hơn cả không partition.</li>



<li><strong>Lời khuyên:</strong> Giữ số lượng partition ở mức hợp lý (dưới 100 thường là con số an toàn cho MySQL, Oracle thì có thể nhiều hơn).</li>
</ul>
</li>



<li><strong>Cẩn thận với Primary Key</strong>: Hầu hết các DB engine (như MySQL) yêu cầu cột dùng để Partition phải nằm trong Primary Key. Điều này có nghĩa là PK của bạn sẽ phải đổi từ <code>(id)</code> sang <code>(id, order_date)</code>. Dev team cần lưu ý chỗ này kẻo code logic bị sai.</li>
</ul>



<h2 id="ket-luan-partitioning-la-mot-tu-duy-khong-chi-la-cong-cu" class="wp-block-heading">Kết Luận: Partitioning Là Một Tư Duy, Không Chỉ Là Công Cụ</h2>



<p>SQL Partitioning không phải là viên đạn bạc cho mọi vấn đề. Nó là một công cụ quản lý dữ liệu cực kỳ mạnh mẽ ở quy mô lớn, với &#8220;tác dụng phụ&#8221; tuyệt vời là cải thiện hiệu suất.</p>



<p><strong>Lời khuyên cuối cùng:</strong> Đừng vội vàng phân vùng mọi bảng bạn có. Hãy luôn bắt đầu bằng việc tối ưu query và index. Chỉ khi bảng dữ liệu đã thực sự phình to (hàng trăm triệu dòng) và các phương pháp khác đã &#8220;bó tay&#8221;, đó mới là lúc bạn rút &#8220;thanh bảo kiếm&#8221; Partitioning ra.</p>



<p>Chúc bạn thành công trên con đường chinh phục dữ liệu!</p>



<p></p>
<p>The post <a href="https://blog.tomosia.com.vn/sql-partitioning-chia-de-tri-du-lieu-khong-lo/">SQL Partitioning: &#8220;Chia Để Trị&#8221; Dữ Liệu Khổng Lồ</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.tomosia.com.vn/sql-partitioning-chia-de-tri-du-lieu-khong-lo/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
		
		<series:name><![CDATA[Web vulnerability]]></series:name>
	</item>
		<item>
		<title>Basic Design và Detail Design trong dự án Nhật (Giới thiệu)</title>
		<link>https://blog.tomosia.com.vn/basic-design-va-detail-design-trong-du-an-nhat-gioi-thieu/</link>
					<comments>https://blog.tomosia.com.vn/basic-design-va-detail-design-trong-du-an-nhat-gioi-thieu/#respond</comments>
		
		<dc:creator><![CDATA[phi lai]]></dc:creator>
		<pubDate>Mon, 02 Feb 2026 06:19:36 +0000</pubDate>
				<category><![CDATA[System Design]]></category>
		<guid isPermaLink="false">https://blog.tomosia.com.vn/?p=3471</guid>

					<description><![CDATA[<p>Tài liệu Basic Design và Detail Design trong dự án Nhật Trong những năm đầu đi làm và&#8230;</p>
<p>The post <a href="https://blog.tomosia.com.vn/basic-design-va-detail-design-trong-du-an-nhat-gioi-thieu/">Basic Design và Detail Design trong dự án Nhật (Giới thiệu)</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<h3 id="tai-lieu-basic-design-va-detail-design-trong-du-an-nhat" class="wp-block-heading">Tài liệu Basic Design và Detail Design trong dự án Nhật</h3>



<p>Trong những năm đầu đi làm và có cơ hội làm việc các dự án Nhật, tôi được tham gia vào nhiều dự án phát triển hệ thống quản lý sản xuất.</p>



<p>Qua quá trình đó, tôi nhận ra cách người Nhật tổ chức tài liệu thiết kế phần mềm </p>



<p>— gồm <strong>Basic Design</strong> và <strong>Detail Design</strong> </p>



<p>— là nền tảng cốt lõi của toàn bộ quy trình phát triển. Mọi thứ được mô tả chi tiết, có quy tắc, và giúp đội dự án hiểu cùng một ngôn ngữ.</p>



<p>Bài viết này chia sẻ cách hiểu, cấu trúc và phương pháp viết tài liệu thiết kế phần mềm trong môi trường dự án Nhật Bản, dựa trên kinh nghiệm thực tế triển khai nhiều module sản xuất khác nhau.</p>



<p><strong>(Lưu ý: các ảnh trong bài đăng được tham khảo từ nhiều nguồn kỹ thuật trang web Nhật để giúp minh họa rõ hơn cho nội dung)</strong></p>



<figure class="wp-block-image size-large"><img decoding="async" src="https://www.zooops-japan.co.jp/wordpress/wp-content/uploads/2019/03/V%E5%AD%97%E3%83%A2%E3%83%87%E3%83%AB%E5%9B%B3800_400pix.jpg" alt="V字モデル システム開発工程"/></figure>



<figure class="wp-block-image size-full is-style-default"><img decoding="async" width="824" height="545" src="https://blog.tomosia.com.vn/wp-content/uploads/2025/10/image-10.png" alt="" class="wp-image-3476" srcset="https://blog.tomosia.com.vn/wp-content/uploads/2025/10/image-10.png 824w, https://blog.tomosia.com.vn/wp-content/uploads/2025/10/image-10-300x198.png 300w, https://blog.tomosia.com.vn/wp-content/uploads/2025/10/image-10-768x508.png 768w, https://blog.tomosia.com.vn/wp-content/uploads/2025/10/image-10-380x251.png 380w, https://blog.tomosia.com.vn/wp-content/uploads/2025/10/image-10-800x529.png 800w" sizes="(max-width: 824px) 100vw, 824px" /></figure>



<p></p>



<h3 id="1-phan-biet-basic-design-va-detail-design" class="wp-block-heading">1. Phân biệt Basic Design và Detail Design</h3>



<p><strong>Basic Design</strong> (基本設計書) là tài liệu mô tả tổng quan hệ thống: định nghĩa phạm vi, quy tắc nghiệp vụ, và luồng xử lý chính. </p>



<p>Đây là giai đoạn <strong>&#8220;thiết kế khung&#8221;</strong>.</p>



<p><strong>Detail Design</strong> (詳細設計書) là tài liệu triển khai chi tiết, mô tả rõ cách hệ thống hoạt động bên trong: logic xử lý, cấu trúc dữ liệu, và cách các thành phần giao tiếp với nhau. </p>



<p>Đây là bước <strong>&#8220;thiết kế chi tiết kỹ thuật&#8221;.</strong></p>



<figure class="wp-block-image size-large"><img decoding="async" width="1024" height="543" src="https://blog.tomosia.com.vn/wp-content/uploads/2025/10/reiwa_sysdesign_06_002-1024x543.jpg" alt="" class="wp-image-3472" srcset="https://blog.tomosia.com.vn/wp-content/uploads/2025/10/reiwa_sysdesign_06_002-1024x543.jpg 1024w, https://blog.tomosia.com.vn/wp-content/uploads/2025/10/reiwa_sysdesign_06_002-300x159.jpg 300w, https://blog.tomosia.com.vn/wp-content/uploads/2025/10/reiwa_sysdesign_06_002-768x407.jpg 768w, https://blog.tomosia.com.vn/wp-content/uploads/2025/10/reiwa_sysdesign_06_002-1536x814.jpg 1536w, https://blog.tomosia.com.vn/wp-content/uploads/2025/10/reiwa_sysdesign_06_002-2048x1086.jpg 2048w, https://blog.tomosia.com.vn/wp-content/uploads/2025/10/reiwa_sysdesign_06_002-380x201.jpg 380w, https://blog.tomosia.com.vn/wp-content/uploads/2025/10/reiwa_sysdesign_06_002-800x424.jpg 800w, https://blog.tomosia.com.vn/wp-content/uploads/2025/10/reiwa_sysdesign_06_002-1160x615.jpg 1160w, https://blog.tomosia.com.vn/wp-content/uploads/2025/10/reiwa_sysdesign_06_002.jpg 2520w" sizes="(max-width: 1024px) 100vw, 1024px" /></figure>



<p><strong>Basic Design</strong> giúp toàn bộ nhóm hiểu <strong>“hệ thống cần làm gì”, </strong></p>



<p><strong>Detail Design</strong> giúp kỹ sư nắm rõ <strong>“hệ thống sẽ làm như thế nào”. </strong></p>



<p>Hai giai đoạn này gắn liền với nhau và quyết định trực tiếp đến chất lượng dự án.</p>



<h3 id="2-cau-truc-cua-basic-design" class="wp-block-heading">2. Cấu trúc của Basic Design</h3>



<p>Trong dự án Nhật, Basic Design thường có cấu trúc tiêu chuẩn như sau:</p>



<ul class="wp-block-list">
<li><strong>Tên hệ thống và module:</strong> ví dụ “生産管理システム (Production Management System)”.</li>



<li><strong>Tên màn hình hoặc chức năng:</strong> ví dụ “P_I201 – 生産指示登録画面 (Màn hình đăng ký lệnh sản xuất)”.</li>



<li><strong>Mục tiêu:</strong> mô tả mục đích nghiệp vụ của chức năng (ví dụ: đăng ký, sửa, xóa lệnh sản xuất theo mã sản phẩm).</li>



<li><strong>Luồng xử lý tổng quan:</strong> trình bày sơ đồ xử lý từ nhập dữ liệu đến lưu trữ và phản hồi.</li>



<li><strong>Dữ liệu chính:</strong> mô tả các bảng liên quan như ProductionOrder, ProductMaster, LineMaster&#8230;</li>



<li><strong>Quy tắc nghiệp vụ:</strong> mô tả ràng buộc (ví dụ: ngày chỉ thị không nhỏ hơn ngày hiện tại, số lượng > 0).</li>
</ul>



<h3 id="3-cau-truc-chi-tiet-cua-detail-design" class="wp-block-heading">3. Cấu trúc chi tiết của Detail Design</h3>



<p>Detail Design cụ thể hóa từng nội dung của Basic Design để lập trình viên có thể triển khai chính xác. Dưới đây là cấu trúc thông thường trong dự án Nhật:</p>



<h4 id="3-1-thong-tin-chung-ve-man-hinh-chuc-nang" class="wp-block-heading">3.1. Thông tin chung về màn hình / chức năng</h4>



<p>Ví dụ: </p>



<p>Mã chức năng: <strong>P_I201</strong> </p>



<p>Tên chức năng: <strong>生産指示登録画面 (Màn hình đăng ký lệnh sản xuất)</strong> </p>



<p>Người dùng: Bộ phận kế hoạch sản xuất </p>



<p>Mục tiêu: Tạo lệnh sản xuất mới hoặc chỉnh sửa lệnh đã có. </p>



<p>Điều kiện truy cập: Người dùng có quyền “製造指示管理”.</p>



<h4 id="3-2-mo-ta-cac-truong-du-lieu-field-definition" class="wp-block-heading">3.2. Mô tả các trường dữ liệu (Field Definition)</h4>



<figure class="wp-block-image size-large"><img decoding="async" src="https://pm-rasinban.com/wp-content/uploads/2019/09/class-chart.jpg" alt="詳細設計 クラス図 サンプル"/></figure>



<p>Các trường trong màn hình được mô tả chi tiết:</p>



<ul class="wp-block-list">
<li>Tên hiển thị: “生産指示番号 (Mã chỉ thị sản xuất)”</li>



<li>Kiểu dữ liệu: String</li>



<li>Bắt buộc: Có</li>



<li>Giá trị mặc định: Tự sinh theo format YYYYMMDD-nnn</li>



<li>Nguồn dữ liệu: Sequence từ bảng ProductionOrder</li>



<li>Mô tả: Mã duy nhất của lệnh sản xuất trong hệ thống</li>
</ul>



<h4 id="3-3-mapping-du-lieu-data-mapping" class="wp-block-heading">3.3. Mapping dữ liệu (Data Mapping)</h4>



<p>Ví dụ: </p>



<p>ProductionOrderNo → ProductionOrder.OrderNo </p>



<p>ProductCode → ProductMaster.ProductCode </p>



<p>PlanDate → ProductionOrder.PlanDate</p>



<h4 id="3-4-logic-xu-ly-processing-logic" class="wp-block-heading">3.4. Logic xử lý (Processing Logic)</h4>



<figure class="wp-block-image size-large"><img decoding="async" src="https://pm-rasinban.com/wp-content/uploads/2019/09/IPO-doc.jpg" alt="IPO図 詳細設計 処理フロー"/><figcaption class="wp-element-caption">出典：PMラシンバン「IPO図（Input Process Output）による処理設計の例」</figcaption></figure>



<p>Ví dụ: </p>



<p>Khi người dùng nhấn “登録 (Đăng ký)” </p>



<p>→ kiểm tra dữ liệu đầu vào </p>



<p>→ ghi vào bảng ProductionOrder </p>



<p>→ hiển thị thông báo “登録が完了しました” (Đăng ký thành công).</p>



<h4 id="3-5-xu-ly-loi-va-thong-bao-error-handling" class="wp-block-heading">3.5. Xử lý lỗi và thông báo (Error Handling)</h4>



<p>Ví dụ: </p>



<p>ERR201 – Ngày sản xuất nhỏ hơn ngày hiện tại → “生産日が本日より前です。”</p>



<p>ERR202 – Mã sản phẩm không tồn tại → “製品コードが存在しません。”</p>



<h4 id="3-6-giao-dien-nguoi-dung-ui-layout-tham-khao" class="wp-block-heading">3.6. Giao diện người dùng (UI Layout) &#8211; tham khảo</h4>



<figure class="wp-block-image size-full"><img loading="lazy" decoding="async" width="901" height="641" src="https://blog.tomosia.com.vn/wp-content/uploads/2025/10/image-12.png" alt="" class="wp-image-3479" srcset="https://blog.tomosia.com.vn/wp-content/uploads/2025/10/image-12.png 901w, https://blog.tomosia.com.vn/wp-content/uploads/2025/10/image-12-300x213.png 300w, https://blog.tomosia.com.vn/wp-content/uploads/2025/10/image-12-768x546.png 768w, https://blog.tomosia.com.vn/wp-content/uploads/2025/10/image-12-380x270.png 380w, https://blog.tomosia.com.vn/wp-content/uploads/2025/10/image-12-800x569.png 800w" sizes="auto, (max-width: 901px) 100vw, 901px" /></figure>



<p>Mô tả bố cục màn hình: </p>



<p>&#8211; Vùng nhập thông tin lệnh sản xuất (sản phẩm, ngày, số lượng) </p>



<p>&#8211; Bảng danh sách lệnh đã tạo </p>



<p>&#8211; Nút chức năng: Đăng ký / Xóa / Tìm kiếm / Reset </p>



<p>&#8211; Điều kiện hiển thị: chỉ bật nút Xóa khi chọn một dòng hợp lệ.</p>



<h3 id="4-moi-lien-he-giua-basic-design-va-detail-design" class="wp-block-heading">4. Mối liên hệ giữa Basic Design và Detail Design</h3>



<p> Ví dụ: </p>



<p>Basic quy định “Ngày chỉ thị không nhỏ hơn ngày hiện tại” </p>



<p>→ Detail cụ thể hóa điều kiện kiểm tra “PlanDate &lt; Today → ERR201”。</p>



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



<p>Việc viết tài liệu Basic Design và Detail Design là kỹ năng quan trọng giúp dự án Nhật Bản duy trì tính chính xác và khả năng bảo trì. </p>



<p>Một tài liệu rõ ràng, thống nhất và có cấu trúc giúp tất cả các bên </p>



<p>— từ kỹ sư, tester, đến quản lý </p>



<p>— phối hợp hiệu quả và hạn chế sai sót.</p>



<p>Nếu Basic Design giúp mọi người hiểu <strong>“cái gì cần làm”</strong>, thì Detail Design hướng dẫn chi tiết <strong>“làm như thế nào”</strong>. </p>



<p>Khi hai phần này được liên kết chặt chẽ, dự án sẽ đạt độ ổn định, chính xác và phản ánh đúng tinh thần kỹ lưỡng của kỹ sư Nhật.</p>
<p>The post <a href="https://blog.tomosia.com.vn/basic-design-va-detail-design-trong-du-an-nhat-gioi-thieu/">Basic Design và Detail Design trong dự án Nhật (Giới thiệu)</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.tomosia.com.vn/basic-design-va-detail-design-trong-du-an-nhat-gioi-thieu/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<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[Web Server]]></category>
		<category><![CDATA[Ruby]]></category>
		<category><![CDATA[Security]]></category>
		<category><![CDATA[Python]]></category>
		<category><![CDATA[Java]]></category>
		<category><![CDATA[PHP]]></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 loading="lazy" 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="auto, (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 loading="lazy" 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="auto, (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 loading="lazy" 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="auto, (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>Laravel Scheduler: Tư duy thiết kế, trade-off và best practices cho hệ thống lớn</title>
		<link>https://blog.tomosia.com.vn/laravel-scheduler-tu-duy-thiet-ke-trade-off-va-best-practices-cho-he-thong-lon/</link>
					<comments>https://blog.tomosia.com.vn/laravel-scheduler-tu-duy-thiet-ke-trade-off-va-best-practices-cho-he-thong-lon/#respond</comments>
		
		<dc:creator><![CDATA[hoa nguyen]]></dc:creator>
		<pubDate>Thu, 25 Dec 2025 09:26:41 +0000</pubDate>
				<category><![CDATA[PHP]]></category>
		<category><![CDATA[php]]></category>
		<category><![CDATA[Laravel]]></category>
		<category><![CDATA[Scheduler]]></category>
		<category><![CDATA[cronjob]]></category>
		<guid isPermaLink="false">https://blog.tomosia.com.vn/?p=3967</guid>

					<description><![CDATA[<p>Nếu bạn đã đi làm vài năm, bạn sẽ thấy &#8220;Scheduler&#8221; không phải chuyện&#160;viết vài dòng&#160;dailyAt(). Nó là&#160;bài&#8230;</p>
<p>The post <a href="https://blog.tomosia.com.vn/laravel-scheduler-tu-duy-thiet-ke-trade-off-va-best-practices-cho-he-thong-lon/">Laravel Scheduler: Tư duy thiết kế, trade-off và best practices cho hệ thống lớn</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Nếu bạn đã đi làm vài năm, bạn sẽ thấy &#8220;Scheduler&#8221; không phải chuyện&nbsp;<em>viết vài dòng&nbsp;<code>dailyAt()</code></em>. Nó là&nbsp;<strong>bài toán reliability + performance + observability</strong>. Sai một ly là: trùng charge, spam notify, kẹt DB lúc 9h, hoặc &#8220;đêm qua không chạy mà không ai biết&#8221;.</p>



<p>Bài này chia sẻ theo góc nhìn hệ thống lớn: <strong>ra quyết định kỹ thuật</strong> khi số job tăng nhanh, dữ liệu lớn, nhiều worker, CI/CD liên tục, thậm chí realtime.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="1-nguyên-tắc-số-1-scheduler-phải-nhẹ--việc-nặng-đẩy-qua-queue"><span id="1-nguyen-tac-so-1-scheduler-phai-nhe-viec-nang-day-qua-queue">1. Nguyên tắc số 1: Scheduler phải &#8220;nhẹ&#8221; — việc nặng đẩy qua queue</span></h2>



<p><strong>Tư duy</strong>: Scheduler là &#8220;orchestrator&#8221;, không phải &#8220;executor&#8221;.</p>



<p>Scheduler chỉ làm 2 việc:</p>



<ul class="wp-block-list">
<li>Quyết định &#8220;đúng giờ thì trigger cái gì&#8221;</li>



<li>Dispatch nhanh, ghi dấu vết (batch metadata)</li>
</ul>



<p>Việc nặng (DB scan, gọi API, gửi notify hàng loạt, reconcile, billing…) → chạy trong&nbsp;<strong>queued jobs</strong>&nbsp;+ nhiều worker.</p>



<p><strong>Trade-off</strong>:</p>



<ul class="wp-block-list">
<li>✅ Ưu: Scale bằng worker, retry/backoff chuẩn, tách tải khỏi scheduler</li>



<li>⚠️ Nhược: Job có thể chạy trễ → phải thiết kế theo &#8220;batch time&#8221; (xem phần 4)</li>
</ul>



<p><strong>Sai lầm thường gặp</strong>: Nhét logic nặng vào scheduler (closure/command) rồi &#8220;đỡ nghẽn&#8221; bằng cách tăng CPU. Đến lúc 100 lịch/ngày thì scheduler thành single-point bottleneck.</p>



<p><strong>Ví dụ</strong>:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\DB;
use Illuminate\Support\Facades\Schedule;

// ❌ SAI: Logic nặng trực tiếp trong scheduler
Schedule::call(function () {
    DB::table('recent_users')->chunk(100, function ($users) {
        foreach ($users as $user) {
            // Process payment, send email, update DB...
            // Nếu có 1000 users, scheduler sẽ bị block rất lâu
        }
    });
})->dailyAt('09:00');

// ✅ ĐÚNG: Scheduler chỉ dispatch job
Schedule::job(new ProcessRecentUsersJob)
    ->dailyAt('09:00');</code></pre>



<p>Scheduler chỉ trigger đúng giờ, không bao giờ chạy logic business trực tiếp. Worker sẽ xử lý job trong queue.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="2-đúng-một-lần-theo-nghĩa-business--đúng-một-lần-theo-nghĩa-kỹ-thuật"><span id="2-dung-mot-lan-theo-nghia-business-dung-mot-lan-theo-nghia-ky-thuat">2. &#8220;Đúng một lần&#8221; theo nghĩa business &gt; &#8220;đúng một lần&#8221; theo nghĩa kỹ thuật</span></h2>



<p>Ở hệ thống lớn, &#8220;at least once&#8221; là mặc định. Scheduler/queue/worker/retry/deploy đều có thể làm job chạy lại.</p>



<p><strong>Best practice</strong>: Thiết kế job theo&nbsp;<strong>idempotent</strong>:</p>



<ul class="wp-block-list">
<li>Chạy 2 lần vẫn ra kết quả đúng</li>



<li>Side-effect (charge/send/update) phải có khóa logic hoặc unique key</li>
</ul>



<p>Dùng lock ở&nbsp;<strong>đúng tầng</strong>:</p>



<ul class="wp-block-list">
<li>Tầng scheduler: Chống chồng / chạy một server (khi HA)</li>



<li>Tầng job/business: Lock theo entity/time-window (mới là thứ bảo vệ cuối)</li>
</ul>



<p><strong>Trade-off</strong>:</p>



<ul class="wp-block-list">
<li>Lock nhiều quá → Giảm throughput, dễ nghẽn nếu lock sai granularity</li>



<li>Lock ít quá → Trùng dữ liệu/side-effect</li>
</ul>



<p><strong>Sai lầm thường gặp</strong>: Tin tuyệt đối vào &#8220;chống chồng&#8221; của scheduler rồi bỏ qua idempotency. Một ngày deploy &#8220;đúng lúc&#8221;, bạn sẽ hiểu.</p>



<p><strong>Ví dụ</strong>:</p>



<p>Job có idempotency key dựa trên&nbsp;<code>entity_id + batch_time</code>. Concept này áp dụng cho mọi job có side-effect quan trọng. Trong scheduler, bạn chỉ dispatch job với metadata:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::job(new ProcessOrdersJob)
    ->dailyAt('09:00');</code></pre>



<p>Job sẽ tự implement idempotency check trong&nbsp;<code>handle()</code>&nbsp;method để đảm bảo không xử lý trùng.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="3-khi-nào-dùng-runinbackground-và-khi-nào-bắt-buộc-dùng-queue"><span id="3-khi-nao-dung-runinbackground-va-khi-nao-bat-buoc-dung-queue">3. Khi nào dùng&nbsp;<code>runInBackground()</code>&nbsp;và khi nào &#8220;bắt buộc&#8221; dùng queue?</span></h2>



<p>Tôi coi&nbsp;<code>runInBackground()</code>&nbsp;là&nbsp;<strong>giải pháp tactical</strong>, không phải chiến lược lâu dài.</p>



<p><strong>Dùng&nbsp;<code>runInBackground()</code>&nbsp;khi</strong>:</p>



<ul class="wp-block-list">
<li>Task là command/exec tương đối ngắn</li>



<li>Không cần scale theo tải</li>



<li>Bạn chỉ cần tránh scheduler bị giữ process quá lâu</li>
</ul>



<p><strong>Bắt buộc đưa qua queue khi</strong>:</p>



<ul class="wp-block-list">
<li>Task có thể chạy vài chục giây/phút</li>



<li>Task có thể &#8220;nở&#8221; theo dữ liệu (data grows)</li>



<li>Task có side-effect quan trọng (payment/notify hàng loạt)</li>



<li>Cần retry/backoff, rate-limit, phân ưu tiên</li>
</ul>



<p><strong>Sai lầm thường gặp</strong>: &#8220;Task đang lâu → thêm&nbsp;<code>runInBackground()</code>&#8221; và coi như xong. Thực tế bạn chỉ chuyển nghẽn từ scheduler sang CPU/DB/connections của cùng máy.</p>



<p><strong>Ví dụ</strong>:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

// ✅ Dùng runInBackground cho command nhẹ, không cần scale
Schedule::command('cache:clear')
    ->hourly()
    ->runInBackground();

Schedule::command('emails:send')
    ->dailyAt('02:00')
    ->runInBackground();

// ✅ Bắt buộc dùng queue cho job có side-effect quan trọng
Schedule::job(new SendOrderConfirmationJob)
    ->dailyAt('09:00'); // Không có runInBackground, vì đã là queued job</code></pre>



<p>Chỉ có 2 loại command dùng&nbsp;<code>runInBackground()</code>:</p>



<ol class="wp-block-list">
<li><strong>Maintenance commands</strong> (cleanup, cache) — không ảnh hưởng business</li>



<li><strong>Monitoring commands</strong> — cần chạy nhanh, không cần retry</li>
</ol>



<p>Tất cả job business đều qua queue để có retry/backoff/scale.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="4-bài-toán-lớn-nhất-của-hệ-queue-job-chạy-trễ--lệch-cửa-sổ-dữ-liệu"><span id="4-bai-toan-lon-nhat-cua-he-queue-job-chay-tre-%e2%86%92-lech-cua-so-du-lieu">4. Bài toán lớn nhất của hệ queue: Job chạy trễ → lệch cửa sổ dữ liệu</span></h2>



<p>Đây là điểm phân biệt hệ &#8220;chạy được&#8221; và hệ &#8220;chạy đúng&#8221;.</p>



<p><strong>Vấn đề</strong>:</p>



<ul class="wp-block-list">
<li>Scheduler trigger 09:00, worker chạy 09:04</li>



<li>Nếu job query theo <code>now()</code> lúc 09:04, bạn vừa:
<ul class="wp-block-list">
<li>Miss dữ liệu 09:00–09:04 (tùy window)</li>



<li>Hoặc duplicate với batch trước</li>
</ul>
</li>
</ul>



<p><strong>Giải pháp</strong>: Scheduler&nbsp;<strong>đóng dấu batch time</strong>&nbsp;ngay lúc dispatch (tôi hay gọi&nbsp;<code>scheduledAt</code>/<code>batchAt</code>). Job luôn dùng&nbsp;<code>scheduledAt</code>&nbsp;để:</p>



<ul class="wp-block-list">
<li>Tính window dữ liệu</li>



<li>Log/audit theo batch</li>



<li>Tạo idempotency key</li>
</ul>



<p><strong>Trade-off</strong>:</p>



<ul class="wp-block-list">
<li>Tốn thêm metadata, thêm &#8220;kỷ luật&#8221; trong code</li>



<li>Đổi lại: Batch chạy trễ vẫn <strong>đúng cửa sổ</strong>, dễ đối soát, dễ replay theo batch</li>
</ul>



<p><strong>Ví dụ — Pattern batch timestamp</strong>:</p>



<p>Concept: Scheduler đóng dấu batch time ngay lúc dispatch. Job nhận batch time và dùng để tính time window, không dùng&nbsp;<code>now()</code>.</p>



<p><strong>Sử dụng trong Laravel 12</strong>:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::job(new ProcessOrdersJob)
    ->dailyAt('09:00');</code></pre>



<p>Job sẽ nhận&nbsp;<code>scheduledAt</code>&nbsp;từ scheduler event và dùng để query đúng time window.</p>



<p><strong>Ví dụ với sub-minute scheduling trong Laravel 12</strong>:</p>



<p>Theo&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#sub-minute-scheduled-tasks">Laravel 12.x Sub-Minute Scheduled Tasks</a>:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

// Laravel 12 hỗ trợ sub-minute scheduling
Schedule::call(function () {
    // Process tasks every 30 seconds
})->everyThirtySeconds();</code></pre>



<p><strong>Lợi ích</strong>:</p>



<ul class="wp-block-list">
<li>Job chạy trễ 3 phút vẫn query đúng cửa sổ 09:00–09:01</li>



<li>Dễ đối soát: &#8220;Batch 09:00 đã xử lý bao nhiêu records?&#8221;</li>



<li>Dễ replay: &#8220;Chạy lại batch 09:00&#8221; → chỉ cần tạo job với <code>jobCreatedAt = 09:00</code></li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="5-tách-workload-shape-phân-queue-theo-ưu-tiên-tránh-job-rác-ăn-tài-nguyên-job-sống-còn"><span id="5-tach-workload-shape-phan-queue-theo-uu-tien-tranh-job-rac-an-tai-nguyen-job-song-con">5. Tách &#8220;workload shape&#8221;: Phân queue theo ưu tiên, tránh job &#8220;rác&#8221; ăn tài nguyên job &#8220;sống còn&#8221;</span></h2>



<p>Một hệ thống lớn sẽ có job kiểu:</p>



<ul class="wp-block-list">
<li><strong>Critical</strong>: Billing, payment, cancel, inventory, security</li>



<li><strong>Default</strong>: Vận hành</li>



<li><strong>Low</strong>: Marketing, remind, digest</li>
</ul>



<p><strong>Best practice</strong>: Tách queue theo priority, scale worker theo queue. Đặt rule: Job low không bao giờ được làm chậm job critical.</p>



<p><strong>Trade-off</strong>:</p>



<ul class="wp-block-list">
<li>Nhiều queue → Config vận hành phức tạp hơn (supervisor/horizon)</li>



<li>Nhưng nếu không tách, bạn sẽ gặp kiểu &#8220;19h campaign bắn notify&#8221; làm nghẽn payment</li>
</ul>



<p><strong>Lưu ý</strong>: Trong scheduler, bạn có thể chỉ định queue khi schedule job:</p>



<p>Theo&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#scheduling-queued-jobs">Laravel 12.x Scheduling Queued Jobs</a>, method&nbsp;<code>job()</code>&nbsp;nhận queue name làm tham số thứ 2:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::job(new SendOrderConfirmationJob, 'critical')
    ->dailyAt('09:00');</code></pre>



<p>Hoặc nếu job class đã có property&nbsp;<code>$queue</code>, scheduler sẽ tự động dùng queue đó.</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="6-thundering-herd-lúc-tròn-giờ-kẻ-thù-của-db-và-api"><span id="6-thundering-herd-luc-tron-gio-ke-thu-cua-db-va-api">6. &#8220;Thundering herd&#8221; lúc tròn giờ: Kẻ thù của DB và API</span></h2>



<p>Hệ thống lớn rất hay bị &#8220;dồn lịch&#8221; vào 00:00, 09:00, 12:00, 19:00…</p>



<p><strong>Best practice</strong>:</p>



<ul class="wp-block-list">
<li><strong>Stagger</strong>: Dàn đều theo phút/giây</li>



<li>Nếu bắt buộc tròn giờ: Chia nhỏ thành nhiều job chunk, có rate-limit/backoff</li>



<li>Không quét toàn bảng: Dùng cursor/chunk theo index, watermark theo batch</li>
</ul>



<p><strong>Sai lầm thường gặp</strong>: Một job &#8220;tổng hợp cuối ngày&#8221; chạy 00:00 quét cả bảng 50 triệu dòng. Đêm đó DB chết, sáng không ai biết vì không có alert.</p>



<p><strong>Ví dụ — Stagger pattern trong scheduler</strong>:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

// Dàn đều theo phút để tránh thundering herd
$times = &#91;'9:03', '10:03', '11:03', '12:03', '13:03', '14:03', '15:03', '16:03', '17:03', '18:03', '19:03', '20:03', '21:03'];

foreach ($times as $time) {
    Schedule::job(new ProcessHourlyTaskJob)
        ->at($time);
}</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="7-observability-là-bắt-buộc-không-ai-biết-scheduler-fail--thất-bại-hệ-thống"><span id="7-observability-la-bat-buoc-khong-ai-biet-scheduler-fail-that-bai-he-thong">7. Observability là bắt buộc: &#8220;Không ai biết scheduler fail&#8221; = thất bại hệ thống</span></h2>



<p>Scheduler fail nguy hiểm vì nó&nbsp;<strong>âm thầm</strong>.</p>



<p><strong>Best practice</strong>:</p>



<ul class="wp-block-list">
<li>Log theo batch (scheduledAt, job name, duration, result)</li>



<li>Có alert khi fail (hook/event/ping tùy hệ)</li>



<li>Có &#8220;schedule inventory&#8221;: Định kỳ audit lịch chạy (ít nhất kiểm lịch sau deploy)</li>
</ul>



<p><strong>Trade-off</strong>:</p>



<ul class="wp-block-list">
<li>Thêm log/metrics → Tốn công + tốn storage</li>



<li>Nhưng thiếu nó thì khi sự cố xảy ra, bạn mất nhiều giờ &#8220;đào mộ&#8221;</li>
</ul>



<p><strong>Ví dụ — Scheduler hooks trong Laravel 12</strong>:</p>



<p>Theo&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#task-hooks">Laravel 12.x Task Hooks</a>:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

Schedule::command('emails:send')
    ->daily()
    ->before(function () {
        // The task is about to execute...
    })
    ->after(function () {
        // The task has executed...
    })
    ->onSuccess(function () {
        // The task succeeded...
    })
    ->onFailure(function () {
        // The task failed...
    });</code></pre>



<p><strong>Scheduler setup</strong>:</p>



<pre class="wp-block-code"><code># Cron entry
* * * * * cd /path-to-app &amp;&amp; php artisan schedule:run >> /dev/null 2>&amp;1</code></pre>



<p><strong>Kiểm tra lịch chạy</strong>:</p>



<pre class="wp-block-code"><code># Laravel 12: Xem tất cả scheduled tasks
php artisan schedule:list</code></pre>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="8-cicd--deploy-đừng-để-deploy-tạo-ra-double-run--half-run"><span id="8-ci-cd-deploy-dung-de-deploy-tao-ra-double-run-half-run">8. CI/CD &amp; Deploy: Đừng để deploy tạo ra double-run / half-run</span></h2>



<p>Ở môi trường deploy liên tục, scheduler/worker cần &#8220;graceful&#8221;:</p>



<p><strong>Best practice</strong>:</p>



<ul class="wp-block-list">
<li>Tránh chạy song song 2 version job logic cho cùng một batch</li>



<li>Worker restart có kiểm soát; job quan trọng có idempotency key để chịu được retry/duplicate</li>



<li>Nếu hệ HA nhiều scheduler node: Phải có chiến lược &#8220;one server&#8221; + shared lock store</li>
</ul>



<p><strong>Sai lầm thường gặp</strong>: Deploy xong &#8220;mọi thứ xanh&#8221;, nhưng lịch 09:00 bị chạy 2 lần vì 2 scheduler node cùng tick.</p>



<p><strong>Ví dụ — Laravel 12&nbsp;<code>onOneServer()</code></strong>:</p>



<p>Theo&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#running-tasks-on-one-server">Laravel 12.x Running Tasks on One Server</a>:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

// Nếu có nhiều scheduler server, dùng onOneServer() để chạy chỉ trên 1 server
Schedule::command('emails:send')
    ->daily()
    ->onOneServer(); // Chỉ chạy trên 1 server, dùng cache để lock</code></pre>



<p><strong>Lưu ý</strong>: Job vẫn phải idempotent vì:</p>



<ul class="wp-block-list">
<li>Retry khi fail</li>



<li>Deploy có thể làm job chạy lại</li>



<li>Worker restart có thể làm job chạy lại</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="9-tổ-chức-code-khi-kernelphp-phình-to"><span id="9-to-chuc-code-khi-kernel-php-phinh-to">9. Tổ chức code: Khi Kernel.php &#8220;phình to&#8221;</span></h2>



<p>Khi số lượng lịch tăng,&nbsp;<code>Kernel.php</code>&nbsp;sẽ rất dài (có thể lên 300+ lines).</p>



<p><strong>Best practice — Laravel 12</strong>: Tách helper functions theo domain để dễ maintain:</p>



<p>Theo&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#defining-schedules">Laravel 12.x Defining Schedules</a>, bạn có thể định nghĩa trong&nbsp;<code>routes/console.php</code>&nbsp;và tách thành helper functions:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

// Tách theo domain để dễ maintain
schedulePaymentTasks();
scheduleNotificationTasks();
scheduleCleanupTasks();

function schedulePaymentTasks(): void
{
    Schedule::command('payments:process')
        ->dailyAt('09:00');

    Schedule::command('payments:retry')
        ->dailyAt('14:00');
}

function scheduleNotificationTasks(): void
{
    Schedule::command('notifications:send')
        ->hourly();

    Schedule::command('notifications:digest')
        ->dailyAt('20:00');
}

function scheduleCleanupTasks(): void
{
    Schedule::command('cache:clear')
        ->hourly()
        ->runInBackground();
}</code></pre>



<p>Hoặc dùng&nbsp;<code>withSchedule</code>&nbsp;trong&nbsp;<code>bootstrap/app.php</code>&nbsp;và tách thành methods:</p>



<pre class="wp-block-code"><code>&lt;?php

// bootstrap/app.php
use Illuminate\Console\Scheduling\Schedule;

->withSchedule(function (Schedule $schedule) {
    schedulePaymentTasks($schedule);
    scheduleNotificationTasks($schedule);
    scheduleCleanupTasks($schedule);
});

function schedulePaymentTasks(Schedule $schedule): void
{
    $schedule->command('payments:process')->dailyAt('09:00');
    $schedule->command('payments:retry')->dailyAt('14:00');
}</code></pre>



<p><strong>Lợi ích</strong>:</p>



<ul class="wp-block-list">
<li>Dễ review: Mỗi function = 1 domain</li>



<li>Dễ test: Có thể test từng domain riêng</li>



<li>Dễ ownership: Team có thể own từng domain</li>



<li>Tránh Kernel.php phình to: Code nằm trong <code>routes/console.php</code> hoặc helper functions</li>
</ul>



<p><strong>Schedule Groups — Laravel 12</strong>:</p>



<p>Theo&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#schedule-groups">Laravel 12.x Schedule Groups</a>, bạn có thể nhóm các tasks có cùng cấu hình để tránh lặp lại code:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

// ❌ SAI: Lặp lại cấu hình cho từng task
Schedule::command('emails:send')
    ->daily()
    ->onOneServer()
    ->timezone('America/New_York');

Schedule::command('emails:prune')
    ->daily()
    ->onOneServer()
    ->timezone('America/New_York');

// ✅ ĐÚNG: Dùng Schedule Groups để nhóm tasks có cùng cấu hình
Schedule::daily()
    ->onOneServer()
    ->timezone('America/New_York')
    ->group(function () {
        Schedule::command('emails:send');
        Schedule::command('emails:prune');
    });</code></pre>



<p><strong>Lợi ích của Schedule Groups</strong>:</p>



<ul class="wp-block-list">
<li><strong>Giảm lặp lại code</strong>: Không cần lặp lại <code>onOneServer()</code>, <code>timezone()</code>, <code>withoutOverlapping()</code> cho từng task</li>



<li><strong>Dễ maintain</strong>: Thay đổi cấu hình một lần áp dụng cho cả nhóm</li>



<li><strong>Tăng tính nhất quán</strong>: Đảm bảo tất cả tasks trong nhóm có cùng cấu hình</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="10-timezone-handling-luôn-nhất-quán"><span id="10-timezone-handling-luon-nhat-quan">10. Timezone handling: Luôn nhất quán</span></h2>



<p><strong>Best practice</strong>: Cố định timezone theo business, không dùng server timezone.&nbsp;<strong>Nguyên tắc</strong>: Tất cả scheduled tasks phải dùng cùng một business timezone để đảm bảo tính nhất quán.</p>



<p><strong>Ví dụ — Laravel 12 timezone</strong>:</p>



<p>Theo&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#timezones">Laravel 12.x Scheduling Timezones</a>, có 2 cách set timezone:</p>



<p><strong>Cách 1: Set timezone trong config (khuyến nghị)</strong>:</p>



<p>Khi tất cả tasks dùng cùng business timezone, thêm&nbsp;<code>schedule_timezone</code>&nbsp;vào&nbsp;<code>config/app.php</code>:</p>



<pre class="wp-block-code"><code>&lt;?php

// config/app.php
return &#91;
    'timezone' => 'UTC', // Application timezone

    'schedule_timezone' => 'Asia/Tokyo', // Business timezone cho scheduled tasks
];</code></pre>



<p>Với cấu hình này, tất cả scheduled tasks sẽ tự động dùng timezone&nbsp;<code>Asia/Tokyo</code>&nbsp;trừ khi được ghi đè bởi method&nbsp;<code>timezone()</code>&nbsp;trong từng task cụ thể.</p>



<p><strong>Cách 2: Set timezone cho từng task riêng</strong>:</p>



<p>Chỉ định timezone cho task cụ thể bằng method&nbsp;<code>timezone()</code>:</p>



<pre class="wp-block-code"><code>&lt;?php

// routes/console.php
use Illuminate\Support\Facades\Schedule;

// Set timezone cho task cụ thể (ghi đè config)
Schedule::command('report:generate')
    ->timezone('America/New_York')
    ->at('2:00');</code></pre>



<p><strong>Lưu ý quan trọng về DST (Daylight Saving Time)</strong>:</p>



<p>Theo&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#timezones">Laravel 12.x Scheduling Timezones</a>, một số timezone sử dụng DST có thể ảnh hưởng đến thời gian thực thi. Khi DST thay đổi, task có thể chạy 2 lần hoặc không chạy.&nbsp;<strong>Nên tránh dùng timezone có DST khi có thể</strong>.</p>



<p><strong>Nguyên tắc áp dụng</strong>:</p>



<ul class="wp-block-list">
<li>✅ <strong>Nên</strong>: Set timezone một lần trong <code>config/app.php</code> với key <code>schedule_timezone</code> nếu tất cả tasks dùng cùng business timezone</li>



<li>⚠️ <strong>Tránh</strong>: Set timezone cho từng task riêng lẻ → dễ quên, dễ sai, khó maintain</li>



<li>⚠️ <strong>Tránh</strong>: Dùng timezone có DST → có thể ảnh hưởng đến thời gian thực thi khi DST thay đổi</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="kết-luận-framework-cho-quyết-định-kỹ-thuật"><span id="ket-luan-framework-cho-quyet-dinh-ky-thuat">Kết luận: Framework cho quyết định kỹ thuật</span></h2>



<p>Khi thêm một scheduled task mới, tôi tự hỏi 6 câu:</p>



<ol class="wp-block-list">
<li><strong>Task này có side-effect quan trọng không?</strong> → Nếu có → Idempotent + audit batch</li>



<li><strong>Dữ liệu tăng thì task tăng theo tuyến nào?</strong> → Nếu O(N) theo bảng lớn → Phải có watermark/chunk/index</li>



<li><strong>Chạy trễ có làm sai cửa sổ dữ liệu không?</strong> → Nếu có → Cần <code>scheduledAt/batchAt</code></li>



<li><strong>Có được phép chạy song song không?</strong> → Nếu không → Overlap guard + lock theo business key</li>



<li><strong>Nó thuộc priority nào?</strong> → Nếu không critical → Đừng để nó chạy chung queue với critical</li>



<li><strong>Fail thì ai biết, trong bao lâu?</strong> → Nếu câu trả lời mơ hồ → Bổ sung observability trước</li>
</ol>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 class="wp-block-heading" id="checklist-áp-dụng"><span id="checklist-ap-dung">Checklist áp dụng</span></h2>



<ul class="wp-block-list">
<li> Scheduler chỉ dispatch queued jobs, không chạy logic nặng</li>



<li> Tất cả job có side-effect đều idempotent</li>



<li> Job dùng <code>batchTime/scheduledAt</code> để tính time window, không dùng <code>now()</code></li>



<li> Job dùng <code>chunkById</code> hoặc cursor pagination cho data lớn</li>



<li> Job có stagger/rate-limit khi gọi external API</li>



<li> Job log theo batch time để dễ đối soát</li>



<li> Scheduler có helper methods theo domain, không phình to <code>schedule()</code></li>



<li> Timezone nhất quán: Business timezone, không dùng server timezone</li>



<li> Queue được phân priority (critical/default/low)</li>



<li> Có alert khi scheduler/job fail</li>
</ul>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<p><strong>Tài liệu tham khảo</strong>:&nbsp;<a href="https://laravel.com/docs/12.x/scheduling#main-content">Laravel 12.x Task Scheduling</a></p>
<p>The post <a href="https://blog.tomosia.com.vn/laravel-scheduler-tu-duy-thiet-ke-trade-off-va-best-practices-cho-he-thong-lon/">Laravel Scheduler: Tư duy thiết kế, trade-off và best practices cho hệ thống lớn</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.tomosia.com.vn/laravel-scheduler-tu-duy-thiet-ke-trade-off-va-best-practices-cho-he-thong-lon/feed/</wfw:commentRss>
			<slash:comments>0</slash:comments>
		
		
			</item>
		<item>
		<title>Hướng dẫn từng bước thiết lập Let&#8217;s Encrypt SSL/TLS cho Nginx trên Docker.</title>
		<link>https://blog.tomosia.com.vn/huong-dan-tung-buoc-thiet-lap-lets-encrypt-ssl-tls-cho-nginx-tren-docker/</link>
					<comments>https://blog.tomosia.com.vn/huong-dan-tung-buoc-thiet-lap-lets-encrypt-ssl-tls-cho-nginx-tren-docker/#respond</comments>
		
		<dc:creator><![CDATA[linh phan]]></dc:creator>
		<pubDate>Mon, 22 Dec 2025 01:11:37 +0000</pubDate>
				<category><![CDATA[Web Server]]></category>
		<category><![CDATA[Web server]]></category>
		<guid isPermaLink="false">https://blog.tomosia.com.vn/?p=3683</guid>

					<description><![CDATA[<p>Trong bài viết này, mình sẽ hướng dẫn từng bước cách thiết lập HTTPS với Let&#8217;s Encrypt cho&#8230;</p>
<p>The post <a href="https://blog.tomosia.com.vn/huong-dan-tung-buoc-thiet-lap-lets-encrypt-ssl-tls-cho-nginx-tren-docker/">Hướng dẫn từng bước thiết lập Let&#8217;s Encrypt SSL/TLS cho Nginx trên Docker.</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Trong bài viết này, mình sẽ hướng dẫn <strong>từng bước</strong> cách thiết lập <strong>HTTPS với Let&#8217;s Encrypt</strong> cho <strong>Nginx chạy trên Docker</strong>, theo hướng <strong>thực tế – dễ vận hành – dùng được cho production</strong>.</p>



<div class="wp-block-group"><div class="wp-block-group__inner-container is-layout-constrained wp-block-group-is-layout-constrained">
<p><strong>Nội dung bao gồm:</strong><br>・Thiết lập Nginx chạy HTTPS trên Docker<br>・Cấp SSL/TLS certificate bằng Certbot<br>・Tự động gia hạn certificate<br>・Redirect HTTP → HTTPS<br>・Áp dụng các security headers cơ bản</p>
</div></div>



<p><strong>Yêu cầu</strong><br>・Docker đã cài đặt<br>・Docker Compose đã cài đặt<br>・Quyền quản trị hệ thống<br>・Domain đã trỏ về IP server<br>・Port 80 và 443 mở và trỏ về server</p>



<p><br><strong>Thiết lập Nginx Webserver với Let&#8217;s Encrypt trên Docker</strong><br>Quy trình thiết lập gồm các bước sau:・Tạo Docker Compose file<br>・Cấu hình Nginx server<br>・Tạo script setup SSL<br>・Chạy Certbot client<br>・Tự động gia hạn certificate</p>



<h2 id="buoc-1-tao-cau-truc-thu-muc-va-env" class="wp-block-heading">Bước 1: Tạo cấu trúc thư mục và <code>.env</code></h2>



<p>Tạo các thư mục cần thiết trong dự án:</p>



<pre class="wp-block-code has-d-4-d-4-d-4-color has-text-color has-875-rem-font-size"><code>mkdir -p docker/nginx/etc/nginx/templates
mkdir -p docker/nginx/etc/nginx/conf
mkdir -p docker/nginx/certs</code></pre>



<p>Cấu trúc thư mục:</p>



<pre class="wp-block-code"><code>docker/
└── nginx/
    ├── setup-ssl.sh
    ├── entrypoint.sh
    ├── etc/nginx/
    │   ├── nginx.conf
    │   ├── conf/
    │   └── templates/
    │       ├── default.conf.template
    │       ├── https.conf.template
    │       └── upstream.conf.template
    └── certs/              # Thư mục chứa certificates (gitignored)</code></pre>



<p><strong>File Structure</strong><br>・<code>entrypoint.sh</code>: Script tự động generate nginx configs từ templates<br>・<code>setup-ssl.sh</code>: Script tạo SSL certificate trên host (chạy ngoài container)<br>・<code>templates/</code>: Nginx config templates<br>・<code>https.conf.template</code>: HTTPS config cho domain<br>・<code>upstream.conf.template</code>: PHP-FPM upstream config<br>・<code>certs/</code>: Thư mục chứa Let&#8217;s Encrypt certificates trên host (gitignored, mounted vào container)</p>



<p><strong>Environment Variables</strong></p>



<figure class="wp-block-table"><table class="has-fixed-layout"><thead><tr><th>Biến</th><th>Mô tả</th><th>Default</th></tr></thead><tbody><tr><td><code>DOCKER_NGINX_EXTERNAL_PORT</code></td><td>HTTP external port</td><td><code>8000</code></td></tr><tr><td><code>DOCKER_NGINX_HTTPS_PORT</code></td><td>HTTPS external port</td><td><code>8443</code></td></tr><tr><td><code>HTTPS_DOMAIN</code></td><td>Domain dùng HTTPS</td><td><code>example.com</code></td></tr><tr><td><code>ENABLE_HTTPS</code></td><td>Bật HTTPS</td><td><code>true</code></td></tr></tbody></table></figure>



<h2 id="buoc-2-tao-dockerfile-cho-nginx" class="wp-block-heading">Bước 2: Tạo Dockerfile cho Nginx</h2>



<p>Tạo file docker/Dockerfile với multi-stage build:</p>



<pre class="wp-block-code"><code>FROM nginx:1.28.0-alpine as nginx

WORKDIR /var/www/html
COPY docker/nginx/ /
COPY docker/nginx/entrypoint.sh /usr/local/bin/entrypoint.sh
RUN chmod +x /usr/local/bin/entrypoint.sh

EXPOSE 80 443

ENTRYPOINT &#91;"/usr/local/bin/entrypoint.sh"]
CMD &#91;"nginx", "-g", "daemon off;"]</code></pre>



<h2 id="buoc-3-tao-docker-compose-file" class="wp-block-heading">Bước 3: Tạo Docker Compose file</h2>



<p>Tạo file docker-compose.yml để chạy Nginx với HTTPS:</p>



<pre class="wp-block-code"><code>services:
  nginx:
    build:
      context: .
      dockerfile: docker/Dockerfile
      target: nginx
    environment:
      ENABLE_BASIC_AUTH: "${ENABLE_BASIC_AUTH-false}"
      PHP_FPM_HOST: app
      PHP_FPM_PORT: 9000
      ENABLE_HTTP_LOCALHOST: "false"
      ENABLE_HTTPS: "true"
      HTTPS_DOMAIN: "${HTTPS_DOMAIN-example.com}"
    ports:
      - "${DOCKER_NGINX_EXTERNAL_PORT-8000}:80"
      - "${DOCKER_NGINX_HTTPS_PORT-8443}:443"
    depends_on:
      - app
      - certbot
    volumes:
      - ".:/var/www/html"
      - "./docker/nginx/certs:/etc/letsencrypt"

  certbot:
    image: certbot/certbot:latest
    volumes:
      - "./docker/nginx/certs:/etc/letsencrypt"
    entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h &amp; wait $${!}; done;'"

  app:
    build:
      context: .
      dockerfile: docker/Dockerfile
      target: app
    volumes:
      - ".:/var/www/html"
    environment:
      PHP_OPCACHE_VALIDATE_TIMESTAMPS: 1
    command: &#91;"development"]</code></pre>



<p>Giải thích:<br><code>webserver</code> (nginx): container Nginx với HTTPS<br><code>certbot</code>: container tự động gia hạn certificate<br><code>app</code>: container Laravel application<br>Volume <code>./docker/nginx/certs:/etc/letsencrypt</code>: chia sẻ certificates giữa containers</p>



<h2 id="buoc-4-tao-file-cau-hinh-nginx" class="wp-block-heading">Bước 4: Tạo file cấu hình Nginx</h2>



<p><strong>4.1. Tạo template HTTP (cho Let&#8217;s Encrypt validation)</strong><br>Tạo file <code>docker/nginx/etc/nginx/templates/default.conf.template</code>:</p>



<pre class="wp-block-code"><code>server {
    listen 80;
    server_name _;

    root /var/www/html/public;
    index index.php;

    client_max_body_size 40m;

    access_log /dev/stdout;
    error_log /dev/stderr;

    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    proxy_hide_header X-Powered-By;

    charset utf-8;

    location = /health {
        return 200 'OK!';
        add_header Content-Type text/plain;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location / {
        auth_basic $basic_auth;
        auth_basic_user_file /etc/nginx/.htpasswd;
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        set $path_info $fastcgi_path_info;
        fastcgi_index index.php;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        fastcgi_pass php-fpm;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}</code></pre>



<p>Lưu ý: Block <code>location ~ /\.(?!well-known).*</code> chặn các file ẩn nhưng cho phép <code>.well-known</code> để Let&#8217;s Encrypt validate.</p>



<p><strong>4.2. Tạo template HTTPS</strong><br>Tạo file <code>docker/nginx/etc/nginx/templates/https.conf.template</code>:</p>



<pre class="wp-block-code"><code># HTTPS server for ${HTTPS_DOMAIN}
server {
    listen 443 ssl;
    http2 on;
    server_name ${HTTPS_DOMAIN};

    root /var/www/html/public;
    index index.php;

    # SSL configuration
    ssl_certificate /etc/letsencrypt/live/${HTTPS_DOMAIN}/fullchain.pem;
    ssl_certificate_key /etc/letsencrypt/live/${HTTPS_DOMAIN}/privkey.pem;

    client_max_body_size 40m;

    access_log /dev/stdout;
    error_log /dev/stderr;

    add_header X-Frame-Options "SAMEORIGIN" always;
    add_header X-Content-Type-Options "nosniff" always;
    add_header Strict-Transport-Security "max-age=31536000; includeSubDomains" always;
    proxy_hide_header X-Powered-By;

    charset utf-8;

    location = /health {
        return 200 'OK!';
        add_header Content-Type text/plain;
    }

    location = /favicon.ico { access_log off; log_not_found off; }
    location = /robots.txt  { access_log off; log_not_found off; }

    error_page 404 /index.php;

    location / {
        auth_basic $basic_auth;
        auth_basic_user_file /etc/nginx/.htpasswd;
        try_files $uri $uri/ /index.php$is_args$args;
    }

    location ~ \.php$ {
        set $path_info $fastcgi_path_info;
        fastcgi_index index.php;
        fastcgi_split_path_info ^(.+\.php)(/.*)$;
        fastcgi_pass php-fpm;
        fastcgi_param SCRIPT_FILENAME $document_root$fastcgi_script_name;

        include fastcgi_params;
    }

    location ~ /\.(?!well-known).* {
        deny all;
    }
}

# Redirect HTTP to HTTPS for ${HTTPS_DOMAIN}
server {
    listen 80;
    server_name ${HTTPS_DOMAIN};

    location / {
        return 301 https://$host$request_uri;
    }
}</code></pre>



<p>Giải thích:<br>Server block đầu: HTTPS trên port 443 với HTTP/2<br>Server block thứ hai: redirect HTTP → HTTPS<br>Security headers: HSTS, X-Frame-Options, X-Content-Type-Options</p>



<h2 id="buoc-5-tao-entrypoint-script" class="wp-block-heading">Bước 5: Tạo Entrypoint script</h2>



<p>Tạo file <code>docker/nginx/entrypoint.sh</code> để tự động generate config từ template:</p>



<pre class="wp-block-code"><code>#!/bin/sh
set -e

# Set BASIC_AUTH_VALUE based on ENABLE_BASIC_AUTH
if &#91; "$ENABLE_BASIC_AUTH" = "true" ]; then
    export BASIC_AUTH_VALUE="Administrator's Area"
else
    export BASIC_AUTH_VALUE="off"
fi

# Generate map config from template
envsubst '$$BASIC_AUTH_VALUE' &lt; /etc/nginx/templates/map.conf.template &gt; /etc/nginx/conf.d/map.conf

# Generate upstream config
envsubst '$$PHP_FPM_HOST $$PHP_FPM_PORT' &lt; /etc/nginx/templates/upstream.conf.template &gt; /etc/nginx/conf.d/upstream.conf

# Generate nginx configs from templates
if &#91; "$ENABLE_HTTP_LOCALHOST" = "true" ]; then
    envsubst '$$PHP_FPM_HOST $$PHP_FPM_PORT' &lt; /etc/nginx/templates/localhost.conf.template &gt; /etc/nginx/conf.d/server-localhost.conf
fi

# Handle HTTPS setup
if &#91; "$ENABLE_HTTPS" = "true" ] &amp;&amp; &#91; -n "$HTTPS_DOMAIN" ]; then
    # Create directories if they don't exist
    mkdir -p /etc/letsencrypt/live/${HTTPS_DOMAIN}

    if &#91; -f "/etc/letsencrypt/live/${HTTPS_DOMAIN}/fullchain.pem" ]; then
        envsubst '$$PHP_FPM_HOST $$PHP_FPM_PORT $$HTTPS_DOMAIN' &lt; /etc/nginx/templates/https.conf.template &gt; /etc/nginx/conf.d/server-https.conf
    fi
fi

# Execute the main command
exec "$@"</code></pre>



<p>Script này:<br>・Tạo config từ template dựa trên biến môi trường<br>・Chỉ tạo HTTPS config nếu certificate tồn tại<br>・Dùng <code>envsubst</code> để thay thế biến</p>



<h2 id="buoc-6-tao-script-setup-ssl" class="wp-block-heading">Bước 6: Tạo script setup SSL</h2>



<p>Tạo file <code>docker/nginx/setup-ssl.sh</code> để tạo certificate trên host:</p>



<pre class="wp-block-code"><code>#!/bin/bash
set -e

# Get script directory and project root
SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE&#91;0]}")" &amp;&amp; pwd)"
PROJECT_ROOT="$(cd "${SCRIPT_DIR}/../.." &amp;&amp; pwd)"

# Configuration
DOMAIN="${HTTPS_DOMAIN:-example.com}"
CERT_DIR="${PROJECT_ROOT}/docker/nginx/certs"

echo "Setting up SSL certificate for ${DOMAIN} on host..."
echo "Certificate will be saved to: ${CERT_DIR}"

# Create directories on host
mkdir -p "${CERT_DIR}"

# Check if certificate already exists
if &#91; -f "${CERT_DIR}/live/${DOMAIN}/fullchain.pem" ]; then
    echo "Certificate already exists for ${DOMAIN}"
    echo "Location: ${CERT_DIR}/live/${DOMAIN}/"
    exit 0
fi

cd "${PROJECT_ROOT}"

# Request certificate using docker (certificate saved on host, mounted into container)
echo "Requesting certificate (port 80 must be available)..."
docker run --rm \
    -v "${CERT_DIR}:/etc/letsencrypt" \
    -p 80:80 \
    certbot/certbot certonly \
    --standalone \
    --agree-tos \
    -d "${DOMAIN}"

echo ""
echo "Certificate created successfully on host!"
echo "Location: ${CERT_DIR}/live/${DOMAIN}/"
echo "Files: fullchain.pem, privkey.pem"
echo ""
echo "Certificate will be automatically mounted into nginx container."
echo "You can now run: docker-compose up -d"</code></pre>



<p>Lưu ý: Script này chạy trên host, không trong container. Certificate được lưu trên host và mount vào container.</p>



<h2 id="buoc-7-chay-certbot-de-lay-certificate" class="wp-block-heading">Bước 7: Chạy Certbot để lấy certificate</h2>



<p><strong>7.1. Test với dry-run (khuyến nghị)</strong><br>Trước khi lấy certificate thật, test với <code>--dry-run</code>:</p>



<pre class="wp-block-code"><code>chmod +x docker/nginx/setup-ssl.sh

# Test với dry-run
docker run --rm \
    -v "$(pwd)/docker/nginx/certs:/etc/letsencrypt" \
    -p 80:80 \
    certbot/certbot certonly \
    --standalone \
    --dry-run \
    --agree-tos \
    -d your-domain.com</code></pre>



<p>Lưu ý: Thay <code>your-domain.com</code> bằng domain của bạn.</p>



<p><strong>7.2. Lấy certificate thật</strong></p>



<p>Nếu dry-run thành công, chạy script để lấy certificate:</p>



<pre class="wp-block-code"><code>HTTPS_DOMAIN=your-domain.com ./docker/nginx/setup-ssl.sh</code></pre>



<p>Hoặc dùng domain mặc định:</p>



<pre class="wp-block-code"><code>./docker/nginx/setup-ssl.sh</code></pre>



<p>Quá trình:</p>



<ol class="wp-block-list">
<li>Script kiểm tra certificate đã tồn tại chưa</li>



<li>Nếu chưa, chạy Certbot với &#8211;standalone mode</li>



<li>Certbot bind port 80 để Let&#8217;s Encrypt validate</li>



<li>Certificate được lưu tại ./docker/nginx/certs/live/your-domain.com/<br><br>Lưu ý:<br>・Đảm bảo port 80 mở và trỏ về server.<br>・Nếu Nginx đang chạy trên port 80, tạm dừng trước khi chạy script.</li>
</ol>



<h2 id="buoc-8-khoi-dong-docker-compose" class="wp-block-heading">Bước 8: Khởi động Docker Compose</h2>



<p>Sau khi có certificate, khởi động services:</p>



<pre class="wp-block-code"><code>docker-compose up -d</code></pre>



<p>Kiểm tra containers đang chạy:</p>



<pre class="wp-block-code"><code>docker-compose ps</code></pre>



<p>Kiểm tra logs:</p>



<pre class="wp-block-code"><code>docker-compose logs nginx
docker-compose logs certbot</code></pre>



<h2 id="buoc-9-kiem-tra-https" class="wp-block-heading">Bước 9: Kiểm tra HTTPS</h2>



<p>Truy cập domain qua HTTPS:</p>



<pre class="wp-block-code"><code>curl -I https://your-domain.com:8443</code></pre>



<p>Hoặc mở trình duyệt và truy cập: <code>https://your-domain.com:8443</code><br><br>Bạn sẽ thấy:<br><br>・Certificate hợp lệ (không có cảnh báo)<br><br>・Tự động redirect HTTP → HTTPS<br><br>・Security headers được áp dụng</p>



<h2 id="buoc-10-gia-han-certificates" class="wp-block-heading">Bước 10: Gia hạn certificates</h2>



<p>Let&#8217;s Encrypt certificates có thời hạn 90 ngày. Có 2 cách gia hạn:<br><br><strong>10.1. Tự động gia hạn (đã cấu hình)</strong></p>



<p>Service <code>certbot</code> trong <code>docker-compose.yml</code> tự động gia hạn mỗi 12 giờ:</p>



<pre class="wp-block-code"><code>certbot:
  image: certbot/certbot:latest
  volumes:
    - "./docker/nginx/certs:/etc/letsencrypt"
  entrypoint: "/bin/sh -c 'trap exit TERM; while :; do certbot renew; sleep 12h &amp; wait $${!}; done;'"</code></pre>



<p>Certbot chỉ gia hạn khi certificate sắp hết hạn (trong vòng 30 ngày).</p>



<p><strong>10.2. Gia hạn thủ công</strong><br><br>Nếu cần gia hạn ngay:</p>



<pre class="wp-block-code"><code>docker run --rm \
    -v "$(pwd)/docker/nginx/certs:/etc/letsencrypt" \
    certbot/certbot renew</code></pre>



<p>Hoặc trong container:</p>



<pre class="wp-block-code"><code>docker-compose exec certbot certbot renew</code></pre>



<p>Sau khi gia hạn, reload Nginx để áp dụng certificate mới:</p>



<pre class="wp-block-code"><code>docker-compose exec nginx nginx -s reload</code></pre>



<p>Hoặc restart container:</p>



<pre class="wp-block-code"><code>docker-compose restart nginx</code></pre>



<h2 id="troubleshooting" class="wp-block-heading">Troubleshooting</h2>



<p><strong>Lỗi: &#8220;Port 80 is already in use&#8221;</strong><br><br>Nguyên nhân: Port 80 đang được sử dụng bởi service khác.<br><br>Giải pháp:</p>



<pre class="wp-block-code"><code># Kiểm tra process đang dùng port 80
sudo lsof -i :80

# Tạm dừng Nginx nếu đang chạy
docker-compose down

# Sau đó chạy lại setup-ssl.sh
./docker/nginx/setup-ssl.sh</code></pre>



<p><strong>Lỗi: &#8220;Failed to obtain certificate&#8221;</strong><br><br>Nguyên nhân:<br>・Domain chưa trỏ về IP server<br>・Port 80 bị firewall chặn<br>・Let&#8217;s Encrypt không thể truy cập domain<br><br>Giải pháp:</p>



<pre class="wp-block-code"><code># Kiểm tra DNS
nslookup your-domain.com

# Kiểm tra port 80
curl -I http://your-domain.com

# Kiểm tra firewall
sudo ufw status</code></pre>



<p><strong>Lỗi: &#8220;Nginx không load HTTPS config&#8221;</strong><br><br>Nguyên nhân:<br>・Certificate chưa tồn tại<br>・Biến môi trường chưa đúng<br>・Volume mount sai<br><br>Giải pháp:</p>



<pre class="wp-block-code"><code># Kiểm tra certificate
ls -la docker/nginx/certs/live/your-domain.com/

# Kiểm tra biến môi trường
docker-compose config

# Kiểm tra volume mount trong container
docker-compose exec nginx ls -la /etc/letsencrypt/live/</code></pre>



<h2 id="tong-ket" class="wp-block-heading">Tổng kết</h2>



<p>Sau khi hoàn thành các bước trên, bạn đã:<br>・Thiết lập Nginx với HTTPS trên Docker<br>・Cấu hình Let&#8217;s Encrypt SSL/TLS certificates<br>・Tự động gia hạn certificates<br>・Redirect HTTP → HTTPS<br>・Áp dụng security headers</p>
<p>The post <a href="https://blog.tomosia.com.vn/huong-dan-tung-buoc-thiet-lap-lets-encrypt-ssl-tls-cho-nginx-tren-docker/">Hướng dẫn từng bước thiết lập Let&#8217;s Encrypt SSL/TLS cho Nginx trên Docker.</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.tomosia.com.vn/huong-dan-tung-buoc-thiet-lap-lets-encrypt-ssl-tls-cho-nginx-tren-docker/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>17</slash:comments>
		
		
			</item>
		<item>
		<title>Đưa Localhost lên Internet miễn phí với Cloudflare Tunnel: Giải pháp thay thế Ngrok để Test Webhook &#038; Demo App</title>
		<link>https://blog.tomosia.com.vn/dua-localhost-len-internet-mien-phi-voi-cloudflare-tunnel-giai-phap-thay-the-ngrok-de-test-webhook-demo-app/</link>
					<comments>https://blog.tomosia.com.vn/dua-localhost-len-internet-mien-phi-voi-cloudflare-tunnel-giai-phap-thay-the-ngrok-de-test-webhook-demo-app/#comments</comments>
		
		<dc:creator><![CDATA[Hoang Nam]]></dc:creator>
		<pubDate>Tue, 09 Dec 2025 01:41:48 +0000</pubDate>
				<category><![CDATA[Web Server]]></category>
		<category><![CDATA[Kinh nghiệm]]></category>
		<category><![CDATA[Solution]]></category>
		<category><![CDATA[Hosting]]></category>
		<guid isPermaLink="false">https://blog.tomosia.com.vn/?p=3752</guid>

					<description><![CDATA[<p>Bạn cần test webhook ZaloPay, Stripe hay demo app cho khách hàng ngay trên localhost? Hướng dẫn sử&#8230;</p>
<p>The post <a href="https://blog.tomosia.com.vn/dua-localhost-len-internet-mien-phi-voi-cloudflare-tunnel-giai-phap-thay-the-ngrok-de-test-webhook-demo-app/">Đưa Localhost lên Internet miễn phí với Cloudflare Tunnel: Giải pháp thay thế Ngrok để Test Webhook &amp; Demo App</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p><em>Bạn cần test webhook ZaloPay, Stripe hay demo app cho khách hàng ngay trên localhost? Hướng dẫn sử dụng Cloudflare Tunnel để expose local server ra internet với HTTPS miễn phí, bảo mật, không cần mở port.</em></p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>



<h2 id="mo-dau-noi-dau-cua-developer" class="wp-block-heading">Mở đầu: Nỗi đau của Developer</h2>



<p>Bạn đang phát triển một ứng dụng local, mọi thứ chạy mượt mà ở&nbsp;localhost:3000. Nhưng vấn đề nảy sinh khi:</p>



<ul class="wp-block-list">
<li>Bạn cần <strong>test Webhook</strong> từ các dịch vụ bên ngoài như ZaloPay, Stripe, Telegram (các bên này bắt buộc phải có HTTPS).</li>



<li>Khách hàng muốn <strong>xem demo ngay lập tức</strong>, nhưng bạn chưa kịp deploy lên server.</li>



<li>Bạn chán ngấy cảnh dùng <strong>Ngrok bản free</strong> vì tên miền bị đổi liên tục sau mỗi lần restart.</li>
</ul>



<p>Giải pháp tối ưu nhất hiện nay chính là&nbsp;<strong>Cloudflare Tunnel</strong>.</p>



<h2 id="1-cloudflare-tunnel-la-gi" class="wp-block-heading">1. Cloudflare Tunnel là gì?</h2>



<p><strong>Cloudflare Tunnel</strong>&nbsp;(trước đây là Argo Tunnel) là công cụ giúp bạn kết nối máy chủ local (hoặc máy ảo) ra internet mà&nbsp;<strong>không cần mở port (port forwarding)</strong>&nbsp;trên router.</p>



<p>Thay vì cho phép người lạ truy cập trực tiếp vào IP của bạn (rất rủi ro), Tunnel tạo một đường hầm bảo mật (outbound connection) từ máy bạn đến mạng lưới Cloudflare Edge. Từ đó, Cloudflare sẽ &#8220;public&#8221; ứng dụng của bạn ra ngoài bằng một tên miền HTTPS xịn xò.</p>



<h3 id="tai-sao-nen-dung-cloudflare-tunnel-thay-vi-ngrok" class="wp-block-heading">Tại sao nên dùng Cloudflare Tunnel thay vì Ngrok?</h3>



<figure class="wp-block-table"><table class="has-fixed-layout"><tbody><tr><td>Tiêu chí</td><td>Ngrok (Free)</td><td>Cloudflare Tunnel</td></tr><tr><td><strong>Domain</strong></td><td>Random, đổi liên tục</td><td><strong>Cố định</strong>&nbsp;(theo domain của bạn)</td></tr><tr><td><strong>Chi phí</strong></td><td>Giới hạn tính năng</td><td><strong>Miễn phí</strong>&nbsp;hoàn toàn</td></tr><tr><td><strong>Bảo mật</strong></td><td>Thấp</td><td>Rất cao (Cloudflare Shield)</td></tr><tr><td><strong>Cài đặt</strong></td><td>Nhanh</td><td>Cần domain riêng (nhưng đáng giá)</td></tr></tbody></table></figure>



<h2 id="2-mo-hinh-hoat-dong" class="wp-block-heading">2. Mô hình hoạt động</h2>



<p>Hãy tưởng tượng luồng dữ liệu sẽ đi như sau:</p>



<blockquote class="wp-block-quote is-layout-flow wp-block-quote-is-layout-flow">
<p><strong>User Browser</strong> -> <strong>Cloudflare Edge (HTTPS)</strong> -> <strong>Cloudflared (trên máy bạn)</strong> -> <strong>Localhost (3000, 8080&#8230;)</strong></p>
</blockquote>



<p>Bạn có thể chạy nhiều dịch vụ cùng lúc qua một đường hầm duy nhất:</p>



<ul class="wp-block-list">
<li>api.domain.com -> trỏ về localhost:3000</li>



<li>admin.domain.com -> trỏ về localhost:8080</li>
</ul>



<h2 id="3-chuan-bi-do-nghe" class="wp-block-heading">3. Chuẩn bị &#8220;đồ nghề&#8221;</h2>



<p>Trước khi bắt đầu, bạn cần:</p>



<ol class="wp-block-list">
<li><strong>Tài khoản Cloudflare</strong> (đăng ký miễn phí).</li>



<li><strong>Một tên miền (Domain)</strong> đã thêm vào Cloudflare.
<ul class="wp-block-list">
<li><em>Lưu ý:</em> Freenom hiện tại không ổn định. Bạn nên mua các domain giá rẻ như .xyz, .dev, .click (chỉ khoảng 1-2$/năm) tại Namecheap hoặc Hostinger để dùng lâu dài.</li>
</ul>
</li>



<li><strong>Máy tính</strong> (Linux/Mac/Windows) để chạy tool.</li>
</ol>



<h2 id="4-huong-dan-trien-khai-chi-tiet-step-by-step" class="wp-block-heading">4. Hướng dẫn triển khai chi tiết (Step-by-step)</h2>



<h3 id="buoc-1-cai-dat-cloudflared" class="wp-block-heading">Bước 1: Cài đặt&nbsp;cloudflared</h3>



<p>Công cụ&nbsp;cloudflared&nbsp;là cầu nối giữa máy bạn và Cloudflare.</p>



<p><strong>Trên Linux (Ubuntu/Debian):</strong>codeBash</p>



<pre class="wp-block-code"><code>curl -L https://github.com/cloudflare/cloudflared/releases/latest/download/cloudflared-linux-amd64.deb -o cloudflared.deb
sudo dpkg -i cloudflared.deb</code></pre>



<p><strong>Trên Windows:</strong><br>Tải file&nbsp;.msi&nbsp;mới nhất từ GitHub chính chủ:&nbsp;<a target="_blank" href="https://www.google.com/url?sa=E&amp;q=https%3A%2F%2Fgithub.com%2Fcloudflare%2Fcloudflared%2Freleases" rel="noreferrer noopener">Cloudflare Downloads</a><br><em>(Mở PowerShell dưới quyền Administrator để chạy các lệnh sau này).</em></p>



<h3 id="buoc-2-dang-nhap-cloudflare" class="wp-block-heading">Bước 2: Đăng nhập Cloudflare</h3>



<p>Chạy lệnh sau để xác thực:codeBash</p>



<pre class="wp-block-code"><code>cloudflared tunnel login</code></pre>



<p>Trình duyệt sẽ bật lên, bạn hãy chọn tên miền muốn sử dụng và bấm&nbsp;<strong>Authorize</strong>.</p>



<h3 id="buoc-3-tao-tunnel" class="wp-block-heading">Bước 3: Tạo Tunnel</h3>



<p>Đặt tên cho tunnel của bạn (ví dụ:&nbsp;my-local-server):codeBash</p>



<pre class="wp-block-code"><code>cloudflared tunnel create my-local-server</code></pre>



<p>Sau khi tạo xong, Cloudflare sẽ sinh ra một&nbsp;<strong>Tunnel ID</strong>&nbsp;và file credentials (thường nằm ở&nbsp;~/.cloudflared/).</p>



<h3 id="buoc-4-cau-hinh-dinh-tuyen-routing" class="wp-block-heading">Bước 4: Cấu hình định tuyến (Routing)</h3>



<p>Đây là bước quan trọng nhất để map domain về localhost.<br>Tạo file&nbsp;config.yml&nbsp;trong thư mục&nbsp;~/.cloudflared/&nbsp;(hoặc cùng thư mục chạy tool) với nội dung:codeYaml</p>



<pre class="wp-block-code"><code>tunnel: &lt;TUNNEL-NAME-HOẶC-ID&gt;
credentials-file: /duong/dan/den/file/json/credentials.json

ingress:
  # Service 1: API Backend
  - hostname: api.namdevlabs.com
    service: http://localhost:3000

  # Service 2: Webhook test (ZaloPay, Stripe)
  - hostname: hook.namdevlabs.com
    service: http://localhost:5678

  # Service 3: Dashboard Admin
  - hostname: admin.namdevlabs.com
    service: http://localhost:8080

  # Rule cuối cùng bắt buộc: Trả về 404 nếu không khớp domain nào
  - service: http_status:404</code></pre>



<h3 id="buoc-5-gan-dns-cho-subdomain" class="wp-block-heading">Bước 5: Gắn DNS cho Subdomain</h3>



<p>Bạn cần báo cho Cloudflare biết subdomain nào sẽ đi vào tunnel này.codeBash</p>



<pre class="wp-block-code"><code># Cú pháp: cloudflared tunnel route dns &lt;TUNNEL_NAME&gt; &lt;SUBDOMAIN&gt;
cloudflared tunnel route dns my-local-server api.namdevlabs.com
cloudflared tunnel route dns my-local-server hook.namdevlabs.com</code></pre>



<p>Lệnh này sẽ tự động tạo bản ghi CNAME trên Cloudflare Dashboard.</p>



<h3 id="buoc-6-kich-hoat-tunnel" class="wp-block-heading">Bước 6: Kích hoạt Tunnel</h3>



<p>Chạy tunnel với file config vừa tạo:codeBash</p>



<pre class="wp-block-code"><code>cloudflared tunnel run my-local-server</code></pre>



<p><em>Mẹo:</em>&nbsp;Nếu file config không nằm ở vị trí mặc định, dùng flag&nbsp;&#8211;config:<br>cloudflared tunnel &#8211;config /path/to/config.yml run</p>



<p> <strong>Xong!</strong> Bây giờ bạn có thể truy cập https://api.namdevlabs.com và thấy nó trỏ thẳng về localhost:3000 của bạn với HTTPS xanh mượt.</p>



<h2 id="5-meo-nang-cao-cho-pro-developer" class="wp-block-heading">5. Mẹo nâng cao cho Pro Developer</h2>



<ul class="wp-block-list">
<li><strong>Chạy ngầm (Service Mode):</strong> Để tunnel tự chạy khi khởi động máy (dành cho server home lab):codeBash<code>sudo cloudflared service install sudo systemctl start cloudflared</code></li>



<li><strong>Bảo mật Zero Trust:</strong> Bạn sợ người lạ vào link demo? Vào Cloudflare Dashboard -> Zero Trust, bật tính năng yêu cầu đăng nhập bằng Email/Google trước khi truy cập vào domain.</li>



<li><strong>Debug lỗi:</strong> Nếu không truy cập được, hãy thêm flag &#8211;loglevel debug khi chạy để xem log chi tiết.</li>
</ul>



<h2 id="6-tong-ket" class="wp-block-heading">6. Tổng kết</h2>



<p>Cloudflare Tunnel thực sự là một vũ khí lợi hại trong bộ công cụ của developer. Nó giúp bạn:</p>



<ol class="wp-block-list">
<li><strong>Tiết kiệm tiền:</strong> Không tốn phí server staging.</li>



<li><strong>Tiết kiệm thời gian:</strong> Không cần deploy để demo.</li>



<li><strong>An toàn:</strong> Không mở port, hạn chế tấn công DDoS.</li>
</ol>



<p>Hy vọng bài viết giúp bạn giải quyết được bài toán đau đầu về Webhook và Demo. Chúc bạn code vui!</p>



<hr class="wp-block-separator has-alpha-channel-opacity"/>
<p>The post <a href="https://blog.tomosia.com.vn/dua-localhost-len-internet-mien-phi-voi-cloudflare-tunnel-giai-phap-thay-the-ngrok-de-test-webhook-demo-app/">Đưa Localhost lên Internet miễn phí với Cloudflare Tunnel: Giải pháp thay thế Ngrok để Test Webhook &amp; Demo App</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.tomosia.com.vn/dua-localhost-len-internet-mien-phi-voi-cloudflare-tunnel-giai-phap-thay-the-ngrok-de-test-webhook-demo-app/feed/</wfw:commentRss>
			<slash:comments>2</slash:comments>
		
		
			</item>
		<item>
		<title>Hướng dẫn viết Cursor Rule cho người mới bắt đầu</title>
		<link>https://blog.tomosia.com.vn/huong-dan-viet-cursor-rule-cho-nguoi-moi-bat-dau/</link>
					<comments>https://blog.tomosia.com.vn/huong-dan-viet-cursor-rule-cho-nguoi-moi-bat-dau/#comments</comments>
		
		<dc:creator><![CDATA[manh nguyen]]></dc:creator>
		<pubDate>Mon, 08 Dec 2025 02:51:54 +0000</pubDate>
				<category><![CDATA[Chưa phân loại]]></category>
		<guid isPermaLink="false">https://blog.tomosia.com.vn/?p=3707</guid>

					<description><![CDATA[<p>Hướng dẫn chi tiết cách viết Cursor Rule cho người mới bắt đầu, giúp bạn hiểu rõ vì&#8230;</p>
<p>The post <a href="https://blog.tomosia.com.vn/huong-dan-viet-cursor-rule-cho-nguoi-moi-bat-dau/">Hướng dẫn viết Cursor Rule cho người mới bắt đầu</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></description>
										<content:encoded><![CDATA[
<p>Hướng dẫn chi tiết cách viết Cursor Rule cho người mới bắt đầu, giúp bạn hiểu rõ vì sao phải dùng rule, cách cấu trúc đúng, cách tránh các lỗi AI hay mắc, và cách tối ưu workflow trong Cursor Editor. Bài viết giải thích từng bước dựa trên logic kỹ thuật, kèm lý do tại sao phải làm như vậy, giúp bạn tạo ra bộ rule ổn định, hạn chế AI phá project, giữ code đồng nhất và phù hợp với version của dự án. Đây là tài liệu nền tảng dành cho người mới dùng Cursor AI nhưng cần độ chính xác cao.<br></p>



<h2 id="1-vi-sao-phai-viet-cursor-rule" class="wp-block-heading"><strong>1. Vì sao phải viết <em>Cursor Rule</em>?</strong></h2>



<h3 id="1-1-de-ai-hieu-dung-cach-ban-muon-no-lam-viec" class="wp-block-heading"><strong>1.1. Để AI hiểu đúng cách bạn muốn nó làm việc</strong></h3>



<p>Khi không có rule, AI đoán phong cách, chất lượng và mục tiêu theo thói quen chung → dễ sai, mơ hồ, hoặc không phù hợp với dự án.<br>Rule giúp cố định cách AI hành động, không bị lệch dù prompt thay đổi.</p>



<h3 id="1-2-de-giu-chat-luong-dau-ra-on-dinh" class="wp-block-heading"><strong>1.2. Để giữ chất lượng đầu ra ổn định</strong></h3>



<p>Khi làm team, nhiều người dùng AI cho chung một dự án. Không có rule → mỗi người cho ra một kiểu.<br>Có rule → kết quả ổn định, “giống một người viết”.</p>



<h3 id="1-3-de-tiet-kiem-thoi-gian" class="wp-block-heading"><strong>1.3. Để tiết kiệm thời gian</strong></h3>



<p>Không phải lặp lại các yêu cầu cố định như coding style, cấu trúc file, cấm/tắt tính năng…<br>Rule giúp giảm 50–80% thời gian bạn cần để “định hướng” AI.</p>



<h3 id="1-4-de-chong-loi-va-giam-rui-ro" class="wp-block-heading"><strong>1.4. Để chống lỗi và giảm rủi ro</strong></h3>



<p>AI không tự biết giới hạn của dự án.<br>Rule giúp chặn:</p>



<ul class="wp-block-list">
<li>Sai kiến trúc</li>



<li>Viết sai chuẩn coding</li>



<li>Phát sinh thư mục rác</li>



<li>Tự ý tạo file không có trong dự án</li>



<li>Sử dụng công nghệ dự án không hỗ trợ</li>
</ul>



<h2 id="2-cau-truc-can-co-cua-mot-cursor-rule" class="wp-block-heading"><strong>2. Cấu trúc cần có của một <em>Cursor Rule</em></strong></h2>



<h3 id="2-1-muc-alwaysapply-de-rule-luon-duoc-kich-hoat" class="wp-block-heading"><strong>2.1. Mục “alwaysApply”: để rule luôn được kích hoạt</strong></h3>



<pre class="wp-block-code"><code>alwaysApply: true</code></pre>



<p>Nếu không bật, rule chỉ hoạt động khi bạn chọn thủ công → vô dụng. Bật lên để toàn bộ dự án luôn tuân theo một chuẩn.</p>



<h3 id="2-2-muc-mo-ta-description-de-ghi-ro-muc-dich" class="wp-block-heading"><strong>2.2. Mục mô tả (description): để ghi rõ mục đích</strong></h3>



<pre class="wp-block-code"><code>description: |
  Quy tắc dùng khi làm việc với dự án X.</code></pre>



<p>Đây là phần giúp AI hiểu bối cảnh tổng quát. Nếu không có, AI dễ “đi lạc” sang phong cách khác.</p>



<h3 id="2-3-danh-sach-rules-rules-de-kiem-soat-tung-hanh-vi" class="wp-block-heading"><strong>2.3. Danh sách rules (rules[]): để kiểm soát từng hành vi</strong></h3>



<p>Một rule có các thành phần cơ bản:</p>



<pre class="wp-block-code"><code>rules:
  - name: "Quy tắc đặt tên file"
    pattern: "*.js"
    description: |
      Tất cả file phải dùng camelCase.
    examples:
      good: &#91;"userService.js"]
      bad: &#91;"User_service.JS"]</code></pre>



<p>Giải thích từng phần:</p>



<figure class="wp-block-table alignleft"><table class="has-fixed-layout"><tbody><tr><td class="has-text-align-left" data-align="left">name</td><td>Để quản lý, tìm kiếm, debug khi nhiều rule.</td></tr><tr><td class="has-text-align-left" data-align="left">pattern</td><td>Giúp rule áp dụng đúng file, không ảnh hưởng toàn dự án. Tránh “tác dụng phụ”.</td></tr><tr><td class="has-text-align-left" data-align="left">description </td><td>Giải thích ý nghĩa để AI hiểu đúng logic, không chỉ làm máy móc.</td></tr><tr><td class="has-text-align-left" data-align="left">examples</td><td>Cách nhanh nhất để training AI. AI bắt chước ví dụ mạnh hơn bất cứ mô tả nào.</td></tr></tbody></table></figure>



<h3 id="2-4-muc-cam-han-che-do-dont" class="wp-block-heading"><strong>2.4. Mục cấm / hạn chế (do/dont)</strong></h3>



<p>Ví dụ:</p>



<pre class="wp-block-code"><code>intent:
  - "Không tự tạo file mới nếu người dùng không yêu cầu."</code></pre>



<p>AI hay tự sinh file → gây rác → ảnh hưởng cấu trúc dự án → không kiểm soát được.<br>Rule này khóa hành vi đó.</p>



<h3 id="2-5-dinh-nghia-style-coding-coding-conventions" class="wp-block-heading"><strong>2.5. Định nghĩa style coding (coding conventions)</strong></h3>



<pre class="wp-block-code"><code>  - name: "Code style"
    description: |
      Viết code theo chuẩn PSR-12.</code></pre>



<p>Nếu không chốt style → mỗi lần AI tạo code là mỗi lần thay đổi format → khó review, khó merge.</p>



<h2 id="3-quy-trinh-viet-mot-cursor-rule-chuan" class="wp-block-heading"><strong>3. Quy trình viết một <em>Cursor Rule</em> chuẩn</strong></h2>



<h3 id="buoc-1-xac-dinh-muc-tieu-du-an" class="wp-block-heading"><strong>Bước 1 — Xác định mục tiêu dự án</strong></h3>



<p>Bạn phải trả lời: “Tôi muốn AI hỗ trợ cái gì?”</p>



<p>Không biết mục tiêu → rule lan man, dài, không hiệu quả → AI rối.</p>



<h3 id="buoc-2-ghi-ro-gioi-han-du-an" class="wp-block-heading"><strong>Bước 2 — Ghi rõ giới hạn dự án</strong></h3>



<p>Ví dụ:</p>



<ul class="wp-block-list">
<li>PHP version: 7.4</li>



<li>Framework: Laravel 8</li>



<li>Không dùng TypeScript</li>



<li>Không tạo file migration mới</li>
</ul>



<p>Giới hạn càng rõ → AI càng ít sai.<br>Không có giới hạn → AI rất hay dùng công nghệ mới, sai version, sai API.</p>



<h3 id="buoc-3-xac-dinh-nhung-hanh-vi-ai-hay-sai-nhat" class="wp-block-heading"><strong>Bước 3 — Xác định những hành vi AI <em>hay sai nhất</em></strong></h3>



<p>Ví dụ:</p>



<ul class="wp-block-list">
<li>Tự sinh file linh tinh</li>



<li>Viết import sai</li>



<li>Sử dụng syntax của version mới</li>



<li>Tạo code không phù hợp folder structure</li>
</ul>



<p>Đây là những điểm tạo ra rule quan trọng nhất. Tập trung 20% rule để sửa 80% lỗi.</p>



<h3 id="buoc-4-viet-rule-bang-ngon-ngu-don-gian-khong-mo-ho" class="wp-block-heading"><strong>Bước 4 — Viết rule bằng ngôn ngữ đơn giản, không mơ hồ</strong></h3>



<p>Ví dụ sai: “Viết code sạch” → AI không hiểu.<br>Ví dụ đúng: “Không dùng biến một ký tự như <code>i, j, k</code>.”</p>



<p>AI không hiểu từ trừu tượng.<br>Muốn AI làm đúng → phải mô tả hành vi cụ thể.</p>



<h3 id="buoc-5-bo-sung-vi-du-tot-xau" class="wp-block-heading"><strong>Bước 5 — Bổ sung ví dụ tốt/xấu</strong></h3>



<p>Đây là phần cực quan trọng nhưng rất nhiều người bỏ qua.</p>



<p>AI học theo ví dụ mạnh hơn theo mô tả.<br>Chỉ 2–3 ví dụ tốt/xấu đã giúp giảm lỗi 60–70%.</p>



<h3 id="buoc-6-test-rule-bang-mot-tac-vu-that" class="wp-block-heading"><strong>Bước 6 — Test rule bằng một tác vụ thật</strong></h3>



<p>Ví dụ: “Tạo controller UserController.”</p>



<p>Rule chỉ có giá trị khi được test trong thực tế.<br>Không test → rule chỉ là lý thuyết.</p>



<h2 id="4-mau-cursor-rule-don-gian-cho-nguoi-moi" class="wp-block-heading"><strong>4. Mẫu <em>Cursor Rule</em> đơn giản cho người mới</strong></h2>



<pre class="wp-block-code"><code>alwaysApply: true

description: |
  Quy tắc hỗ trợ viết mã PHP, Laravel 8 cho dự án cũ.

rules:
  - name: "Không dùng syntax Laravel mới"
    description: |
      Laravel trong dự án là bản 8. Không dùng các API của Laravel 9+.
    examples:
      bad: &#91;"Route::controller(...)"]
      good: &#91;"Route::get(...)", "Route::post(...)"]

  - name: "Không tự tạo file"
    description: |
      Chỉ tạo file khi người dùng yêu cầu.

  - name: "Style code"
    description: |
      Tất cả code phải tuân theo PSR-12.</code></pre>



<h2 id="ket-luan" class="wp-block-heading"><strong>Kết luận</strong></h2>



<p>Viết <em>Cursor Rule</em> không phải để “làm màu”, mà để:</p>



<ul class="wp-block-list">
<li>Giữ AI chạy đúng đường.</li>



<li>Giảm lỗi.</li>



<li>Tăng tốc độ phát triển.</li>



<li>Chuẩn hóa output trong team.</li>



<li>Tạo ra môi trường làm việc ổn định.</li>
</ul>



<p>Muốn viết rule tốt → phải hiểu vấn đề, hiểu dự án, và hiểu AI dễ sai ở đâu.</p>



<p></p>
<p>The post <a href="https://blog.tomosia.com.vn/huong-dan-viet-cursor-rule-cho-nguoi-moi-bat-dau/">Hướng dẫn viết Cursor Rule cho người mới bắt đầu</a> appeared first on <a href="https://blog.tomosia.com.vn">Tomoshare</a>.</p>
]]></content:encoded>
					
					<wfw:commentRss>https://blog.tomosia.com.vn/huong-dan-viet-cursor-rule-cho-nguoi-moi-bat-dau/feed/</wfw:commentRss>
			<slash:comments>3</slash:comments>
		
		
			</item>
	</channel>
</rss>
