Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 0 additions & 3 deletions Chanyeol/.env

This file was deleted.

4 changes: 4 additions & 0 deletions Chanyeol/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,10 @@ dependencies {
// Swagger
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-ui:3.0.1'
implementation 'org.springdoc:springdoc-openapi-starter-webmvc-api:3.0.1'

// Security
implementation 'org.springframework.boot:spring-boot-starter-security'
testImplementation 'org.springframework.security:spring-security-test'
}

tasks.named('test') {
Expand Down
63 changes: 0 additions & 63 deletions Chanyeol/keyword-summary/ch07.md

This file was deleted.

66 changes: 66 additions & 0 deletions Chanyeol/keyword-summary/ch08.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,66 @@
- Spring Security가 무엇인가?

## 스프링 기반의 애플리케이션의 보안을 담당하는 프레임워크

Spring Security는 필터 기반으로 동작한다.

❗️필터: 요청이 Dispatcher Servlet으로 가기 전에 위치해있으며, 클라이언트와 자원 사이에서 요청과 응답 정보를 이용해 다양한 처리를 하는 컴포넌트

### 동작흐름

HTTP 요청
Filter Chain (여러 보안 필터 순차 실행)
인증 처리 (AuthenticationManager)
인가 처리 (AccessDecisionManager)
컨트롤러 도달 (or 403/401 반환)

Spring Security는 `SecurityFilterChain`을 통해 요청마다 보안 처리를 수행한다.

![SecurityFilterChain](attachment:f16ac6dc-fafe-4c80-b041-8b6b695c9047:스크린샷_2026-05-18_오후_1.41.47.png)

SecurityFilterChain

![Spring Security 인증 처리 과정](attachment:27a6f073-a478-47f1-8f2c-50703ef6fd13:스크린샷_2026-05-18_오후_1.43.36.png)

Spring Security 인증 처리 과정

### 주요 특징

| 특징 | 설명 |
| --- | --- |
| **다양한 인증 방식** | Form 로그인, HTTP Basic, JWT, OAuth2/OIDC, SAML 등 |
| **CSRF 보호** | Cross-Site Request Forgery 공격 방어 |
| **세션 관리** | 세션 고정 공격 방지, 동시 세션 제어 |
| **비밀번호 암호화** | BCrypt, Argon2 등 강력한 해시 알고리즘 |
| **Spring 통합** | Spring MVC, Spring Boot와 완벽하게 통합 |

### 자주 사용되는 시나리오

- **JWT 기반 인증** — REST API + 모바일/SPA 앱
- **OAuth2 소셜 로그인** — 카카오, 구글, 네이버 로그인
- **세션 기반 인증** — 전통적인 MVC 웹 앱
- **메서드 레벨 보안** — `@PreAuthorize`, `@Secured` 어노테이션
- 인증(Authentication)vs 인가(Authorization)

![스크린샷 2026-05-18 오후 1.55.14.png](attachment:bb447443-1268-43fd-8edb-041dc7f1dfe0:스크린샷_2026-05-18_오후_1.55.14.png)

- 인증(Authentication) : 본인이누구인지확인 (로그인)
- 승인(Authorization) : 특정 리소스에권한이 있는지확인 (등급 권한)
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Authorization은 일반적으로 ‘승인’보다 ‘인가’로 번역하는 것이 적절합니다. 인증은 사용자가 누구인지 확인하는 과정이고, 인가는 인증된 사용자가 특정 리소스나 행위를 수행할 권한이 있는지 확인하는 과정으로 구분해 정리하는 것을 권장합니다.


| | 인증(Authentication) | 인가(Authorization) |
| --- | --- | --- |
| 기능 | 자격 증명 확인 | 권한 허기/거부 |
| 진행 방식 | 비밀번호, 생체인식, 일회용 핀 또는 앱 | 보안 팀에서 관리하는 설정 사용 |
| 사용자 확인 가능 여부 | 가능 | 불가능 |
| 사용자 변경 가능 여부 | 부분적 가능 | 불가능 |
| 데이터 전송 | ID 토큰 사용 | 액세스 토큰 사용 |
- Stateful vs Stateless
- Stateful: ****서버가 클라이언트 상태를 **기억**
- Stateless**:** 서버가 클라이언트 상태를 **기억하지 않음**

Stateful → 서버가 기억 → 세션/쿠키 → 전통적 웹
Stateless → 서버가 무상태 → JWT 토큰 → REST API / 모바일
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Original file line number Diff line number Diff line change
Expand Up @@ -5,8 +5,10 @@
import com.example.umc10thweek4.domain.ask.exception.code.AskSuccessCode;
import com.example.umc10thweek4.domain.ask.service.AskService;
import com.example.umc10thweek4.global.apiPayload.ApiResponse;
import com.example.umc10thweek4.global.security.util.SecurityUtil;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

import java.util.List;
Expand All @@ -22,28 +24,28 @@ public class AskController {
* 문의 등록
*/
@PostMapping("/v1/asks")
public ApiResponse<AskResDTO.Create> createAsk(@RequestBody @Valid AskReqDTO.Create request) {
Long currentMemberId = 1L; // TODO: SecurityContext에서 가져오기
public ResponseEntity<ApiResponse<AskResDTO.Create>> createAsk(@RequestBody @Valid AskReqDTO.Create request) {
Long currentMemberId = SecurityUtil.getCurrentMemberId();

AskResDTO.Create response = askService.createAsk(currentMemberId, request);
return ApiResponse.onSuccess(AskSuccessCode.OK, response);
return ApiResponse.onSuccessResponse(AskSuccessCode.CREATE_SUCCESS, response);
}

/**
* 내가 작성한 문의 목록
*/
@GetMapping("/v1/users/{userId}/asks")
public ApiResponse<List<AskResDTO.GetList>> getMyAsks(@PathVariable Long userId) {
public ResponseEntity<ApiResponse<List<AskResDTO.GetList>>> getMyAsks(@PathVariable Long userId) {
List<AskResDTO.GetList> response = askService.getMyAsks(userId);
return ApiResponse.onSuccess(AskSuccessCode.OK, response);
return ApiResponse.onSuccessResponse(AskSuccessCode.LIST_SUCCESS, response);
}

/**
* 문의 상세 조회
*/
@GetMapping("/v1/asks/{askId}")
public ApiResponse<AskResDTO.GetDetail> getAskDetail(@PathVariable Long askId) {
public ResponseEntity<ApiResponse<AskResDTO.GetDetail>> getAskDetail(@PathVariable Long askId) {
AskResDTO.GetDetail response = askService.getAskDetail(askId);
return ApiResponse.onSuccess(AskSuccessCode.OK, response);
return ApiResponse.onSuccessResponse(AskSuccessCode.DETAIL_SUCCESS, response);
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -11,10 +11,10 @@ public enum AskSuccessCode implements BaseSuccessCode {

OK(HttpStatus.OK, "ASK200_1", "요청 성공"),
CREATE_SUCCESS(HttpStatus.CREATED, "ASK201_1", "문의 등록 성공"),
LIST_SUCCESS(HttpStatus.OK, "ASK202_1", "문의 목록 조회 성공"),
DETAIL_SUCCESS(HttpStatus.OK, "ASK203_1", "문의 상세 조회 성공");
LIST_SUCCESS(HttpStatus.OK, "ASK200_2", "문의 목록 조회 성공"),
DETAIL_SUCCESS(HttpStatus.OK, "ASK200_3", "문의 상세 조회 성공");

private final HttpStatus status;
private final String code;
private final String message;
}
}
Original file line number Diff line number Diff line change
@@ -1,12 +1,11 @@
package com.example.umc10thweek4.domain.member.controller;

import com.example.umc10thweek4.domain.member.dto.MemberReqDTO;
import com.example.umc10thweek4.domain.member.dto.MemberResDTO;
import com.example.umc10thweek4.domain.member.exception.code.MemberSuccessCode;
import com.example.umc10thweek4.domain.member.service.MemberService;
import com.example.umc10thweek4.global.apiPayload.ApiResponse;
import jakarta.validation.Valid;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;

@RestController
Expand All @@ -16,13 +15,8 @@ public class MemberController {

private final MemberService memberService;

@PostMapping("/v1/users")
public ApiResponse<MemberResDTO.SignUp> signUp(@RequestBody @Valid MemberReqDTO.SignUp request) {
return ApiResponse.onSuccess(MemberSuccessCode.SIGN_UP_SUCCESS, memberService.signUp(request));
}

@GetMapping("/v1/users/{userId}")
public ApiResponse<MemberResDTO.GetInfo> getMyPage(@PathVariable Long userId) {
return ApiResponse.onSuccess(MemberSuccessCode.MY_PAGE_SUCCESS, memberService.getMyPage(userId));
public ResponseEntity<ApiResponse<MemberResDTO.GetInfo>> getMyPage(@PathVariable Long userId) {
return ApiResponse.onSuccessResponse(MemberSuccessCode.MY_PAGE_SUCCESS, memberService.getMyPage(userId));
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -34,18 +34,20 @@ public class Member extends BaseEntity {
@Column(nullable = false)
private Gender gender;

@Column(nullable = false)
@Column(nullable = false, unique = true)
private String email;

@Column(name = "phone_number", nullable = false)
@Column(name = "phone_number")
private String phoneNumber;

@Column(nullable = false)
private String password;

@Builder.Default
@Column(name = "total_points")
private Long totalPoints = 0L;

@Builder.Default
@Column(name = "complete_num")
private Integer completeNum = 0;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,6 +9,8 @@
@RequiredArgsConstructor
public enum MemberErrorCode implements BaseErrorCode {

INVALID_BIRTHDAY(HttpStatus.BAD_REQUEST, "MEMBER400_1", "생년월일은 yyyy-MM-dd 형식이어야 합니다."),
INVALID_GENDER(HttpStatus.BAD_REQUEST, "MEMBER400_2", "성별은 MALE, FEMALE, NONE 중 하나여야 합니다."),
DUPLICATE_EMAIL(HttpStatus.CONFLICT, "MEMBER409_1", "이미 사용 중인 이메일입니다."),
DUPLICATE_NICKNAME(HttpStatus.CONFLICT, "MEMBER409_2", "이미 사용 중인 닉네임입니다."),
MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "존재하지 않는 회원입니다."),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,9 +11,9 @@ public enum MemberSuccessCode implements BaseSuccessCode {

OK(HttpStatus.OK, "MEMBER200_1", "요청 성공"),
SIGN_UP_SUCCESS(HttpStatus.CREATED, "MEMBER201_1", "회원가입 성공"),
MY_PAGE_SUCCESS(HttpStatus.OK, "MEMBER202_1", "마이페이지 조회 성공");
MY_PAGE_SUCCESS(HttpStatus.OK, "MEMBER200_2", "마이페이지 조회 성공");

private final HttpStatus status;
private final String code;
private final String message;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,13 @@
import com.example.umc10thweek4.domain.member.repository.MemberNoticeSettingRepository;
import com.example.umc10thweek4.domain.member.repository.MemberRepository;
import lombok.RequiredArgsConstructor;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.List;

@Service
Expand All @@ -28,6 +30,7 @@ public class MemberService {
private final MemberRepository memberRepository;
private final MemberNoticeSettingRepository noticeSettingRepository;
private final MemberFoodPreferenceRepository foodPreferenceRepository;
private final PasswordEncoder passwordEncoder;

/**
* 회원가입
Expand All @@ -43,15 +46,17 @@ public MemberResDTO.SignUp signUp(MemberReqDTO.SignUp request) {
throw new MemberException(MemberErrorCode.DUPLICATE_NICKNAME);
}

LocalDate birthday = parseBirthday(request.birthday());
Gender gender = parseGender(request.gender());

Member member = memberRepository.save(
Member.builder()
.name(request.name())
.nickname(request.nickname())
.email(request.email())
.password(request.password()) // TODO: 비밀번호 암호화
.birth(LocalDate.parse(request.birthday(),
DateTimeFormatter.ofPattern("yyyy-MM-dd")))
.gender(Gender.valueOf(request.gender().toUpperCase()))
.password(passwordEncoder.encode(request.password()))
.birth(birthday)
.gender(gender)
.phoneNumber(request.phoneNum())
.build()
);
Expand Down Expand Up @@ -80,6 +85,22 @@ public MemberResDTO.SignUp signUp(MemberReqDTO.SignUp request) {
return MemberConverter.toSignUpRes(member);
}

private LocalDate parseBirthday(String birthday) {
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

생년월일·성별 형식 검증을 Service에서 보완한 점은 좋습니다. 다음 단계에서는 @Pattern, enum 전용 요청 타입, 커스텀 Validator 중 어떤 방식이 Controller 진입 전 입력 계약을 더 명확히 표현하는지 비교해 보는 것을 권장합니다.

try {
return LocalDate.parse(birthday, DateTimeFormatter.ofPattern("yyyy-MM-dd"));
} catch (DateTimeParseException e) {
throw new MemberException(MemberErrorCode.INVALID_BIRTHDAY);
}
}

private Gender parseGender(String gender) {
try {
return Gender.valueOf(gender.toUpperCase());
} catch (IllegalArgumentException e) {
throw new MemberException(MemberErrorCode.INVALID_GENDER);
}
}

/**
* 마이페이지 조회
*/
Expand All @@ -93,4 +114,4 @@ public MemberResDTO.GetInfo getMyPage(Long userId) {

return MemberConverter.toGetInfoRes(member, ns);
}
}
}
Loading