Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
22 commits
Select commit Hold shift + click to select a range
ac02ae1
setting: JWT 및 Spring Security 의존성 추가
LATE-BL00MER May 22, 2026
6365805
fix: AuthMember 사용자명 반환 로직 수정
LATE-BL00MER May 25, 2026
46c2f7e
setting: OAuth2 Client 의존성 추가
LATE-BL00MER May 25, 2026
6442e0c
feat: 카카오 OAuth 로그인 서비스 구현
LATE-BL00MER May 25, 2026
7684c28
feat: 소셜 로그인 회원 조회 로직 추가
LATE-BL00MER May 25, 2026
6079171
feat: JWT 인증 필터 로직 추가
LATE-BL00MER May 25, 2026
4e5e728
feat: JWT 토큰 생성 및 검증 로직 추가
LATE-BL00MER May 25, 2026
224a2e2
feat: 카카오 OAuth 응답 DTO 추가
LATE-BL00MER May 25, 2026
225aa1f
feat: Member 엔티티에 소셜 로그인 필드 추가
LATE-BL00MER May 25, 2026
4bfc059
feat: Member 컨트롤러에 로그인 및 내 정보 조회 API 추가
LATE-BL00MER May 25, 2026
ec8ac31
feat: MemberConverter에 소셜 로그인 변환 로직 추가
LATE-BL00MER May 25, 2026
414655b
feat: 회원 인증 관련 에러 코드 추가
LATE-BL00MER May 25, 2026
1699bd3
feat: 소셜 로그인 회원 조회 메서드 추가
LATE-BL00MER May 25, 2026
6c7e3e4
feat: 로그인 요청 DTO 추가
LATE-BL00MER May 25, 2026
55cd692
feat: 로그인 응답 DTO 추가
LATE-BL00MER May 25, 2026
bfe4d2f
feat: 일반 로그인 서비스 로직 추가
LATE-BL00MER May 25, 2026
bdb8f08
feat: OAuthDTO 인터페이스 추가
LATE-BL00MER May 26, 2026
e0aeae1
feat: OAuthMember 인증 객체 구현
LATE-BL00MER May 26, 2026
087564b
feat: OAuth 로그인 성공 핸들러 구현
LATE-BL00MER May 26, 2026
35f1247
feat: SecurityConfig에 JWT 및 OAuth 인증 설정 추가
LATE-BL00MER May 26, 2026
ca25ccb
feat: 소셜 로그인 타입 enum 추가
LATE-BL00MER May 26, 2026
76cc906
docs: 9주차 핵심 키워드 정리 추가
LATE-BL00MER May 26, 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
9 changes: 9 additions & 0 deletions Jinyong/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,15 @@ dependencies {
// 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
95 changes: 95 additions & 0 deletions Jinyong/keyword_summary/ch09.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,95 @@
- 세션과 토큰의 차이는?

**세션 기반 인증**: 서버가 사용자의 로그인 상태를 기억하는 방식

→ 사용자가 로그인 하면 서버는 세션 생성

브라우저에 JSESSIONID 같은 세션 ID가 담긴 쿠키 전달

이후 사용자가 요청을 보낼 때마다 쿠키가 함께 전송되고, 서버는 그 세션 ID를 보고 “이 사용자는 로그인한 사용자!”라고 판단함

**토큰 기반 인증**: 서버가 로그인 상태를 직접 저장하지 않음. 클라이언트가 토큰을 들고 다니는 방식임

→ 로그인에 성공하면 서버가 JWT 같은 토큰 발급

클라이언트는 이후 요청마다 `Authorization: Bearer 토큰값` 형태로 토큰을 보냄

→ 서버는 토큰의 서명과 만료 시간 검증해 사용자 인증

| 구분 | 세션 방식 | 토큰 방식 |
| --- | --- | --- |
| 저장 위치 | 서버가 세션 저장 | 클라이언트가 토큰 저장 |
| 서버 상태 | Stateful | Stateless |
| 요청 방식 | 쿠키 기반 | Authorization Header 기반 |
| 장점 | 서버에서 세션 삭제 가능, 제어 쉬움 | 서버 확장에 유리 |
| 단점 | 서버가 세션을 관리해야 함 | 토큰 탈취 시 만료 전까지 위험 |
| 예시 | 전통적인 웹 로그인 | JWT 기반 REST API 로그인 |

최근에는 토큰 방식으로 로그인을 받는 추세이지만, 보안이 중요한 금융쪽이나 몇몇 분야는 세션방식을 고집한다.

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

**Access Token(액세스 토큰)**은 사용자가 보호된 API에 접근할 때 사용하는 실제 인증 토큰이다.

Ex) 마이페이지 조회, 리뷰 작성, 미션 등록 같은 private API 호출할 때 클라이언트는 Access Token을 `Authorization` 헤더에 담아 보낸다.

<aside>

Authorization: Bearer accessToken값

</aside>

서버는 토큰을 검증 후 정상이면 사용자를 인증 사용자로 처리한다.



**Refresh Token(리프레시 토큰)은 Access Token이 만료되었을 때 새로운 Access Token을 발급받기 위한 토큰이**다.

→ API 요청에 직접 사용하는 토큰이 아니라 재발급용 토큰

Access Token은 보통 탈취 위험을 줄이기 위해 짧게 유지한다(30분~2시간)

하지만 Access Token이 너무 자주 만료되면 사용자는 계속 다시 로그인해야

하므로 불편함!

→ 이 문제를 해결하기 위해 Refresh Token을 사용하는 것이다.

| 구분 | Access Token | Refresh Token |
| --- | --- | --- |
| 목적 | API 접근 | Access Token 재발급 |
| 사용 위치 | 매 요청마다 사용 | 토큰 재발급 시 사용 |
| 만료 시간 | 짧게 설정 | 상대적으로 길게 설정 |
| 노출 위험 | 자주 전송되기에 탈취 위험 있음 | 더 오래 유효하기 때문에 안전하게 보관 필요 |
| 예시 | 마이페이지 요청, 리뷰 작성 | Access Token 재발급 요청 |

Access Token은 “출입증”에 가깝고, Refresh Token은 “출입증을 다시 발급받을 수 있는 재발급 권한”에 가깝다.

- OAuth 1.0과 OAuth 2.0의 차이는?

OAuth는 사용자의 비밀번호를 직접 공유하지 않고, 제3자 애플리케이션이 사용자의 리소스에 제한적으로 접근할 수 있게 해주는 권한 위임 프로토콜이다.

쉽게 말하면, “카카오로 로그인”, “구글로 로그인” 같은 기능이 OAuth 기반으로 동작하는 것이다.
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.

OAuth2를 현재 표준이라고 정리한 점은 좋습니다. 추가로 Authorization Code Grant에서 Client, Authorization Server, Resource Server가 각각 어떤 책임을 가지는지 함께 정리하는 것을 권장합니다. 그러면 OAuth2가 단순한 소셜 로그인 구현이 아니라 권한 위임 프로토콜이라는 점이 더 명확해집니다.


우리 서비스가 사용자의 카카오 비밀번호를 직접 받는 것이 아니라, 카카오가 인증을 처리하고 우리 서버는 카카오로부터 사용자 정보를 받아 회원가입 또는 로그인을 처리한다.

OAuth 1.0은 초기 OAuth 프로토콜이며, 요청마다 복잡한 서명 과정을 거쳐야 하고 클라이언트가 요청을 만들 때 암호화 서명값을 포함해야 한다.

OAuth 2.0은 위의 OAuth 1.0을 대체한 방식이다. OAuth 2.0 명세는 OAuth 1.0을 대체한다고 명시하고 있으며, 현재 대부분의 소셜 로그인과 API 권한 위임에서 OAuth 2.0이 사용된다.

OAuth 2.0은 HTTPS를 전제로 하며, OAuth 1.0처럼 매 요청마다 복잡한 서명을 직접 만드는 방식보다 구현이 단순하다. 또한 웹, 모바일 앱, SPA, 서버 간 통신 등 다양한 환경에 맞는 인증 흐름을 제공한다.

| 구분 | OAuth 1.0 | OAuth 2.0 |
| --- | --- | --- |
| 등장 시기 | 초기 방식 | OAuth 1.0 대체 |
| 보안 방식 | 요청마다 서명 필요 | HTTPS 기반 보호 |
| 구현 난이도 | 비교적 복잡 | 비교적 단순 |
| 토큰 방식 | Access Token 중심 | Access Token, Refresh Token 등 활용 |
| 사용 환경 | 초기 웹 중심 | 웹, 모바일, API 서버 등 다양함 |
| 현재 사용성 | 거의 안 쓰는 추세 | 현재 표준 |

OAuth 1.0은 요청마다 직접 서명을 만들어야 해서 보안적으로 엄격하지만 구현이 복잡하다.

OAuth 2.0은 HTTPS를 기반으로 보안을 확보하고, Access Token과 Refresh Token을 이용해 더 단순하고 유연한 구조를 제공한다.

따라서 현재 카카오, 구글, 네이버 로그인 같은 소셜 로그인은 대부분 OAuth 2.0 기반으로 이해하면 된다.
Original file line number Diff line number Diff line change
@@ -1,13 +1,21 @@
package com.example.umc10th.domain.member.controller;

import com.example.umc10th.domain.member.code.MemberSuccessCode;
import com.example.umc10th.domain.member.dto.MemberResDTO;
import com.example.umc10th.domain.member.dto.MemberReqDTO;
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.security.entity.AuthMember;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.security.core.annotation.AuthenticationPrincipal;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequiredArgsConstructor
Expand All @@ -16,21 +24,35 @@ public class MemberController {

private final MemberService memberService;

// 마이페이지
@GetMapping("/v1/users/me")
public ApiResponse<MemberResDTO.GetInfo> getInfo(
@RequestParam Long memberId
public ResponseEntity<ApiResponse<MemberResDTO.GetInfo>> getInfo(
@AuthenticationPrincipal AuthMember authMember
) {
BaseSuccessCode code = MemberSuccessCode.OK;
return ApiResponse.onSuccess(code, memberService.getInfo(memberId));
return ResponseEntity
.status(HttpStatus.OK)
.body(ApiResponse.onSuccess(code, memberService.getInfo(authMember)));
}

// 회원가입
@PostMapping("/v1/auth/signup")
public ApiResponse<MemberResDTO.SignUp> signUp(
public ResponseEntity<ApiResponse<MemberResDTO.SignUp>> signUp(
@RequestBody MemberReqDTO.SignUp request
) {
BaseSuccessCode code = MemberSuccessCode.CREATED;
return ApiResponse.onSuccess(code, memberService.signUp(request));
return ResponseEntity
.status(HttpStatus.CREATED)
.body(ApiResponse.onSuccess(code, memberService.signUp(request)));
}

// 로그인
@PostMapping("/v1/auth/login")
public ResponseEntity<ApiResponse<MemberResDTO.Login>> login(
@RequestBody MemberReqDTO.Login request
) {
BaseSuccessCode code = MemberSuccessCode.OK;
return ResponseEntity
.status(HttpStatus.OK)
.body(ApiResponse.onSuccess(code, memberService.login(request)));
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@
import com.example.umc10th.domain.member.dto.MemberResDTO;
import com.example.umc10th.domain.member.entity.Gender;
import com.example.umc10th.domain.member.entity.Member;
import com.example.umc10th.global.security.dto.OAuthDTO;

import java.time.LocalDate;

Expand All @@ -13,14 +14,14 @@ public class MemberConverter {
public static MemberResDTO.GetInfo toGetInfo(Member member) {

// 빌더 패턴을 사용하여 엔티티의 데이터를 DTO에 매핑합니다.
return MemberResDTO.GetInfo.builder()
.email(member.getEmail())
.name(member.getName())
.point(member.getPoint())
.nickname(member.getNickname())
.gender(member.getGender().name())
.phoneNumber(null)
.build();
return new MemberResDTO.GetInfo(
member.getName(),
member.getEmail(),
null,
member.getPoint(),
member.getNickname(),
member.getGender().name()
);
}

public static Member toMember(MemberReqDTO.SignUp request, String encodedPassword) {
Expand All @@ -35,10 +36,31 @@ public static Member toMember(MemberReqDTO.SignUp request, String encodedPasswor
.build();
}

public static MemberResDTO.SignUp toSignUp(Member member) {
return MemberResDTO.SignUp.builder()
.memberId(member.getId())
.email(member.getEmail())
// 회원가입
public static Member toMember(OAuthDTO dto) {
return Member.builder()
.email(dto.getSocialEmail())
.password("")
.name(dto.getName())
.nickname(dto.getName())
.gender(Gender.NONE)
.point(0)
.birth(LocalDate.now())
.socialType(dto.getSocialType())
.socialUid(dto.getSocialUid())
.build();
}

public static MemberResDTO.SignUp toSignUp(Member member) {
return new MemberResDTO.SignUp(
member.getId(),
member.getEmail()
);
}


// 로그인
public static MemberResDTO.Login toLogin(String accessToken) {
return new MemberResDTO.Login(accessToken);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -6,8 +6,15 @@ public record GetInfo(
Long id
){}

// 회원가입
public record SignUp(
String email,
String password
){}

// 로그인
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.

로그인과 회원가입 요청 DTO가 분리된 점은 적절합니다. 다음 단계에서는 @Email, @NotBlank, 비밀번호 길이 조건처럼 입력 계약을 DTO에 명시하고 Controller에서 @Valid로 검증하는 것을 권장합니다. 인증 API는 잘못된 입력이 Service까지 내려가지 않도록 경계를 세우는 연습이 중요합니다.

public record Login(
String email,
String password
){}
}
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,16 @@ public record GetInfo(
String gender
) {}

// 회원가입
@Builder
public record SignUp(
Long memberId,
String email
) {}

// 로그인
@Builder
public record Login(
String accessToken
) {}
}
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
package com.example.umc10th.domain.member.entity;

import com.example.umc10th.domain.BaseEntity;
import com.example.umc10th.global.security.entity.SocialType;
import jakarta.persistence.*;
import lombok.*;
import java.time.LocalDate;
Expand Down Expand Up @@ -38,4 +39,9 @@ public class Member extends BaseEntity {

@Column(nullable = false)
private LocalDate birth;
}

@Enumerated(EnumType.STRING)
private SocialType socialType;

private String socialUid;
}
Original file line number Diff line number Diff line change
@@ -1,27 +1,45 @@
package com.example.umc10th.domain.member.exception.code;

import com.example.umc10th.global.apiPayload.code.BaseErrorCode;
import lombok.AllArgsConstructor;
import lombok.Getter;
import org.springframework.http.HttpStatus;

@Getter
@AllArgsConstructor
public enum MemberErrorCode implements BaseErrorCode {

MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND,
"MEMBER404",
"해당 사용자를 찾을 수 없습니다."),
MEMBER_ALREADY_EXISTS(HttpStatus.CONFLICT,
"MEMBER409_1",
"이미 존재하는 이메일입니다.");
"이미 존재하는 이메일입니다."),
INVALID_PASSWORD(HttpStatus.UNAUTHORIZED,
"MEMBER401_1",
"비밀번호가 일치하지 않습니다."),
NOT_SUPPORT_SOCIAL_PROVIDER(HttpStatus.BAD_REQUEST,
"MEMBER400_1",
"지원하지 않는 소셜 로그인 제공자입니다.");

private final HttpStatus httpStatus;
private final String code;
private final String message;

MemberErrorCode(HttpStatus httpStatus, String code, String message) {
this.httpStatus = httpStatus;
this.code = code;
this.message = message;
}

@Override
public HttpStatus getStatus() {
return httpStatus;
}
}

@Override
public String getCode() {
return code;
}

@Override
public String getMessage() {
return message;
}
}
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
package com.example.umc10th.domain.member.repository;

import com.example.umc10th.domain.member.entity.Member;
import com.example.umc10th.global.security.entity.SocialType;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.Optional;

public interface MemberRepository extends JpaRepository<Member, Long> {
Optional<Member> findByEmail(String email);
Optional<Member> findBySocialTypeAndSocialUid(SocialType socialType, String socialUid);
}
Loading