Skip to content
Open
Show file tree
Hide file tree
Changes from 34 commits
Commits
Show all changes
35 commits
Select commit Hold shift + click to select a range
7b6dd04
refactor: 미션 생성시 status READY상태
cha-hyunwoo May 13, 2026
591549d
feat: ResponseEntity로 HTTP status 통일
cha-hyunwoo May 15, 2026
90074ba
fix: MissionSuccessCode CREATED 상태코드 201로 수정
cha-hyunwoo May 15, 2026
eae3b62
feat: Spring Security 의존성 추가
cha-hyunwoo May 18, 2026
bf625a0
feat: password 컬럼 추가
cha-hyunwoo May 18, 2026
d1fdf40
feat: findByEmail메서드 추가
cha-hyunwoo May 18, 2026
f4e8c49
feat: Spring Security 보안 설정 추가
cha-hyunwoo May 18, 2026
34b76bb
feat: 사용자 상세 정보 로드 서비스 추가
cha-hyunwoo May 18, 2026
be1f287
feat: Spring Security 사용자 인증 정보 클래스 추가
cha-hyunwoo May 18, 2026
89f89d7
feat: 권한이 없는 사용자가 접근할 때 JSON으로 응답해주는 핸들러 작성
cha-hyunwoo May 18, 2026
272e7a1
feat: 로그인 안했을떄 사용자가 접근할 때 JSON으로 응답해주는 핸들러 작성
cha-hyunwoo May 18, 2026
462e5ac
feat: 예외상황 핸들러 추가
cha-hyunwoo May 18, 2026
5941632
refactor: 전역 MemberSuccessCode를 Member 도메인으로 통합
cha-hyunwoo May 18, 2026
23e31fe
chore: StoreController 미사용 import 제거
cha-hyunwoo May 18, 2026
72b1311
refactor: 인증 관련 엔드포인트 수정
cha-hyunwoo May 18, 2026
1d669cf
chore: ReviewController 미사용 import 제거
cha-hyunwoo May 18, 2026
b36f639
chore: MissionController 미사용 import 제거
cha-hyunwoo May 18, 2026
2a4f073
feat: 회원가입 메서드 signUp 작성
cha-hyunwoo May 18, 2026
b115f9b
feat: 회원가입 SignUpReqDTO, SignUpResDTO 작성
cha-hyunwoo May 18, 2026
23936bd
feat: 회원가입 signUp메서드 작성
cha-hyunwoo May 18, 2026
d35a759
feat: 회원가입 toMember, toSignUp 컨버터 메서드 작성
cha-hyunwoo May 18, 2026
fa60ede
docs: 8주차 핵심키워드 작성
cha-hyunwoo May 18, 2026
e114058
feat: getUserEmail() 추가
cha-hyunwoo May 25, 2026
5f3909f
refactor: 매개변수명 username->email 변경
cha-hyunwoo May 25, 2026
d788e2b
feat: JWT authentication filter 추가
cha-hyunwoo May 25, 2026
0bd8310
feat: JWT 토큰 생성 및 검증 유틸리티 추가
cha-hyunwoo May 25, 2026
db73402
refactor: 폼 로그인 -> jwt로 변경
cha-hyunwoo May 25, 2026
cf7a1ab
feat: 로그인 api 작성
cha-hyunwoo May 25, 2026
03dbccc
feat: jwt 설정
cha-hyunwoo May 25, 2026
4af0329
feat: 커스텀 필터 생성
cha-hyunwoo May 25, 2026
fcfbf0c
feat: 커스텀 필터 생성
cha-hyunwoo May 25, 2026
5592953
feat: 마이페이지 API 개선
cha-hyunwoo May 25, 2026
50c9b0d
feat: OAuth 설정 추가
cha-hyunwoo May 25, 2026
48a7b66
docs: ch09 핵심 키워드 정리
cha-hyunwoo May 25, 2026
7f558a2
Chore: resolve merge conflicts with Hyeonu
cha-hyunwoo May 25, 2026
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
13 changes: 13 additions & 0 deletions Hyeonu/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,19 @@ 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'

// Jwt
implementation 'io.jsonwebtoken:jjwt-api:0.12.3'
implementation 'io.jsonwebtoken:jjwt-impl:0.12.3'
implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3'
implementation 'org.springframework.boot:spring-boot-configuration-processor'

// OAuth
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}

tasks.named('test') {
Expand Down
115 changes: 115 additions & 0 deletions Hyeonu/keyword_summary/ch08.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,115 @@
- Spring Security가 무엇인가?

자바 기반 웹 애플리케이션의 보안을 담당하는 프레임워크이다.

개발자가 보안 로직을 직접 구현하지않고 몇가지 설정만으로 아래 3가지를 처리할 수 있음

- 인증(Authentication)- 이 사용자가 누구인지 식별(로그인)
- 인가(Authorization)- 이 사용자가 해당 리소스에 접근할 권한이 있는지 식별(권한 확인)
- 보안 위협 방어- CSRF, XSS 등 각종 공격으로부터 보호

Filter Chain을 통해 HTTP요청을 순차적으로 필터링하며, SecurityConfig를 통해 커스텀 보안 설정을 적용할 수 있다.



- 인증(Authentication)vs 인가(Authorization)



**인증(Authentication)**

목적

사용자 신원 확인

실패 시

401 Unauthorized

예시

로그인, 회원가입

Spring Security

`AuthenticationEntryPoint`



**인가(Authorization)**

목적

리소스 접근 권한 확인

실패 시

403 Forbidden

예시

관리자 페이지 접근

Spring Security

`AccessDeniedHandler`

인증 먼저 그 다음 인가

- Stateful vs Stateless



**Stateful**

상태 저장

서버가 저장

방식

세션

동작

로그인 시 서버에 세션 저장 → 요청마다 세션 확인

장점

구현 간단
즉시 로그아웃 가능

단점

서버 부하
확장성 낮음

예시

폼 로그인



**Stateless**

상태 저장

서버가 저장 안함

방식

JWT 토큰

동작

토큰에 사용자 정보 담아서 → 요청마다 토큰 검증

장점

토큰 탈취 시 만료 전까지 막기 어려움

예시

JWT

이번 주차는 Stateful 방식
58 changes: 58 additions & 0 deletions Hyeonu/keyword_summary/ch09.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,58 @@
- 세션과 토큰의 차이는?

| 구분 | 세션 기반 인증 | 토큰 기반 인증 |
| --- | --- | --- |
| 인증 정보 저장 위치 | 서버 |클라이언트(브라우저/앱) |
| 상태 유지 | Stateful(상태 유지) | Stateless(상태 없음) |
| 인증 방식 | 서버가 세션 ID 기억 | 토큰 자체로 사용자 인증 |
| 저장 형태 | 쿠키(JSESSIONID) | JWT 등 토큰 |
| 확장성 | 서버 늘어나면 관리 복잡 | 서버 확장 쉬움 |
| 보안 | 상대적으로 안전 | 토큰 탈취 시 위험 |

**세션은 서버가 로그인 상태를 기억하고, 토큰은 토큰 자체가 인증 정보를 가진다.**


- 엑세스 토큰과 리프레시 토큰이란?

**Access Token**

실제 인증에 사용하는 메인 토큰

- API 요청 시 사용
- Authorization 헤더에 담아 전송
- 유효기간이 짧음

**Refresh Token**

- 로그인 유지 목적
- Access Token 만료 시 새 토큰 발급
- 유효기간이 김 (2주~ 1개월)

흐름

- 로그인 성공
- Access + Refresh Token 발급
- Access Token 만료
- Refresh Token으로 재발급
- 재로그인 없이 계속 사용


- OAuth 1.0과 OAuth 2.0의 차이는?

**OAuth**

비밀번호를 직접 공유하지 않고 제 3자 서비스가 권한을 위임받는 인증 방식

예) 카카오, 구글, 네이버 로그인

| 구분 | OAuth 1.0 | OAuth 2.0 |
| --- | --- | --- |
| 보안 방식 | 복잡한 서명(Signature) | HTTPS 기반 |
| 구현 난이도 | 어려움 | 쉬움 |
| 성능 | 느림 | 빠름 |
| 사용성 | 낮음 | 높음 |
| 현재 사용 | 거의 안 씀 | 대부분 사용 |

OAuth 1.0은 서명 기반으로 복잡하고, OAuth 2.0은 토큰 기반으로 단순하고 확장성이 좋다.
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.

OAuth 2.0을 Access Token 기반이라고만 정리하면 핵심 흐름이 다소 단순화됩니다. Authorization Code Grant 기준으로 client, resource owner, authorization server, resource server의 역할과 redirect URI, scope가 왜 필요한지까지 함께 정리하는 것을 권장합니다.


OAuth 2.0은 OAuth 1.0의 복잡한 서명 방식을 제거하고 Access Token 기반 인증을 도입하여 구현이 단순해지고 확장성이 향상된 방식이다.
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,11 @@
import com.example.umc10th.domain.member.dto.MemberResDTO;
import com.example.umc10th.domain.member.service.MemberService;
import com.example.umc10th.global.apiPayload.ApiResponse;
import com.example.umc10th.global.apiPayload.code.BaseSuccessCode;
import com.example.umc10th.global.apiPayload.code.MemberSuccessCode;
import com.example.umc10th.domain.member.exception.code.MemberSuccessCode;
import com.example.umc10th.global.security.entity.AuthMember;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
Expand All @@ -21,12 +23,34 @@ public class MemberController {

// 마이페이지
@PostMapping("/v1/members/me")
public ApiResponse<MemberResDTO.GetInfo> getInfo(
public ResponseEntity<ApiResponse<MemberResDTO.GetInfo>> getInfo(
// 받은 JSON 데이터를 자바 객체(dto)로 변환해서 씀
@RequestBody MemberReqDTO.GetInfo dto
// @RequestBody MemberReqDTO.GetInfo dto
// 헤더에 담긴 토큰을 가지고 사용자 정보 리턴
@AuthenticationPrincipal AuthMember member
){
BaseSuccessCode code= MemberSuccessCode.OK;
return ApiResponse.onSuccess(code,memberService.getInfo(dto));
return ResponseEntity
.status(MemberSuccessCode.OK.getStatus())
.body(ApiResponse.onSuccess(MemberSuccessCode.OK,memberService.getInfo(member)));
}

// 회원가입
@PostMapping("/auth/sign-up")
public ResponseEntity<ApiResponse<MemberResDTO.SignUpResDTO>> signUp(
@RequestBody MemberReqDTO.SignUpReqDTO dto
){
return ResponseEntity
.status(MemberSuccessCode.SIGN_UP.getStatus())
.body(ApiResponse.onSuccess(MemberSuccessCode.SIGN_UP,memberService.signUp(dto)));
}

// 로그인
@PostMapping("/auth/login")
public ResponseEntity<ApiResponse<MemberResDTO.LoginResDTO>>login(
@RequestBody MemberReqDTO.LoginReqDTO dto
){
return ResponseEntity
.status(MemberSuccessCode.LOGIN.getStatus())
.body(ApiResponse.onSuccess(MemberSuccessCode.LOGIN,memberService.login(dto)));
}
}
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package com.example.umc10th.domain.member.converter;

import com.example.umc10th.domain.member.dto.MemberReqDTO;
import com.example.umc10th.domain.member.dto.MemberResDTO;
import com.example.umc10th.domain.member.entity.Member;
import com.example.umc10th.global.security.dto.OAuthDTO;

public class MemberConverter {
public static MemberResDTO.GetInfo toGetInfo(Member member) {
Expand All @@ -17,4 +19,38 @@ public static MemberResDTO.GetInfo toGetInfo(Member member) {
.status(member.getStatus())
.build();
}

// DTO -> Entity 변환
public static Member toMember(MemberReqDTO.SignUpReqDTO dto, String encodedPassword) {
return Member.builder()
.name(dto.name())
.gender(dto.gender())
.birth(dto.birth())
.address(dto.address())
.detailAddress(dto.detailAddress())
.phoneNumber(dto.phoneNumber())
.email(dto.email())
.password(encodedPassword) // BCrypt 암호화된 비밀번호
.build();
}

public static MemberResDTO.SignUpResDTO toSignUpResDTO(Member member) {
return new MemberResDTO.SignUpResDTO(member.getId());
}

// 로그인
public static MemberResDTO.LoginResDTO toLoginResDTO(String token){
return MemberResDTO.LoginResDTO.builder()
.accessToken(token)
.build();
}

public static Member toMember(OAuthDTO dto) {
return Member.builder()
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.

OAuth 회원을 Member로 변환할 때 일반 회원가입에서 필요한 birth, address, detailAddress, phoneNumber 값이 채워지지 않습니다. 현재 엔티티에서는 해당 컬럼이 nullable=false이므로 소셜 로그인 저장 시 DB 제약 조건과 충돌할 수 있습니다. 일반 회원과 소셜 회원의 필수 정보 정책을 먼저 나누고, 엔티티 제약 조건이나 추가 정보 입력 흐름을 그 정책에 맞추는 것을 권장합니다.

.email(dto.getSocialEmail())
.name(dto.getName())
.socialType(dto.getSocialType())
.socialUid(dto.getSocialUid())
Comment on lines +49 to +53
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

P1 Badge 소셜 회원 생성 시 필수 컬럼을 채워 저장하라

문제는 MemberConverter.toMember(OAuthDTO)email/name/social만 채워서 Member를 저장한다는 점입니다. 현재 Memberbirth/address/detailAddress/phoneNumbernullable=false라서 소셜 신규 사용자의 첫 로그인 시 INSERT가 제약조건 위반으로 실패하고 인증 흐름이 끊깁니다. 소셜 가입 전용 플로우를 분리해 필수 프로필을 추가로 입력받거나, 소셜 모델에 맞게 엔티티/스키마 제약을 재설계해 저장 시점에 필수값이 항상 보장되도록 바꾸는 것이 좋습니다. 다음으로 엔티티 제약(nullable)과 가입 플로우 분리 개념을 학습해 보세요.

Useful? React with 👍 / 👎.

.build();
}
}
Original file line number Diff line number Diff line change
@@ -1,9 +1,32 @@
package com.example.umc10th.domain.member.dto;

import com.example.umc10th.domain.member.enums.Address;
import com.example.umc10th.domain.member.enums.Gender;

import java.time.LocalDate;

public class MemberReqDTO {

// 마이페이지
public record GetInfo(
Long id
){}

// 회원가입
public record SignUpReqDTO(
String name,
Gender gender,
LocalDate birth,
Address address,
String detailAddress,
String phoneNumber,
String email,
String password
){}

// 로그인
public record LoginReqDTO(
String email,
String password
){}
Copy link
Copy Markdown

Choose a reason for hiding this comment

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

email 필드의 검증 조건이 없다면, swagger에서 확인했을 때 예시 값이 'string'으로 보여 email 형식으로 로그인을 강제하고 있는지 판단하기 어렵습니다. Reqeust DTO는 항상 검증 조건을 체크해야 합니다.

}
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package com.example.umc10th.domain.member.dto;

import com.example.umc10th.domain.member.service.MemberService;
import lombok.Builder;

import java.time.LocalDate;
Expand All @@ -18,4 +19,17 @@ public record GetInfo(
Integer point,
Enum status
){}

// 회원가입
@Builder
public record SignUpResDTO(
Long id
){}

// 로그인

@Builder
public record LoginResDTO(
String accessToken
){}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.example.umc10th.domain.member.enums.Address;
import com.example.umc10th.domain.member.enums.Gender;
import com.example.umc10th.domain.member.enums.MemberStatus;
import com.example.umc10th.domain.member.enums.SocialType;
import jakarta.persistence.*;
import lombok.AllArgsConstructor;
import lombok.Builder;
Expand All @@ -27,7 +28,7 @@ public class Member extends BaseEntity {
@Column(name="name", nullable=false)
private String name;

@Column(name="gender", nullable=false)
@Column(name="gender")
@Enumerated(EnumType.STRING)
private Gender gender;

Expand All @@ -47,6 +48,9 @@ public class Member extends BaseEntity {
@Column(name="email", nullable=false)
private String email;

@Column(name="password")
private String password;

@Column(name="point", nullable=false)
@Builder.Default
private int point=0;
Expand All @@ -56,4 +60,11 @@ public class Member extends BaseEntity {
@Builder.Default
private MemberStatus status=MemberStatus.ACTIVE;

@Column(name="social_type")
@Enumerated(EnumType.STRING)
private SocialType socialType;

@Column(name="social_uid")
private String socialUid;

}
Loading