<?xml version="1.0" encoding="utf-8"?>
<rss version="2.0" xmlns:atom="http://www.w3.org/2005/Atom">
    <channel>
        <title>may_yun.log</title>
        <link>https://velog.io/</link>
        <description>하루 일지 보단 행동 고찰 과정에 대한 개발 블로그</description>
        <lastBuildDate>Fri, 05 Dec 2025 17:08:46 GMT</lastBuildDate>
        <docs>https://validator.w3.org/feed/docs/rss2.html</docs>
        <generator>https://github.com/jpmonette/feed</generator>
        <image>
            <title>may_yun.log</title>
            <url>https://velog.velcdn.com/images/may_yun/profile/9281065c-bf82-480f-81a9-5b8a0d2a6aca/image.jpg</url>
            <link>https://velog.io/</link>
        </image>
        <copyright>Copyright (C) 2019. may_yun.log. All rights reserved.</copyright>
        <atom:link href="https://v2.velog.io/rss/may_yun" rel="self" type="application/rss+xml"/>
        <item>
            <title><![CDATA[[Renoflow] 패키지 구조]]></title>
            <link>https://velog.io/@may_yun/Renoflow-%ED%8C%A8%ED%82%A4%EC%A7%80-%EA%B5%AC%EC%A1%B0</link>
            <guid>https://velog.io/@may_yun/Renoflow-%ED%8C%A8%ED%82%A4%EC%A7%80-%EA%B5%AC%EC%A1%B0</guid>
            <pubDate>Fri, 05 Dec 2025 17:08:46 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>DDD + Layered Architecture(계층형 구조) 형태의 패키지 구조
기능 단위(feature/domain)로 분리</p>
</blockquote>
<p>1) 도메인(feature)별로 분리되어 유지보수가 쉬움
2) 계층 구조로 역할이 명확함 DDD
3) 규모가 커져도 확장 가능
4) MSA 전환도 쉬움
만약 나중에 각 기능을 MSA로 분리해야 하면,
각 도메인을 그대로 떼서 새로운 서비스로 추출할 수 있음.</p>
<hr>
<blockquote>
<p>Mysql 사용</p>
</blockquote>
<p>공사/시공 관리 ERP
고객, 파트너, 공정, 자재, 견적 등 관계형 데이터 구조가 강함</p>
<ul>
<li>선후관계가 존재
트랜잭션 중요(공정 등록 → 체크리스트 등록 → 작업일정 등록)</li>
</ul>
<p>데이터 구조가 명확한 편
👉 따라서 관계형 DB(RDBMS)를 쓰는 게 필수</p>
<ul>
<li>AWS RDS(MySQL)
가장 안정적, 장애 자동 복구, 백업 자동, 운영 부담 거의 없음
실제 서비스 운영(회사/스타트업)에서 대부분 선택하는 방식<ul>
<li>월 비용 발생 (초기 최소 2~3만원부터)</li>
</ul>
</li>
</ul>
<hr>
<p>참고 
<a href="https://romcanrom.tistory.com/75#toc2">https://romcanrom.tistory.com/75#toc2</a></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[RenoFlow] 기획 과정]]></title>
            <link>https://velog.io/@may_yun/RenoFlow-%EA%B8%B0%ED%9A%8D-%EA%B3%BC%EC%A0%95</link>
            <guid>https://velog.io/@may_yun/RenoFlow-%EA%B8%B0%ED%9A%8D-%EA%B3%BC%EC%A0%95</guid>
            <pubDate>Tue, 02 Dec 2025 08:01:16 GMT</pubDate>
            <description><![CDATA[<h1 id="renoflow-소개">RenoFlow 소개</h1>
<blockquote>
<p>인테리어 감리·현장 관리·실측·공정 일정 관리 등 
인테리어 프로젝트 운영 전반을 통합 관리하기 위한 맞춤형 ERP 솔루션
현장에 최적화된 태블릿/노트북 기반 입력 환경을 제공하여
실측 → 공정 관리 → 자재 → 견적 → A/S까지 단일 흐름으로 관리합니다.</p>
</blockquote>
<p><code>공사 현장 소음, 쉴 새 없이 울리는 카톡, 종이 영수증 더미, 엑셀 파일이 켜진 노트북</code>
<code>견적, 발주, 정산...언제까지 엑셀과 카톡방 뒤지며 일하시겠습니까?</code></p>
<p>고객 상담부터 견적, 공정 관리, 자재 발주, A/S까지 이어지는 모든 업무를 하나의 플랫폼에서 처리할 수 있어, 현장에서의 판단 속도를 높이고 내부 업무의 효율을 극대화합니다.</p>
<p>Renoflow는 인테리어 현장의 모든 순간을 기록하고 연결하여
더 빠르고 정확한 의사결정, 현장 중심의 효율, 체계적인 프로젝트 운영을 가능하게 합니다.</p>
<p>🔑 복잡한 공정을 다루는 인테리어 업계에서
Renoflow는 <strong>“현장의 실무를 가장 잘 이해하는 ERP”</strong>를 목표로
지속적으로 기능을 확장해 나가고 있습니다.</p>
<hr>
<h1 id="problem--solution">Problem | Solution</h1>
<blockquote>
<p>인테리어 프로젝트는 복잡하고 관리 리스크가 크다</p>
</blockquote>
<h3 id="💥-problem-과도한-업무-집중-1인이-모든-것을-처리하는-구조">💥 Problem) 과도한 업무 집중: <code>1인이 모든 것을 처리하는 구조</code></h3>
<p>소규모 인테리어 회사는 
실장 혹은 디자이너 한 명이 상담·실측·공정관리·자재·견적·고객 대응까지 담당합니다 
이로 인해 다음과 같은 문제가 고착화되어 있습니다</p>
<ul>
<li>사람 의존형 프로세스로 인수인계 불가 → 인력 교체 시 업무 중단</li>
<li>한 사람의 ‘머릿속’에 저장된 정보가 시스템화되지 않음</li>
<li>문서·데이터 누락 및 중복 발생</li>
<li>개인의 방식에 의존한 업무 운영</li>
<li>표준화된 템플릿 부재로 정합성 유지 어려움</li>
</ul>
<h3 id="🔑-solution-업무-표준화--템플릿-기반-자동화">🔑 Solution) 업무 표준화 &amp; 템플릿 기반 자동화</h3>
<p>✔ 단일 흐름으로 관리되는 All-in-One 프로세스</p>
<ul>
<li>실측 → 공정 체크 → 자재 → 견적까지
모두 표준 템플릿과 자동화 로직으로 정리</li>
<li>신규 인력이 투입돼도 동일 형태의 작업 가능</li>
<li>태블릿·노트북 기반의 현장 친화 인터페이스 제공, 사무실은 실시간 상태 확인 
→ 원스톱으로 모든 업무 수행</li>
<li>실측 기록, 자동 공정 체크리스트, 자재 관리 연동
견적 자동 생성, 고객 변경사항 기록, 일정 및 이슈 팀내 공유
→ 문서 누락 없음 / 중복 없음 / 인수인계 간편</li>
</ul>
<h3 id="💥-problem-공정·일정-관리의-복잡성">💥 Problem) 공정·일정 관리의 복잡성</h3>
<ul>
<li>자재 입고 기록이 엑셀·카톡·메모·사진으로 분산</li>
<li>어느 자재가 언제/어디로 입고되었는지 추적 불가</li>
<li>현장 도착 전에 자재 누락이 발생해 시공 지연</li>
<li>공정 간 의존성이 높아 순서가 어긋나면 시공 지연 및 비용 증가</li>
<li>현장·사무·발주팀 간 정보가 분리되어 의사결정이 느림</li>
<li>별도의 체크 체계 부재 → 누락 시 큰 비용 발생</li>
<li>상담·실측·공정·자재·A/S 등 단계별로 관리해야 할 데이터가 방대</li>
<li>고객 소통 기록의 누락이 분쟁으로 이어짐</li>
</ul>
<h3 id="🔑-solution-데이터-중앙관리">🔑 Solution) 데이터 중앙관리</h3>
<p>✔ 하나의 프로젝트가 하나의 데이터셋으로 구성</p>
<ul>
<li>카톡·메모·엑셀 등 산발적 문서를 제거
흐름을 한눈에 확인하는 “통합 관리”
프로젝트 전체 과정이 단일 플랫폼에서 운영됨</li>
<li>누구라도 프로젝트 전체 상황을 즉시 파악 가능
→ 정보가 단일화되고, 조직 전체의 협업 효율이 높아진다.
실장, 디자이너, 관리자, 발주 담당, A/S 담당 등
각자의 역할에 맞춰 필요한 데이터를 볼 수 있음
조직 내 정보 불균형 해소</li>
<li>현장 직배송 / 사무실 입고 / 픽업 / 협력업체 전달 등
자재 입고 경로별 상태를 시스템에서 즉시 확인
어떤 자재가 언제 도착했는지 자동 로그 기록</li>
<li>공정별로 필요한 자재 체크
시공 전 준비해야 할 자재 자동 검증
→ 자재 누락으로 인한 공정 지연을 원천 차단</li>
<li>커뮤니케이션 비용 절감</li>
</ul>
<h3 id="💥-problem-견적-오류로-인한-직접적-손실">💥 Problem) 견적 오류로 인한 직접적 손실</h3>
<p>인테리어 견적은 인건비·자재 단가가 지속적으로 변동하기 때문에
항상 최신 정보를 기초로 견적을 산출해야 합니다.</p>
<p>그러나 업계에서는 여전히:</p>
<ul>
<li>종이 지류, 엑셀, 수기로 견적 관리</li>
<li>어떤 부분이 수정되었는지 이력 추적 불가</li>
<li>이동 중(현장 → 사무실) 문서 확인 어렵고 최신 여부 파악 불가</li>
<li>잘못된 단가 입력 → 회사의 직접적 손실 발생
→ 견적 실수는 곧바로 재료비·인건비 차액으로 이어져 기업에 손해를 준다.</li>
</ul>
<h3 id="🔑-solution-견적서-자동-생성--최신-단가-연동">🔑 Solution) 견적서 자동 생성 &amp; 최신 단가 연동</h3>
<p>✔ 견적서 자동 생성 &amp; 최신 단가 연동
Renoflow는 실무에서 가장 큰 리스크인 견적 실수를 제거합니다.</p>
<ul>
<li>인건비·자재 단가 변경 반영하여 견적서 자동 생성</li>
<li>품목별 수정 이력 자동 기록</li>
<li>어디서 단가가 바뀌었는지 추적 가능</li>
<li>현장에서 즉시 견적 생성 가능</li>
<li>지류 및 수기 관리로 발생하는 ‘가격 오류 리스크’ 제거
→ 잘못된 단가 입력으로 발생하는 손실 방지</li>
</ul>
<h3 id="💥-problem-현장에서-모바일은-비효율">💥 Problem) 현장에서 모바일은 비효율</h3>
<ul>
<li>화면이 작아 실측, 체크리스트, 사진 업로드가 어려움</li>
<li>고객과 상담 중 입력 행동 자체가 집중도 저해</li>
</ul>
<h3 id="🔑-solution-현장에서-쓰기-좋은-화면-구성">🔑 Solution) 현장에서 쓰기 좋은 화면 구성</h3>
<p>Renoflow는 현장의 특성을 반영하여 
‘태블릿·노트북 기반의 이동성’과 ‘PC 수준의 데이터 처리 환경’을 제공하도록 설계되었습니다.</p>
<ul>
<li>태블릿 최적화 UI</li>
<li>손가락 입력 중심 인터랙션</li>
<li>사진·치수 입력 자동 저장</li>
<li>상담 중 집중도 저해 최소화</li>
</ul>
<h3 id="💥-problem-고객과의-소통-기록-부족">💥 Problem) 고객과의 소통 기록 부족</h3>
<ul>
<li>변경 요청 및 의사결정 기록 부재
→ 분쟁·오해·추가 비용 발생</li>
</ul>
<h3 id="💥-solution-고객-관리-서비스">💥 Solution) 고객 관리 서비스</h3>
<ul>
<li>고객 서비스 요청,제공 모두 기록 보유</li>
<li>데이터를 통한 고객 맞춤 서비스 제공 가능</li>
<li>고객 정보 관리 </li>
</ul>
<hr>
<h2 id="📌-핵심-기능과-기능이-필요한-이유">📌 핵심 기능과 기능이 필요한 이유</h2>
<p>Renoflow의 핵심 기능은 
<code>프로젝트 관리</code>와 <code>견적서 자동 생성</code>, <code>자재 관리</code> 에 있습니다.
이 외의 고객관리, 거래처 관리가 있지만 이는 Renoflow를 사용하면 기본적으로 사용 가능합니다</p>
<h3 id="1-프로젝트-관리"><strong>1. 프로젝트 관리</strong></h3>
<ul>
<li>공간별 공정 구성, 일정 계획, 작업 순서 관리</li>
<li>공사 전/직전/중/후/A/S 단계별 체크리스트</li>
<li>현장에서 발생하는 이슈를 즉시 기록하고 공유</li>
<li>부서 간 정보 불균형 해소 및 동일한 기준으로 프로젝트 운영</li>
</ul>
<h3 id="2-견적서-자동-생성"><strong>2. 견적서 자동 생성</strong></h3>
<ul>
<li>현장에서 입력한 실측·자재 정보 기반으로 <strong>즉시 견적서 생성</strong></li>
<li>공정별 단가·수량 자동 계산</li>
<li>반복되는 견적 작성 시간을 획기적으로 절감</li>
<li>상담 현장에서 고객 의사결정 속도 상승</li>
</ul>
<h3 id="3-자재-관리"><strong>3. 자재 관리</strong></h3>
<ul>
<li>각 공정별 필요 자재 자동 연동</li>
<li>자재 소요량·발주 시점 관리</li>
<li>중복 발주 방지 및 비용 관리 정확성 향상</li>
</ul>
<h3 id="기본-제공-기능-고객-관리-·-거래처-관리"><strong>(기본 제공 기능)</strong> 고객 관리 · 거래처 관리</h3>
<ul>
<li>프로젝트와 연동된 고객/거래처 정보 자동 생성</li>
<li>별도 CRM 없이도 기본 관리 가능</li>
</ul>
<hr>
<h4 id="1-프로젝트-관리-현장-관리">1. 프로젝트 관리 (현장 관리)</h4>
<p>: 현장 관리 카테고리는 공사 단위가 한 프로젝트 입니다. </p>
<ul>
<li><p>view 1) 현장 관리 대시보드
매 월 진행중인 날과 견적예정인 현장을 한눈에 파악 가능하도록 구현했고,
프로젝트 검색이 가능하도록하여 프로젝트 조회를 빠르게 할 수 있도록 구현했습니다
로그인한 담당자가 진행중인 현장을 관리할 수 있도록 하여 담당자 프로젝트 관리에 집중할 수 있도록 기획했습니다
<img src="https://velog.velcdn.com/images/may_yun/post/fa317c70-3d77-44d3-8062-914ab8e0abc5/image.png" alt=""></p>
</li>
<li><p>view 2) 새 프로젝트 등록
사업자에게 있어 데이터 수집은 마케팅을 위해 중요한 정보가 됩니다.
<img src="https://velog.velcdn.com/images/may_yun/post/fa3941f8-556b-4b06-a736-308d99e83b06/image.png" alt=""> 
예를 들어 work-in 오프라인으로 온 고객의 경우 두 가지 케이스로 나뉘어볼 수 있습니다</p>
<ul>
<li>단순 견적을 알고싶은 고객의 경우: WORK-IN 고객으로 등록하여 상담을 진행하게 됩니다</li>
<li>현장을 밝히고 실제 견적을 내는 고객의 경우: 고객 등록 후 상담을 진행하게 됩니다
순서에 있어서 고객의 상태 파악이 가능할것으로 예상됩니다
어떤 담당자가 상담한 고객인지 확인 또한 가능합니다.</li>
</ul>
</li>
<li><p>view 3) 프로젝트 별 관리 대시보드 (개요)
프로젝트 (공사 단위)의 디테일을 설정하고 관리하는 페이지 입니다.
<img src="https://velog.velcdn.com/images/may_yun/post/84e78f86-a0b6-4170-b6db-c5e0ab109347/image.png" alt=""></p>
</li>
<li><p>view 3-2) 프로젝트 별 관리: 공간별 체크리스트 <code>현장</code>
공간별 체크리스트를 관리하여 담당자가 신규로 프로젝트에 투입되어도 체크리스트를 보고 대응이 가능하도록 업무의 통일화와 일관성을 줄 수 있습니다
<img src="https://velog.velcdn.com/images/may_yun/post/a7068985-22fa-412e-925e-762660eb7e84/image.png" alt=""><img src="https://velog.velcdn.com/images/may_yun/post/ff5700b9-1878-4d37-ae8c-d46e8d90fa57/image.png" alt=""></p>
</li>
<li><p>view 3-3) 프로젝트 별 관리: 공간별 실측 관리 (등록 및 조회) <code>현장</code>
<img src="https://velog.velcdn.com/images/may_yun/post/1ab857bd-a7b2-4aca-b5ec-7dea5fb07f14/image.png" alt=""> <img src="https://velog.velcdn.com/images/may_yun/post/be719027-81c2-4462-8b73-4eafc5b8bca0/image.png" alt=""> <img src="https://velog.velcdn.com/images/may_yun/post/62f2413e-48a1-40c9-844f-8ca28b128d2a/image.png" alt=""> <img src="https://velog.velcdn.com/images/may_yun/post/958a0b29-4d8e-4073-b669-1cb7d29f216b/image.png" alt=""></p>
</li>
<li><p>view 3-4) 프로젝트 별 관리: 공정표
프로젝트 공정과 시공자 시공자 시공 항목을 매칭합니다
공정표는 현장에서 관리하는것이 아닌, 상담 후 담당자가 지정하는 것이기 때문에 세부 설정이 가능합니다.
<img src="https://velog.velcdn.com/images/may_yun/post/92f08563-324c-4c5e-a0bd-23ed1201642f/image.png" alt=""> <img src="https://velog.velcdn.com/images/may_yun/post/da84c29a-1b66-4a48-8143-f3ed857ec7f4/image.png" alt=""></p>
</li>
<li><p>view 3-5) 프로젝트 별 관리: 자재 관리
<img src="https://velog.velcdn.com/images/may_yun/post/b4e37f40-97f3-44fb-9ef2-eb59e463e10c/image.png" alt=""> <img src="https://velog.velcdn.com/images/may_yun/post/e7098b44-e248-40af-9f74-c080efdd1ac8/image.png" alt=""> <img src="https://velog.velcdn.com/images/may_yun/post/e1d55c00-9455-4991-8571-b5e909c4f25a/image.png" alt=""> 등록된 자재 조회 <img src="https://velog.velcdn.com/images/may_yun/post/bb09eb6b-a4b7-4c22-82ee-7b69c4eae123/image.png" alt=""> <img src="https://velog.velcdn.com/images/may_yun/post/f620fe28-7b53-43c4-8091-aaba085f2d90/image.png" alt=""> <img src="https://velog.velcdn.com/images/may_yun/post/86cfeb6a-31d3-4d4c-8524-ab22e0ecb2a0/image.png" alt=""></p>
</li>
<li><p>view 3-6) 프로젝트별 관리 : 수익/지출 관리
<img src="https://velog.velcdn.com/images/may_yun/post/1e65e042-8d68-4083-9b75-885fcf9f6e01/image.png" alt=""></p>
</li>
</ul>
<h4 id="2-견적-관리">2. 견적 관리</h4>
<p>: 견적서 생성 및 관리</p>
<ul>
<li><p>view 1) 견적서 생성
<img src="https://velog.velcdn.com/images/may_yun/post/18a777a1-1c7c-4932-89ce-130c0a49ae2a/image.png" alt=""> 견적서 생성: 현장 검색 후 견적 항목 추가 <img src="https://velog.velcdn.com/images/may_yun/post/9a57081c-8b06-464b-a5dd-b88c28f93050/image.png" alt=""> <img src="https://velog.velcdn.com/images/may_yun/post/131f641c-4ce0-4491-80d8-8caf03e863fb/image.png" alt=""> <img src="https://velog.velcdn.com/images/may_yun/post/9cda4ee5-371a-4108-8573-88b38ef91259/image.png" alt=""> <img src="https://velog.velcdn.com/images/may_yun/post/b2072617-7d69-4f53-acd1-9bc7e098f480/image.png" alt=""></p>
</li>
<li><p>view 2) 단가표 계산기
<img src="https://velog.velcdn.com/images/may_yun/post/85a7c3d4-dec4-451c-b6df-c7c8dc00cfeb/image.png" alt=""> <img src="https://velog.velcdn.com/images/may_yun/post/0a4de842-ec65-46d9-bd0e-57cf6032ae35/image.png" alt=""></p>
</li>
<li><p>view 3) 빠른 견적 (템플릿)
<img src="https://velog.velcdn.com/images/may_yun/post/5966d163-6d0c-465a-ad5a-cfd4d2aeba94/image.png" alt=""></p>
</li>
</ul>
<h4 id="3-일정-관리">3. 일정 관리</h4>
<ul>
<li>view 1) 일정관리 대시보드
<img src="https://velog.velcdn.com/images/may_yun/post/480c402a-31f6-4bdf-bdea-b9d2123d9bf8/image.png" alt=""></li>
<li>view 1-2) 월간 플래너: 각 일정 클릭 시 해당 현장 정보 조회
<img src="https://velog.velcdn.com/images/may_yun/post/41cc6eaa-bf54-49aa-9e65-2e379c897b28/image.png" alt=""> <img src="https://velog.velcdn.com/images/may_yun/post/25a90161-9e79-463d-ade7-afbb32afccf8/image.png" alt=""></li>
<li>view 1-3) 주간 플래너: 주간 일정 관리
<img src="https://velog.velcdn.com/images/may_yun/post/bb8af4e0-44c2-4261-a4c6-c3ee362f2219/image.png" alt=""> <img src="https://velog.velcdn.com/images/may_yun/post/85ca61e6-354c-4550-bb4a-2366c4197a93/image.png" alt=""></li>
</ul>
<hr>
<h1 id="renoflow가-제공하는-가치">Renoflow가 제공하는 가치</h1>
<ol>
<li><p>업무 표준화 및 인력 대체 가능성 확보
현장의 문서·관리 방식이 실장 개인의 습관과 경험에 의존하는 문제를 해결합니다.
Renoflow는 실측, 자재, 고객 소통, 일정, 공정 등의 모든 프로세스를 표준화된 데이터 구조로 전환하여 인수인계가 용이하여 대체 인력 투입 가능하게 됩니다
실장 부재 시 업무 공백 최소화라는 운영 안정성을 제공합니다.</p>
</li>
<li><p>견적 자동 생성으로 오차·손실 최소화
인건비·자재 단가는 계속 변동됩니다. 
종이나 엑셀로 관리할 경우 어느 부분을 변경했는지 추적할 수 없고
이동하거나 현장에서 수정하기 어렵고 과거 견적의 버전 관리가 불가능합니다.
Renoflow는 실측·자재 데이터와 최신 단가 정보를 기반으로
정확한 견적서를 자동 생성하고
변경 이력을 자동 관리하여 견적 오류로 발생하는 손해를 근본적으로 차단합니다.</p>
</li>
<li><p>현장 중심의 효율성 강화
작업자는 태블릿 하나로 실측·사진·메모·공정 체크까지 현장에서 즉시 입력할 수 있습니다.
현장에서 수집된 모든 데이터는 실시간으로 내부 직원과 공유되며,
누락되기 쉬운 요소를 자동 더블체크하여 불필요한 커뮤니케이션과 재확인 시간을 줄여줍니다.</p>
</li>
<li><p>고객 소통 품질 향상
프로젝트의 모든 히스토리가 기록되므로
고객과의 상담 내용, 변경 요청, 일정 조율 등이 명확하게 관리되어 분쟁 예방과 신뢰 강화에 도움을 줍니다.</p>
</li>
<li><p>일정 리스크 최소화
공정 간 의존 관계를 고려한 일정 관리로
작업 순서 오류·공정 간 충돌을 예방하여 시공 지연을 최소화합니다.</p>
</li>
<li><p>부서 간 정보 불균형 해소
현장팀, 사무팀, 발주팀 모두 동일한 데이터를 공유함으로써
각 부서가 독립적으로 움직이던 방식에서 → 데이터 기반 협업 구조로 전환됩니다.</p>
</li>
</ol>
<hr>
<h1 id="renoflow-사용자-주요-기능">Renoflow 사용자 주요 기능</h1>
<h2 id="1-👩🦰-현장-실측·고객-상담을-담당하는-실장"><strong>1) 👩‍🦰 현장 실측·고객 상담을 담당하는 실장</strong></h2>
<h3 id="주요-pain-point"><strong>주요 Pain Point</strong></h3>
<ul>
<li>상담 중 신규 문의가 이어지며 기존 업무 누락 발생</li>
<li>빠르게 변하는 자재 트렌드·가격 변동을 지속적으로 반영하기 어려움</li>
<li>견적서 수정 시 변경 이력 관리가 어려워 손실 위험 존재</li>
<li>현장 실측 후 자료 정리·전달 과정이 비효율적</li>
<li>공간별·공정별 체크리스트가 통일되어 있지 않아 인수인계 어려움</li>
</ul>
<h3 id="renoflow-제공-기능"><strong>Renoflow 제공 기능</strong></h3>
<ul>
<li><p><strong>상담·고객 관리 자동화</strong></p>
<p>  상담 기록, 고객 정보, 메모를 자동 정리해 누락 방지</p>
</li>
<li><p><strong>자재 가격·트렌드 실시간 반영</strong></p>
<p>  업데이트되는 자재 DB로 최신 정보 기반 견적 산출</p>
</li>
<li><p><strong>1초 견적 생성 및 이력 관리</strong></p>
<p>  수정 내역 자동 기록으로 손해 리스크 최소화</p>
</li>
<li><p><strong>현장 실측 데이터 즉시 저장</strong></p>
<p>  사진·치수·메모를 현장에서 바로 업로드</p>
</li>
<li><p><strong>공간·공정별 표준 체크리스트 제공</strong></p>
<p>  실수와 누락을 줄이고 팀 간 인수인계용 템플릿으로 활용</p>
</li>
</ul>
<hr>
<h2 id="2-👫-반셀프-인테리어를-고민하는-일반-사용자"><strong>2) 👫 반셀프 인테리어를 고민하는 일반 사용자</strong></h2>
<h3 id="주요-pain-point-1"><strong>주요 Pain Point</strong></h3>
<ul>
<li>공정 순서를 정확히 모름</li>
<li>어떤 공사가 어느 시점에 진행되어야 하는지 판단 어려움</li>
<li>각 공정별로 무엇을 체크해야 하는지 불확실</li>
<li>스케줄 조율이 어려워 공사 지연·중복 발생 가능</li>
</ul>
<h3 id="renoflow-제공-기능-1"><strong>Renoflow 제공 기능</strong></h3>
<ul>
<li><p><strong>공정 순서 자동 가이드</strong></p>
<p>  전체 공사 흐름을 한눈에 이해할 수 있도록 구조화</p>
</li>
<li><p><strong>공정 일정 자동 조율</strong></p>
<p>  이전/다음 공정과의 의존성을 기반으로 일정 추천</p>
</li>
<li><p><strong>공정 체크리스트 제공</strong></p>
<p>  각 공정별 사전 준비사항·확인 포인트 제공</p>
</li>
<li><p><strong>내 공사의 실시간 진행도 확인</strong></p>
<p>  투명하고 명확한 공사 관리 경험 제공</p>
</li>
</ul>
<hr>
<h2 id="3-👨🔧-현장-공유가-필요한-인테리어-업자시공팀-하도급팀"><strong>3) 👨‍🔧 현장 공유가 필요한 인테리어 업자(시공팀, 하도급팀)</strong></h2>
<h3 id="주요-pain-point-2"><strong>주요 Pain Point</strong></h3>
<ul>
<li>사무실 담당자와 현장 담당자 간 소통 오류로 문제 발생</li>
<li>공정 변경·자재 부족·지연 등 핵심 정보 전달이 늦음</li>
<li>사진, 메모, 자재 위치 등의 정보 공유가 각각의 메신저·문서에 분산</li>
<li>동일한 정보를 여러 번 설명해야 하는 비효율</li>
</ul>
<h3 id="renoflow-제공-기능-2"><strong>Renoflow 제공 기능</strong></h3>
<ul>
<li><p><strong>현장–사무실 간 실시간 정보 공유 플랫폼</strong></p>
<p>  공정 진행, 자재 입고, 작업 상태 등을 한 번에 공유</p>
</li>
<li><p><strong>공정 변경·자재 이슈 알림</strong></p>
<p>  즉시 전달되어 오류와 재작업 최소화</p>
</li>
<li><p><strong>사진·파일·메모 자동 정리</strong></p>
<p>  현장에서 촬영 시 즉시 프로젝트에 업로드</p>
</li>
<li><p><strong>하청팀까지 포함한 공정 일정 통합 관리</strong></p>
<p>  전체 일정과 작업자의 투입 상황을 명확하게 공유</p>
</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[SEO-BETTER-PAY] membership 기획]]></title>
            <link>https://velog.io/@may_yun/SEO-BETTER-PAY-membership-%EA%B8%B0%ED%9A%8D</link>
            <guid>https://velog.io/@may_yun/SEO-BETTER-PAY-membership-%EA%B8%B0%ED%9A%8D</guid>
            <pubDate>Mon, 24 Mar 2025 11:27:27 GMT</pubDate>
            <description><![CDATA[<h2 id="membership-회원-관리">membership 회원 관리</h2>
<p>✅ 1. 회원 가입 / 탈퇴</p>
<ul>
<li><input disabled="" type="checkbox"> 이메일, 휴대폰 번호, 비밀번호 등 입력받아 등록</li>
<li><input disabled="" type="checkbox"> 필수 정보 유효성 검사</li>
<li><input disabled="" type="checkbox"> 탈퇴 요청 처리 (soft delete 또는 완전 삭제)</li>
</ul>
<p>✅ 2. 로그인 / 로그아웃</p>
<ul>
<li><input disabled="" type="checkbox"> 로그인: ID/PW 또는 OAuth2, 인증 성공 시 토큰(JWT 등) 발급</li>
<li><input disabled="" type="checkbox"> 로그아웃: 토큰 무효화 (Blacklist 저장 등)</li>
</ul>
<p>✅ 3. 인증(Authentication)</p>
<ul>
<li><input disabled="" type="checkbox"> 비밀번호 확인</li>
<li><input disabled="" type="checkbox"> 토큰 검증</li>
<li><input disabled="" type="checkbox"> 토큰 갱신 (Refresh Token)</li>
</ul>
<p>✅ 4. 인가(Authorization)</p>
<ul>
<li><input disabled="" type="checkbox"> 역할(Role), 권한(Permission) 관리 (예: USER, ADMIN)</li>
<li><input disabled="" type="checkbox"> 서비스 접근 제어를 위한 권한 검증</li>
</ul>
<p>✅ 5. 회원 정보 조회 / 수정</p>
<ul>
<li><input disabled="" type="checkbox"> 마이페이지: 이름, 이메일, 휴대폰번호 등</li>
<li><input disabled="" type="checkbox"> 비밀번호 변경</li>
<li><input disabled="" type="checkbox"> 이메일 / 휴대폰 인증</li>
</ul>
<p>✅ 6. 소셜 로그인 (선택)</p>
<ul>
<li><input disabled="" type="checkbox"> Google, Kakao, Naver 등 OAuth2 기반 로그인</li>
<li><input disabled="" type="checkbox"> 소셜 계정 연동 / 해제</li>
</ul>
<p>✅ 7. 사용자 상태 관리</p>
<ul>
<li><input disabled="" type="checkbox"> 활성 / 비활성 / 탈퇴 / 정지 상태</li>
<li><input disabled="" type="checkbox"> 서비스 이용 제한 관리</li>
</ul>
<p>✅ 8. 보안 관련 기능</p>
<ul>
<li><input disabled="" type="checkbox"> 비밀번호 암호화 (예: bcrypt)</li>
<li><input disabled="" type="checkbox"> 비밀번호 찾기 (임시 비밀번호, 이메일 링크 등)</li>
<li><input disabled="" type="checkbox"> 2차 인증 (선택)</li>
</ul>
<p>✅ 9. 사용자 Activity 로그 (선택)</p>
<ul>
<li><input disabled="" type="checkbox"> 로그인 이력</li>
<li><input disabled="" type="checkbox"> 비정상 접근 기록</li>
<li><input disabled="" type="checkbox"> 계정 변경 이력</li>
</ul>
<p>✅ 10. 기타 연동 기능</p>
<ul>
<li><input disabled="" type="checkbox"> 다른 서비스(예: 결제, 알림, 고객센터 등)에서 회원 ID로 조회할 수 있도록 API 제공</li>
<li><input disabled="" type="checkbox"> 내부 인증용 API (예: 회원 ID → 유효성 검증)</li>
</ul>
<hr>
<h2 id="🛡️-1-인증authentication">🛡️ <strong>1. 인증(Authentication)</strong></h2>
<h3 id="🔐-1-1-비밀번호-관리">🔐 1-1. 비밀번호 관리</h3>
<ul>
<li><strong>암호화 저장</strong>:  <ul>
<li><code>bcrypt</code> 또는 <code>argon2</code> 사용  </li>
<li>절대 평문 저장 ❌  </li>
</ul>
</li>
<li><strong>비밀번호 정책</strong>:  <ul>
<li>최소 8자 이상, 대소문자+숫자+특수문자 조합  </li>
<li>반복 문자 제한, 과거 비밀번호 재사용 금지 등  </li>
</ul>
</li>
</ul>
<h3 id="🔑-1-2-로그인-방식">🔑 1-2. 로그인 방식</h3>
<ul>
<li><strong>ID/PW 로그인</strong>  <ul>
<li>로그인 시도 횟수 제한 (예: 5회 실패 시 잠금)</li>
<li>지연 응답 방식으로 brute-force 방지</li>
</ul>
</li>
<li><strong>OAuth2 지원</strong> (Google, Kakao 등)  <ul>
<li>소셜 토큰 검증 후 자체 유저 등록 or 로그인 처리  </li>
</ul>
</li>
</ul>
<h3 id="📱-1-3-2fa-2차-인증-선택">📱 1-3. 2FA (2차 인증, 선택)</h3>
<ul>
<li>OTP (예: Google Authenticator)  </li>
<li>SMS 또는 Email 기반 1회용 코드 인증</li>
</ul>
<hr>
<h2 id="🛡️-2-인가authorization">🛡️ <strong>2. 인가(Authorization)</strong></h2>
<h3 id="👤-2-1-rolepermission-기반-접근-제어">👤 2-1. Role/Permission 기반 접근 제어</h3>
<ul>
<li>예: <code>USER</code>, <code>ADMIN</code>, <code>SUSPENDED</code>  </li>
<li>Spring Security에서 <code>@PreAuthorize(&quot;hasRole(&#39;ADMIN&#39;)&quot;)</code> 등으로 제한  </li>
</ul>
<h3 id="📦-2-2-서비스-간-인가-처리-msa-환경">📦 2-2. 서비스 간 인가 처리 (MSA 환경)</h3>
<ul>
<li>내부 서비스 간 통신 시 <code>internal-token</code> 혹은 <code>mTLS</code> 사용  </li>
<li>Gateway에서 인증하고 내부 요청은 <code>X-USER-ID</code> header로 전달</li>
</ul>
<hr>
<h2 id="🛡️-3-세션--토큰-관리">🛡️ <strong>3. 세션 &amp; 토큰 관리</strong></h2>
<h3 id="🪪-3-1-jwt-access--refresh-token">🪪 3-1. JWT (Access / Refresh Token)</h3>
<ul>
<li><strong>Access Token</strong>:  <ul>
<li>수명 짧게 (예: 15분~30분)</li>
<li>요청 시 헤더에 <code>Authorization: Bearer &lt;token&gt;</code></li>
</ul>
</li>
<li><strong>Refresh Token</strong>:  <ul>
<li>수명 길게 (예: 2주~1달)</li>
<li>DB 저장 + Redis 저장 (보안 고려)</li>
</ul>
</li>
</ul>
<h3 id="💥-3-2-토큰-탈취-방지">💥 3-2. 토큰 탈취 방지</h3>
<ul>
<li>HTTP-only, Secure cookie로 전달 (특히 Refresh Token)</li>
<li>IP / User-Agent 조합 검사</li>
<li>Logout 시 Refresh Token 강제 삭제</li>
</ul>
<hr>
<h2 id="🛡️-4-공격-방어">🛡️ <strong>4. 공격 방어</strong></h2>
<h3 id="🧠-4-1-csrf-방지">🧠 4-1. CSRF 방지</h3>
<ul>
<li>REST API일 경우 CSRF 필요 없음 (stateless)</li>
<li>만약 세션/폼기반이라면 CSRF 토큰 사용</li>
</ul>
<h3 id="🕵️-4-2-xss--sql-injection-방지">🕵️ 4-2. XSS / SQL Injection 방지</h3>
<ul>
<li>입력값 검증 (서버측 Validation + Frontend도 가능하면 적용)</li>
<li>ORM 사용 (JPA, MyBatis 등에서 PreparedStatement 사용)</li>
<li>Content-Security-Policy 설정</li>
</ul>
<h3 id="🔄-4-3-rate-limiting">🔄 4-3. Rate Limiting</h3>
<ul>
<li>로그인 / 인증 API에는 초당 호출 수 제한</li>
<li>Redis + Spring Cloud Gateway Filter로 구현 가능</li>
</ul>
<hr>
<h2 id="🛡️-5-로깅--감사audit">🛡️ <strong>5. 로깅 &amp; 감사(Audit)</strong></h2>
<ul>
<li>로그인/로그아웃 시도 로그</li>
<li>비밀번호 변경, 탈퇴, 권한 변경 등 중요한 이벤트 로그 기록</li>
<li>별도 <code>Audit Log Service</code> 구성 가능</li>
<li>로그 내 개인정보 마스킹 필수 (예: 이메일 → <code>j***@gmail.com</code>)</li>
</ul>
<hr>
<h2 id="🛡️-6-암호화--데이터-보호">🛡️ <strong>6. 암호화 &amp; 데이터 보호</strong></h2>
<ul>
<li>비밀번호 외에도 중요한 개인정보(휴대폰, 이메일)는 <strong>AES</strong> 등으로 암호화 저장 가능</li>
<li>민감한 데이터 전송 시 SSL/TLS 적용</li>
<li>S3나 외부 저장소에 파일 업로드 시에도 서버단 암호화 고려</li>
</ul>
<hr>
<h2 id="🛡️-7-계정-보호-기능">🛡️ <strong>7. 계정 보호 기능</strong></h2>
<ul>
<li>로그인 시 타 기기에서 로그인 여부 표시  </li>
<li>최근 접속 기록 조회</li>
<li>계정 정지 기능 (<code>SUSPENDED</code>, <code>BLOCKED</code> 등 상태값)</li>
</ul>
<hr>
<h2 id="🛡️-8-기타-고려사항">🛡️ <strong>8. 기타 고려사항</strong></h2>
<ul>
<li><strong>탈퇴 후 데이터 관리</strong>: 법적 보관 vs 즉시 삭제</li>
<li><strong>비밀번호 찾기/재설정</strong>: 메일/SMS 통한 링크 방식, 링크는 일정 시간 내 만료</li>
<li><strong>IP 기반 접근 제어</strong> (특정 관리자 기능은 사내망 IP만 허용)</li>
</ul>
<hr>
<h2 id="✅-정리-보안-기능-체크리스트-예시">✅ 정리: 보안 기능 체크리스트 예시</h2>
<table>
<thead>
<tr>
<th>항목</th>
<th>구현 여부</th>
<th>메모</th>
</tr>
</thead>
<tbody><tr>
<td>비밀번호 암호화 저장 (bcrypt)</td>
<td>✅</td>
<td></td>
</tr>
<tr>
<td>JWT Access/Refresh 구분</td>
<td>✅</td>
<td></td>
</tr>
<tr>
<td>로그인 시도 제한</td>
<td>✅</td>
<td></td>
</tr>
<tr>
<td>2차 인증</td>
<td>⬜️</td>
<td>선택 사항</td>
</tr>
<tr>
<td>토큰 저장 시 IP/User-Agent 검사</td>
<td>⬜️</td>
<td>추천</td>
</tr>
<tr>
<td>관리자 권한 분리</td>
<td>✅</td>
<td></td>
</tr>
<tr>
<td>감사 로그 기록</td>
<td>✅</td>
<td>로그 마스킹 포함</td>
</tr>
<tr>
<td>Rate Limiting 적용</td>
<td>⬜️</td>
<td>Redis 추천</td>
</tr>
<tr>
<td>민감정보 암호화 저장</td>
<td>⬜️</td>
<td>AES256 고려</td>
</tr>
<tr>
<td>CSRF/XSS 대응</td>
<td>✅</td>
<td></td>
</tr>
</tbody></table>
<hr>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링시큐리티] 사용자 정보 조회 / 인증 상세]]></title>
            <link>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C-%EC%9D%B8%EC%A6%9D-%EC%83%81%EC%84%B8</link>
            <guid>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EC%82%AC%EC%9A%A9%EC%9E%90-%EC%A0%95%EB%B3%B4-%EC%A1%B0%ED%9A%8C-%EC%9D%B8%EC%A6%9D-%EC%83%81%EC%84%B8</guid>
            <pubDate>Thu, 19 Dec 2024 14:14:47 GMT</pubDate>
            <description><![CDATA[<h1 id="userdetailsservice">UserDetailsService</h1>
<pre><code class="language-java">@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { 
http
.authorizeHttpRequests(auth -&gt; auth
.requestMatchers(&quot;/css/**&quot;, &quot;/images/**&quot;, &quot;/js/**&quot;, &quot;/favicon.*&quot;, &quot;/*/icon-*&quot;).permitAll() .requestMatchers(&quot;/&quot;).permitAll()
.anyRequest().authenticated())
.formLogin(form -&gt; form.loginPage(&quot;/login&quot;).permitAll())
.userDetailsService(userDetailsService) // 커스텀 UserDetailService 설정; 

return http.build();
}</code></pre>
<h2 id="userdetailsservice-구현">UserDetailsService 구현</h2>
<p><img src="https://velog.velcdn.com/images/may_yun/post/2f78d05c-93b0-4607-991d-be58d2209aad/image.png" alt=""></p>
<pre><code class="language-java">@Service(&quot;userDetailsService&quot;)
@RequiredArgsConstructor
public class FormUserDetailsService implements UserDetailsService {

private final UserRepository userRepository;

@Override
public UserDetails loadUserByUsername(String username (사용자가 입력한 username)) throws UsernameNotFoundException {
  Account account = userRepository.findByUsername(username); 
  if (account == null) {
    if (userRepository.countByUsername(username) == 0) {
    throw new UsernameNotFoundException(&quot;No user found with username: &quot; + username);
      }
  }
  List&lt;GrantedAuthority&gt; authorities = List.of(new SimpleGrantedAuthority(account.getRoles())); // 권한 설정 ModelMapper mapper = new ModelMapper();
  AccountDto accountDto = mapper.map(account, AccountDto.class);
  }
    return new AccountContext(accountDto, authorities); 
    // AccountContext 는 UserDetails 를 구현한 클래스로서 AccountDto 를 Wrapping 함
}</code></pre>
<hr>
<h1 id="authenticationprovider">AuthenticationProvider</h1>
<p>AuthenticationProvider안에 UserDetails가 포함되어있는 것으로
AuthenticationProvider를 구현하여 적용하면 UserDetails를 설정하지 않아도 된다</p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/ccb71db4-2f95-4555-876f-0d7e6c1c50c9/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/a37e4161-6a90-4c61-bff0-b0d73d20c0e1/image.png" alt=""></p>
<pre><code class="language-java">@Override
public boolean support(Class&lt;?&gt; authentication){
    return authentication.isAssignablefrom(UsernamePasswordAuthenticationToken.class);
}</code></pre>
<hr>
<h1 id="커스텀-인증상세-구현하기">커스텀 인증상세 구현하기</h1>
<p>: 사용자가 클라이언트에서 서버로 인증요청을 할 때 아이디와 비빌번호를 입력해서 보내주는데
이때 인증객체 Authentication에서 인증 작업을 하고, Details 객체에는 사용자의 IP 주소와 세션 ID와 같은 정보를 가지고 있다. 이 Details에 저장되어있는 객체가 WebAuthenticationDetails 객체이다</p>
<h2 id="webauthenticationdetails">WebAuthenticationDetails</h2>
<ul>
<li>HTTP 요청과 관련된 인증 세부 정보를 포함하는 클래스로서 기본적으로 사용자의 IP 주소와 세션 ID와 같은 정보를 가지고 있다</li>
<li>특정 인증 메커니즘에서 요청의 추가적인 정보를 인증 객체에 추가할 때 사용할 수 있으며 Authentication 객체와 함께 사용된다</li>
</ul>
<h2 id="authenticationdetailssource">AuthenticationDetailsSource</h2>
<ul>
<li>Authentication -&gt; Details 저장하는 역할을 한다</li>
<li>인증 과정 중에 Authentication 객체에 세부 정보(WebAuthenticationDetails)를 제공하는 소스 역할을 한다.</li>
<li>WebAuthenticationDetails 객체를 생성하는 데 사용되며 인증 필터에서 참조한다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/3e50a8d9-e7aa-46aa-92be-684322abd5c7/image.png" alt=""></p>
<h3 id="인증상세-구현">인증상세 구현</h3>
<ol>
<li>view에서 로그인시 header로 secret key를 보내준다</li>
</ol>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링시큐리티] 비밀번호 보안 PasswordEncoder]]></title>
            <link>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EB%B3%B4%EC%95%88-PasswordEncoder</link>
            <guid>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EB%B9%84%EB%B0%80%EB%B2%88%ED%98%B8-%EB%B3%B4%EC%95%88-PasswordEncoder</guid>
            <pubDate>Thu, 19 Dec 2024 13:33:51 GMT</pubDate>
            <description><![CDATA[<h1 id="passwordencoder">PasswordEncoder</h1>
<ul>
<li>스프링 시큐리티의 PasswordEncoder 인터페이스는 비밀번호를 안전하게 저장하기 위해 비밀번호의 단방향 변환을 수행하는 데 사용</li>
<li>일반적으로 PasswordEncoder는 사용자의 비밀번호를 암호화하여 저장하거나 인증 시 검증을 위해 <code>입력한 비밀번호</code>와 암호화 되어 <code>저장된 비밀번호</code>를 서로 비교 해야할때 사용된다
<img src="https://velog.velcdn.com/images/may_yun/post/1fe7b49a-ffa1-4d57-8cee-53fcead9a9b0/image.png" alt=""></li>
</ul>
<h1 id="delegatingpasswordencoder">DelegatingPasswordEncoder</h1>
<ul>
<li>DelegatingPasswordEncoder 는 {id} 형식의 접두사를 사용하여 비밀번호가 어떤 방식으로 인코딩되었는지 식별하는 클래스로 예를 들어 {bcrypt}
<code>접두사</code>는 비밀번호가 BCrypt 방식으로 인코딩되었음을 나타낸다.
<img src="https://velog.velcdn.com/images/may_yun/post/6401cc4b-a2bd-4e8c-aefa-8364d54c9386/image.png" alt=""></li>
<li>DelegatingPasswordEncoder 는 어플리케이션에서 사용하는 기본 인코딩 방식을 변경할 수 있도록 해 주며 새로운 인코딩 방식이 권장되거나 필요할 때 비밀번호 인코딩 전략을 유연하게 유지할 수 있다</li>
<li>내부적으로 여러 암호화 알고리즘을 가지고 있고 이를 맞게 위임해주는 역할을 한다. 설정을 따로 하지 않으면 {bcrypt}이 기본값으로 설정된다</li>
</ul>
<h2 id="빈-정의">빈 정의</h2>
<pre><code class="language-java">@Bean
public PasswordEncoder passwordEncoder() {
    return PasswordEncoderFactories.createDelegatingPasswordEncoder(); 
    // 기본적으로 bcrypt 암호화 알고리즘의 BCryptPasswordEncoder 객체를 생성하고 사용하게 된다
}</code></pre>
<ul>
<li>알고리즘 지정 생성</li>
</ul>
<pre><code class="language-java">@Bean
public PasswordEncoder passwordEncoder() {
    String encodingId = &quot;pbkdf2&quot;;
    Map&lt;String, PasswordEncoder&gt; encoders = new HashMap&lt;&gt;();
    encoders.put(encodingId, Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8());
    DelegatingPasswordEncoder delegatingPasswordEncoder = new DelegatingPasswordEncoder(encodingId, encoders); 
    return delegatingPasswordEncoder;
}</code></pre>
<ul>
<li>알고리즘 유형<pre><code class="language-java">String encodingId = &quot;bcrypt&quot;;
Map&lt;String, PasswordEncoder&gt; encoders = new HashMap&lt;&gt;();
encoders.put(encodingId, new BCryptPasswordEncoder());
encoders.put(&quot;ldap&quot;, new org.springframework.security.crypto.password.LdapShaPasswordEncoder()); encoders.put(&quot;MD4&quot;, new org.springframework.security.crypto.password.Md4PasswordEncoder()); encoders.put(&quot;MD5&quot;, new org.springframework.security.crypto.password.MessageDigestPasswordEncoder(&quot;MD5&quot;)); encoders.put(&quot;noop&quot;, org.springframework.security.crypto.password.NoOpPasswordEncoder.getInstance()); encoders.put(&quot;pbkdf2&quot;, Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_5()); encoders.put(&quot;pbkdf2@SpringSecurity_v5_8&quot;, Pbkdf2PasswordEncoder.defaultsForSpringSecurity_v5_8()); encoders.put(&quot;scrypt&quot;, SCryptPasswordEncoder.defaultsForSpringSecurity_v4_1()); encoders.put(&quot;scrypt@SpringSecurity_v5_8&quot;, SCryptPasswordEncoder.defaultsForSpringSecurity_v5_8()); encoders.put(&quot;SHA-1&quot;, new org.springframework.security.crypto.password.MessageDigestPasswordEncoder(&quot;SHA-1&quot;)); encoders.put(&quot;SHA-256&quot;,
new org.springframework.security.crypto.password.MessageDigestPasswordEncoder(&quot;SHA-256&quot;)); encoders.put(&quot;sha256&quot;, new org.springframework.security.crypto.password.StandardPasswordEncoder()); encoders.put(&quot;argon2&quot;, Argon2PasswordEncoder.defaultsForSpringSecurity_v5_2()); encoders.put(&quot;argon2@SpringSecurity_v5_8&quot;, Argon2PasswordEncoder.defaultsForSpringSecurity_v5_8());</code></pre>
</li>
</ul>
<h1 id="사용">사용</h1>
<pre><code class="language-java">@PostMapping(value=&quot;/signup&quot;)
public String signup(AccountDto accountDto) {
  ModelMapper mapper = new ModelMapper();
  Account account = mapper.map(accountDto, Account.class);
  account.setPassword(passwordEncoder.encode(accountDto.getPassword()));
  userService.createUser(account); 
  return &quot;redirect:/&quot;;
}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[에러 노트]]></title>
            <link>https://velog.io/@may_yun/%EC%97%90%EB%9F%AC-%EB%85%B8%ED%8A%B8</link>
            <guid>https://velog.io/@may_yun/%EC%97%90%EB%9F%AC-%EB%85%B8%ED%8A%B8</guid>
            <pubDate>Wed, 18 Dec 2024 16:55:55 GMT</pubDate>
            <description><![CDATA[<ul>
<li>발생 예외<pre><code>2024-12-19 01:52:23 Web application could not be started as there was no org.springframework.boot.web.servlet.server.ServletWebServerFactory bean defined in the context.
2024-12-19 01:52:23 
2024-12-19 01:52:23 Action:
2024-12-19 01:52:23 
2024-12-19 01:52:23 Check your application&#39;s dependencies for a supported servlet web server.
2024-12-19 01:52:23 Check the configured web application type.</code></pre></li>
<li>해결
@SpringBootApplication</li>
</ul>
<hr>
<ul>
<li>docker compose 로 이미지 build시 에러 발생</li>
</ul>
<pre><code>2024-12-23 22:41:49 Caused by: io.jsonwebtoken.security.WeakKeyException: The specified key byte array is 0 bits which is not secure enough for any JWT HMAC-SHA algorithm.  The JWT JWA Specification (RFC 7518, Section 3.2) states that keys used with HMAC-SHA algorithms MUST have a size &gt;= 256 bits (the key size must be greater than or equal to the hash output size).  Consider using the Jwts.SIG.HS256.key() builder (or HS384.key() or HS512.key()) to create a key guaranteed to be secure enough for your preferred HMAC-SHA algorithm.  See https://tools.ietf.org/html/rfc7518#section-3.2 for more information.</code></pre><ul>
<li>해결</li>
</ul>
<p>8초 동안 Thought about WeakKeyException 해결법
이 오류 메시지는 JWT 서명용 키가 잘못 설정되어 “길이 0bit(즉, 실제로 값이 없음)”으로 인식되었기 때문에 발생합니다. HMAC-SHA 알고리즘을 사용할 때는 RFC 7518, Section 3.2에 의해 최소 256비트 이상의 키를 사용해야 합니다. 즉, 적어도 32바이트(= 256비트) 이상의 길이를 갖는 키가 필요합니다.</p>
<p>매개변수에서 Jwts.SIG.HS256.key() 제거</p>
<hr>
<ul>
<li>문제<pre><code>org.springframework.beans.factory.BeanCreationException: Error creating bean with name &#39;loggingProducer&#39; defined in URL [jar:nested:/seo-better-pay.jar/!BOOT-INF/lib/logging-service-0.0.1-SNAPSHOT-plain.jar!/com/yun/loggingservice/kafka/LoggingProducer.class]: Failed to instantiate [com.yun.loggingservice.kafka.LoggingProducer]: Constructor threw exception
2024-12-24 22:20:31     at org.springframework.beans.factory.support.ConstructorResolver.instantiate(ConstructorResolver.java:318) ~[spring-beans-6.1.4.jar!/:6.1.4]</code></pre></li>
<li>해결
docker compose 환경 변수</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링시큐리티] securityConfig ]]></title>
            <link>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-securityConfig</link>
            <guid>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-securityConfig</guid>
            <pubDate>Wed, 18 Dec 2024 16:12:44 GMT</pubDate>
            <description><![CDATA[<pre><code class="language-java">package com.yun.preapisecure.configuration;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.HttpStatus;
import org.springframework.security.access.AccessDeniedException;
import org.springframework.security.access.hierarchicalroles.RoleHierarchy;
import org.springframework.security.access.hierarchicalroles.RoleHierarchyImpl;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.authorization.AuthorizationManager;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.security.core.session.SessionRegistry;
import org.springframework.security.core.session.SessionRegistryImpl;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.access.AccessDeniedHandler;
import org.springframework.security.web.access.intercept.RequestAuthorizationContext;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
import org.springframework.security.web.util.matcher.RequestMatcherEntry;
import org.springframework.web.servlet.handler.HandlerMappingIntrospector;

import java.io.IOException;
import java.util.ArrayList;
import java.util.List;

@EnableWebSecurity
@Configuration
public class SecurityConfig {

    private static final Logger LOGGER = LoggerFactory.getLogger(SecurityConfig.class);

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

       // AuthenticationManagerBuilder builder = http.getSharedObject(AuthenticationManagerBuilder.class);
        //builder.authenticationProvider(new CustomAuthenticationProvider());
        //builder.authenticationProvider(new CustomAuthenticationProvider2()); 여러개 추가할 경우


        http
            .authorizeHttpRequests(auth -&gt; auth
                    .requestMatchers(&quot;/logoutSuccess&quot;).authenticated()
                    .anyRequest().authenticated())
            .rememberMe(remember -&gt; remember
                    //.alwaysRemember(true) 보안에 약하다
                    .tokenValiditySeconds(3600)
                    .userDetailsService(userDetailsService())
                    .rememberMeParameter(&quot;remember&quot;)
                    .rememberMeCookieName(&quot;remember&quot;)
                    .key(&quot;security&quot;)
            )
            .formLogin(form -&gt; form
                    //.loginPage(&quot;/loginPage&quot;)
                    .loginProcessingUrl(&quot;/loginProc&quot;)
                    .defaultSuccessUrl(&quot;/&quot;, false)
                    .failureUrl(&quot;/failed&quot;)
                    .usernameParameter(&quot;userId&quot;)
                    .usernameParameter(&quot;passwd&quot;)
                    /*.successHandler(new AuthenticationSuccessHandler() {
                        @Override
                        public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {
                            LOGGER.info(&quot;authentication : {}&quot;, authentication);
                            response.sendRedirect(&quot;/home&quot;);
                        }
                    })
                    .failureHandler(new AuthenticationFailureHandler() {
                        @Override
                        public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
                            LOGGER.info(&quot;authentication exception : {}&quot;, exception.getMessage());
                            response.sendRedirect(&quot;/loginPage&quot;);
                        }
                    })*/
                    .permitAll()

            )
            .httpBasic(basic -&gt; basic.authenticationEntryPoint(new AuthenticationEntryPoint() {
                @Override
                public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                    response.setHeader(&quot;WWW-Authenticate&quot;, &quot;Basic realm=security&quot;);
                    response.sendError(HttpStatus.UNAUTHORIZED.value(), HttpStatus.UNAUTHORIZED.getReasonPhrase());
                }})
            )
            .sessionManagement(fixation -&gt; fixation
                    .sessionFixation()
                    .changeSessionId()
                    //.migrateSession()
                    //.newSession()
            )
            .sessionManagement(session -&gt; session
                    .invalidSessionUrl(&quot;/invalidSessionUrl&quot;)
                    .maximumSessions(1)
                    .maxSessionsPreventsLogin(true)
                    .expiredUrl(&quot;/expiredUrl&quot;)
            )
            //.authenticationProvider(new CustomAuthenticationProvider()) http에 바로 추가해도 되고, 위와 같이 따로 추가해도 됨
            .logout(logout -&gt; logout
                    .logoutUrl(&quot;/logoutProc&quot;)
                    .logoutRequestMatcher(new AntPathRequestMatcher(&quot;/logout&quot;, &quot;POST&quot;))
                    .logoutSuccessUrl(&quot;/logoutSuccess&quot;)
                    .deleteCookies(&quot;JSSESIONID&quot;, &quot;remember-me&quot;)
                    .invalidateHttpSession(true)
                    .clearAuthentication(true)
                    .addLogoutHandler(new LogoutHandler() {
                        @Override
                        public void logout(HttpServletRequest request, HttpServletResponse response, Authentication authentication) {
                            try {
                                response.sendRedirect(&quot;/logoutSuccess&quot;);
                            } catch (IOException e) {
                                throw new RuntimeException(e);
                            }
                        }
                    })
                    .permitAll()
            )//login 또는 index로 보냄
            .exceptionHandling(exception -&gt; exception
                    .authenticationEntryPoint(new AuthenticationEntryPoint() {
                        @Override
                        public void commence(HttpServletRequest request, HttpServletResponse response, AuthenticationException authException) throws IOException, ServletException {
                            System.out.println(&quot;exception: &quot; + authException.getMessage());
                            response.sendRedirect(&quot;/logout&quot;);//사용자 정의로 커스텀하게 만들었을 경우 login, logout 페이지를 직접 만들어줘야한다.
                        }
                    })
                    .accessDeniedHandler(new AccessDeniedHandler() {
                        @Override
                        public void handle(HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException) throws IOException, ServletException {
                            System.out.println(&quot;exception: &quot; + accessDeniedException.getMessage());
                            response.sendRedirect(&quot;/denied&quot;);
                        }
                    })
            );

        //SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL);
        return http.build();
    }

    //spring security User 사용자 정의 1. yml 파일 설정 / 2. User 설정 단 1번과 2번이 충돌할 경우 2번이 우선순위
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withUsername(&quot;user&quot;)
                .password(&quot;{noop}1234&quot;)
                .roles(&quot;USER&quot;)
                .build();

        return new InMemoryUserDetailsManager(user);
    }

}
</code></pre>
<pre><code class="language-java">package com.yun.preapisecure.configuration;

import com.yun.preapisecure.CustomAuthenticationFilter;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.AuthenticationEntryPoint;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
import org.springframework.security.web.authentication.logout.LogoutHandler;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.io.IOException;

@EnableWebSecurity
@Configuration
public class SecurityCustomFilterConfig {

    private static final Logger LOGGER = LoggerFactory.getLogger(SecurityCustomFilterConfig.class);

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {

        AuthenticationManagerBuilder builder = http.getSharedObject(AuthenticationManagerBuilder.class);
        AuthenticationManager authenticationManager = builder.build();


        http
            .authorizeHttpRequests(auth -&gt; auth
                    .requestMatchers(&quot;/logoutSuccess&quot;).authenticated()
                    .requestMatchers(&quot;/login&quot;).permitAll()
                    .anyRequest().authenticated())
            .formLogin(Customizer.withDefaults())
                //.securityContext(securityContext -&gt; securityContext.requireExplicitSave(true))//자동으로 session에 저장한다.
            .authenticationManager(authenticationManager)
            .addFilterBefore(customAuthenticationFilter(http, authenticationManager), UsernamePasswordAuthenticationFilter.class);
        return http.build();
    }

/*    @Bean
    @Order(1)
    public SecurityFilterChain securityFilterChain2(HttpSecurity http) throws Exception {

        AuthenticationManagerBuilder builder = http.getSharedObject(AuthenticationManagerBuilder.class);
        AuthenticationManager authenticationManager = builder.build();

        http
                .securityMatchers(matchers -&gt; matchers.requestMatchers(&quot;/open-banking/oauth/&quot;))
                .authorizeHttpRequests(authorize -&gt; authorize.anyRequest().permitAll());
        return http.build();
    }*/

    public CustomAuthenticationFilter customAuthenticationFilter(HttpSecurity httpSecurity, AuthenticationManager manager) {
        CustomAuthenticationFilter customAuthenticationFilter = new CustomAuthenticationFilter(httpSecurity);
        customAuthenticationFilter.setAuthenticationManager(manager);
        return customAuthenticationFilter;
    }

    //spring security User 사용자 정의 1. yml 파일 설정 / 2. User 설정 단 1번과 2번이 충돌할 경우 2번이 우선순위
    @Bean
    public UserDetailsService userDetailsService() {
        UserDetails user = User.withUsername(&quot;user&quot;)
                .password(&quot;{noop}1234&quot;)
                .roles(&quot;USER&quot;)
                .build();

        return new InMemoryUserDetailsManager(user);
    }


}
</code></pre>
<pre><code class="language-java">package com.yun.preapisecure.configuration;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.session.MapSession;
import org.springframework.session.MapSessionRepository;
import org.springframework.session.SessionRepository;
import org.springframework.session.config.annotation.web.http.EnableSpringHttpSession;
import org.springframework.session.web.http.CookieSerializer;
import org.springframework.session.web.http.DefaultCookieSerializer;

import java.util.concurrent.ConcurrentHashMap;

@Configuration
@EnableSpringHttpSession
public class HttpSessionConfig {

    @Bean
    public CookieSerializer cookieSerializer() {
        DefaultCookieSerializer serializer = new DefaultCookieSerializer();
        serializer.setUseHttpOnlyCookie(true);
        serializer.setUseSecureCookie(true);
        serializer.setSameSite(&quot;Lax&quot;);//none인 경우 https로 추가조치 해야한다.
        return serializer;
    }

    @Bean
    public SessionRepository&lt;MapSession&gt; sessionRepository() {
        return new MapSessionRepository(new ConcurrentHashMap&lt;&gt;());
    }
}
</code></pre>
<pre><code class="language-java">package com.yun.preapisecure.provider;

import org.springframework.security.authentication.AuthenticationProvider;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Component;

@Component
public class CustomAuthenticationProvider implements AuthenticationProvider {
    private final UserDetailsService userDetailsService;

    public CustomAuthenticationProvider(UserDetailsService userDetailsService) {
        this.userDetailsService = userDetailsService;
    }

    @Override
    public Authentication authenticate(Authentication authentication) throws AuthenticationException {
        String loginId = authentication.getName();
        String password = authentication.getCredentials().toString();

        //아이디 검증
        UserDetails user = userDetailsService.loadUserByUsername(loginId);
        if (user == null) {
            throw new UsernameNotFoundException(&quot;사용자가 존재하지 않는다.&quot;);
        }
        //비밀번호 검증

        return new UsernamePasswordAuthenticationToken(
                user.getUsername(), user.getPassword(), user.getAuthorities());
    }

    @Override
    public boolean supports(Class&lt;?&gt; authentication) {
        //일반적으로는 넘어온 authentication token와 인증 토큰이 맞아야 true 반환
        return authentication.isAssignableFrom(UsernamePasswordAuthenticationToken.class);
    }
}
</code></pre>
<pre><code class="language-java">package com.yun.preapisecure.provider;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;

import java.util.Collection;

public class CustomUserDetails implements UserDetails { //DB에서 가져온 데이터를 return

    @Override
    public Collection&lt;? extends GrantedAuthority&gt; getAuthorities() {
        return null;
    }

    @Override
    public String getPassword() {
        return null;
    }

    @Override
    public String getUsername() {
        return null;
    }

    @Override
    public boolean isAccountNonExpired() {
        return false;
    }

    @Override
    public boolean isAccountNonLocked() {
        return false;
    }

    @Override
    public boolean isCredentialsNonExpired() {
        return false;
    }

    @Override
    public boolean isEnabled() {
        return false;
    }
}
</code></pre>
<pre><code class="language-java">package com.yun.preapisecure;

import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AbstractAuthenticationProcessingFilter;
import org.springframework.security.web.context.DelegatingSecurityContextRepository;
import org.springframework.security.web.context.HttpSessionSecurityContextRepository;
import org.springframework.security.web.context.RequestAttributeSecurityContextRepository;
import org.springframework.security.web.context.SecurityContextRepository;
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;

import java.io.IOException;

public class CustomAuthenticationFilter extends AbstractAuthenticationProcessingFilter {

    protected final ObjectMapper objectMapper = new ObjectMapper();

    public CustomAuthenticationFilter(HttpSecurity httpSecurity) {
        super(new AntPathRequestMatcher(&quot;/api/login&quot;, &quot;GET&quot;));
        setSecurityContextRepository(getSecurityContextRepository(httpSecurity));
    }

    private SecurityContextRepository getSecurityContextRepository(HttpSecurity httpSecurity) {
        SecurityContextRepository securityContextRepository = httpSecurity.getSharedObject(SecurityContextRepository.class);
        if (securityContextRepository == null) {
            securityContextRepository = new DelegatingSecurityContextRepository(new HttpSessionSecurityContextRepository(), new RequestAttributeSecurityContextRepository());
        }
        return securityContextRepository;
    }

    @Override
    public Authentication attemptAuthentication(HttpServletRequest request, HttpServletResponse response) throws AuthenticationException, IOException, ServletException {
        String username = request.getParameter(&quot;username&quot;);
        String password = request.getParameter(&quot;password&quot;);

        UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(username, password);

        return this.getAuthenticationManager().authenticate(token);
    }
}
</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링시큐리티] 보안강화]]></title>
            <link>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EB%B3%B4%EC%95%88%EA%B0%95%ED%99%94</link>
            <guid>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EB%B3%B4%EC%95%88%EA%B0%95%ED%99%94</guid>
            <pubDate>Wed, 18 Dec 2024 10:28:09 GMT</pubDate>
            <description><![CDATA[<h1 id="다중-보안-설정">다중 보안 설정</h1>
<p>: 요청에 따른 보안 기능 설정을 하여 실행</p>
<ul>
<li>Spring Security 는 여러 SecurityFilterChain @Bean 을 등록해서 다중 보안 기능을 구성 할 수 있다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/c473d196-ae2a-4257-ab0b-d864e93c3a41/image.png" alt=""></p>
<h2 id="초기화-구성">초기화 구성</h2>
<ul>
<li>다중보안 설정하는 경우 각각의 SecurityFilterChain이 생기고 proxy에 SecurityFilterChains 생성되는 것</li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/7a4093f3-2306-4a9e-802e-86a9d072957e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/7004d5fa-9a09-41e5-80aa-6ec7e0c959ce/image.png" alt=""></p>
<hr>
<h1 id="custom-dsls">Custom DSLs</h1>
<ul>
<li>Spring Security 는 사용자 정의 DSL 을 구현할 수 있도록 지원한다</li>
<li>DSL을구성하면필터,핸들러,메서드,속성등을한곳에정의하여처리할수있는편리함을제공한다</li>
</ul>
<h2 id="abstracthttpconfigurerabstracthttpconfigurer-httpsecuritybuilder">AbstractHttpConfigurer&lt;AbstractHttpConfigurer, HttpSecurityBuilder&gt;</h2>
<ul>
<li>사용자 DSL 을 구현하기 위해서 상속받는 추상 클래스로서 구현 클래스는 두 개의 메서드를 오버라이딩 한다<ul>
<li>init(B builder) : HttpSecurity 의 구성요소를 설정 및 공유하는 작업 등..</li>
<li>configure(B builder) : 공통클래스를 구성 하거나 사용자 정의 필터를 생성하는 작업 등..</li>
</ul>
</li>
</ul>
<h2 id="dsl-적용하기-위한-api">DSL 적용하기 위한 API</h2>
<pre><code class="language-java">//apply diprecate
HttpSecurity.with(C configurer, Customizer&lt; C &gt; customizer)</code></pre>
<ul>
<li>configurer는AbstractHttpConfigurer을상속하고DSL을구현한클래스가들어간다</li>
<li>customizer는DSL구현클래스에서정의한여러API를커스트마이징한다</li>
<li>동일한클래스를여러번설정하더라도한번만적용된다 (with를 여러번 하더라도 한번만 적용)</li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/aa0a2411-e9d1-4cae-823e-bf839b78a486/image.png" alt=""></p>
<hr>
<h1 id="이중화-설정">이중화 설정</h1>
<p>서버 이중화 상태에서 인증/인가 데이터를 하나의 애플리케이션에서 가지고 있다면 비정상 종료시 데이터가 날아간다. 데이터가 캐시 쿠키 서버에 저장되고 이를 바라보고 있다고 생각</p>
<p>세션을 공유할 수 있도록 설정하는 것</p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/7a61b82e-f50b-4c24-ba80-6b05aa261469/image.png" alt=""></p>
<pre><code class="language-java">implementation &#39;org.springframework.session:spring-session-data-redis&#39; 
implementation &#39;org.springframework.boot:spring-boot-starter-data-redis&#39;</code></pre>
<pre><code class="language-yaml">spring.data.redis.host=localhost 
spring.data.redis.port= 6379</code></pre>
<pre><code class="language-java">@Configuration 
⭐️@EnableRedisHttpSession 
public class RedisConfig {

@Value(&quot;${spring.data.redis.host}&quot;) 
private String host; 
@Value(&quot;${spring.data.redis.port}&quot;) private int port;
@Bean

public RedisConnectionFactory redisConnectionFactory() { 
return new LettuceConnectionFactory(host, port);
}

}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링시큐리티] Servlet API, Servlet MVC 통합]]></title>
            <link>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-Servlet-API-Servlet-MVC-%ED%86%B5%ED%95%A9</link>
            <guid>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-Servlet-API-Servlet-MVC-%ED%86%B5%ED%95%A9</guid>
            <pubDate>Wed, 18 Dec 2024 07:44:10 GMT</pubDate>
            <description><![CDATA[<ul>
<li>스프링시큐리티는 다양한 프레임워크 및 API와의 통합을 제공하고 있으며 Servlet3 과Spring MVC와 통합을 통해 여러 편리한 기능들을 사용 할 수 있다</li>
<li>인증 관련 기능들을 필터가 아닌 서블릿 영역에서 처리 할 수 있다</li>
</ul>
<h1 id="1-servlet-api-통합">1. Servlet API 통합</h1>
<h2 id="servlet-3-통합">Servlet 3+ 통합</h2>
<p>1) SecurityContextHolderAwareRequestFilter</p>
<ul>
<li>HTTP 요청이 처리될 때 HttpServletRequest 에 보안 관련 메소드를 추가적으로 제공하는 래퍼(SecurityContextHolderAwareRequestWrapper) 클래스를 적용한다 • 이를통해개발자는서블릿API의보안메소드를사용하여인증,로그인,로그아웃등의작업을수행할수있다</li>
<li>request 객체 + 보안 관련 메소드 -&gt; warpper</li>
<li>HttpServlet3RequestFactory 생성</li>
</ul>
<p>2) HttpServlet3RequestFactory</p>
<ul>
<li>Servlet 3 API 와의 통합을 제공하기 위한 Servlet3SecurityContextHolderAwareRequestWrapper 객체를 생성한다</li>
<li>Servlet3SecurityContextHolderAwareRequestWrapper 클래스는 SecurityContextHolderAwareRequestFilter를 상속받은 자식 클래스(구현체)이다.</li>
</ul>
<p>3) Servlet3SecurityContextHolderAwareRequestWrapper</p>
<ul>
<li>HttpServletRequest 의 래퍼 클래스로서 Servlet 3.0의 기능을 지원하면서 동시에 SecurityContextHolder 와의 통합을 제공한다(인증 관련 기능 제공)</li>
<li>이 래퍼를 사용함으로써 SecurityContext 에 쉽게 접근할 수 있고 Servlet 3.0의 비동기 처리와 같은 기능을 사용하는 동안 보안 컨텍스트를 올바르게 관리할 수 있다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/77600889-17cb-4a8f-8b5d-fe228216e39f/image.png" alt=""></p>
<pre><code class="language-java">@GetMapping(&quot;/login&quot;)
public String login(HttpServletRequest request, MemberDto memberDto) throws ServletException, IOException {
  request.login(memberDto.getUsername(),memberDto.getPassword()); 
  System.out.println(&quot;PreAuthorize&quot;);
  return &quot;/index&quot;;
}

@GetMapping(&quot;/users&quot;)
public List&lt;User&gt; users(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
  boolean authenticate = request.authenticate(response); 
  if(authenticate){
      UserService.users(); 
  }
  return Collection.emptyList(); 
}</code></pre>
<hr>
<h1 id="2-spring-mvc-통합">2. Spring MVC 통합</h1>
<h2 id="authenticationprincipal">@AuthenticationPrincipal</h2>
<ul>
<li>Spring Security는 Spring MVC 인수에 대한 현재 Authentication.getPrincipal()을 자동으로 해결 할 수 있는 ⭐️AuthenticationPrincipalArgumentResolver 를 제공한다
• Spring MVC 에서 @AuthenticationPrincipal 을 메서드 인수에 선언하게 되면 Spring Security 와 독립적으로 사용할 수 있다</li>
<li>AuthenticationPrincipalArgumentResolver가 어노테이션의 작업을 구현하여 실행하는 과정을 수행하는것</li>
<li>📌기본적으로 인증을 받지 못한 사용자는 Principal이 anonymouseUser 익명 사용자 문자열로 들어온다 따라서 객체명, 필드명이 존재하지 않는다. <pre><code class="language-java">@Target({ ElementType.PARAMETER, ElementType.TYPE }) 
@Retention(RetentionPolicy.RUNTIME)
@Documented
@AuthenticationPrincipal(expression = &quot;#this == &#39;anonymouseUser&#39; ? null : User&quot;)
public @interface CurrentUser {
</code></pre>
</li>
</ul>
<p>}</p>
<p>//#this 현재 Principal을 의미</p>
<pre><code>

![](https://velog.velcdn.com/images/may_yun/post/392e2ee2-fa29-4e43-8ee9-8201403f67d8/image.png)

![](https://velog.velcdn.com/images/may_yun/post/425700e1-8c7e-4903-9a02-3d7d58970076/image.png)


## 차이
- Authentication 내의 Principal 객체를 의미한다. 이미 해당 Principal 객체가 존재해야하는 것. Authentication에서 Principal 객체를 바로 가져오는 것
![](https://velog.velcdn.com/images/may_yun/post/9772c9fc-d9ea-4e26-a64d-432331d68dea/image.png)

- Principal에 속하는 Userdetails안에 값을 사용하기 위한것 
customer의 객체, 필드 등

![](https://velog.velcdn.com/images/may_yun/post/52ce6e6e-4c55-431b-918e-d0491fb52fb4/image.png)

---

# 3. Spring MVC 비동기 통합 

- Spring Security 는 Spring MVC Controller 에서 Callable 을 실행하는 비동기 스레드에 SecurityContext 를 자동으로 설정하도록 지원한다
- Spring Security 는 WebAsyncManager 와 통합하여 SecurityContextHolder 에서 사용 가능한 SecurityContext 를 Callable 에서 접근 가능하도록 해 준다
- 부모 스레드와 자식 스레드가 있을 때 부모스레드의 SecurityContext를 자식스레드에서도 사용할 수 있도록 자동 설정되는 것

## WebAsyncManagerIntegrationFilter
- SecurityContext 와 WebAsyncManager 사이의 통합을 제공하며 WebAsyncManager 를 생성하고 SecurityContextCallableProcessingInterceptor를 WebAsyncManager 에 등록한다

## WebAsyncManager
- 스레드 풀의 비동기 스레드를 생성하고 Callable 를 받아 실행시키는 주체로서 등록된 SecurityContextCallableProcessingInterceptor 를 통해 현재 스레드 가 보유하고 있는 SecurityContext 객체를 비동기 스레드의 ThreadLocal 에 저장시킨다

![](https://velog.velcdn.com/images/may_yun/post/4ff44c8f-cd44-44a9-9c72-394759ae6416/image.png)

- 비동기 스레드가 수행하는 Callable 영역 내에서 자신의 ThreadLocal 에 저장된 SecurityContext 를 참조할 수 있으며 이는 부모 스레드가 가지고 있는 SecurityContext 와 동일한 객체이다
- @Async 나 다른 비동기 기술은 스프링 시큐리티와 통합되어 있지 않기 때문에 비동기 스레드에 SecurityContext 가 적용되지 않는다 즉 Callable 영역내에서만 가능하다.
- Callable으로 반환이 되어야한다

![](https://velog.velcdn.com/images/may_yun/post/5076f99e-fda9-439e-bc5a-63485513c957/image.png)

- mode 설정으로 Callable 구현하지 않고 SecurityContext객체를 공유할 수 있도록 사용할 수 있다
```java
SecurityContextHolder.setStrategyName(SecurityContextHolder.MODE_INHERITABLETHREADLOCAL); </code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링시큐리티] 인증/인가 관련 이벤트 처리]]></title>
            <link>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80-%EA%B4%80%EB%A0%A8-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%B2%98%EB%A6%AC</link>
            <guid>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EC%9D%B8%EC%A6%9D%EC%9D%B8%EA%B0%80-%EA%B4%80%EB%A0%A8-%EC%9D%B4%EB%B2%A4%ED%8A%B8-%EC%B2%98%EB%A6%AC</guid>
            <pubDate>Wed, 18 Dec 2024 06:12:26 GMT</pubDate>
            <description><![CDATA[<h1 id="authenticationevents">AuthenticationEvents</h1>
<ul>
<li>스프링 시큐리티는 인증이 성공하거나 실패하게 되면 AuthenticationSuccessEvent 또는 AuthenticationFailureEvent 를 발생시킨다</li>
<li>이벤트를 수신하려면 ApplicationEventPublisher 를 사용하거나 시큐리티에서 제공하는 AuthenticationEventPublisher 를 사용해서 발행해야 한다</li>
<li>AuthenticationEventPublisher 의 구현체로 DefaultAuthenticationEventPublisher 가 제공된다</li>
</ul>
<p>🚩 DefaultAuthenticationEventPublisher는 예외(실패)와 이벤트를 매핑하고 있다
예를 들어 A예외가 발생하면 a이벤트 발행
따라서 예외만 전달하더라도 해당 이벤트를 발행하도록 되어있다</p>
<h2 id="이벤트-발행">이벤트 발행</h2>
<ol>
<li>ApplicationEventPublisher.publishEvent(ApplicationEvent)</li>
<li>AuthenticationEventPublisher.publishAuthenticationSuccess(Authentication) , AuthenticationEventPublisher.publishAuthenticationFailure(AuthenticationException, Authentication)</li>
</ol>
<h2 id="이벤트-수신">이벤트 수신</h2>
<p>발행된 이벤트 타입을 수신에 넣어야 수신할 수 있다</p>
<pre><code class="language-java">@Component
public class AuthenticationEvents {
@EventListener
public void onSuccess(AuthenticationSuccessEvent success) {...}
@EventListener
public void onFailure(AbstractAuthenticationFailureEvent failures) {...} 
}</code></pre>
<p>상위 이벤트는 각각의 이벤트가 수신될 때 마다 수신된다
<img src="https://velog.velcdn.com/images/may_yun/post/92b46ebf-e81d-4973-8f83-a3090a084f4d/image.png" alt=""></p>
<h2 id="구현">구현</h2>
<p><img src="https://velog.velcdn.com/images/may_yun/post/c6edef94-4a9d-41d0-94c1-9b4974d8b216/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/838cc141-53a6-4d58-8baa-6f7d1c98eca2/image.png" alt=""></p>
<hr>
<h1 id="authenticationeventpublisher-활용">AuthenticationEventPublisher 활용</h1>
<p>🚩 DefaultAuthenticationEventPublisher는 예외(실패)와 이벤트를 매핑하고 있다
예를 들어 A예외가 발생하면 a이벤트 발행
따라서 예외만 전달하더라도 해당 이벤트를 발행하도록 되어있다
이를 커스텀하게 만들어서 원하는 이벤트를 발행하도록 할 수 있다</p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/d886bf44-daef-43c9-acd7-fcb0db610c5a/image.png" alt=""></p>
<pre><code class="language-java">//매핑
Collections.singletonMap(CustomException.class, CustomAuthenticationFailureEvent.class);

authenticationEventPublisher.setAdditionalExceptionMappings(mapping); // CustomException 을 던지면 CustomAuthenticationFailureEvent 를 발행하도록 추가 함
authenticationEventPublisher.setDefaultAuthenticationFailureEvent(CustomDefaultAuthenticationFailureEvent.class); // 기본 이벤트 설정

//발행
authenticationEventPublisher.publishAuthenticationFailure(new CustomException(&quot;CustomException &quot;), authentication);

//수신
@EventListener
public void onFailure(CustomAuthenticationFailureEvent failures) { 
// 커스텀 예외에 대해 이벤트를 수신할 수 있다 System.out.println(&quot; failures = &quot; + failures.getException().getMessage());
}</code></pre>
<p><img src="https://velog.velcdn.com/images/may_yun/post/02d815da-1116-4698-9c89-18fd6d5abaa7/image.png" alt=""></p>
<hr>
<h1 id="authorization-events">Authorization Events</h1>
<p><img src="https://velog.velcdn.com/images/may_yun/post/478afae2-8342-4ca7-8dd6-fc6bff351fcd/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/3a9ee125-fc61-42ac-a2e3-e87b112c2b7c/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/17d5078c-1802-4355-89b4-a7e82728a507/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링시큐리티] AOP 메소드 보안]]></title>
            <link>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-AOP-%EB%A9%94%EC%86%8C%EB%93%9C-%EB%B3%B4%EC%95%88</link>
            <guid>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-AOP-%EB%A9%94%EC%86%8C%EB%93%9C-%EB%B3%B4%EC%95%88</guid>
            <pubDate>Tue, 17 Dec 2024 16:13:25 GMT</pubDate>
            <description><![CDATA[<p>MethodInterceptor(스프링시큐리티 지원), Pointcut, Advisor, AuthorizationManager 등을 커스텀하게 생성하여 AOP 메서드 보안을 구현 할 수 있다</p>
<ul>
<li>순수한 AOP 기술만 가지고도 메서드 보안을 구현할 수 있는 것</li>
<li>joinpoint: Advice를 적용할 수 있는 대상 자체를 의미한다. (클래스, 필드, 메서드 중 어느곳에 적용할 것인지 만약 클래스 단위로 적용한다면 joinpoint는 클래스가 되는 것)
단, 스프링에서는 메서드에만 Advice를 적용할 수 있도록 한정한다 따라서 joinpoint 또한 메서드에 한정.</li>
<li>joinpoint 대상에서 실행되어야 하는 지점, 조건이 pointCut이 된다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/df017bf3-bf62-48bd-8980-273f56e5c5df/image.png" alt=""></p>
<h2 id="초기화">초기화</h2>
<p><img src="https://velog.velcdn.com/images/may_yun/post/6f6063f0-ab96-4eb8-8b64-27572cc104de/image.png" alt=""></p>
<ul>
<li>CGLibAopProxy : 프록시 객체를 생성하는 객체</li>
</ul>
<h2 id="구현-예제">구현 예제</h2>
<ul>
<li>MethodInterceptor를 구현받을 때 aop로 구현</li>
</ul>
<pre><code class="language-java">public class CustomMethodInterceptor implements MethodInterceptor {
private final AuthorizationManager&lt;MethodInvocation&gt; authorizationManager;
public CustomMethodInterceptor(AuthorizationManager&lt;MethodInvocation&gt; authorizationManager) { this.authorizationManager=authorizationManager; //메서드보안검사를수행할인가관리자를전달한다
}
@Override
public Object invoke(MethodInvocation invocation) throws Throwable {
Authentication authentication = SecurityContextHolder.getContext().getAuthentication(); 
if(//권한 심사 
authorizationManager.check(() -&gt;authentication, invocation).isGranted()) {
returninvocation.proceed(); //실제대상객체를호출한다 }else{
throw new AccessDeniedException(&quot;Access Denied&quot;); }
}
}

@Bean
public MethodInterceptor customMethodInterceptor() {
AuthorizationManager&lt;MethodInvocation&gt; authorizationManager = AuthenticatedAuthorizationManager.authenticated(); return new CustomMethodInterceptor(authorizationManager); // AOP 어라운드 어드바이스를 선언한다
}

@Bean
public Pointcut servicePointcut() {
AspectJExpressionPointcut pointcut = new AspectJExpressionPointcut();
pointcut.setExpression(&quot;execution(* io.security.Myservice.*(..))&quot;); // AOP 수행 대상 클래스와 대상 메소드를 지정한다 return pointcut;
}

@Bean
public Advisor serviceAdvisor(MethodInterceptor customMethodInterceptor, Pointcut servicePointcut) { // 초기화 시 Advisor 목록에 포함된다 return new DefaultPointcutAdvisor(servicePointcut, customMethodInterceptor);
}</code></pre>
<ul>
<li>DefaultPointcutAdvisor : 스프링시큐리티에서 기본적으로 생성하는 Advisor
Pointcut, MethodInterceptor를 담고 있다</li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링시큐리티] 인가 아키텍처 ]]></title>
            <link>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EC%9D%B8%EA%B0%80-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</link>
            <guid>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EC%9D%B8%EA%B0%80-%EC%95%84%ED%82%A4%ED%85%8D%EC%B2%98</guid>
            <pubDate>Tue, 17 Dec 2024 09:15:25 GMT</pubDate>
            <description><![CDATA[<h1 id="authorization">Authorization</h1>
<ul>
<li>인가 즉 권한부여는 특정자원에 접근할 수 있는 사람을 결정하는 것을 의미 (신원 확인 후 어떤 권한을 부여할 것인가)</li>
<li>Spring Security 는 GrantedAuthority 클래스를 통해 권한 목록을 관리하고 있으며 사용자의 Authentication 객체와 연결한다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/e9edbc53-df69-456f-92ee-861e3adef57b/image.png" alt=""></p>
<ul>
<li>스프링 시큐리티는 Authentication 에 GrantedAuthority 권한 목록을 저장하며 이를 통해 인증 주체에게 부여된 권한을 사용하도록 한다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/f9d6c545-8c6c-496f-9dde-0f73d411099d/image.png" alt=""></p>
<p>권한 관련된 내용들이 문자열로 저장되어있다
SimpleGrantedAuthority 생성시 접두사를 사용하여 ROLE_USER 등의 권한으로 저장되어 Collection에 담기고
Authentication 인증 객체안에 collection으로 GrantedAuthority가 저장된다</p>
<h2 id="접두사-커스텀">접두사 커스텀</h2>
<p>스프링 시큐리티는 ROLE_접두사를 기본적으로 사용하나
커스텀하여 사용이 가능하다</p>
<pre><code class="language-java">@Bean
static GrantedAuthorityDefaults grantedAuthorityDefaults() { 
        return new GrantedAuthorityDefaults(&quot;MYPREFIX_&quot;);
}

//hasRole(&quot;ADMIN&quot;) =&gt; ROLE_ADMIN
hasRole(&quot;ADMIN&quot;) =&gt; MYPREFIX_ADM//IN</code></pre>
<hr>
<h1 id="authorizationmanager-인가관리자">AuthorizationManager 인가관리자</h1>
<p><img src="https://velog.velcdn.com/images/may_yun/post/f7f3554b-9dce-47dc-a248-34f4fccd24cf/image.png" alt=""></p>
<p>verify()는 디폴트로 check()으로 구현된 메서드를 호출한다</p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/9f72ccb9-2f6d-420d-b9d1-387c7cecef1e/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/0bfdc8d3-0462-431d-8af2-1663b3ae1a36/image.png" alt=""></p>
<hr>
<h1 id="요청기반-인가-관리자">요청기반 인가 관리자</h1>
<ul>
<li>요청 기반의 인증된 사용자 및 특정권한을 가진 사용자의 자원접근 허용여부를 결정하는 인가 관리자 클래스들을 제공</li>
<li>대표적으로 AuthorityAuthorizationManager, AuthenticatedAuthorizationManager 와 대리자인 RequestMatcherDelegatingAuthorizationManager 가 있다</li>
</ul>
<h2 id="스프링-시큐리티-인가처리">스프링 시큐리티 인가처리</h2>
<ul>
<li>스프링시큐리티가 내부에 가지고 있는 정보
<img src="https://velog.velcdn.com/images/may_yun/post/a552c433-9e07-4617-b1fe-6f555a871f15/image.png" alt=""></li>
<li>client에게 받아야 하는 정보
<img src="https://velog.velcdn.com/images/may_yun/post/c5b4c5a6-2c50-4831-bc1c-b7db195807c1/image.png" alt=""></li>
</ul>
<p>위 둘을 권한 심사하는 것</p>
<h2 id="⭐️내부-구조">⭐️내부 구조</h2>
<p><img src="https://velog.velcdn.com/images/may_yun/post/617a5626-4e94-4803-92c1-38770b4a3a41/image.png" alt=""></p>
<ol>
<li>클라이언트 요청 </li>
<li>요청에 대한 인가처리 authorizationFilter</li>
<li>필터가 요청을 받고 securityContextHolder로 보내서 인증 객체를 가져온다</li>
<li>request 요청정보와 인증객체(권한 정보를가지고 있다)를 authorizatinoManager(인가관리자)에 보낸다</li>
<li>적절한 인가 관리자를 배정(위임)하는것
AuthorizationManager에서 requestMatcher를 통해 URL을 찾을 때 목록의 순회를 돌아서 if문으로 해당 조건이 있는지 확인하는 과정을 거친다
<img src="https://velog.velcdn.com/images/may_yun/post/128f41cb-9c6e-4794-a2fa-4a11cdd39c03/image.png" alt=""></li>
<li>요청자원 접근에 대한 최종 승인 혹은 거부 결과 반환</li>
<li>인증방식에 따라 적절한 Strategy를 선택해서 인증 여부 확인
<img src="https://velog.velcdn.com/images/may_yun/post/3a7457a7-5de5-417b-be0a-1e399044b6df/image.png" alt=""></li>
</ol>
<hr>
<h1 id="authenticatedauthorizationmanager-흐름도--구조">AuthenticatedAuthorizationManager 흐름도 / 구조</h1>
<p><img src="https://velog.velcdn.com/images/may_yun/post/3ff6219f-0c93-446c-befa-02204d0687a1/image.png" alt=""></p>
<h1 id="authorityauthorizationmanager">AuthorityAuthorizationManager</h1>
<p><img src="https://velog.velcdn.com/images/may_yun/post/59310d54-0a17-41b0-baf6-90ac601625ec/image.png" alt=""></p>
<ul>
<li>매핑은 EndPoint 기준으로 mapping된다 따라서 requestMatchers가 4개라도 엔드포인트가 5개이면 5개 매핑
<img src="https://velog.velcdn.com/images/may_yun/post/2dc50659-f5f8-48a4-8bb7-7ad5a2a5ba4c/image.png" alt=""></li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/8f2ebd3c-4d4e-4cfd-9437-5caf1b021ec1/image.png" alt=""></p>
<hr>
<h1 id="요청-기반-custom_authorizationmanager-구현-하여-권한-심사하기">요청 기반 Custom_AuthorizationManager 구현 하여 권한 심사하기</h1>
<ul>
<li>스프링 시큐리티 인가 설정 시 선언적 방식이 아닌 프로그래밍 방식으로 구현할 수 있으며 access(AuthorizationManager) API 를 사용한다</li>
<li>access() 에는 AuthorizationManager&lt; RequestAuthorizationContext &gt; 타입의 객체를 전달할 수 있으며 사용자의 요청에 대한 권한 검사를 access()
에 지정한 AuthorizationManager 가 처리하게 된다</li>
<li>access() 에 지정한 AuthorizationManager 객체는 RequestMatcherDelegatingAuthorizationManager 의 매핑 속성에 저장된다<pre><code class="language-java">http.authorizeHttpRequests(auth -&gt; auth.requestMatcher().access(AuthorizationManager)</code></pre>
</li>
</ul>
<ol>
<li>특정한 엔드포인트에 대한 권한 검사를 수행하기 위해 AuthorizationManager 를 구현하여 설정한다
<img src="https://velog.velcdn.com/images/may_yun/post/5c2f33e9-c558-40fc-a0cb-d6f2c20777f6/image.png" alt=""></li>
<li>&quot;/user&quot;, &quot;/myPage&quot;, &quot;/admin&quot; 요청 패턴의 권한 검사는 AuthorityAuthorizationManager 가 처리한다
&quot;/api“ 요청 패턴의 권한 검사는 CustomAuthorizationManager 가 처리</li>
</ol>
<pre><code class="language-java">public class CustomAuthorizationManager implements AuthorizationManager&lt;RequestAuthorizationContext&gt; { 

private static final String REQUIRED_ROLE = &quot;ROLE_SECURE&quot;;

@Override
public AuthorizationDecision check(Supplier&lt;Authentication&gt; authentication, RequestAuthorizationContext object) { 
  //인증 정보 가져오기    
  Authentication auth = authentication.get();
  //인증정보가없거나인증되지않은경우
  if (auth == null || !auth.isAuthenticated() || auth instanceof AnonymousAuthenticationToken) { 
      return new AuthorizationDecision(false);
  }
  // &quot;ROLE_SECURE&quot; 권한을 가진 사용자인지 확인
  boolean hasRequiredRole = auth
    .getAuthorities()
    .stream()
    .anyMatch(grantedAuthority -&gt; REQUIRED_ROLE.equals(grantedAuthority.getAuthority())
  );

  return new AuthorizationDecision(hasRequiredRole); 
  }
}</code></pre>
<hr>
<h1 id="requestmatcherdelegatingauthorizationmanager-인가-설정-응용하기">RequestMatcherDelegatingAuthorizationManager 인가 설정 응용하기</h1>
<ul>
<li>RequestMatcherDelegatingAuthorizationManager 의 mappings 속성에 직접 RequestMatcherEntry 객체를 생성하고 추가한다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/372a0c53-bf54-4877-8f75-b05adb6c08b7/image.png" alt=""></p>
<p>요청 패턴에 매핑된 AuthorizationManager 객체를 반환
요청 패턴을 저장한 RequestMatcher 객체를 반환</p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/79062584-33b8-4b3f-8a48-137b24d91865/image.png" alt=""></p>
<h2 id="사용">사용</h2>
<p><img src="https://velog.velcdn.com/images/may_yun/post/b25b4dac-8498-4ea2-a573-87c756a932be/image.png" alt=""></p>
<pre><code class="language-java">http.authorizeHttpRequests(auth -&gt; auth
.anyRequest().access(new CustomRequestMatcherDelegatingAuthorizationManager())</code></pre>
<p>모든 요청에 적용하기 때문에 url을 따로 지정하지 않아도 된다</p>
<p>1.RequestMatcherDelegatingAuthorizationManager 내부적으로 무조건 생성되고 mappings에 add함 이것은 변경할 수 없는 필수 절차이다.
2. 1번에 생성된 manager가 custom으로 만든 manager안에 들어가 있는 것이다.
3. 2번 객체안에 또 RequestMatcherDelegatingAuthorizationManager를 넣었지만 이 구조는 테스트용 구조로 좋은 구조는 아니다. 원리를 알기위해 학습용 구현</p>
<ul>
<li>springSecurityConfig<pre><code class="language-java">//하드코딩으로 수동적인 방법
</code></pre>
</li>
</ul>
<p>@Bean
public SecurityFilterChain defaultSecurityFilterChain(HttpSecurity http) throws Exception {</p>
<p>return http.authorizeHttpRequests(auth -&gt; auth
  .anyRequest()
  .access(authorizationManager(null)))
  .build();
}</p>
<p>@Bean
public AuthorizationManager<RequestAuthorizationContext> authorizationManager(HandlerMappingIntrospector introspector){ 
  List&lt;RequestMatcherEntry&lt;AuthorizationManager<RequestAuthorizationContext>&gt;&gt; mappings = new ArrayList&lt;&gt;();</p>
<p>  RequestMatcherEntry&lt;AuthorizationManager<RequestAuthorizationContext>&gt; requestMatcherEntry1 =
  new RequestMatcherEntry&lt;&gt;(new MvcRequestMatcher(introspector, &quot;/user&quot;), AuthorityAuthorizationManager.hasAuthority(&quot;ROLE_USER&quot;));</p>
<p>  RequestMatcherEntry&lt;AuthorizationManager<RequestAuthorizationContext>&gt; requestMatcherEntry2 =
  new RequestMatcherEntry&lt;&gt;(new MvcRequestMatcher(introspector, &quot;/admin&quot;), AuthorityAuthorizationManager.hasRole(&quot;ADMIN&quot;));</p>
<p>  RequestMatcherEntry&lt;AuthorizationManager<RequestAuthorizationContext>&gt; requestMatcherEntry3 =
  new RequestMatcherEntry&lt;&gt;(AnyRequestMatcher.INSTANCE, new AuthenticatedAuthorizationManager&lt;&gt;());</p>
<p>  mappings.add(requestMatcherEntry1);
  mappings.add(requestMatcherEntry2);
  mappings.add(requestMatcherEntry3);</p>
<p>  return new CustomRequestMatcherDelegatingAuthorizationManager(mappings);
}</p>
<pre><code>
```java
public class CustomRequestMatcherDelegatingAuthorizationManager implements AuthorizationManager&lt;RequestAuthorizationContext&gt; { 

private final RequestMatcherDelegatingAuthorizationManager manager;

  public CustomRequestMatcherDelegatingAuthorizationManager(List&lt;RequestMatcherEntry&lt;AuthorizationManager&lt;RequestAuthorizationContext&gt;&gt;&gt; mappings) { 
    Assert.notEmpty(mappings, &quot;mappings cannot be empty&quot;);
    manager = RequestMatcherDelegatingAuthorizationManager.builder()
      .mappings(maps -&gt; maps.addAll(mappings))
      .build();
    }

  @Override
  public AuthorizationDecision check(Supplier&lt;Authentication&gt; authentication, RequestAuthorizationContext object) { 
  //단순 manager에 위임하는 일
  //TODO: 중복되는 위임 과정이 존재하기 때문에 개선 필요
      return manager.check(authentication,object.getRequest());
  }

  @Override
  public void verify(Supplier&lt;Authentication&gt; authentication, RequestAuthorizationContext object) { 
      AuthorizationManager.super.verify(authentication, object);
  } 

}</code></pre><ul>
<li>요청에 대한 권한 검사를 RequestMatcherDelegatingAuthorizationManager 객체가 수행하도록 한다
• RequestMatcherDelegatingAuthorizationManager &gt; CustomRequestMatcherDelegatingAuthorizationManager &gt;
RequestMatcherDelegatingAuthorizationManager 구조는 조금 더 개선이 필요하다</li>
</ul>
<hr>
<h1 id="메서드-기반-인가-관리자">메서드 기반 인가 관리자</h1>
<ul>
<li>스프링 시큐리티는 메서드 기반의 인증된 사용자 및 특정권한을 가진 사용자의 자원접근 허용여부를 결정하는 인가 관리자 클래스들을 제공한다</li>
<li>PreAuthorizeAuthorizationManager, PostAuthorizeAuthorizationManager, Jsr250AuthorizationManager, SecuredAuthorizationManager 가 있다</li>
<li>🚩 요청기반과의 차이점 : 메서드 기반 권한 부여는 내부적으로 AOP 방식에 의해 초기화 설정이 이루어지며 메서드의 호출을 MethodInterceptor(advice) 가 가로 채어 처리하고 있다
처리과정과 초기화과정이 메서드 기반과 요청기반 인가 처리는 다르다</li>
</ul>
<hr>
<ul>
<li><p>권한에 따른 메서드가 매핑이되어 스프링시큐리티에 저장된다</p>
<pre><code class="language-java">@PreAuthorize(&quot;hasAuthority(&#39;ROLE_USER&#39;)&quot;) 
public List&lt;User&gt; users() {

  System.out.println(&quot;users: &quot; + UserRepositiry.findAll()); 
}</code></pre>
</li>
<li><p>메서드 호출자의 관점
<img src="https://velog.velcdn.com/images/may_yun/post/db15b521-1895-43e3-ac2b-2115ffb721f0/image.png" alt=""></p>
</li>
</ul>
<p>위 두가지를 비교하여 인가 처리 진행하는 것 </p>
<h2 id="⭐️초기화-과정-aop">⭐️초기화 과정 (AOP)</h2>
<p><img src="https://velog.velcdn.com/images/may_yun/post/5218443e-5565-4098-9bbd-a89796311bf0/image.png" alt=""></p>
<p>📌보안이 설정된 메소드가 있는 경우 그 빈의 프록시 객체를 자동으로 생성하는것이고
없다면 생성되지않는다 생성된 proxy 객체로 어떠한 작업을 진행하기 때문.</p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/ebce054f-2017-44ab-86e0-24ab237f5f44/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/faa932c1-aa50-4f6e-8f45-124eee6e3687/image.png" alt=""></p>
<hr>
<p><img src="https://velog.velcdn.com/images/may_yun/post/3c12e829-16c6-4b4a-a973-2e0aed91db2f/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/e0dd20c8-c38b-47a1-9d60-2660dbf581e2/image.png" alt=""></p>
<hr>
<h2 id="메서드-기반-custom-authorizationmanager-구현">메서드 기반 Custom AuthorizationManager 구현</h2>
<ul>
<li>사용자 정의 AuthorizationManager 를 생성함으로 메서드 보안을 구현할 수 있다</li>
</ul>
<pre><code class="language-java">@EnableMethodSecurity(prePostEnabled=false) 
//클래스 단위에 적용. 
//시큐리티가 제공하는 클래스들을 비활성화한다. 그렇지않으면 중복해서 검사하게 된다</code></pre>
<pre><code class="language-java">//메서드에 어노테이션 사용
@Bean 
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) 
public Advisor preAuthorize() {

    return AuthorizationManagerBeforeMethodInterceptor.preAuthorize(new MyPreAuthorizationManager()); 

}</code></pre>
<p><img src="https://velog.velcdn.com/images/may_yun/post/bec13b72-f423-47f3-8e1c-2f3490e267f7/image.png" alt=""></p>
<h2 id="포인트-컷-메서드-보안-구현">포인트 컷 메서드 보안 구현</h2>
<blockquote>
<p>위의 메서드 보안 기반 권한 규칙을 어노테이션 사용을 하지않고 선언할 수 있다</p>
</blockquote>
<ul>
<li>메서드 보안은 AOP를 기반으로 구축되었기 때문에 <code>어노테이션이 아닌 패턴 형태</code>로 <code>권한 규칙을 선언</code>할수있으며 이는 요청 수준의 인가와 유사한 방식이다</li>
<li>자체 어드바이저(Advisor)를 발행하거나 <code>포인트컷(PointCut)</code>을 사용하여 <code>AOP 표현식</code>을 애플리케이션의 인가 규칙에 맞게 매칭할 수 있으며 이를 통해 어노테이션을 사용하지 않고도 <code>메소드 수준</code>에서 보안 정책을 구현할 수 있다</li>
</ul>
<p>📌 AspectJExpressionPointcut을 사용하기 위해서
의존성 추가해야 한다.</p>
<pre><code>implementation &#39;org.springframework.boot:spring-boot-starter-aop:3.4.0&#39;
</code></pre><p><img src="https://velog.velcdn.com/images/may_yun/post/217872f8-2499-4b0c-ab98-f60289563963/image.png" alt=""></p>
<p>여러가지 표현식이 존재한다.</p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/75abdfbc-7646-408c-93bb-9d1e5122ee8c/image.png" alt=""></p>
<p>포인트컷 사용의 장점
유연한 적용 범위 지정:
애너테이션 기반 접근 제어(@PreAuthorize 등)는 주로 특정 메소드나 클래스에 직접 부착하는 방식으로 접근 제어를 수행한다. 반면 포인트컷을 사용하면 메소드 시그니처(메소드명 패턴), 패키지 구조, 파라미터 타입 등 다양한 기준을 통해 원하는 범위에 접근 제어를 유연하게 적용할 수 있다.</p>
<p>비침투적(Non-invasive) 접근 제어:
포인트컷을 사용하면 대상 코드(비즈니스 로직)에는 별다른 주석이나 어노테이션을 추가하지 않고도 권한 검사를 적용할 수 있다. 즉, 보안 로직이 애플리케이션 로직에 덜 침투적이며, 관심사의 분리가 더욱 명확해진다.</p>
<p>재사용성과 유지보수성 향상:
특정 패키지 혹은 특정 네이밍 컨벤션에 따라 접근 제어를 일괄 적용하거나 변경할 때, 포인트컷 정의만 수정하면 전체 적용 대상에 영향을 줄 수 있다. 이를 통해 재사용성과 유지보수성을 높일 수 있다.</p>
<p>복잡한 규칙 적용 용이:
애너테이션만으로 처리하기 애매한 복잡한 권한 규칙(예: 특정 파라미터 값 조건, 특정 리턴 타입에 따른 접근 제어 등)도 AOP 포인트컷 표현식과 어드바이스 로직을 조합하면 유연하게 처리할 수 있다.</p>
<hr>
<h1 id="참고">참고</h1>
<p>만약 PRE_FILTER와 PRE_AUTHORIZE 둘다 설정되어있는 경우 PRE_FILTER가 먼저 수행된다. </p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/7f980f8b-7926-4080-be8a-ee494a67a271/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링시큐리티] 계층적 권한]]></title>
            <link>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EA%B3%84%EC%B8%B5%EC%A0%81-%EA%B6%8C%ED%95%9C</link>
            <guid>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EA%B3%84%EC%B8%B5%EC%A0%81-%EA%B6%8C%ED%95%9C</guid>
            <pubDate>Tue, 17 Dec 2024 08:54:00 GMT</pubDate>
            <description><![CDATA[<h1 id="rolehirerachy">RoleHirerachy</h1>
<p>: 기본적으로 스프링시큐리티에서 권한과 역할은 계층적이거나 상하관계로 구분하지 않는다.
그래서 인증 주체가 다양한 역할과 권한을 부여 받아야 한다</p>
<p>RoleHirerachy는 역할간의 계층구조를 정의하고 관리하는데 사용되며 보다 간편하게 역할간의 계층 구조를 설정하고 이를 기반으로 사용자에 대한 액세스 규칙을 정의 할 수 있다</p>
<pre><code>&lt;property name=&quot;hierarchy&quot;&gt; 
&lt;value&gt;
ROLE_A &gt; ROLE_B
ROLE_B &gt; ROLE_C
ROLE_C &gt; ROLE_D
&lt;/value&gt; 
&lt;/property&gt;</code></pre><p><img src="https://velog.velcdn.com/images/may_yun/post/f0ae50f6-d21d-445a-b55c-544db613e832/image.png" alt=""></p>
<ul>
<li><p>setHirerachy
• 역할 계층을 설정하고 각 역할에 대해 해당 역할의 하위계층에 속하는 모든 역할 집합을 미리 정해 놓는다
• 역할 계층 : ROLE_A &gt; ROLE_B &gt; ROLE_C</p>
</li>
<li><p>getReachableGrantedAuthorities
• 모든 도달 가능한 권한의 배열을 반환한다
• 도달가능한권한은직접할당된권한에더해역할계층에서이들로부터도달가능한모든권한을의미한다
• 직접 할당된 권한 : ROLE_B
• 도달 가능한 권한 : ROLE_B, ROLE_C</p>
</li>
<li><p>사용 규칙</p>
<ul>
<li>권한의 범위를 &gt;, =, &lt; 등으로 관계 표현을 해줘야한다</li>
<li>다음 권한으로 넘어갈때 개행을 해줘야한다</li>
</ul>
</li>
</ul>
<pre><code class="language-java">@Bean
static RoleHierarchy roleHierarchy() {
  RoleHierarchyImpl hierarchy = new RoleHierarchyImpl(); 

  hierarchy.setHierarchy(&quot;ROLE_ADMIN &gt; ROLE_MANAGER\n&quot; +
    &quot;ROLE_MANAGER &gt; ROLE_USER\n&quot; +
    &quot;ROLE_USER &gt; ROLE_GUEST&quot;); 
  return hierarchy;
}</code></pre>
<p>권한의 역할 계층을 설정하면 
RoleHierarchy가 알아서 도달 가능한 권한을 부여한다</p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/801a1ee2-c071-409a-acc9-711e06016a23/image.png" alt=""></p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링시큐리티] 정적 자원 관리]]></title>
            <link>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EC%A0%95%EC%A0%81-%EC%9E%90%EC%9B%90-%EA%B4%80%EB%A6%AC</link>
            <guid>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EC%A0%95%EC%A0%81-%EC%9E%90%EC%9B%90-%EA%B4%80%EB%A6%AC</guid>
            <pubDate>Mon, 16 Dec 2024 11:25:42 GMT</pubDate>
            <description><![CDATA[<h1 id="정적-자원-관리">정적 자원 관리</h1>
<ul>
<li>스프링시큐리티에서RequestMatcher인스턴스를등록하여무시해야할요청을지정할수있다</li>
<li>주로 정적자원(이미지,CSS,JavaScript파일 등)에 대한 요청이나  <code>특정 엔드포인트가 보안필터를 거치지 않도록 설정</code>할 때 사용</li>
</ul>
<h2 id="사용-방법">사용 방법</h2>
<pre><code class="language-java">@Bean
public WebSecurityCustomizer webSecurityCustomizer() { 

  return (webSecurity) -&gt; {
  webSecurity
    .ignoring()
    .requestMatchers(PathRequest.toStaticResources()
    .atCommonLocations()); 
   };

}</code></pre>
<p>enum으로 정의되어있다.
<img src="https://velog.velcdn.com/images/may_yun/post/ad913991-b160-4567-9a64-3fe4cd7a9664/image.png" alt=""></p>
<p>📌 Ignoring 보다 permitAll 권장
이전 스프링시큐리티 버전에는 모든 요청마다 세션을 확인해야 해서 성능 저하가 있었지만 
스프링 시큐리티 6 부터는 권한 부여 규칙에서 필요한 경우를 제외하고는 세션을 확인하지 않는다.</p>
<p>지연로딩으로 <code>성능 문제가 해결</code>되었기 때문에 모든 요청에 대해서 permitAll을 사용할 것을 권장하며 정적 자원에 대한 요청일지라도 <code>안전한 헤더</code>를 작성할 수 있어 더 안전하다.
스프링시큐리티 보안필터를 거치지 않는 경우 XSS 인젝션 공격에 취약할 수 있다하지만 permitAll로 필터를 거치는 경우 보안적인 측면에서 안전할 수 있다</p>
<pre><code class="language-java">http
.authorizeHttpRequests(auth -&gt; auth
//filter를 거쳐야한다
.requestMatchers(&quot;/css/**&quot;, &quot;/images/**&quot;, &quot;/js/**&quot;, &quot;/webjars/**&quot;, &quot;/favicon.*&quot;, &quot;/*/icon-*&quot;).permitAll() 
//filter를 거치지않는다
.requestMatchers(PathRequest.toStaticResources().atCommonLocations()).permitAll() 
.anyRequest().authenticated());</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA] prometheus, Grafana 설치]]></title>
            <link>https://velog.io/@may_yun/MSA-prometheus-Grafana-%EC%84%A4%EC%B9%98</link>
            <guid>https://velog.io/@may_yun/MSA-prometheus-Grafana-%EC%84%A4%EC%B9%98</guid>
            <pubDate>Tue, 03 Dec 2024 11:00:09 GMT</pubDate>
            <description><![CDATA[<blockquote>
<ul>
<li>prometheus: metric 데이터 조회,저장</li>
</ul>
</blockquote>
<ul>
<li>grafana: 시각화
위 두 오픈소스를 같이 설치할 수 있는 Prometheus Operator를 이용하여 설치
<a href="https://suave-zinc-9c8.notion.site/Telemetry-1ce6909121cb4565bdd13080739e75e7">https://suave-zinc-9c8.notion.site/Telemetry-1ce6909121cb4565bdd13080739e75e7</a></li>
</ul>
<p>cloud9 환경 설정
<a href="https://www.notion.so/terraform-23390af706ba4605862eedaf420b74d3?pvs=4">https://www.notion.so/terraform-23390af706ba4605862eedaf420b74d3?pvs=4</a></p>
<pre><code>ubuntu:~/environment $ cd k8s-demo
ubuntu:~/environment/k8s-demo $ terraform init</code></pre><p><img src="https://velog.velcdn.com/images/may_yun/post/3e1fd6f8-9def-43ac-bef4-74d3eea4287b/image.png" alt=""></p>
<h3 id="사전-작업-k8s-클러스터-설치--ingress-controller-설치">사전 작업 k8s 클러스터 설치 / Ingress controller 설치</h3>
<ul>
<li>이전 실습 내용 정리<ul>
<li>weave scope 삭제<ul>
<li>kubectl delete -f <a href="https://github.com/weaveworks/scope/releases/download/v1.13.2/k8s-scope.yaml">https://github.com/weaveworks/scope/releases/download/v1.13.2/k8s-scope.yaml</a></li>
</ul>
</li>
</ul>
</li>
<li>K8S Cluster 생성<pre><code>//위치 k8s-demo
$ terraform apply --auto-approve</code></pre></li>
<li>Terraform 코드 내용 변경<ul>
<li>노드 리소스 증가<ul>
<li>Auto Scaling Group의 Desired Capacity를 변경 (2→3)</li>
<li>t3.medium으로 노드 스케일 업</li>
<li>capacity type - SPOT</li>
</ul>
</li>
<li>EKS Add-ons 및 Add-on의 실행 권한에 맞는 Role 추가<ul>
<li>aws-ebs-csi-driver, vpc-cni<pre><code>#main.tf
provider &quot;aws&quot; {
region = local.region
}
</code></pre></li>
</ul>
</li>
</ul>
</li>
</ul>
<p>provider &quot;kubernetes&quot; {
  host                   = module.eks.cluster_endpoint
  cluster_ca_certificate = base64decode(module.eks.cluster_certificate_authority_data)
  token                  = data.aws_eks_cluster_auth.this.token
}</p>
<p>data &quot;aws_eks_cluster_auth&quot; &quot;this&quot; {
  name = module.eks.cluster_name
}</p>
<p>data &quot;aws_availability_zones&quot; &quot;available&quot; {}</p>
<p>locals {
  name   = basename(path.cwd)
  region = &quot;ap-northeast-2&quot;</p>
<p>  vpc_cidr = &quot;10.0.0.0/16&quot;
  azs      = slice(data.aws_availability_zones.available.names, 0, 3)</p>
<p>  tags = {
    Cluster  = local.name
  }
}</p>
<p>################################################################################</p>
<h1 id="cluster">Cluster</h1>
<p>################################################################################</p>
<p>module &quot;eks&quot; {
  source  = &quot;terraform-aws-modules/eks/aws&quot;
  version = &quot;~&gt; 19.16&quot;</p>
<p>  cluster_name                   = local.name
  cluster_version                = &quot;1.27&quot;
  cluster_endpoint_public_access = true</p>
<p>  vpc_id     = module.vpc.vpc_id
  subnet_ids = module.vpc.private_subnets</p>
<p>  eks_managed_node_groups = {
    default_node_group = {
      instance_types = [&quot;t3.medium&quot;]
      capacity_type  = &quot;SPOT&quot;</p>
<pre><code>  min_size     = 2
  max_size     = 5
  desired_size = 2

}</code></pre><p>  }</p>
<p>  node_security_group_additional_rules = {
    ingress_cluster_api_ephemeral_ports_tcp = {
      description   = &quot;Cluster API to kubeseal services&quot;
      protocol      = &quot;tcp&quot;
      from_port     = 8080
      to_port       = 8080
      type          = &quot;ingress&quot;
      source_cluster_security_group = true
    }
  }</p>
<p>  cluster_addons = {
    aws-ebs-csi-driver = {
      service_account_role_arn = module.ebs_csi_addon_irsa_role.iam_role_arn
    }
    vpc-cni = {
      resolve_conflicts = &quot;OVERWRITE&quot;
      service_account_role_arn = module.vpc_cni_addon_irsa_role.iam_role_arn
    }
  }</p>
<p>  tags = local.tags
}</p>
<p>module &quot;load_balancer_controller_irsa_role&quot; {
  source = &quot;terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks&quot;</p>
<p>  role_name                              = &quot;load-balancer-controller&quot;
  attach_load_balancer_controller_policy = true</p>
<p>  oidc_providers = {
    ex = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = [&quot;kube-system:aws-load-balancer-controller&quot;]
    }
  }</p>
<p>  tags = local.tags
}</p>
<p>module &quot;ebs_csi_addon_irsa_role&quot; {
  source = &quot;terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks&quot;</p>
<p>  role_name             = &quot;ebs-csi-controller&quot;
  attach_ebs_csi_policy = true</p>
<p>  oidc_providers = {
    ex = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = [&quot;kube-system:ebs-csi-controller-sa&quot;]
    }
  }</p>
<p>  tags = local.tags
}</p>
<p>module &quot;vpc_cni_addon_irsa_role&quot; {
  source  = &quot;terraform-aws-modules/iam/aws//modules/iam-role-for-service-accounts-eks&quot;</p>
<p>  role_name             = &quot;vpc-cni&quot;
  attach_vpc_cni_policy = true
  vpc_cni_enable_ipv4   = true</p>
<p>  oidc_providers = {
    main = {
      provider_arn               = module.eks.oidc_provider_arn
      namespace_service_accounts = [&quot;kube-system:aws-node&quot;]
    }
  }</p>
<p>  tags = local.tags
}</p>
<p>################################################################################</p>
<h1 id="supporting-resoruces">Supporting Resoruces</h1>
<p>################################################################################</p>
<p>module &quot;vpc&quot; {
  source  = &quot;terraform-aws-modules/vpc/aws&quot;
  version = &quot;~&gt; 5.0&quot;</p>
<p>  name = local.name
  cidr = local.vpc_cidr</p>
<p>  azs             = local.azs
  public_subnets   = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k)]
  private_subnets  = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 3)]
  database_subnets = [for k, v in local.azs : cidrsubnet(local.vpc_cidr, 8, k + 6)]</p>
<p>  create_database_subnet_group = true</p>
<p>  enable_nat_gateway = true
  single_nat_gateway = true</p>
<p>  public_subnet_tags = {
    &quot;kubernetes.io/role/elb&quot; = 1
  }</p>
<p>  private_subnet_tags = {
    &quot;kubernetes.io/role/internal-elb&quot; = 1
  }</p>
<p>  tags = local.tags
}</p>
<pre><code>![](https://velog.velcdn.com/images/may_yun/post/ddbfeb16-ae1c-4bb7-99af-bfb331a14497/image.png)

- Ingress Controller 설치 (Grafana 외부 접속 시 사용)
    - 위치: k8s-demo
    - https://github.com/attachnplay/fastcampus-pay-bootstrap/blob/main/install_ingress_controller.md

AWS Load Balancer Ingress Controller 설치
Terraform 기반으로 K8S Cluster를 실행 후에 설치 진행</code></pre><h1 id="kubeconfig-설정">Kubeconfig 설정</h1>
<p>aws eks --region ap-northeast-2 update-kubeconfig --name k8s-demo</p>
<h1 id="k8s-cluster-연결-확인">K8S Cluster 연결 확인</h1>
<p>kubectl get no</p>
<h1 id="helm-charts-repo-추가">Helm charts repo 추가</h1>
<p>helm repo add eks <a href="https://aws.github.io/eks-charts">https://aws.github.io/eks-charts</a></p>
<h1 id="aws-load-balancer-ingress-controller-설치">AWS Load Balancer Ingress Controller 설치</h1>
<h1 id="-590974975982-부분은-본인-aws-계정으로-변경">!! 590974975982 부분은 본인 AWS 계정으로 변경</h1>
<p>helm install aws-load-balancer-controller eks/aws-load-balancer-controller <br>  -n kube-system --set clusterName=k8s-demo --set serviceAccount.create=true <br>  --set serviceAccount.name=aws-load-balancer-controller <br>  --set serviceAccount.annotations.&quot;eks.amazonaws.com/role-arn&quot;=&quot;arn:aws:iam::123123123123:role/load-balancer-controller&quot;</p>
<p>#123123123123이 숫자는 본인의 AWS 계정과 연동하면 됨</p>
<pre><code>
helm 설치
https://whchoi98.gitbook.io/k8s/eks-2/helm

---

## Prometheus + Grafana 설치

- Prometheus Operator 사용
    - https://github.com/attachnplay/kube-prometheus
    - Physical Volume 관련 설정 추가함
- Prometheus 설치
    - [https://prometheus-operator.dev/docs/user-guides/getting-started/](https://prometheus-operator.dev/docs/prologue/quick-start/)
</code></pre><p>git clone <a href="mailto:git@github.com">git@github.com</a>:attachnplay/kube-prometheus.git</p>
<p>cd kube-prometheus</p>
<h1 id="create-the-namespace-and-crds-and-then-wait-for-them-to-be-availble-before-creating-the-remaining-resources">Create the namespace and CRDs, and then wait for them to be availble before creating the remaining resources</h1>
<p>kubectl create -f manifests/setup</p>
<h1 id="wait-until-the-servicemonitors-crd-is-created-the-message-no-resources-found-means-success-in-this-context">Wait until the &quot;servicemonitors&quot; CRD is created. The message &quot;No resources found&quot; means success in this context.</h1>
<p>until kubectl get servicemonitors --all-namespaces ; do date; sleep 1; echo &quot;&quot;; done</p>
<p>kubectl create -f manifests/</p>
<pre><code>
![](https://velog.velcdn.com/images/may_yun/post/f30c7342-c7f4-4040-85d9-e4ce65fb4b19/image.png)

![](https://velog.velcdn.com/images/may_yun/post/ff55da9a-0448-4513-b9e3-3fefcea1736c/image.png)

![](https://velog.velcdn.com/images/may_yun/post/c1b0cde2-0567-47a8-a954-cfd08bfb2c0c/image.png)

![](https://velog.velcdn.com/images/may_yun/post/1f6227d8-1c42-4687-9017-cfc5fa4c0a8b/image.png)

![](https://velog.velcdn.com/images/may_yun/post/57839d5a-56a7-4546-a14f-ed8c533269f7/image.png)

- grafana url: 

📌 XTERNAL-IP pending

Cloud9에서 k9s를 실행하면서 grafana-loadbalancer-svc의 External-IP가 Pending 상태로 나오는 이유는 Kubernetes 클러스터의 LoadBalancer 타입 서비스가 External IP를 할당하지 못한 것과 관련이 있습니다. 이는 일반적으로 Cloud9 환경과 관련된 네트워크 설정 또는 Kubernetes 클러스터의 LoadBalancer 구현 문제 때문입니다.

주요 원인 및 해결 방법
1. 클라우드 제공자 LoadBalancer 지원 여부
Cloud9은 AWS에서 실행되며 Kubernetes 클러스터가 AWS에서 운영 중일 경우, LoadBalancer 서비스는 AWS의 Elastic Load Balancer(ELB)를 통해 External IP를 할당합니다.

문제: 클러스터가 적절한 IAM 권한이나 설정을 가지지 않으면 ELB 생성이 실패합니다.
해결 방법:
Kubernetes 클러스터에 적절한 IAM 역할이 부여되었는지 확인합니다.
bash
코드 복사

-&gt; localhost:3000으로 접속

---

## Log 기록을 위한 Loki 설치
: 운영 수준의 구성은 다소 무겁기 때문에 Loki 사용 방법 확인을 목적으로 함

https://grafana.com/docs/loki/latest/setup/install/

- monolithic 구성으로 설치</code></pre><p>helm repo add grafana <a href="https://grafana.github.io/helm-charts">https://grafana.github.io/helm-charts</a></p>
<p>helm repo update</p>
<p>helm pull grafana/loki</p>
<p>tar zxf (내려받은 파일 예, loki-5.39.0).tgz</p>
<pre><code>
![](https://velog.velcdn.com/images/may_yun/post/43c5ab7e-7bbd-4613-be88-f895f9341256/image.png)

![](https://velog.velcdn.com/images/may_yun/post/3563b372-7d93-4f65-b4f0-846c661dbc16/image.png)
</code></pre><p>$ vim values-dev.yaml</p>
<pre><code></code></pre><p>loki:
  commonConfig:
    replication_factor: 1
  schemaConfig:
    configs:
      - from: &quot;2024-04-01&quot;
        store: tsdb
        object_store: s3
        schema: v13
        index:
          prefix: loki_index_
          period: 24h
  pattern_ingester:
      enabled: true
  limits_config:
    allow_structured_metadata: true
    volume_enabled: true
    retention_period: 672h # 28 days retention
  compactor:
    retention_enabled: true 
    delete_request_store: s3
  ruler:
    enable_api: true</p>
<p>minio:
  enabled: true</p>
<p>deploymentMode: SingleBinary</p>
<p>singleBinary:
  replicas: 1</p>
<h1 id="zero-out-replica-counts-of-other-deployment-modes">Zero out replica counts of other deployment modes</h1>
<p>backend:
  replicas: 0
read:
  replicas: 0
write:
  replicas: 0</p>
<p>ingester:
  replicas: 0
querier:
  replicas: 0
queryFrontend:
  replicas: 0
queryScheduler:
  replicas: 0
distributor:
  replicas: 0
compactor:
  replicas: 0
indexGateway:
  replicas: 0
bloomCompactor:
  replicas: 0
bloomGateway:
  replicas: 0</p>
<pre><code>** vim 에서 붙여넣기 할 때 형식이 제대로 안될 경우
:set paste 를 하고 i(insert)모드를 눌러서 다시 붙여넣기

![](https://velog.velcdn.com/images/may_yun/post/c452f6b3-ba29-46c0-96d2-92412c7a7a09/image.png)

![](https://velog.velcdn.com/images/may_yun/post/122143f6-e79f-4d48-a778-480c4a2d16ce/image.png)

X-Scope-OrgID : 1

---


</code></pre>]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA] Axon Framework CQRS, API Aggregation]]></title>
            <link>https://velog.io/@may_yun/MSA-Axon-Framework-CQRS-API-Aggregation</link>
            <guid>https://velog.io/@may_yun/MSA-Axon-Framework-CQRS-API-Aggregation</guid>
            <pubDate>Fri, 29 Nov 2024 17:01:49 GMT</pubDate>
            <description><![CDATA[<ul>
<li>Aggregate(command model): 어그리게이트는 상태를 관리하고 명령(command)을 처리하여 이벤트를 생성한다.</li>
<li>Default 생성자는 Axon에서 리플렉션을 통해 Aggregate를 초기화할 때 필요하다.<h2 id="동작">동작</h2>
Command: 명령이 Aggregate에서 처리되며, 이벤트가 발행.
Event: 이벤트가 저장소(Event Store)에 저장되고 Query Model로 전파.
Query: Query Model은 이벤트를 기반으로 데이터를 읽기 데이터베이스에 저장하여 최적화된 읽기 작업을 수행.</li>
</ul>
<h2 id="command-클래스-쓰기-작업-요청-정의">command 클래스: 쓰기 작업 요청 정의</h2>
<pre><code class="language-java">package com.yun.money.adapter.axon.command;

import com.yun.common.SelfValidating;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.*;
import org.axonframework.modelling.command.TargetAggregateIdentifier;

@Getter
@ToString
@NoArgsConstructor(access = AccessLevel.PRIVATE)
@EqualsAndHashCode(callSuper = false)
public class MemberMoneyCreateCommand extends SelfValidating&lt;MemberMoneyCreateCommand&gt; {
    @NotNull
    @NotEmpty
    @TargetAggregateIdentifier // 명령이 어느 Aggregate에 전달될지 식별
    private String membershipId;

    public MemberMoneyCreateCommand(String membershipId) {
        this.membershipId = membershipId;
        this.validateSelf();
    }
}
</code></pre>
<h2 id="aggregate-클래스">Aggregate 클래스</h2>
<pre><code class="language-java">1. Aggregate 클래스: 명령 처리를 위한 클래스
@Aggregate 명시

2. @AggregateIdentifier: ID 필드에 Aggregate의 고유 식별자 (Aggregate ID)
private String id;

3. 명령 핸들러: CreateOrderCommand를 처리
@CommandHandler 명시
public MemberMoneyAggregate(MemberMoneyCreateCommand command) {
    //명령 실행시 command가 존재하는지 유효성 검사를 할 수 있다.
    log.info(&quot;MemberMoneyAddCommand Handler&quot;);
    //이벤트 발행 (MemberMoneyCreateEvent)
    apply(new MemberMoneyCreateEvent(id, command.getMembershipId()));
}

4. 이벤트 소싱 핸들러: MemberMoneyCreateEvent를 처리하여 상태를 업데이트
@EventSourcingHandler
public void on(MemberMoneyCreateEvent event) {
    log.info(&quot;MemberMoneyAddEvent Sourcing Handler&quot;);
    id = UUID.randomUUID().toString();
    membershipId = event.getMembershipId();
    balance = 0;
}

5. 다음 명령 핸들러: UpdateOrderStatusCommand를 처리
@CommandHandler
public void handle(UpdateOrderStatusCommand command) {
    // 상태 변경 가능 여부를 검증
    if (!&quot;CREATED&quot;.equals(this.status)) {
            throw new IllegalStateException(&quot;Order cannot be updated in current state&quot;);
    }
    // 이벤트를 발행 (OrderStatusUpdatedEvent)
    apply(new OrderStatusUpdatedEvent(command.getOrderId(), command.getNewStatus()));
}

6. 이벤트 소싱 핸들러: OrderStatusUpdatedEvent를 처리하여 상태를 업데이트
@EventSourcingHandler
public void on(OrderStatusUpdatedEvent event) {
    this.status = event.getNewStatus();
}</code></pre>
<h2 id="event-클래스--상태-변경의-결과">Event 클래스 : 상태 변경의 결과</h2>
<pre><code class="language-java">package com.yun.money.adapter.axon.event;

import com.yun.common.SelfValidating;
import com.yun.money.adapter.axon.command.MemberMoneyCreateCommand;
import lombok.AllArgsConstructor;
import lombok.EqualsAndHashCode;
import lombok.Getter;
import lombok.NoArgsConstructor;

@Getter
@NoArgsConstructor
@AllArgsConstructor
@EqualsAndHashCode(callSuper = false)
public class MemberMoneyCreateEvent extends SelfValidating&lt;MemberMoneyCreateCommand&gt; {
    private String aggregateIdentifier;
    private String membershipId;
}
</code></pre>
<hr>
<h2 id="query-model">Query Model</h2>
<p>Query Model은 이벤트를 구독하고 읽기 전용 데이터베이스 업데이트.</p>
<pre><code class="language-java">package com.yun.moneyqueryservice.adapter.in.axon;

import com.yun.moneyqueryservice.application.port.out.GetMemberAddressInfoPort;
import com.yun.moneyqueryservice.application.port.out.InsertMoneyIncreaseEventByAddress;
import com.yun.moneyqueryservice.application.port.out.MemberAddressInfo;
import lombok.extern.slf4j.Slf4j;
import org.axonframework.eventhandling.EventHandler;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class MoneyAdjustEventHandler {
    //⭐️RequestFirmbankingFinishedEvent를 구독하여 읽기 모델 업데이트
    @EventHandler
    public void handler(RequestFirmbankingFinishedEvent event
            , GetMemberAddressInfoPort getMemberAddressInfoPort
            , InsertMoneyIncreaseEventByAddress insertMoneyIncreaseEventByAddress) {
        log.info(&quot;money adjust event receiced : {}&quot;, event.toString());

        //고객 주소 정보
        MemberAddressInfo memberAddressInfo = getMemberAddressInfoPort.getMemberAddressInfo(event.getMembershipId());

        //⭐️dynamoDB insert : 읽기 데이터베이스에 주문 저장
        String address = memberAddressInfo.address();
        int moneyAmount = event.getMoneyAmount();
        log.info(&quot;dynamo insert: {}, {}&quot;, address, moneyAmount);
        insertMoneyIncreaseEventByAddress.insertMoneyIncreaseEventByAddress(address, moneyAmount);
    }
}
</code></pre>
<p>DynamoDBAdapter 읽기 전용 DB</p>
<pre><code class="language-java">package com.yun.moneyqueryservice.adapter.out.aws.dynamodb;

import com.yun.moneyqueryservice.application.port.out.GetMemberAddressInfoPort;
import com.yun.moneyqueryservice.application.port.out.InsertMoneyIncreaseEventByAddress;
import com.yun.moneyqueryservice.application.port.out.MemberAddressInfo;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
import software.amazon.awssdk.regions.Region;
import software.amazon.awssdk.services.dynamodb.DynamoDbClient;
import software.amazon.awssdk.services.dynamodb.model.*;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@Slf4j
@RequiredArgsConstructor
@Component
public class DynamoDBAdapter implements GetMemberAddressInfoPort, InsertMoneyIncreaseEventByAddress {
    @Value(&quot;money.query.table&quot;)
    private String TABLE_NAME;
    @Value(&quot;money.dynamo.access&quot;)
    private String ACCESS_KEY;
    @Value(&quot;money.dynamo.secret&quot;)
    private String SECRET_KEY;
    private final DynamoDbClient dynamoDbClient;
    private final MoneySumByAddressMapper mapper;

    public DynamoDBAdapter() {
        this.mapper = new MoneySumByAddressMapper();
        AwsBasicCredentials awsBasicCredentials = AwsBasicCredentials.create(ACCESS_KEY, SECRET_KEY);
        this.dynamoDbClient = DynamoDbClient.builder()
                .region(Region.AP_NORTHEAST_2)
                .credentialsProvider(StaticCredentialsProvider.create(awsBasicCredentials))
                .build();
    }

    /*@Override
    public void insertMoneyIncreaseEventByAddress(String addressName, int moneyIncrease) {
        //1. raw event insert
        String pk = addressName + &quot;#&quot; + LocalDateTime.now();
        int sk = moneyIncrease;
        putItem(pk, sk);

        //2. 지역 정보 잔액 증가
        //2-1. 지역별/일별 정보
        String summaryPk = pk + &quot;summary&quot;;
        int summarySk = -1;
        MoneySumByAddress item = getItem(summaryPk, summarySk);
        if (item == null) {
            putItem(summaryPk, summarySk, moneyIncrease);
        }

        //합산 TODO: domain으로 분리
        int balance = item.getBalance();
        balance += moneyIncrease;
        //update

        //2-2. 지역별 정보
        String addressPk = addressName;
        int addressSk = -1;
        MoneySumByAddress addressItem = getItem(addressPk, addressSk);
        if (addressItem == null) {
            putItem(addressPk, addressSk, moneyIncrease);
        }
        //합산 TODO: domain으로 분리
        int addressItemBalance = addressItem.getBalance();
        addressItemBalance += moneyIncrease;
        putItem(addressPk, addressSk, addressItemBalance);
    }*/

    @Override
    public void insertMoneyIncreaseEventByAddress(String addressName, int moneyIncrease) {
        // 3개의 일을 해야될 것이에요.

        // 1. raw event insert (Insert, put)
        // PK: 강남구#230728 SK: 5,000, balance, 5,000
        String pk = addressName + &quot;#&quot; + &quot;230728&quot;;
        int sk = moneyIncrease;
        putItem(pk, sk, moneyIncrease);

        // 2. 지역 정보 잔액 증가시켜야 해요. (Query, Update)
        // 2-1. 지역별/일별 정보
        //  - PK: 강남구#230728#summary SK: -1 balance: + 5,000
        String summaryPk = pk + &quot;#summary&quot;;
        int summarySk = -1;
        MoneySumByAddress moneySumByAddress = getItem(summaryPk, summarySk);
        if (moneySumByAddress == null) {
            putItem(summaryPk, summarySk, moneyIncrease);
        } else{
            int balance = moneySumByAddress.getBalance();
            balance += moneyIncrease;
            updateItem(summaryPk, summarySk, balance);
        }

        // 2-2. 지역별 정보
        // - PK: 강남구 SK: -1 balance: + 5,000
        String summaryPk2 = addressName;
        int summarySk2 = -1;
        MoneySumByAddress moneySumByAddress2 = getItem(summaryPk2, summarySk2);
        if (moneySumByAddress2 == null) {
            putItem(summaryPk2, summarySk2, moneyIncrease);
        } else{
            int balance2 = moneySumByAddress2.getBalance();
            balance2 += moneyIncrease;
            updateItem(summaryPk2, summarySk2, balance2);
        }
    }

    @Override
    public MemberAddressInfo getMemberAddressInfo(String membershipId) {
        return null;
    }

    private MoneySumByAddress getItem(String pk, int sk) {
        try {
            HashMap&lt;String, AttributeValue&gt; attrMap = new HashMap&lt;&gt;();
            attrMap.put(&quot;PK&quot;, AttributeValue.builder().s(pk).build());
            attrMap.put(&quot;SK&quot;, AttributeValue.builder().n(Integer.toString(sk)).build());

            GetItemRequest request = GetItemRequest.builder()
                    .tableName(TABLE_NAME)
                    .key(attrMap)
                    .build();

            GetItemResponse response = dynamoDbClient.getItem(request);

            if (response.hasItem()) {
                mapper.mapToMoneySumByAddress(response.item());
            }

        } catch (DynamoDbException e) {
            log.error(&quot;Error getting an item from the table: {} &quot;, e.getMessage());
        }
        return null;
    }

    private void putItem(String pk, int sk) {
        try {
            HashMap&lt;String, AttributeValue&gt; attrMap = new HashMap&lt;&gt;();
            attrMap.put(&quot;PK&quot;, AttributeValue.builder().s(pk).build());
            attrMap.put(&quot;SK&quot;, AttributeValue.builder().s(Integer.toString(sk)).build());

            PutItemRequest request = PutItemRequest.builder()
                    .tableName(TABLE_NAME)
                    .item(attrMap)
                    .build();

            dynamoDbClient.putItem(request);
        } catch (DynamoDbException e) {
            log.error(&quot;Error adding an item to the table: {} &quot;, e.getMessage());
        }
    }

    private void putItem(String pk, int sk, int balance) {
        try {
            HashMap&lt;String, AttributeValue&gt; attrMap = new HashMap&lt;&gt;();
            attrMap.put(&quot;PK&quot;, AttributeValue.builder().s(pk).build());
            attrMap.put(&quot;SK&quot;, AttributeValue.builder().s(Integer.toString(sk)).build());
            attrMap.put(&quot;Balance&quot;, AttributeValue.builder().n(Integer.toString(balance)).build());

            PutItemRequest request = PutItemRequest.builder()
                    .tableName(TABLE_NAME)
                    .item(attrMap)
                    .build();

            dynamoDbClient.putItem(request);
        } catch (DynamoDbException e) {
            log.error(&quot;Error adding an item to the table: {} &quot;, e.getMessage());
        }
    }

    private void queryItem(String pk) {
        try {
            // PK 만 써도 돼요.
            HashMap&lt;String, Condition&gt; attrMap = new HashMap&lt;&gt;();
            attrMap.put(&quot;PK&quot;, Condition.builder()
                    .attributeValueList(AttributeValue.builder().s(pk).build())
                    .comparisonOperator(ComparisonOperator.EQ)
                    .build());

            QueryRequest request = QueryRequest.builder()
                    .tableName(TABLE_NAME)
                    .keyConditions(attrMap)
                    .build();

            QueryResponse response = dynamoDbClient.query(request);
            response.items().forEach((value) -&gt; log.info(&quot;value: {}&quot;, value));
        } catch (DynamoDbException e) {
            log.error(&quot;Error getting an item from the table: {} &quot;, e.getMessage());
        }
    }

    private void updateItem(String pk, int sk, int balance) {
        try {
            HashMap&lt;String, AttributeValue&gt; attrMap = new HashMap&lt;&gt;();
            attrMap.put(&quot;PK&quot;, AttributeValue.builder().s(pk).build());
            attrMap.put(&quot;SK&quot;, AttributeValue.builder().s(Integer.toString(sk)).build());

            String balanceStr = String.valueOf(balance);
            // Create an UpdateItemRequest
            UpdateItemRequest updateItemRequest = UpdateItemRequest.builder()
                    .tableName(TABLE_NAME)
                    .key(attrMap)
                    .attributeUpdates(
                            new HashMap&lt;String, AttributeValueUpdate&gt;() {{
                                put(&quot;balance&quot;, AttributeValueUpdate.builder()
                                        .value(AttributeValue.builder().n(balanceStr).build())
                                        .action(AttributeAction.PUT)
                                        .build());
                            }}
                    ).build();


            UpdateItemResponse response = dynamoDbClient.updateItem(updateItemRequest);

            // 결과 출력.
            Map&lt;String, AttributeValue&gt; attributes = response.attributes();
            if (attributes != null) {
                for (Map.Entry&lt;String, AttributeValue&gt; entry : attributes.entrySet()) {
                    String attributeName = entry.getKey();
                    AttributeValue attributeValue = entry.getValue();
                    System.out.println(attributeName + &quot;: &quot; + attributeValue);
                }
            } else {
                System.out.println(&quot;Item was updated, but no attributes were returned.&quot;);
            }
        } catch (DynamoDbException e) {
            System.err.println(&quot;Error getting an item from the table: &quot; + e.getMessage());
        }
    }
}
</code></pre>
<hr>
<h2 id="cqrs">CQRS</h2>
<p>command(데이터변동)과 query(데이터 조회)를 분리함으로써 비즈니스 로직이 포함된 API를 구현하는 패턴이다. </p>
<h2 id="api-aggregation">API Aggregation</h2>
<p>클라이언트와 서버 간 통신의 효율성을 높이기 위한 설계로 여러 API 호출을 하나의 API로 <strong>집계(aggregation)</strong>하여 클라이언트가 한 번의 호출로 필요한 데이터를 얻을 수 있도록 하는 설계 방식이며 주로 API Gateway나 Aggregator 서비스를 통해 구현된다.</p>
<ul>
<li>여러 마이크로서비스로 분리된 데이터를 통합하여 한 번에 제공.</li>
<li>클라이언트가 여러 개의 API를 직접 호출하지 않도록 함.</li>
<li>응답 시간이 느려질 수 있음(여러 API 호출로 인해).</li>
<li>Aggregator API가 병목이 될 가능성.</li>
</ul>
<p>단점으로
요청한 데이터가 많을수록 해당 서비스에서 많은 API를 호출받게 될 것인데 이때 DB 부하 측면에서 괜찮다고해도 응답 시간이 느려질 수 있다.이를 해결하기 위해 CQRS 패턴 도입</p>
<h2 id="api-aggregation--cqrs">API Aggregation + CQRS</h2>
<p>CQRS 기반의 시스템에서 API Aggregation을 통해 클라이언트 응답 성능을 최적화</p>
]]></description>
        </item>
        <item>
            <title><![CDATA[[MSA] Event Sourcing, Event Driven, Event Driven Architecture]]></title>
            <link>https://velog.io/@may_yun/MSA-Event-Sourcing-Event-Driven-Event-Driven-Architecture</link>
            <guid>https://velog.io/@may_yun/MSA-Event-Sourcing-Event-Driven-Event-Driven-Architecture</guid>
            <pubDate>Fri, 29 Nov 2024 15:48:49 GMT</pubDate>
            <description><![CDATA[<h1 id="이벤트">이벤트</h1>
<p>이벤트를 발행(produce)하고 비동기 방식으로 필요한 곳에서 consume 한다</p>
<p>발생(produce)는 누락될 가능성이 거의 없다 (성공률이 높다)
이벤트 자체만으로는 어떤 도메인과도 직접적인 의존성이 없는 느신한 결합을 가능하게 만든다</p>
<h2 id="1-event-sourcing">1. Event Sourcing</h2>
<p>Event Sourcing는 데이터 상태를 저장하는 대신, 시스템의 상태 변화를 나타내는 이벤트들의 순서를 기록하고 이 기록을 기반으로 현재 상태를 재구성하는 방법론입니다.</p>
<ul>
<li><p>핵심 개념:
데이터의 상태 변경이 이벤트로 저장됩니다.
시간 순서로 저장된다.
모든 상태는 기록된 이벤트를 재생(Replay)하여 복원됩니다.
이벤트는 이벤트 스트림으로 저장.</p>
</li>
<li><p>특징:
이력 관리: 시스템의 모든 변경 사항을 추적하고, 과거 상태를 복원할 수 있습니다.
디버깅과 감사: 이벤트 로그를 통해 왜 특정 상태가 되었는지 추적 가능.
Event Store: 전통적인 DB 대신 이벤트 저장소를 사용.</p>
</li>
<li><p>사용 예:
금융 시스템 (거래 기록 추적).
비즈니스 규칙이 복잡한 도메인.
이벤트 기반 시스템의 데이터 무결성 보장.</p>
</li>
<li><p>장점:
데이터 변경이력 추적과 감시에 유용하다.</p>
</li>
</ul>
<hr>
<p><code>은행 계좌</code>
은행은 현재 잔액만 저장하는 것이 아니라, 모든 입출금 내역을 기록합니다.
예를 들어:</p>
<ul>
<li>잔액: 0원</li>
<li>이벤트:
2023-11-01 → 1000원 입금
2023-11-02 → 500원 출금
2023-11-03 → 200원 입금</li>
<li>현재 잔액: 1000 - 500 + 200 = 700원
여기서 중요한 점은 <strong>&quot;현재 잔액을 따로 저장하지 않고, 입출금 기록만을 가지고도 잔액을 계산할 수 있다&quot;</strong>는 것</li>
</ul>
<p><code>쇼핑몰 주문 시스템</code>
상황: 고객이 쇼핑몰에서 상품을 주문하고, 결제, 배송 등의 상태가 변하는 과정을 관리해야 합니다.</p>
<ol>
<li>상태 변화 이벤트
Event Sourcing에서는 주문 상태를 아래처럼 이벤트로 기록합니다:</li>
</ol>
<ul>
<li>이벤트 기록:
OrderCreated (주문 생성됨) → &quot;주문 번호 1234, 고객: Alice, 상품: 책&quot;
PaymentCompleted (결제 완료됨) → &quot;주문 번호 1234, 결제 금액: 20,000원&quot;
OrderShipped (배송 시작됨) → &quot;주문 번호 1234, 배송 시작&quot;
OrderDelivered (배송 완료됨) → &quot;주문 번호 1234, 배송 완료&quot;</li>
</ul>
<ol start="2">
<li>현재 상태 복원
사용자는 <strong>이벤트를 재생(Replay)</strong>하여 현재 주문 상태를 재구성할 수 있습니다:
주문 생성됨 → 결제 완료됨 → 배송 시작됨 → 배송 완료됨
결과: 주문 상태 = &quot;배송 완료&quot;</li>
<li>장점
이력 관리: 과거의 상태를 복원할 수 있음. (예: &quot;주문이 배송 중이던 시점은 언제였지?&quot;)
디버깅: 문제가 생긴 시점을 파악 가능. (예: &quot;결제가 실패했었다면 왜 그랬을까?&quot;)
복구: 특정 시점까지 상태를 되돌리기 쉽다.</li>
</ol>
<h2 id="2-event-driven">2. Event-Driven</h2>
<p>Event-Driven(이벤트 중심)은 이벤트를 중심으로 시스템이 동작하는 패턴으로, 이벤트가 발생했을 때 이를 감지하고 처리하는 방식입니다.</p>
<ul>
<li><p>핵심 개념:
이벤트 프로듀서가 이벤트를 발생시키고, 이벤트 컨슈머가 이를 처리.
비동기적으로 동작하며, 느슨하게 결합된 시스템 설계.</p>
</li>
<li><p>특징:
비동기성: 이벤트가 발생하면 즉시 처리되거나, 적절한 시점에 처리됨.
느슨한 결합: 컴포넌트 간 직접적인 의존성 감소.
확장성: 이벤트 기반으로 컴포넌트를 쉽게 추가 가능.</p>
</li>
<li><p>사용 예:
알림 시스템 (SMS, 이메일).
IoT 환경 (센서 데이터 처리).
메시징 플랫폼 (Kafka, RabbitMQ).</p>
</li>
</ul>
<p>이벤트 기준으로 모든 데이터의 변경을 처리, 조회
비즈니스를 이벤트를 기반으로 구현하는 방법
(이벤트 소싱 포함)</p>
<p>데이터의 변경</p>
<ul>
<li>동기</li>
<li>비동기 : membership-svc &gt; 변경 요청 이벤트 소비(consume) &gt; 도메인의 최종 버전 정보를 변경</li>
</ul>
<p>데이터의 조회
하나의 도메인(DB)를 특정할 수 있는 key(PK)를 조건으로 최종 버전 정보를 조회</p>
<p>장애나 서버, 인프라에 문제가 발생할 지라도 앱이 복구되면 메세지 브로커(이벤트)에 의존해서 일관성(트랜잭션)을 구현할 수 있다.</p>
<ul>
<li>request-response driven에서 100번의 요청을 했을 때 100번의 프로세싱을 해야하지만
event-driven의 경우 100번의 요청을 받아 1번의 insert로 자원의 효율적 사용이 가능하다.</li>
</ul>
<h2 id="3-event-driven-architecture-eda">3. Event-Driven Architecture (EDA)</h2>
<p><strong>Event-Driven Architecture (EDA)</strong>는 Event-Driven 개념을 전체 시스템 설계에 적용한 구조로, 시스템 구성 요소가 이벤트를 생성, 소비, 처리하며 상호 작용합니다.</p>
<ul>
<li><p>핵심 개념:
시스템이 이벤트 중심으로 설계됨.
프로듀서와 컨슈머가 독립적으로 동작하며, 이벤트 버스를 통해 통신.</p>
</li>
<li><p>특징:
확장성: 다양한 마이크로서비스가 이벤트 기반으로 확장 가능.
유연성: 변경 사항이 특정 컴포넌트에만 영향을 미침.
이벤트 브로커: Kafka, RabbitMQ, AWS SNS/SQS와 같은 브로커 사용.</p>
</li>
<li><p>사용 예:
마이크로서비스 아키텍처에서 서비스 간 통합.
실시간 데이터 처리 시스템.
대규모 분산 시스템.</p>
</li>
</ul>
<blockquote>
<p>전체적으로 아키텍처의 모든것을 이벤트를 기반으로 구현</p>
</blockquote>
<h3 id="단점">단점</h3>
<ol>
<li>이벤트가 한번 잘못 발행, 혹은 consume 이후 잘못된 처리를 하는 순간
모든 데이터 정합성과 트랜잭션이 크게 망가질 수 있는 상황이 생길수 있다<ul>
<li>백업, 통제 정책 등을 잘 수립해야 한다.</li>
<li>너무 risky한 비즈니스에서는 오히려 안 어울릴 수 있다 이때 솔루션으로 eventuate, axon framework가 있다.<pre><code>Event-Driven Architecture
├── Event-Driven
│     ├── Event Sourcing (이벤트를 저장하고 상태 복원)
│     └── Saga 트랜잭션 오케스트레이션 (분산 트랜잭션 관리)
└── 시스템 간 통합 (이벤트를 기반으로 서비스 연결)</code></pre></li>
</ul>
</li>
</ol>
<hr>
<h2 id="차이점">차이점</h2>
<table>
<thead>
<tr>
<th><strong>항목</strong></th>
<th><strong>Event Sourcing</strong></th>
<th><strong>Event-Driven</strong></th>
<th><strong>Event-Driven Architecture</strong></th>
</tr>
</thead>
<tbody><tr>
<td><strong>목적</strong></td>
<td>데이터 상태 변화를 이벤트로 기록</td>
<td>이벤트 기반으로 동작하는 비동기 처리</td>
<td>이벤트 기반으로 시스템 전체를 설계</td>
</tr>
<tr>
<td><strong>중점</strong></td>
<td>데이터의 상태 복원 및 이력 관리</td>
<td>이벤트 생성과 처리</td>
<td>이벤트의 생성, 소비, 전파</td>
</tr>
<tr>
<td><strong>주요 사용 기술</strong></td>
<td>Event Store (ex. Kafka, DB)</td>
<td>메시징 시스템 (ex. RabbitMQ, Kafka)</td>
<td>이벤트 브로커 (ex. Kafka, AWS SQS/SNS)</td>
</tr>
<tr>
<td><strong>적용 범위</strong></td>
<td>데이터 관리</td>
<td>특정 이벤트 기반 프로세스</td>
<td>시스템 전반의 설계</td>
</tr>
<tr>
<td><strong>예</strong></td>
<td>금융 거래, 감사 시스템</td>
<td>알림 시스템, IoT 데이터 처리</td>
<td>마이크로서비스 통합, 실시간 처리</td>
</tr>
</tbody></table>
<p>이 세 가지 개념은 서로 관련되어 있으나, 다루는 범위와 목적이 다릅니다. Event Sourcing은 데이터 관리에 집중하고, Event-Driven은 동작 방식에 초점을 맞추며, EDA는 시스템 전반의 설계를 아우릅니다.</p>
<hr>
<h2 id="기술적-동작-방법">기술적 동작 방법</h2>
<ul>
<li>이벤트 저장소:
모든 이벤트는 이벤트 저장소(Event Store)에 기록됩니다.
예: Kafka, Event Store DB, DynamoDB 등.</li>
<li>이벤트 재생:
시스템이 재시작될 때, 이벤트를 순서대로 읽고 현재 상태를 재구성합니다.</li>
<li>CQRS와의 결합:
Event Sourcing은 보통 CQRS(Command Query Responsibility Segregation)와 함께 사용됩니다.
Command: 상태를 변경하는 작업 → 이벤트를 생성하여 저장.
Query: 현재 상태를 조회 → 이벤트를 기반으로 상태를 재구성.</li>
</ul>
<hr>
<h2 id="event-sourcing과-cqrs를-조합하는-이유">Event Sourcing과 CQRS를 조합하는 이유</h2>
<p>Event Sourcing에서는 데이터의 최종상태를 바로 조회할 수 없다
따라서 데이터를 조회하려면 이벤트를 차례로 수행하여 현재 상태를 조회해야하기 때문에 조회에 비효율적이다</p>
<p>CQRS를 조합할 경우
읽기 모델과 쓰기 모델을 분리하여, 읽기 작업(Query)에서 이벤트 수행없이 최적화된 데이터를 바로 조회할 수 있다.</p>
<p>Event Sourcing으로 쓰기 작업을 처리하고, 
CQRS의 Query Model을 사용해 읽기 성능을 개선하는 방법을 사용</p>
<ul>
<li>읽기 전용 데이터베이스: Query Model은 캐시나 읽기 전용 데이터베이스로 설계 가능</li>
<li>Event Sourcing으로 이력을 관리하고, CQRS로 다양한 방식으로 이력을 조회하고 활용할 수 있다</li>
<li>CQRS는 Event Sourcing의 비동기 이벤트 처리 방식과 자연스럽게 통합</li>
</ul>
<h1 id="axon-framework">Axon Framework</h1>
<h2 id="axon-framwork">Axon Framwork</h2>
<ul>
<li>event sourcing 기반으로, 데이터를 관리할 수 있는 도구를 제공하고
이 도구를 사용하여 CQRS, DDD를 구현할 수 있도록 돕는다.</li>
<li>springboot 프로젝트에 의존성으로 사용하여 다양한 어노테이션 기반을 위 기능을 구현할 수 있다.</li>
</ul>
<h2 id="axon-server">Axon server</h2>
<ul>
<li>Axon Framework를 사용하여 배포된 어플리케이션들의 조율자 역할</li>
<li>Axon Framework를 사용하는 어플리케이션들로부터 발행된 이벤트들을 저장하는 역할</li>
<li>각 어플리케이션들의 상태 관리, imbedding된 큐잉을 내재하여 유량 조절</li>
<li>고가용성에 집중된 내부 구현이 되어있고 </li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링시큐리티] 인가 - 요청 / 메서드 기반 권한 부여 ]]></title>
            <link>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EC%9D%B8%EA%B0%80-%EC%9A%94%EC%B2%AD-%EA%B8%B0%EB%B0%98-%EA%B6%8C%ED%95%9C-%EB%B6%80%EC%97%AC</link>
            <guid>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EC%9D%B8%EA%B0%80-%EC%9A%94%EC%B2%AD-%EA%B8%B0%EB%B0%98-%EA%B6%8C%ED%95%9C-%EB%B6%80%EC%97%AC</guid>
            <pubDate>Sun, 03 Nov 2024 17:38:41 GMT</pubDate>
            <description><![CDATA[<h1 id="요청-기반-권한-부여httpsecurityauthorizehttprequests">요청 기반 권한 부여HttpSecurity.authorizeHttpRequests()</h1>
<p>📌보통 요청기반 권한 부여는 컨트롤러에서 받도록하고 메서드 기반 권한 부여는 서비스단에서 필터링하여 이중 보안으로 사용한다</p>
<p>클라이언트 요청(일반적으로 브라우저로 URL로 요청하는 것) request(경로 등)를 통해서 서버가 어떤 권한을 부여할 것인지 모델링 하는 것
HttpSecurity 인스턴스를 사용하여 권한 규칙을 선언한다.</p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/7f3f69d0-4bad-4233-a599-04787fedda15/image.png" alt=""></p>
<p>위 설정을 통해 스프링시큐리티가 서버측에 설정한 권한을 사용할 것이라 알리는 것</p>
<ul>
<li>요청, 권한 규칙 -&gt; 어떠한 요청에 대하여 , 어떠한 권한을 줄 것인가</li>
</ul>
<h1 id="authorizehttprequests-api">authorizeHttpRequests() API</h1>
<h2 id="1-requestmatchers">1. requestMatchers()</h2>
<p>: HTTP 요청의 URL 패턴, HTTP 메소드, 요청 파라미터 등을 기반으로 제어</p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/fb87ed04-a3fe-4675-830c-b216ba407201/image.png" alt=""></p>
<p> <code>서버가 보호해야할 자원</code></p>
<ul>
<li>매개변수를 통해 동일한 API만 사용을 달리할 수 있다. </li>
<li>문자열로 URL 필터를 주는 방법, 여러개의 파라미터를 지정할 수 있고 이는 서버가 보호해야할 자원이다. </li>
<li>파라미터로 requestMatcher를 전달</li>
<li>메소드와 파라미터 전달</li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/494cb795-37f6-44cc-a388-7e7b4549fe32/image.png" alt=""></p>
<p>보호된 자원을 어떤식으로 보호할 것인지 명시해야한다. </p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/8b8e39c2-8aed-459b-882f-9c67941da9ca/image.png" alt=""></p>
<p>당연한 말이지만 인가는 인증을 받은 사용자에 한해 권한을 확인하는 것.
⭐️⭐️ 스프링 시큐리티는 클라이언트 요청에 대하여 위에서부터 아래로 순차대로 처리하고 하위의 권한을 확인하지 않는다
⭐️ <code>좁은 범위의 경로를 먼저 정의</code>하고 그것보다 큰 범위의 경로를 다음으로 설정하여 걸러내야한다.</p>
<h2 id="2-securitymatcher--securitymatcherscustomizer--requestmatcherconfigurer-">2. securityMatcher() / securityMatchers(Customizer &lt; RequestMatcherConfigurer&gt; )</h2>
<p>: <code>특정 패턴</code>에 해당하는 요청에만 보안 규칙(보안 권한 부여하는 것)을 적용하도록 설정할 수 있으며 중복해서 정의할 경우 마지막에 설정한 것으로 대체한다</p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/5074bf1e-c39c-4afa-ac2e-7dfd6bf01528/image.png" alt=""></p>
<ul>
<li>패턴 설정<pre><code class="language-java">//api로 시작하는지부터 확인하는 것 
//api로 시작하는 경우에만 authorizeHttpRequests 권한 확인
http
.securityMatcher(&quot;/api/**&quot;)
.authorizeHttpRequests(
  auth -&gt; auth.requestMatchers(...)
)</code></pre>
</li>
<li>내부 흐름
<img src="https://velog.velcdn.com/images/may_yun/post/75f6a9bd-ac18-4ddd-acfd-aa653e00b203/image.png" alt=""><ul>
<li>HttpSecurity 를 /api/로 시작하는 URL에만 적용하도록 구성한다
Spring MVC가 클래스 경로에 있으면 MvcRequestMatcher 가 사용되고, 그렇지 않으면 AntPathRequestMatcher 가 사용된다</li>
</ul>
</li>
</ul>
<h3 id="securitymatcherscustomizer--requestmatcherconfigurer--">securityMatchers(Customizer &lt; RequestMatcherConfigurer &gt; )</h3>
<p>: 다중 패턴 설정
 특정 패턴에 해당하는 요청을 단일이 아닌 다중 설정으로 구성해서 보안 규칙을 적용할 수 있으며 현재의 규칙은 이전의 규칙을 대체하지 않는다</p>
<pre><code class="language-java">//패턴1
http. securityMatchers((matchers) -&gt; matchers.requestMatchers(&quot;/api/**&quot;, &quot;/oauth/**&quot;)); 
//패턴2
http. securityMatchers((matchers) -&gt; matchers.requestMatchers(&quot;/api/**&quot;).requestMatchers(&quot;/oauth/**&quot;)); 
//패턴3
http.securityMatchers((matchers) -&gt; matchers.requestMatchers(&quot;/api/**&quot;) .securityMatchers((matchers) -&gt; matchers.requestMatchers(&quot;/oauth/**&quot;));</code></pre>
<p><img src="https://velog.velcdn.com/images/may_yun/post/3acad130-9e50-40a7-b5d7-b4327429e1d0/image.png" alt=""></p>
<pre><code class="language-java">    @Bean
    public SecurityFilterChain securityFilterChain2(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorize -&gt; authorize.anyRequest().authenticated());
        return http.build();
    }

    @Bean
    @Order(1) ⭐️//좁은 범위부터 필터링 실행하여야 한다.
    public SecurityFilterChain securityFilterChain2(HttpSecurity http) throws Exception {
        http
                .securityMatchers(matchers -&gt; matchers.requestMatchers(&quot;/open-banking/oauth/&quot;))
                .authorizeHttpRequests(authorize -&gt; authorize.anyRequest().permitAll());
        return http.build();
    }</code></pre>
<hr>
<h3 id="권한-규칙-종류">권한 규칙 종류</h3>
<p><img src="https://velog.velcdn.com/images/may_yun/post/58cc92fc-d28b-445d-a9d2-458605b04a5e/image.png" alt=""></p>
<p>권한의 경우 계층적 개념을 적용할 수 있지만 (ex: admin이 manager를 포함하고 있다)
위에서 권한을 확인하는 것은 단순 문자열로 비교하는 것이다</p>
<hr>
<h1 id="표현식-및-커스텀-권한-구현">표현식 및 커스텀 권한 구현</h1>
<p><img src="https://velog.velcdn.com/images/may_yun/post/58133bfc-fcab-4858-8a9a-a4f66fa15bdb/image.png" alt=""></p>
<ul>
<li>access는 new WebExpressionAuthorizationManager() 즉, AuthorizationManager의 구현체가 반드시 들어가야 한다. </li>
<li>매개변수는 표현식 구문을 전달한다.</li>
<li>적용하기의 첫번째 reqeust는 구문 조건식에 부합하면 requestMatchers의 경로로 이동할 수 있다는 의미이다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/a022dc3d-4186-47ca-af74-2ce8abb0b4fc/image.png" alt=""></p>
<ol>
<li>access에 (권한)들어가는 AuthorizationManager의 구현체에 들어갈 빈을 생성하여 주입할 수 있다.</li>
<li>requestMatcher에 들어갈 URI를 만들 수 있다 (엔드포인트).</li>
</ol>
<p><img src="https://velog.velcdn.com/images/may_yun/post/721d0d67-3f73-402c-9402-72ea41d8443e/image.png" alt=""></p>
<hr>
<h1 id="메서드-기반-권한부여">메서드 기반 권한부여</h1>
<p>@PreAuthorize, @PostAuthorize</p>
<ul>
<li>메서드 기반 권한 부여는 서비스단에서 필터링하여 이중 보안으로 사용한다 컨트롤러에서 사용한다고 안되는것은 아니지만 요청기반은 컨트롤러 메서드 기반은 서비스에서 사용하는것이 목적에 부합하다 볼 수 있다</li>
<li>Spring Security 는 요청 수준의 권한 부여뿐만 아니라 메서드 수준에서의 권한 부여를 지원</li>
<li>메서드 수준 권한 부여를 활성화 하기 위해서는 설정 클래스에 @EnableMethodSecurity 어노테이션을 추가해야 한다</li>
<li>SpEL(Spring Expression Language) 표현식을 사용하여 다양한 보안 조건을 정의할 수 있다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/570b738c-b4b3-4783-a7a5-cab1c621df40/image.png" alt=""></p>
<h2 id="preauthorize">@PreAuthorize</h2>
<p>: <code>메소드가 실행되기 전에</code> 특정한 보안 조건이 충족되는지 확인하는 데 사용되며 보통 서비스 또는 컨트롤러 레이어의 메소드에 적용되어 해당 메소드가 호출되기 전에 사용자의 인증 정보와 권한을 검사한다</p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/8169d23c-01da-4a1e-b04c-9bba2dc86015/image.png" alt=""></p>
<h2 id="secured">@Secured</h2>
<p>: 어노테이션을 메소드에 적용하면 지정된 권한(역할)을 가진 사용자만 해당 메소드를 호출할 수 있으며 더 풍부한 형식을 지원하는 @PreAuthorize 사용을 권장한다
@Secured는 아주 단순한 권한만 지정할 수 있다</p>
<ul>
<li>어노테이션을 사용하려면 스프링 시큐리티 설정에서 @EnableMethodSecurity(securedEnabled = true) 설정을 활성화해야 한다</li>
</ul>
<p><code>본 어노테이션은 잘 사용하지 않는다</code></p>
<pre><code class="language-java">@Secured(&quot;ROLE_USER&quot;)
public void performUserOperation() {
// ROLE_USER 권한을 가진 사용자만 이 메소드를 실행할 수 있습니다.
}</code></pre>
<h2 id="postauthorize">@PostAuthorize</h2>
<p>: @PostAuthorize 어노테이션은 메소드가 실행된 후에 보안 검사를 수행하는 데 사용된다
• @PreAuthorize 와는 달리, @PostAuthorize는 메소드 실행 후 <code>결과에 대한</code> <code>보안 조건을 검사</code>하여 <code>특정 조건을 만족</code>하는 경우에<code>만</code> 사용자가 결과를 받을 수 있도록 한다</p>
<p>PostAuthorize 조건을 충족해야지만 결과를 반환한다</p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/727df146-e8fb-46d4-aae3-464fd91b491c/image.png" alt=""></p>
<h2 id="prefilter">@PreFilter</h2>
<p>: 메소드가 실행되기 전에 메소드에 전달된 <code>컬렉션 타입의 파라미터에 대한 필터링</code>을 수행하는데 사용. 사용자가 보내온 <code>컬렉션(배열, 리스트, 맵, 스트림) 내의 객체들을 특정 기준에 따라 필터링</code>하고 그 중 보안 조건을 만족하는 객체들에 대해서만 메소드가 처리하도록 할 때 사용된다</p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/96a2f241-721f-4690-936a-7d55796618bc/image.png" alt=""></p>
<h2 id="postfilter">@PostFilter</h2>
<p>: 메소드가 반환하는 컬렉션 타입의 결과에 대해 필터링을 수행하는 데 사용된다
• @PostFilter 어노테이션은 메소드가 컬렉션을 반환할 때 반환되는 각 객체가 특정 보안 조건을 충족하는지 확인하고 조건을 만족하지 않는 객체들을 결과에서 제거한다</p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/d3a779b8-316e-4c2e-80b9-964b95550350/image.png" alt=""></p>
<h2 id="jsr-250">JSR-250</h2>
<p>: JSR-250 기능을 적용하면 @RolesAllowed, @PermitAll 및 @DenyAll 어노테이션 보안 기능이 활성화 된다</p>
<ul>
<li>JSR-250 어노테이션을 사용하려면 스프링 시큐리티 설정에서 @EnableMethodSecurity(jsr250Enabled = true) 설정을 활성화해야 한다</li>
</ul>
<pre><code class="language-java">@RolesAllowed(&quot;USER&quot;) public void editDocument() {
// &#39;ROLE_USER&#39; 권한을 가진 사용자만 문서를 편집할 수 있습니다.
}

@PermitAll
public void viewDocument() { //모든사용자가문서를볼수있습니다.
}

@DenyAll
public void hiddenMethod() {
// 어떠한 사용자에게도 접근이 허용되지 않습니다.
}</code></pre>
<h2 id="메타-주석-사용">메타 주석 사용</h2>
<p>: 메서드 보안은 애플리케이션의 특정 사용을 위해 편리성과 가독성을 높일 수 있는 메타 주석을 지원한다</p>
<ul>
<li>어노테이션을 생성하여 해당 어노테이션을 사용하여 가독성을 높일 수 있다
<img src="https://velog.velcdn.com/images/may_yun/post/48f9d527-43a4-4345-927f-d72ae6570a22/image.png" alt=""></li>
</ul>
<h2 id="특정-주석-활성화">특정 주석 활성화</h2>
<p>: Method Security 의 사전 구성을 비활성화한 다음 @PostAuthorize 를 활성화한다</p>
<pre><code class="language-java">@EnableMethodSecurity(prePostEnabled = false) 
class MethodSecurityConfig {

@Bean 
@Role(BeanDefinition.ROLE_INFRASTRUCTURE) 
Advisor postAuthorize() {
    return AuthorizationManagerAfterMethodInterceptor.postAuthorize(); }

}</code></pre>
<h2 id="커스텀-빈을-사용하여-표현식-구현">커스텀 빈을 사용하여 표현식 구현</h2>
<p>: 사용자 정의 빈을 생성하고 새로운 표현식으로 사용할 메서드를 정의하고 권한 검사 로직을 구현한다</p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/cd59546b-e10c-44d0-ab98-739d05490a5b/image.png" alt=""></p>
<hr>
<h1 id="클래스-레벨-권한-부여">클래스 레벨 권한 부여</h1>
<p>: 모든 메소드는 클래스 수준의 권한 처리 동작을 상속하나 메서드에 어노테이션을 선언한 메소드는 클래스 수준의 어노테이션을 덮어쓰게 된다 (메소드 우선 적용)</p>
<ul>
<li>클래스 수준</li>
</ul>
<pre><code class="language-java">@Controller 
@PreAuthorize(&quot;hasAuthority(&#39;ROLE_USER&#39;)&quot;) 
public class MyController {

@GetMapping(&quot;/endpoint&quot;)
public String endpoint() { ... } 

}</code></pre>
<p>📌 인터페이스에도 동일한 규칙이 적용되지만 
클래스가 두 개의 다른 인터페이스로부터 동일한 메서드의 어노테이션을 상속받는 경우에는 
시작할 때 실패한다. </p>
<p>그래서 구체적인 메소드에
어노테이션을 추가함으로써 모호성을 해결할 수 있다</p>
<pre><code class="language-java">@Controller 
@PreAuthorize(&quot;hasAuthority(&#39;ROLE_USER&#39;)&quot;) 
public class MyController {

@GetMapping(&quot;/endpoint&quot;)
@PreAuthorize(&quot;hasAuthority(&#39;ROLE_ADMIN&#39;)&quot;) // 이 설정이 우선적으로 동작한다
public String endpoint() { ... }

}</code></pre>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링시큐리티] sameSite]]></title>
            <link>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-sameSite</link>
            <guid>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-sameSite</guid>
            <pubDate>Sun, 03 Nov 2024 17:24:40 GMT</pubDate>
            <description><![CDATA[<blockquote>
<p>sameSite
: 크로스 사이트 간 쿠키 전송에 대한 제어를 핸들링하는 것
쿠키 설정할 때 지정.</p>
</blockquote>
<ul>
<li>스프링시큐리티에서는 SameSite 속성에 대한 지원을 제공하지 않지만 spring Session에서는 해당 속성을 지원한다.</li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/04e2c61c-de06-4df7-916e-61b5d8d394b8/image.png" alt=""></p>
<ol>
<li>Strict : </li>
</ol>
<ul>
<li>사용자가 A서비스에 접속을하여 서비스는 사용자에게 쿠키를 발급한다</li>
<li>세션쿠키라 가정했을 때 브라우저에 저장된다</li>
<li>사용자가 A -&gt; A로 요청할 경우 자동적으로 브라우저가 전달해주나, A -&gt; B로 이동하고 다시 B -&gt; A로 다시 요청할 경우 쿠키가 전송되지 않는다. </li>
<li>가장 제한적인 방식으로 만약 사용자가 A 쇼핑몰에서 물건을 장바구니에 담고 PG사를 통해 결제를 한 경우 두 도메인이 다르기 때문에 쿠키값은 유실되고 결제 후 다시 A 쇼핑몰로 왔을 때 로그인 페이지로 다시 이동하는 현상이 발생할 수 있다.
보안에는 완벽하지만 편의성이 떨어진다.</li>
</ul>
<ol start="2">
<li>Lax : </li>
</ol>
<ul>
<li>구글 크롬 80 버전부터 기본 값 Lax </li>
<li>사용자가 읽기전용 메소드, a태그, window.location.replace, 302리다이렉트 등을 통해 이동할 경우에는 타 사이트 이동시에도 쿠키를 전송해준다. 하지만 iframe, img 를 문서에 삽입하거나 AJAX 통신 등은 쿠키가 전송되지 않는다.</li>
<li>대부분은 Lax로 기본설정이 되어 있다.</li>
</ul>
<ol start="3">
<li>none : </li>
</ol>
<ul>
<li>보안에 좋지 않다</li>
<li>모든 경우에 쿠키가 전송되는 것</li>
<li>HTTPS인 경우 SameSite none으로 설정된 쿠키는 Secure 설정이 안된경우 생성이 안된다
SameSite=None; Secure </li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/c69e5571-46f3-43f6-8eb9-567aa0c8e796/image.png" alt=""></p>
<h1 id="spring-session-samesite-사용방법">Spring Session SameSite 사용방법</h1>
<h2 id="dependency">dependency</h2>
<pre><code class="language-java">implementation group: &#39;org.springframework.session&#39;, name: &#39;spring-session-core&#39;, version: &#39;3.2.1&#39;</code></pre>
<h2 id="코드">코드</h2>
<ul>
<li>⭐️@EnableSpringHttpSession : JSESSIONID(WAS가 발급)가 아닌 SESSION 으로 발급된다.<pre><code class="language-java">@Configuration 
⭐️@EnableSpringHttpSession 
@Bean
public CookieSerializer cookieSerializer() {//HttpSession 쿠키 설정
  DefaultCookieSerializer serializer = new DefaultCookieSerializer(); 
  serializer.setUseSecureCookie(true);//쿠키를 보안쿠키로 사용하겠다
  serializer.setUseHttpOnlyCookie(true);//http통신에만 쿠키를 사용하겠다
  serializer.setSameSite(&quot;Lax&quot;);//sameSite 설정 기본값이 Lax이다.
  return serializer;
}
@Bean
public SessionRepository&lt;MapSession&gt; sessionRepository() {
    return new MapSessionRepository(new ConcurrentHashMap&lt;&gt;());//session을 저장할 저장소 
}
}</code></pre>
</li>
</ul>
<p>참고</p>
<ul>
<li><a href="https://web.dev/articles/samesite-cookies-explained?hl=ko">https://web.dev/articles/samesite-cookies-explained?hl=ko</a></li>
<li><a href="https://coding-factory.tistory.com/843">https://coding-factory.tistory.com/843</a></li>
</ul>
]]></description>
        </item>
        <item>
            <title><![CDATA[[스프링시큐리티] 사이트 간 요청 위조 | 2. CSRF (Cross Site Request Forgery) ]]></title>
            <link>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EA%B0%84-%EC%9A%94%EC%B2%AD-%EC%9C%84%EC%A1%B0-2.-CSRF-Cross-Site-Request-Forgery</link>
            <guid>https://velog.io/@may_yun/%EC%8A%A4%ED%94%84%EB%A7%81%EC%8B%9C%ED%81%90%EB%A6%AC%ED%8B%B0-%EC%82%AC%EC%9D%B4%ED%8A%B8-%EA%B0%84-%EC%9A%94%EC%B2%AD-%EC%9C%84%EC%A1%B0-2.-CSRF-Cross-Site-Request-Forgery</guid>
            <pubDate>Fri, 01 Nov 2024 16:18:09 GMT</pubDate>
            <description><![CDATA[<h1 id="csrf">CSRF</h1>
<p><img src="https://velog.velcdn.com/images/may_yun/post/29605d18-7bd0-4df0-8338-dc05f720da80/image.png" alt=""></p>
<ul>
<li>웹 사이트는 사용자가 이미 인증을 받았기 때문에 인증받은 쿠키로 접속을 하면 누가 됐든 인증 받은 상태로 인식한다. 사용자의 브라우저가 자동으로 보낼 수 있는 인증 정보 예를 들어 쿠키나 기본 인증 세션등과 함께 공격용 페이지와 함께 웹사이트로 요청이 된다 </li>
<li>위와 같은 공격을 막기위해 스프링시큐리티에서는 CSRF 기능 활성화를 하여 이를 방어한다 </li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/cf7a95c8-25e1-499b-bf4f-df1b4157d4f2/image.png" alt=""></p>
<ul>
<li>데이터를 변경할 수 있는 메서드에 대해서만 CSRF 토큰 검사를 수행한다.</li>
<li>쿠키에 토큰을 요구하는 것은 위험하다.</li>
</ul>
<h1 id="기능">기능</h1>
<ul>
<li><p>쿠키나 세션을 사용하지 않는 경우에는 CSRF 기능을 비활성화 하는 것이 효율적이다.
이유는 POST, DELETE와 같은 검사가 필요한 메서드를 요청할때마다 토큰을 보내주고 관리해야하기 때문에 비효율적일 수 있다.
<img src="https://velog.velcdn.com/images/may_yun/post/5df01f50-8672-4260-8703-8d76a52c8aff/image.png" alt=""></p>
</li>
<li><p>CSRF 기능이 활성화 되어 있을때는 스프링시큐리티에서 form hidden 타임으로 _csrf 토큰값을 보낸다 이를 잘 확인하여 요청 헤더 또는 매개변수에 넣어서 보내도록 해야한다.</p>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/647fc3f4-b898-4d84-9ed1-415e8f9548a1/image.png" alt=""></p>
<h1 id="csrf-토큰-유지">CSRF 토큰 유지</h1>
<p>스프링시큐리티는 기본적으로 CSRF 토큰이 생성되면 세션에 저장을 한다. </p>
<p>⭐️</p>
<ul>
<li>formLogin의 경우 스프링시큐리티가 input 타입 hidden으로 _csrf 토큰값을 value에 생성해서 서버에 보내준다</li>
<li>기본적으로 CSRF 기능이 켜져 있기 때문에 헤더나 파라미터로 해당 토큰값을 보내줘야한다</li>
</ul>
<h2 id="1-세션에-토큰-저장">1. 세션에 토큰 저장</h2>
<ul>
<li>CsrfTokenRespository: 인터페이스
일회성 토큰이 아닌 발급받은 토큰을 유지하여 영속화하여 사용할 수 있도록 한다. </li>
<li>세션과 토큰 구현체를 통해 토큰을 유지할 수 있다</li>
<li>토큰은 (디폴트)기본적으로 세션에 저장되도록 초기화 된다</li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/5d02b1f5-548c-489f-a0c3-b7922f853e27/image.png" alt=""></p>
<p>헤더와 매개변수로 왔을 때 아래와 같은 이름으로 토큰을 읽는다</p>
<ul>
<li>HTTP 요청 헤더 일 때: X-CSRF-TOKEN </li>
<li>요청 매개변수 일 때 : _csrf </li>
</ul>
<h2 id="2-쿠키에-토큰-저장">2. 쿠키에 토큰 저장</h2>
<p>쿠키는 response에 담아서 브라우저에 저장이 된다
일반 스크립트에서는 읽을 수 없고 http 통신에서만 읽을 수 있다</p>
<p>쿠키는에 토큰을 저장하는 것은 설정하는 방식이 두가지가 있다
둘 중 한가지만 선택해야한다
<img src="https://velog.velcdn.com/images/may_yun/post/afd45c31-bc83-418a-833b-dda6a453a66a/image.png" alt=""></p>
<pre><code class="language-java"> 쿠키로 저장해서 쿠키로 읽는 것
 보안을 위해서 스크립트로 읽을 수 없도록 되어있다. http 통신으로만 읽을 수 있다. (선호)
방법1. http.csrf(csrf -&gt; csrf.csrfTokenRepository(repository));

 쿠키를 클라이언트(프론트엔드) 자바스트립트로 읽을 수 있도록 설정. 
방법2. http.csrf(csrf -&gt; csrf.csrfTokenRepository(CookieCsrfTokenRepository.withHttpOnlyFalse()));</code></pre>
<ul>
<li>쿠키명 : XSRF-TOKEN</li>
<li>세션과의 차이 : 쿠키명이 다르다</li>
</ul>
<hr>
<h1 id="csrf-토큰-처리">CSRF 토큰 처리</h1>
<ul>
<li>📌CsrfTokenRequestHandler : 토큰의 유효성을 검사하는 클래스
<img src="https://velog.velcdn.com/images/may_yun/post/a3d6b441-df15-45fd-a41a-c3509c7c45d2/image.png" alt=""></li>
</ul>
<ul>
<li>설정해주지 않아도 스프링시큐리티가 자동으로 디폴트값으로 초기화 된다. 위에서는 설정하는 방법</li>
<li>클라이언트 요청마다 UUID 값으로 생성된 CSRF 토큰값을 난수로 인코팅하는 클래스가 XorCsrfRequestAtttributeHandler(구현체) 이다.
서버에서 받은 토큰값은 인코딩된 토큰 값이고 원본 토큰 값을 얻기 위해 디코딩한다. 즉 인코딩/디코딩 기능을 가지고 있는 것이고 CsrfTokenRequestHandler는 원본 그대로를 비교한다.</li>
<li>인코딩된 토큰값을 클라이언트에게 보내는 것 (HTML hidden 타입)</li>
<li>토큰 UUID는 기본적으로 세션에 저장된다. 서버에서 받은 토큰값은 인코딩된 값이기 때문에 이를 디코딩하여 원본 토큰 값과 비교를 한다. 이렇게 유효성을 검사하는 것</li>
<li>클라이언트가 보는 값 -&gt; 인코딩 된 토큰값 (actualToken) / 서버에서 만든 원본 토큰값 csrfToken 따라서 인코딩된 값을 디코딩해주는 작업을 한다 CsrfFilter.class</li>
</ul>
<h1 id="csrf-토큰-지연-로딩">CSRF 토큰 지연 로딩</h1>
<p>: 토큰을 매요청마다 가져오는 것이 아니라 세션으로부터 CSRF 토큰이 꼭 필요할때 호출하겠다는 것 </p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/2eed40fe-1e93-4d5f-b50b-eaf3d61530e8/image.png" alt=""></p>
<ul>
<li>null로 보내는 경우 모든 지연로딩이 실행되어 값이 반환된다. 따라서 값을 바로 가져오는 것. (성능상 권장하지 않음)</li>
<li>SecurityContext가 서플라이어로 감싸서 사용되는 것 처럼 많은 부분이 지연로딩으로 변경되었다.</li>
</ul>
<hr>
<h1 id="csrf-토큰을-springmvc에서-사용하는-방법">CSRF 토큰을 springMVC에서 사용하는 방법</h1>
<p>기본적으로 세션에서 데이터가 변경되지 않는 메소드 요청을 하는 경우에는 csrf 토큰이 생성되지 않는다.
POST, PUT, DELETE등의 데이터 변경을 요청하는 메소드의 경우에 토큰이 생성되는데
이때도 메소드 검증을 하기전에 csrf를 준비하는 단계에서 request 요청에 csrf토큰으로 지연 로딩으로 저장이된다.
이를 컨트롤러에서 호출하여 사용하는 것 </p>
<pre><code class="language-java">CsrfToken token = HttpServletRequest.getAttribute(CsrfToken.class.getName());
token.getToken();

or

HttpServletRequest.getAttribute(&quot;_csrf&quot;);</code></pre>
<p>springboot에서 CsrfToken을 매개변수로 받아서 바로 사용할 수 있다. 
내부적으로는 위의 코드와 비슷한 맥락으로 실행</p>
<hr>
<h1 id="csrf-통합">CSRF 통합</h1>
<p><img src="https://velog.velcdn.com/images/may_yun/post/761290f7-3cf0-4210-badb-d2df28d20b0e/image.png" alt=""></p>
<h2 id="1-html-forms">1. HTML Forms</h2>
<ul>
<li>csrf token 값을 hidden 값으로 포함해야한다.
위 코드를 작성하지 않아도 자동으로 포함하는 뷰가 있다.<ul>
<li>thymeleaf (최근 뷰를 사용한다면 많이 사용하는 방법)</li>
<li>spring 폼 태그 라이브러리</li>
</ul>
</li>
</ul>
<p><img src="https://velog.velcdn.com/images/may_yun/post/c20111d6-f396-49c9-a720-1dafe4b33217/image.png" alt=""></p>
<p><img src="https://velog.velcdn.com/images/may_yun/post/6f13a6fc-dd15-41af-9f89-6fa6da7f649b/image.png" alt=""></p>
]]></description>
        </item>
    </channel>
</rss>