chore: readme 수정

This commit is contained in:
DongHyeonka
2026-07-29 18:03:21 +09:00
parent 8daa568746
commit 741c79b69f
180 changed files with 62890 additions and 13 deletions
@@ -0,0 +1,16 @@
# 로그인 후 요청에서 달라지는 네 인증 경계
## Alternative text
AP1, AP2, AP3, AP4의 브라우저 입력, 중간 변환, 보호 자원 credential과 브라우저 출력을 같은 네 축으로 비교한 다이어그램.
## Long description
왼쪽부터 AP1 SPA direct, AP2 token mediator, AP3 BFF, AP4 edge forward-auth를 읽는다. 각 항목은 브라우저가 보내는 입력, 중간 계층의 변환, 보호 자원이 실제로 받는 credential, 브라우저가 받는 출력을 같은 순서로 보여 준다. AP1과 AP2는 브라우저가 Bearer JWT로 API를 직접 호출하고, AP3는 BFF가 Bearer JWT를 조립하며, AP4는 Nginx가 인증 결과를 identity header와 internal token으로 바꾼다.
## Elements and evidence
- **AP1 · SPA direct** (concept): 브라우저가 보유한 access token으로 Resource Server를 직접 호출하는 패턴. Evidence: L90L101, L107L109.
- **AP2 · token mediator** (concept): Mediator가 refresh token을 보관하지만 access token을 브라우저에 전달해 직접 API 호출을 유지하는 패턴. Evidence: L90L101, L107L110.
- **AP3 · BFF** (concept): 브라우저 session을 BFF가 server-held access token 기반의 downstream Bearer 요청으로 바꾸는 패턴. Evidence: L90L101, L107L111.
- **AP4 · edge forward-auth** (concept): Nginx와 oauth2-proxy가 proxy session을 검증하고 allowlisted identity header와 internal token을 upstream에 전달하는 패턴. Evidence: L90L101, L107L112.
@@ -0,0 +1,15 @@
# 로그인 후 요청에서 달라지는 네 인증 경계
# Question: AP1부터 AP4까지 브라우저 입력, credential 변환, 보호 자원 입력과 최종 출력은 어떻게 다른가?
direction: right
n0: "AP1 · SPA direct" {
shape: rectangle
}
n1: "AP2 · token mediator" {
shape: rectangle
}
n2: "AP3 · BFF" {
shape: rectangle
}
n3: "AP4 · edge forward-auth" {
shape: rectangle
}
@@ -0,0 +1,9 @@
digraph techviz {
graph [rankdir=LR, splines=ortho, nodesep=0.55, ranksep=0.85];
node [fontname=Helvetica, fontsize=11, margin="0.18,0.12", style="rounded,filled", fillcolor=white, color="#2d4357", penwidth=1.5];
edge [fontname=Helvetica, fontsize=10, color="#364b5f", penwidth=1.4, arrowsize=0.75];
n0 [label="AP1 · SPA direct", shape=box, style="rounded,filled"];
n1 [label="AP2 · token mediator", shape=box, style="rounded,filled"];
n2 [label="AP3 · BFF", shape=box, style="rounded,filled"];
n3 [label="AP4 · edge forward-auth", shape=box, style="rounded,filled"];
}
@@ -0,0 +1,23 @@
<?xml version="1.0" encoding="UTF-8"?>
<mxfile host="app.diagrams.net" modified="2026-07-23T00:00:00.000Z" agent="techviz-harness" version="24.7.17" type="device">
<diagram id="four-pattern-request-boundaries" name="로그인 후 요청에서 달라지는 네 인증 경계">
<mxGraphModel dx="1126" dy="300" grid="1" gridSize="10" guides="1" tooltips="1" connect="1" arrows="1" fold="1" page="1" pageScale="1" pageWidth="1126" pageHeight="1169" math="0" shadow="0">
<root>
<mxCell id="0"/>
<mxCell id="1" parent="0"/>
<mxCell id="n_ap1" value="AP1 · SPA direct&lt;br/&gt;입력 · Bearer access token&lt;br/&gt;변환 · 없음&lt;br/&gt;자원 · 동일 Bearer JWT&lt;br/&gt;출력 · /api/me JSON" tooltip="브라우저가 보유한 access token으로 Resource Server를 직접 호출하는 패턴. | Evidence: L90-L101, L107-L109" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;strokeColor=#2563eb;strokeWidth=2;" vertex="1" parent="1">
<mxGeometry x="45.0" y="45.0" width="220.0" height="142.0" as="geometry"/>
</mxCell>
<mxCell id="n_ap2" value="AP2 · token mediator&lt;br/&gt;입력 · session 뒤 Bearer&lt;br/&gt;변환 · access JSON 반환&lt;br/&gt;자원 · browser-created JWT&lt;br/&gt;출력 · token JSON + /api/me" tooltip="Mediator가 refresh token을 보관하지만 access token을 브라우저에 전달해 직접 API 호출을 유지하는 패턴. | Evidence: L90-L101, L107-L110" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="317.0" y="45.0" width="220.0" height="142.0" as="geometry"/>
</mxCell>
<mxCell id="n_ap3" value="AP3 · BFF&lt;br/&gt;입력 · AP3_SESSION&lt;br/&gt;변환 · BFF Bearer 생성&lt;br/&gt;자원 · BFF Bearer JWT&lt;br/&gt;출력 · 중계된 JSON" tooltip="브라우저 session을 BFF가 server-held access token 기반의 downstream Bearer 요청으로 바꾸는 패턴. | Evidence: L90-L101, L107-L111" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="589.0" y="45.0" width="220.0" height="142.0" as="geometry"/>
</mxCell>
<mxCell id="n_ap4" value="AP4 · edge forward-auth&lt;br/&gt;입력 · AP4_SESSION&lt;br/&gt;변환 · auth 결과 투영&lt;br/&gt;자원 · identity + internal&lt;br/&gt;출력 · /edge/me JSON" tooltip="Nginx와 oauth2-proxy가 proxy session을 검증하고 allowlisted identity header와 internal token을 upstream에 전달하는 패턴. | Evidence: L90-L101, L107-L112" style="whiteSpace=wrap;html=1;rounded=1;strokeWidth=2;fontSize=14;fontStyle=1;fillColor=#ffffff;strokeColor=#2d4357;verticalAlign=middle;" vertex="1" parent="1">
<mxGeometry x="861.0" y="45.0" width="220.0" height="142.0" as="geometry"/>
</mxCell>
</root>
</mxGraphModel>
</diagram>
</mxfile>
@@ -0,0 +1,289 @@
{
"type": "excalidraw",
"version": 2,
"source": "techviz-harness",
"elements": [
{
"id": "node-ap1",
"type": "rectangle",
"x": 45.0,
"y": 45.0,
"width": 220.0,
"height": 142.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 725805455,
"version": 1,
"versionNonce": 16032614,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-ap1",
"type": "text",
"x": 55.0,
"y": 55.0,
"width": 200.0,
"height": 122.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1345418364,
"version": 1,
"versionNonce": 956861127,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "AP1 · SPA direct\n입력 · Bearer access token\n변환 · 없음\n자원 · 동일 Bearer JWT\n출력 · /api/me JSON",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "AP1 · SPA direct\n입력 · Bearer access token\n변환 · 없음\n자원 · 동일 Bearer JWT\n출력 · /api/me JSON",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-ap2",
"type": "rectangle",
"x": 317.0,
"y": 45.0,
"width": 220.0,
"height": 142.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1770767914,
"version": 1,
"versionNonce": 437466311,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-ap2",
"type": "text",
"x": 327.0,
"y": 55.0,
"width": 200.0,
"height": 122.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1794487571,
"version": 1,
"versionNonce": 1211856887,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "AP2 · token mediator\n입력 · session 뒤 Bearer\n변환 · access JSON 반환\n자원 · browser-created JWT\n출력 · token JSON + /api/me",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "AP2 · token mediator\n입력 · session 뒤 Bearer\n변환 · access JSON 반환\n자원 · browser-created JWT\n출력 · token JSON + /api/me",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-ap3",
"type": "rectangle",
"x": 589.0,
"y": 45.0,
"width": 220.0,
"height": 142.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 134610251,
"version": 1,
"versionNonce": 1259056791,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-ap3",
"type": "text",
"x": 599.0,
"y": 55.0,
"width": 200.0,
"height": 122.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1719537796,
"version": 1,
"versionNonce": 237559748,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "AP3 · BFF\n입력 · AP3_SESSION\n변환 · BFF Bearer 생성\n자원 · BFF Bearer JWT\n출력 · 중계된 JSON",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "AP3 · BFF\n입력 · AP3_SESSION\n변환 · BFF Bearer 생성\n자원 · BFF Bearer JWT\n출력 · 중계된 JSON",
"autoResize": true,
"lineHeight": 1.25
},
{
"id": "node-ap4",
"type": "rectangle",
"x": 861.0,
"y": 45.0,
"width": 220.0,
"height": 142.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "#ffffff",
"fillStyle": "solid",
"strokeWidth": 2,
"strokeStyle": "solid",
"roughness": 1,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1479126963,
"version": 1,
"versionNonce": 1497424249,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false
},
{
"id": "node-label-ap4",
"type": "text",
"x": 871.0,
"y": 55.0,
"width": 200.0,
"height": 122.0,
"angle": 0,
"strokeColor": "#1e1e1e",
"backgroundColor": "transparent",
"fillStyle": "solid",
"strokeWidth": 1,
"strokeStyle": "solid",
"roughness": 0,
"opacity": 100,
"groupIds": [],
"frameId": null,
"index": null,
"roundness": {
"type": 3
},
"seed": 1313196111,
"version": 1,
"versionNonce": 1204290805,
"isDeleted": false,
"boundElements": [],
"updated": 0,
"link": null,
"locked": false,
"fontSize": 15,
"fontFamily": 5,
"text": "AP4 · edge forward-auth\n입력 · AP4_SESSION\n변환 · auth 결과 투영\n자원 · identity + internal\n출력 · /edge/me JSON",
"textAlign": "center",
"verticalAlign": "middle",
"containerId": null,
"originalText": "AP4 · edge forward-auth\n입력 · AP4_SESSION\n변환 · auth 결과 투영\n자원 · identity + internal\n출력 · /edge/me JSON",
"autoResize": true,
"lineHeight": 1.25
}
],
"appState": {
"gridSize": 10,
"viewBackgroundColor": "#ffffff",
"currentItemFontFamily": 5
},
"files": {}
}
@@ -0,0 +1,32 @@
{
"harness_version": "0.2.0",
"spec_id": "four-pattern-request-boundaries",
"spec_version": "1.1",
"spec_sha256": "11344b11427ef0a66e5dc29869606f51063afdb71489fc22f7ad52d7c3458e79",
"source_context": {
"document": "document.md",
"document_sha256": "df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371",
"anchor": {
"kind": "marker",
"value": "four-pattern-request-boundaries",
"line": 114
}
},
"outputs": [
"four-pattern-request-boundaries.svg",
"four-pattern-request-boundaries.drawio",
"four-pattern-request-boundaries.mmd",
"four-pattern-request-boundaries.d2",
"four-pattern-request-boundaries.dot",
"four-pattern-request-boundaries.excalidraw",
"four-pattern-request-boundaries.alt.md"
],
"lint_issue_count": 0,
"assumption_count": 0,
"assumptions_allowed": false,
"composition_profile": "comparison",
"reference_ids": [
"contract-comparison"
],
"diagram_only": true
}
@@ -0,0 +1,7 @@
%% 로그인 후 요청에서 달라지는 네 인증 경계
%% question: AP1부터 AP4까지 브라우저 입력, credential 변환, 보호 자원 입력과 최종 출력은 어떻게 다른가?
flowchart LR
n0["AP1 · SPA direct"]
n1["AP2 · token mediator"]
n2["AP3 · BFF"]
n3["AP4 · edge forward-auth"]
@@ -0,0 +1,90 @@
<?xml version="1.0" encoding="UTF-8"?>
<svg xmlns="http://www.w3.org/2000/svg" width="1126" height="300" viewBox="0 0 1126 300" role="img" aria-labelledby="diagram-title diagram-description">
<title id="diagram-title">로그인 후 요청에서 달라지는 네 인증 경계</title>
<desc id="diagram-description">왼쪽부터 AP1 SPA direct, AP2 token mediator, AP3 BFF, AP4 edge forward-auth를 읽는다. 각 항목은 브라우저가 보내는 입력, 중간 계층의 변환, 보호 자원이 실제로 받는 credential, 브라우저가 받는 출력을 같은 순서로 보여 준다. AP1과 AP2는 브라우저가 Bearer JWT로 API를 직접 호출하고, AP3는 BFF가 Bearer JWT를 조립하며, AP4는 Nginx가 인증 결과를 identity header와 internal token으로 바꾼다.</desc>
<metadata>{&quot;techviz&quot;:{&quot;spec_version&quot;:&quot;1.1&quot;,&quot;id&quot;:&quot;four-pattern-request-boundaries&quot;,&quot;profile&quot;:&quot;comparison&quot;},&quot;source_context&quot;:{&quot;document&quot;:&quot;document.md&quot;,&quot;document_sha256&quot;:&quot;df4d1a604c74e756672b5b40510abfedb8c67b39af280a5f51985ea9972f5371&quot;,&quot;anchor&quot;:{&quot;kind&quot;:&quot;marker&quot;,&quot;value&quot;:&quot;four-pattern-request-boundaries&quot;,&quot;line&quot;:114}},&quot;evidence_policy&quot;:&quot;Each factual element cites source lines or is marked assumption.&quot;,&quot;diagram_only&quot;:true}</metadata>
<defs>
<marker id="arrow" viewBox="0 0 10 10" refX="9" refY="5" markerWidth="7" markerHeight="7" orient="auto-start-reverse">
<path d="M 0 0 L 10 5 L 0 10 z" />
</marker>
<style>
:root { color-scheme: light; }
text { font-family: Inter, Pretendard, ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif; fill: #111827; }
.canvas { fill: #ffffff; }
.group-box { fill: #ffffff; stroke: #9ca3af; stroke-width: 1.4; stroke-dasharray: 7 5; }
.group-label-bg { fill: #ffffff; }
.group-label { font-size: 13px; font-weight: 650; fill: #374151; }
.edge { fill: none; stroke: #374151; stroke-width: 1.8; stroke-linejoin: round; stroke-linecap: round; marker-end: url(#arrow); }
.edge.style-dashed, .edge.semantic-dashed, .edge.assumption { stroke-dasharray: 7 5; }
.edge.style-dotted { stroke-dasharray: 2 5; }
.edge.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.edge.emphasis-muted { stroke: #9ca3af; }
.edge.emphasis-warning, .edge.kind-failure, .edge.kind-error { stroke: #dc2626; stroke-width: 2.2; }
.edge-label-bg { fill: #ffffff; }
.edge-label { font-size: 12px; font-weight: 560; text-anchor: middle; }
.node-shape { fill: #ffffff; stroke: #4b5563; stroke-width: 1.7; }
.node-shape.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-shape.emphasis-muted { stroke: #9ca3af; fill: #f9fafb; }
.node-shape.emphasis-warning { stroke: #d97706; stroke-width: 2; fill: #fffdf5; }
.node-shape.kind-database, .node-shape.kind-datastore, .node-shape.kind-storage { fill: #f8fafc; }
.node-shape.kind-queue, .node-shape.kind-event, .node-shape.kind-topic { fill: #fafafa; }
.node-shape.assumption { stroke-dasharray: 4 4; }
.storage-bottom, .controller-divider { fill: none; stroke: #4b5563; stroke-width: 1.4; }
.controller-led { fill: #4b5563; }
.actor-symbol { fill: none; stroke: #4b5563; stroke-width: 1.8; stroke-linecap: round; }
.actor-symbol.emphasis-primary { stroke: #2563eb; stroke-width: 2.2; }
.node-label { font-size: 14px; font-weight: 650; text-anchor: middle; }
.node-role { font-size: 10px; letter-spacing: 0.04em; text-anchor: middle; fill: #6b7280; }
.node-detail-divider { stroke: #d1d5db; stroke-width: 1; }
.node-detail { font-size: 11px; fill: #374151; }
.assumption-badge { font-size: 9px; font-weight: 700; fill: #92400e; }
.failure-mark { stroke: #dc2626; stroke-width: 4; stroke-linecap: round; }
.lifeline { stroke: #9ca3af; stroke-width: 1.2; stroke-dasharray: 5 5; }
.timeline-axis { stroke: #374151; stroke-width: 1.8; marker-end: url(#arrow); }
.timeline-stem { stroke: #6b7280; stroke-width: 1.3; }
.timeline-marker { fill: #ffffff; stroke: #374151; stroke-width: 1.7; }
.timeline-marker.primary { fill: #2563eb; stroke: #2563eb; }
.timeline-marker.warning { fill: #dc2626; stroke: #dc2626; }
.timeline-label { font-size: 13px; font-weight: 650; text-anchor: middle; }
.timeline-detail { font-size: 11px; fill: #4b5563; text-anchor: middle; }
</style>
</defs>
<rect class="canvas" width="1126" height="300" />
<g id="node-ap1">
<rect class="node-shape kind-concept emphasis-primary role-option" data-evidence="90-101,107-109" x="45.0" y="45.0" width="220.0" height="142.0" rx="7" />
<text class="node-label" x="155.0" y="82.0">AP1 · SPA direct</text>
<line class="node-detail-divider" x1="59.0" y1="103.0" x2="251.0" y2="103.0" />
<text class="node-detail" x="61.0" y="120.0">입력 · Bearer access token</text>
<text class="node-detail" x="61.0" y="136.0">변환 · 없음</text>
<text class="node-detail" x="61.0" y="152.0">자원 · 동일 Bearer JWT</text>
<text class="node-detail" x="61.0" y="168.0">출력 · /api/me JSON</text>
</g>
<g id="node-ap2">
<rect class="node-shape kind-concept emphasis-normal role-option" data-evidence="90-101,107-110" x="317.0" y="45.0" width="220.0" height="142.0" rx="7" />
<text class="node-label" x="427.0" y="82.0">AP2 · token mediator</text>
<line class="node-detail-divider" x1="331.0" y1="103.0" x2="523.0" y2="103.0" />
<text class="node-detail" x="333.0" y="120.0">입력 · session 뒤 Bearer</text>
<text class="node-detail" x="333.0" y="136.0">변환 · access JSON 반환</text>
<text class="node-detail" x="333.0" y="152.0">자원 · browser-created JWT</text>
<text class="node-detail" x="333.0" y="168.0">출력 · token JSON + /api/me</text>
</g>
<g id="node-ap3">
<rect class="node-shape kind-concept emphasis-normal role-option" data-evidence="90-101,107-111" x="589.0" y="45.0" width="220.0" height="142.0" rx="7" />
<text class="node-label" x="699.0" y="82.0">AP3 · BFF</text>
<line class="node-detail-divider" x1="603.0" y1="103.0" x2="795.0" y2="103.0" />
<text class="node-detail" x="605.0" y="120.0">입력 · AP3_SESSION</text>
<text class="node-detail" x="605.0" y="136.0">변환 · BFF Bearer 생성</text>
<text class="node-detail" x="605.0" y="152.0">자원 · BFF Bearer JWT</text>
<text class="node-detail" x="605.0" y="168.0">출력 · 중계된 JSON</text>
</g>
<g id="node-ap4">
<rect class="node-shape kind-concept emphasis-normal role-option" data-evidence="90-101,107-112" x="861.0" y="45.0" width="220.0" height="142.0" rx="7" />
<text class="node-label" x="971.0" y="73.0">AP4 · edge</text>
<text class="node-label" x="971.0" y="91.0">forward-auth</text>
<line class="node-detail-divider" x1="875.0" y1="112.0" x2="1067.0" y2="112.0" />
<text class="node-detail" x="877.0" y="129.0">입력 · AP4_SESSION</text>
<text class="node-detail" x="877.0" y="145.0">변환 · auth 결과 투영</text>
<text class="node-detail" x="877.0" y="161.0">자원 · identity + internal</text>
<text class="node-detail" x="877.0" y="177.0">출력 · /edge/me JSON</text>
</g>
</svg>

After

Width:  |  Height:  |  Size: 7.1 KiB