From 3d2448f019f1c98890ef913bd1066dde82977c99 Mon Sep 17 00:00:00 2001 From: chazicer Date: Tue, 26 May 2026 19:58:40 +0900 Subject: [PATCH 1/9] =?UTF-8?q?refactor:=20=EC=9D=B8=EC=A6=9D=20=EC=9C=A0?= =?UTF-8?q?=EC=8A=A4=EC=BC=80=EC=9D=B4=EC=8A=A4=EB=A5=BC=20auth=20?= =?UTF-8?q?=ED=8C=A8=ED=82=A4=EC=A7=80=EB=A1=9C=20=EB=B6=84=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../{member => auth}/controller/AuthController.java | 8 ++++---- .../domain/{member => auth}/dto/SignupRequestDto.java | 2 +- .../domain/{member => auth}/dto/SignupResponseDto.java | 2 +- .../example/umc10th/domain/auth/service/AuthService.java | 9 +++++++++ .../domain/{member => auth}/service/AuthServiceImpl.java | 6 +++--- .../umc10th/domain/member/service/AuthService.java | 9 --------- 6 files changed, 18 insertions(+), 18 deletions(-) rename src/main/java/com/example/umc10th/domain/{member => auth}/controller/AuthController.java (77%) rename src/main/java/com/example/umc10th/domain/{member => auth}/dto/SignupRequestDto.java (98%) rename src/main/java/com/example/umc10th/domain/{member => auth}/dto/SignupResponseDto.java (79%) create mode 100644 src/main/java/com/example/umc10th/domain/auth/service/AuthService.java rename src/main/java/com/example/umc10th/domain/{member => auth}/service/AuthServiceImpl.java (97%) delete mode 100644 src/main/java/com/example/umc10th/domain/member/service/AuthService.java diff --git a/src/main/java/com/example/umc10th/domain/member/controller/AuthController.java b/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java similarity index 77% rename from src/main/java/com/example/umc10th/domain/member/controller/AuthController.java rename to src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java index deb7483c..8d8db4c8 100644 --- a/src/main/java/com/example/umc10th/domain/member/controller/AuthController.java +++ b/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java @@ -1,9 +1,9 @@ -package com.example.umc10th.domain.member.controller; +package com.example.umc10th.domain.auth.controller; -import com.example.umc10th.domain.member.dto.SignupRequestDto; -import com.example.umc10th.domain.member.dto.SignupResponseDto; +import com.example.umc10th.domain.auth.dto.SignupRequestDto; +import com.example.umc10th.domain.auth.dto.SignupResponseDto; +import com.example.umc10th.domain.auth.service.AuthService; import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; -import com.example.umc10th.domain.member.service.AuthService; import com.example.umc10th.global.apiPayload.ApiResponse; import jakarta.validation.Valid; import lombok.RequiredArgsConstructor; diff --git a/src/main/java/com/example/umc10th/domain/member/dto/SignupRequestDto.java b/src/main/java/com/example/umc10th/domain/auth/dto/SignupRequestDto.java similarity index 98% rename from src/main/java/com/example/umc10th/domain/member/dto/SignupRequestDto.java rename to src/main/java/com/example/umc10th/domain/auth/dto/SignupRequestDto.java index 3ece0e2b..ef9288fe 100644 --- a/src/main/java/com/example/umc10th/domain/member/dto/SignupRequestDto.java +++ b/src/main/java/com/example/umc10th/domain/auth/dto/SignupRequestDto.java @@ -1,4 +1,4 @@ -package com.example.umc10th.domain.member.dto; +package com.example.umc10th.domain.auth.dto; import com.example.umc10th.domain.member.enums.Gender; import jakarta.validation.Valid; diff --git a/src/main/java/com/example/umc10th/domain/member/dto/SignupResponseDto.java b/src/main/java/com/example/umc10th/domain/auth/dto/SignupResponseDto.java similarity index 79% rename from src/main/java/com/example/umc10th/domain/member/dto/SignupResponseDto.java rename to src/main/java/com/example/umc10th/domain/auth/dto/SignupResponseDto.java index d826215b..2520ed76 100644 --- a/src/main/java/com/example/umc10th/domain/member/dto/SignupResponseDto.java +++ b/src/main/java/com/example/umc10th/domain/auth/dto/SignupResponseDto.java @@ -1,4 +1,4 @@ -package com.example.umc10th.domain.member.dto; +package com.example.umc10th.domain.auth.dto; import java.time.LocalDateTime; diff --git a/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java b/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java new file mode 100644 index 00000000..69a781ed --- /dev/null +++ b/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java @@ -0,0 +1,9 @@ +package com.example.umc10th.domain.auth.service; + +import com.example.umc10th.domain.auth.dto.SignupRequestDto; +import com.example.umc10th.domain.auth.dto.SignupResponseDto; + +public interface AuthService { + + SignupResponseDto signup(SignupRequestDto request); +} diff --git a/src/main/java/com/example/umc10th/domain/member/service/AuthServiceImpl.java b/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java similarity index 97% rename from src/main/java/com/example/umc10th/domain/member/service/AuthServiceImpl.java rename to src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java index e5483bfc..08f347d7 100644 --- a/src/main/java/com/example/umc10th/domain/member/service/AuthServiceImpl.java +++ b/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java @@ -1,4 +1,4 @@ -package com.example.umc10th.domain.member.service; +package com.example.umc10th.domain.auth.service; import com.example.umc10th.domain.category.entity.FoodCategory; import com.example.umc10th.domain.category.entity.mapping.MemberFoodCategory; @@ -6,8 +6,8 @@ import com.example.umc10th.domain.category.exception.code.CategoryErrorCode; import com.example.umc10th.domain.category.repository.FoodCategoryRepository; import com.example.umc10th.domain.category.repository.MemberFoodCategoryRepository; -import com.example.umc10th.domain.member.dto.SignupRequestDto; -import com.example.umc10th.domain.member.dto.SignupResponseDto; +import com.example.umc10th.domain.auth.dto.SignupRequestDto; +import com.example.umc10th.domain.auth.dto.SignupResponseDto; import com.example.umc10th.domain.member.entity.Member; import com.example.umc10th.domain.member.entity.MemberAddress; import com.example.umc10th.domain.member.exception.MemberException; diff --git a/src/main/java/com/example/umc10th/domain/member/service/AuthService.java b/src/main/java/com/example/umc10th/domain/member/service/AuthService.java deleted file mode 100644 index dad99b47..00000000 --- a/src/main/java/com/example/umc10th/domain/member/service/AuthService.java +++ /dev/null @@ -1,9 +0,0 @@ -package com.example.umc10th.domain.member.service; - -import com.example.umc10th.domain.member.dto.SignupRequestDto; -import com.example.umc10th.domain.member.dto.SignupResponseDto; - -public interface AuthService { - - SignupResponseDto signup(SignupRequestDto request); -} From de3a0d17f856fcc024c4a0a28002e8dc35ccef14 Mon Sep 17 00:00:00 2001 From: chazicer Date: Tue, 26 May 2026 20:23:37 +0900 Subject: [PATCH 2/9] =?UTF-8?q?feat:=20JWT=20=EC=9D=98=EC=A1=B4=EC=84=B1?= =?UTF-8?q?=20=EB=B0=8F=20=ED=86=A0=ED=81=B0=20=EC=9C=A0=ED=8B=B8=20?= =?UTF-8?q?=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 5 + .../umc10th/global/security/JwtUtil.java | 93 +++++++++++++++++++ src/main/resources/application.yml | 6 ++ 3 files changed, 104 insertions(+) create mode 100644 src/main/java/com/example/umc10th/global/security/JwtUtil.java diff --git a/build.gradle b/build.gradle index 3314b4f8..bb7ece9c 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-security' + // 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' testImplementation 'org.springframework.boot:spring-boot-starter-test' testImplementation 'org.springframework.security:spring-security-test' testRuntimeOnly 'org.junit.platform:junit-platform-launcher' diff --git a/src/main/java/com/example/umc10th/global/security/JwtUtil.java b/src/main/java/com/example/umc10th/global/security/JwtUtil.java new file mode 100644 index 00000000..d09c9fdb --- /dev/null +++ b/src/main/java/com/example/umc10th/global/security/JwtUtil.java @@ -0,0 +1,93 @@ +package com.example.umc10th.global.security; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.stream.Collectors; +import javax.crypto.SecretKey; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +@Component +public class JwtUtil { + + private final SecretKey secretKey; + private final Duration accessExpiration; + + public JwtUtil( + @Value("${jwt.token.secretKey}") String secret, + @Value("${jwt.token.expiration.access}") Long accessExpiration + ) { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.accessExpiration = Duration.ofMillis(accessExpiration); + } + + // AccessToken 생성 + public String createAccessToken(AuthMember member) { + return createToken(member, accessExpiration); + } + + /** + * 토큰에서 이메일 가져오기 + * + * @param token 유저 정보를 추출할 토큰 + * @return 유저 이메일을 토큰에서 추출합니다 + */ + public String getEmail(String token) { + try { + return getClaims(token).getPayload().getSubject(); // Parsing해서 Subject 가져오기 + } catch (JwtException e) { + return null; + } + } + + /** + * 토큰 유효성 확인 + * + * @param token 유효한지 확인할 토큰 + * @return True, False 반환합니다 + */ + public boolean isValid(String token) { + try { + getClaims(token); + return true; + } catch (JwtException e) { + return false; + } + } + + // 토큰 생성 + private String createToken(AuthMember member, Duration expiration) { + Instant now = Instant.now(); + + // 인가 정보 + String authorities = member.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + return Jwts.builder() + .subject(member.getUsername()) // User 이메일을 Subject로 + .claim("role", authorities) + .claim("email", member.getUsername()) + .issuedAt(Date.from(now)) // 언제 발급한지 + .expiration(Date.from(now.plus(expiration))) // 언제까지 유효한지 + .signWith(secretKey) // sign할 Key + .compact(); + } + + // 토큰 정보 가져오기 + private Jws getClaims(String token) throws JwtException { + return Jwts.parser() + .verifyWith(secretKey) + .clockSkewSeconds(60) + .build() + .parseSignedClaims(token); + } +} diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 0c1bd46f..f4d1726d 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -19,3 +19,9 @@ spring: format_sql: true # ???? SQL ??? ?? ?? ??? query: fail_on_pagination_over_collection_fetch: true + +jwt: + token: + secretKey: ${JWT_SECRET_KEY} + expiration: + access: 1800000 # 30분 From c28048bc774ccda57dda3d262b7386aba1cf7b90 Mon Sep 17 00:00:00 2001 From: chazicer Date: Tue, 26 May 2026 21:56:06 +0900 Subject: [PATCH 3/9] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20=EC=9D=91=EB=8B=B5=20=EB=AA=A8=EB=8D=B8?= =?UTF-8?q?=EA=B3=BC=20=EC=9D=B8=EC=A6=9D=20=EC=8B=A4=ED=8C=A8=20=EC=BD=94?= =?UTF-8?q?=EB=93=9C=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/dto/LoginRequestDto.java | 14 +++++++++++++ .../domain/auth/dto/LoginResponseDto.java | 6 ++++++ .../domain/auth/exception/AuthException.java | 11 ++++++++++ .../auth/exception/code/AuthErrorCode.java | 20 +++++++++++++++++++ .../exception/code/MemberSuccessCode.java | 3 ++- 5 files changed, 53 insertions(+), 1 deletion(-) create mode 100644 src/main/java/com/example/umc10th/domain/auth/dto/LoginRequestDto.java create mode 100644 src/main/java/com/example/umc10th/domain/auth/dto/LoginResponseDto.java create mode 100644 src/main/java/com/example/umc10th/domain/auth/exception/AuthException.java create mode 100644 src/main/java/com/example/umc10th/domain/auth/exception/code/AuthErrorCode.java diff --git a/src/main/java/com/example/umc10th/domain/auth/dto/LoginRequestDto.java b/src/main/java/com/example/umc10th/domain/auth/dto/LoginRequestDto.java new file mode 100644 index 00000000..f9017d2e --- /dev/null +++ b/src/main/java/com/example/umc10th/domain/auth/dto/LoginRequestDto.java @@ -0,0 +1,14 @@ +package com.example.umc10th.domain.auth.dto; + +import jakarta.validation.constraints.Email; +import jakarta.validation.constraints.NotBlank; + +public record LoginRequestDto( + @NotBlank(message = "이메일은 필수입니다.") + @Email(message = "이메일 형식이 올바르지 않습니다.") + String email, + + @NotBlank(message = "비밀번호는 필수입니다.") + String password +) { +} diff --git a/src/main/java/com/example/umc10th/domain/auth/dto/LoginResponseDto.java b/src/main/java/com/example/umc10th/domain/auth/dto/LoginResponseDto.java new file mode 100644 index 00000000..4cb11021 --- /dev/null +++ b/src/main/java/com/example/umc10th/domain/auth/dto/LoginResponseDto.java @@ -0,0 +1,6 @@ +package com.example.umc10th.domain.auth.dto; + +public record LoginResponseDto( + String accessToken +) { +} diff --git a/src/main/java/com/example/umc10th/domain/auth/exception/AuthException.java b/src/main/java/com/example/umc10th/domain/auth/exception/AuthException.java new file mode 100644 index 00000000..9cbc6486 --- /dev/null +++ b/src/main/java/com/example/umc10th/domain/auth/exception/AuthException.java @@ -0,0 +1,11 @@ +package com.example.umc10th.domain.auth.exception; + +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.exception.ProjectException; + +public class AuthException extends ProjectException { + + public AuthException(BaseErrorCode errorCode) { + super(errorCode); + } +} diff --git a/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthErrorCode.java b/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthErrorCode.java new file mode 100644 index 00000000..1c92c995 --- /dev/null +++ b/src/main/java/com/example/umc10th/domain/auth/exception/code/AuthErrorCode.java @@ -0,0 +1,20 @@ +package com.example.umc10th.domain.auth.exception.code; + +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import lombok.Getter; +import org.springframework.http.HttpStatus; + +@Getter +public enum AuthErrorCode implements BaseErrorCode { + INVALID_LOGIN(HttpStatus.UNAUTHORIZED, "AUTH401", "이메일 또는 비밀번호가 올바르지 않습니다."); + + private final HttpStatus status; + private final String code; + private final String message; + + AuthErrorCode(HttpStatus status, String code, String message) { + this.status = status; + this.code = code; + this.message = message; + } +} diff --git a/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java b/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java index 84793ab5..15baf7dd 100644 --- a/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java +++ b/src/main/java/com/example/umc10th/domain/member/exception/code/MemberSuccessCode.java @@ -7,7 +7,8 @@ @Getter public enum MemberSuccessCode implements BaseSuccessCode { MEMBER_CREATED(HttpStatus.CREATED, "MEMBER201", "회원 생성에 성공했습니다."), - MEMBER_FOUND(HttpStatus.OK, "MEMBER200", "회원 조회에 성공했습니다."); + MEMBER_FOUND(HttpStatus.OK, "MEMBER200", "회원 조회에 성공했습니다."), + MEMBER_LOGIN(HttpStatus.OK, "MEMBER200_1", "성공적으로 유저를 조회했습니다."); private final HttpStatus status; private final String code; From 9558170ba13bdf3bfb99cf112bd8ecffab98bd4f Mon Sep 17 00:00:00 2001 From: chazicer Date: Tue, 26 May 2026 21:56:29 +0900 Subject: [PATCH 4/9] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20API?= =?UTF-8?q?=EC=97=90=20JWT=20=ED=86=A0=ED=81=B0=20=EB=B0=9C=EA=B8=89=20?= =?UTF-8?q?=EC=A0=81=EC=9A=A9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 8 +++++++ .../domain/auth/service/AuthService.java | 4 ++++ .../domain/auth/service/AuthServiceImpl.java | 21 +++++++++++++++++++ 3 files changed, 33 insertions(+) diff --git a/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java b/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java index 8d8db4c8..958dbf89 100644 --- a/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java +++ b/src/main/java/com/example/umc10th/domain/auth/controller/AuthController.java @@ -1,5 +1,7 @@ package com.example.umc10th.domain.auth.controller; +import com.example.umc10th.domain.auth.dto.LoginRequestDto; +import com.example.umc10th.domain.auth.dto.LoginResponseDto; import com.example.umc10th.domain.auth.dto.SignupRequestDto; import com.example.umc10th.domain.auth.dto.SignupResponseDto; import com.example.umc10th.domain.auth.service.AuthService; @@ -24,4 +26,10 @@ public ApiResponse signup(@Valid @RequestBody SignupRequestDt SignupResponseDto response = authService.signup(request); return ApiResponse.onSuccess(MemberSuccessCode.MEMBER_CREATED, response); } + + @PostMapping("/login") + public ApiResponse login(@Valid @RequestBody LoginRequestDto request) { + LoginResponseDto response = authService.login(request); + return ApiResponse.onSuccess(MemberSuccessCode.MEMBER_LOGIN, response); + } } diff --git a/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java b/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java index 69a781ed..2b128187 100644 --- a/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java +++ b/src/main/java/com/example/umc10th/domain/auth/service/AuthService.java @@ -1,9 +1,13 @@ package com.example.umc10th.domain.auth.service; +import com.example.umc10th.domain.auth.dto.LoginRequestDto; +import com.example.umc10th.domain.auth.dto.LoginResponseDto; import com.example.umc10th.domain.auth.dto.SignupRequestDto; import com.example.umc10th.domain.auth.dto.SignupResponseDto; public interface AuthService { SignupResponseDto signup(SignupRequestDto request); + + LoginResponseDto login(LoginRequestDto request); } diff --git a/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java b/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java index 08f347d7..f2672699 100644 --- a/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java +++ b/src/main/java/com/example/umc10th/domain/auth/service/AuthServiceImpl.java @@ -6,8 +6,12 @@ import com.example.umc10th.domain.category.exception.code.CategoryErrorCode; import com.example.umc10th.domain.category.repository.FoodCategoryRepository; import com.example.umc10th.domain.category.repository.MemberFoodCategoryRepository; +import com.example.umc10th.domain.auth.dto.LoginRequestDto; +import com.example.umc10th.domain.auth.dto.LoginResponseDto; import com.example.umc10th.domain.auth.dto.SignupRequestDto; import com.example.umc10th.domain.auth.dto.SignupResponseDto; +import com.example.umc10th.domain.auth.exception.AuthException; +import com.example.umc10th.domain.auth.exception.code.AuthErrorCode; import com.example.umc10th.domain.member.entity.Member; import com.example.umc10th.domain.member.entity.MemberAddress; import com.example.umc10th.domain.member.exception.MemberException; @@ -24,6 +28,8 @@ import com.example.umc10th.domain.term.exception.code.TermErrorCode; import com.example.umc10th.domain.term.repository.MemberTermAgreementRepository; import com.example.umc10th.domain.term.repository.TermRepository; +import com.example.umc10th.global.security.AuthMember; +import com.example.umc10th.global.security.JwtUtil; import java.util.LinkedHashMap; import java.util.List; import java.util.Map; @@ -46,6 +52,7 @@ public class AuthServiceImpl implements AuthService { private final FoodCategoryRepository foodCategoryRepository; private final MemberFoodCategoryRepository memberFoodCategoryRepository; private final PasswordEncoder passwordEncoder; + private final JwtUtil jwtUtil; @Override @Transactional @@ -99,6 +106,20 @@ public SignupResponseDto signup(SignupRequestDto request) { ); } + @Override + @Transactional(readOnly = true) + public LoginResponseDto login(LoginRequestDto request) { + Member member = memberRepository.findByEmail(request.email()) + .orElseThrow(() -> new AuthException(AuthErrorCode.INVALID_LOGIN)); + + if (!passwordEncoder.matches(request.password(), member.getPasswordHash())) { + throw new AuthException(AuthErrorCode.INVALID_LOGIN); + } + + String accessToken = jwtUtil.createAccessToken(AuthMember.from(member)); + return new LoginResponseDto(accessToken); + } + private Map toTermAgreementMap(List terms) { Map termAgreementMap = new LinkedHashMap<>(); for (SignupRequestDto.TermAgreementRequest term : terms) { From 33cfa31076d0ebd6a808478580ec44a756dd203a Mon Sep 17 00:00:00 2001 From: chazicer Date: Wed, 27 May 2026 11:53:17 +0900 Subject: [PATCH 5/9] =?UTF-8?q?feat:=20JWT=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20SecurityConfig=EC=97=90=20=EC=97=B0?= =?UTF-8?q?=EA=B2=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/config/SecurityConfig.java | 22 ++++-- .../global/security/JwtAuthFilter.java | 69 +++++++++++++++++++ 2 files changed, 84 insertions(+), 7 deletions(-) create mode 100644 src/main/java/com/example/umc10th/global/security/JwtAuthFilter.java diff --git a/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index 3e4adb16..dafd89aa 100644 --- a/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -2,14 +2,19 @@ import com.example.umc10th.global.security.CustomAccessDeniedHandler; import com.example.umc10th.global.security.CustomAuthenticationEntryPoint; +import com.example.umc10th.global.security.CustomUserDetailsService; +import com.example.umc10th.global.security.JwtAuthFilter; +import com.example.umc10th.global.security.JwtUtil; import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; +import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; @Configuration @EnableWebSecurity @@ -18,10 +23,11 @@ public class SecurityConfig { private final CustomAuthenticationEntryPoint customAuthenticationEntryPoint; private final CustomAccessDeniedHandler customAccessDeniedHandler; + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; private static final String[] PUBLIC_URLS = { "/api/auth/**", - "/login", "/swagger-ui/**", "/swagger-ui.html", "/v3/api-docs/**", @@ -36,16 +42,13 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers(PUBLIC_URLS).permitAll() .anyRequest().authenticated() ) + .formLogin(AbstractHttpConfigurer::disable) + .sessionManagement(AbstractHttpConfigurer::disable) + .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) .exceptionHandling(exceptionHandling -> exceptionHandling .authenticationEntryPoint(customAuthenticationEntryPoint) .accessDeniedHandler(customAccessDeniedHandler) ) - .formLogin(form -> form - .loginProcessingUrl("/login") - .usernameParameter("email") - .passwordParameter("password") - .permitAll() - ) .logout(logout -> logout .logoutUrl("/logout") .permitAll() @@ -58,4 +61,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } + + @Bean + public JwtAuthFilter jwtAuthFilter() { + return new JwtAuthFilter(jwtUtil, customUserDetailsService); + } } diff --git a/src/main/java/com/example/umc10th/global/security/JwtAuthFilter.java b/src/main/java/com/example/umc10th/global/security/JwtAuthFilter.java new file mode 100644 index 00000000..e493d975 --- /dev/null +++ b/src/main/java/com/example/umc10th/global/security/JwtAuthFilter.java @@ -0,0 +1,69 @@ +package com.example.umc10th.global.security; + +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseErrorCode; +import com.example.umc10th.global.apiPayload.code.GeneralErrorCode; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.FilterChain; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import lombok.RequiredArgsConstructor; +import org.springframework.lang.NonNull; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.core.userdetails.UserDetails; +import org.springframework.web.filter.OncePerRequestFilter; + +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + try { + // 토큰 가져오기 + String token = request.getHeader("Authorization"); + // token이 없거나 Bearer가 아니면 넘기기 + if (token == null || !token.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + // Bearer이면 추출 + token = token.replace("Bearer ", ""); + // AccessToken 검증하기: 올바른 토큰이면 + if (jwtUtil.isValid(token)) { + // 토큰에서 이메일 추출 + String email = jwtUtil.getEmail(token); + // 인증 객체 생성: 이메일로 찾아온 뒤, 인증 객체 생성 + UserDetails user = customUserDetailsService.loadUserByUsername(email); + Authentication auth = new UsernamePasswordAuthenticationToken( + user, + null, + user.getAuthorities() + ); + // 인증 완료 후 SecurityContextHolder에 넣기 + SecurityContextHolder.getContext().setAuthentication(auth); + } + filterChain.doFilter(request, response); + } catch (Exception e) { + ObjectMapper mapper = new ObjectMapper(); + BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED; + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + ApiResponse errorResponse = ApiResponse.onFailure(code, null); + + mapper.writeValue(response.getOutputStream(), errorResponse); + } + } +} From 669df367ecdfcc5322c665bc171eb0fa990687e9 Mon Sep 17 00:00:00 2001 From: chazicer Date: Wed, 27 May 2026 12:38:33 +0900 Subject: [PATCH 6/9] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20API=EB=A5=BC=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=EC=82=AC=EC=9A=A9=EC=9E=90=20=EA=B8=B0=EB=B0=98=EC=9C=BC?= =?UTF-8?q?=EB=A1=9C=20=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/domain/member/controller/MemberController.java | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index e9bd0b38..58e0b914 100644 --- a/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -4,9 +4,10 @@ import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; import com.example.umc10th.domain.member.service.MemberService; import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.security.AuthMember; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; -import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController @@ -21,9 +22,9 @@ public MemberController(MemberService memberService) { @GetMapping("/me") public ApiResponse getMyPage( - @RequestParam Long memberId + @AuthenticationPrincipal AuthMember authMember ) { - MemberResponseDto response = memberService.getMyPage(memberId); + MemberResponseDto response = memberService.getMyPage(authMember.getMemberId()); return ApiResponse.onSuccess(MemberSuccessCode.MEMBER_FOUND, response); } From 37ca77b5ceb2c81572a058264928fde6c4146fe9 Mon Sep 17 00:00:00 2001 From: chazicer Date: Wed, 27 May 2026 15:58:44 +0900 Subject: [PATCH 7/9] =?UTF-8?q?chore:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20OAut?= =?UTF-8?q?h=20=ED=99=98=EA=B2=BD=EB=B3=80=EC=88=98=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 8 ++++++++ src/main/resources/application.yml | 20 ++++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 .env.example diff --git a/.env.example b/.env.example new file mode 100644 index 00000000..634efe83 --- /dev/null +++ b/.env.example @@ -0,0 +1,8 @@ +DB_USER=root +DB_PW=1234 +DB_URL=jdbc:mysql://localhost:3306/umc10th + +JWT_SECRET_KEY=BASE64_32 + +KAKAO_CLIENT_ID=KAKAO_REST_API_KEY +KAKAO_CLIENT_SECRET=KAKAO_CLIENT_SECRET diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index f4d1726d..c12daec3 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -20,6 +20,26 @@ spring: query: fail_on_pagination_over_collection_fetch: true + security: + oauth2: + client: + registration: + kakao: + client-id: ${KAKAO_CLIENT_ID} + client-secret: ${KAKAO_CLIENT_SECRET} + client-authentication-method: client_secret_post + authorization-grant-type: authorization_code + redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" + scope: + - profile_nickname + - account_email + provider: + kakao: + authorization-uri: https://kauth.kakao.com/oauth/authorize + token-uri: https://kauth.kakao.com/oauth/token + user-info-uri: https://kapi.kakao.com/v2/user/me + user-name-attribute: id + jwt: token: secretKey: ${JWT_SECRET_KEY} From 8e3519a42657917c7cf50ded1e001841ced871be Mon Sep 17 00:00:00 2001 From: chazicer Date: Wed, 27 May 2026 16:25:10 +0900 Subject: [PATCH 8/9] =?UTF-8?q?feat:=20=EC=86=8C=EC=85=9C=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=ED=9A=8C=EC=9B=90=20=EC=83=9D=EC=84=B1=20?= =?UTF-8?q?=ED=9D=90=EB=A6=84=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 2 ++ .../umc10th/domain/member/entity/Member.java | 26 +++++++++++++++++++ .../exception/code/MemberErrorCode.java | 3 ++- .../member/repository/MemberRepository.java | 3 +++ 4 files changed, 33 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index bb7ece9c..8ba5e44d 100644 --- a/build.gradle +++ b/build.gradle @@ -22,6 +22,8 @@ dependencies { implementation 'org.springframework.boot:spring-boot-starter-web' implementation 'org.springframework.boot:spring-boot-starter-validation' implementation 'org.springframework.boot:spring-boot-starter-security' + // OAuth + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // Jwt implementation 'io.jsonwebtoken:jjwt-api:0.12.3' implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' diff --git a/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/src/main/java/com/example/umc10th/domain/member/entity/Member.java index 37ae35a8..d9beb5eb 100644 --- a/src/main/java/com/example/umc10th/domain/member/entity/Member.java +++ b/src/main/java/com/example/umc10th/domain/member/entity/Member.java @@ -91,6 +91,22 @@ private Member( this.phoneNumber = phoneNumber; } + private Member( + SocialType socialType, + String socialUid, + String email, + String nickname, + String profileImageUrl + ) { + this.name = nickname; + this.nickname = nickname; + this.gender = Gender.NONE; + this.socialType = socialType; + this.socialUid = socialUid; + this.email = email; + this.profileImageUrl = profileImageUrl; + } + public static Member createLocal( String name, String nickname, @@ -102,4 +118,14 @@ public static Member createLocal( ) { return new Member(name, nickname, gender, birth, email, passwordHash, phoneNumber); } + + public static Member createSocial( + SocialType socialType, + String socialUid, + String email, + String nickname, + String profileImageUrl + ) { + return new Member(socialType, socialUid, email, nickname, profileImageUrl); + } } diff --git a/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index 57287eab..3f49227f 100644 --- a/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -8,7 +8,8 @@ public enum MemberErrorCode implements BaseErrorCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404", "회원을 찾을 수 없습니다."), REQUIRED_TERM_NOT_AGREED(HttpStatus.BAD_REQUEST, "MEMBER400_1", "필수 약관에 동의해야 합니다."), - EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER409", "이미 사용 중인 이메일입니다."); + EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER409", "이미 사용 중인 이메일입니다."), + NOT_SUPPORT_SOCIAL_PROVIDER(HttpStatus.BAD_REQUEST, "MEMBER400_2", "지원하지 않는 소셜 로그인 제공자입니다."); private final HttpStatus status; private final String code; diff --git a/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index 5486aba6..b2b6128b 100644 --- a/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -1,6 +1,7 @@ package com.example.umc10th.domain.member.repository; import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.enums.SocialType; import java.util.Optional; import org.springframework.data.jpa.repository.JpaRepository; @@ -8,5 +9,7 @@ public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); + Optional findBySocialTypeAndSocialUid(SocialType socialType, String socialUid); + boolean existsByEmail(String email); } From 24b6b366dfb3339f72e891eaa8c7125f2fad9ed9 Mon Sep 17 00:00:00 2001 From: chazicer Date: Wed, 27 May 2026 16:26:42 +0900 Subject: [PATCH 9/9] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20OAuth?= =?UTF-8?q?=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20JWT=20=EB=B0=9C=EA=B8=89=20?= =?UTF-8?q?=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/auth/oauth/CustomOAuthService.java | 109 ++++++++++++++++++ .../domain/auth/oauth/OAuthMember.java | 43 +++++++ .../auth/oauth/OAuthSuccessHandler.java | 46 ++++++++ .../domain/auth/oauth/dto/KakaoDTO.java | 16 +++ .../domain/auth/oauth/dto/OAuthDTO.java | 16 +++ .../umc10th/global/config/SecurityConfig.java | 16 +++ 6 files changed, 246 insertions(+) create mode 100644 src/main/java/com/example/umc10th/domain/auth/oauth/CustomOAuthService.java create mode 100644 src/main/java/com/example/umc10th/domain/auth/oauth/OAuthMember.java create mode 100644 src/main/java/com/example/umc10th/domain/auth/oauth/OAuthSuccessHandler.java create mode 100644 src/main/java/com/example/umc10th/domain/auth/oauth/dto/KakaoDTO.java create mode 100644 src/main/java/com/example/umc10th/domain/auth/oauth/dto/OAuthDTO.java diff --git a/src/main/java/com/example/umc10th/domain/auth/oauth/CustomOAuthService.java b/src/main/java/com/example/umc10th/domain/auth/oauth/CustomOAuthService.java new file mode 100644 index 00000000..57fa9854 --- /dev/null +++ b/src/main/java/com/example/umc10th/domain/auth/oauth/CustomOAuthService.java @@ -0,0 +1,109 @@ +package com.example.umc10th.domain.auth.oauth; + +import com.example.umc10th.domain.auth.oauth.dto.KakaoDTO; +import com.example.umc10th.domain.auth.oauth.dto.OAuthDTO; +import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.enums.SocialType; +import com.example.umc10th.domain.member.exception.code.MemberErrorCode; +import com.example.umc10th.domain.member.repository.MemberRepository; +import java.util.Map; +import lombok.RequiredArgsConstructor; +import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService; +import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.OAuth2Error; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; +import org.springframework.transaction.annotation.Transactional; + +@Service +@RequiredArgsConstructor +public class CustomOAuthService extends DefaultOAuth2UserService { + + private static final String KAKAO_ACCOUNT = "kakao_account"; + private static final String PROFILE = "profile"; + private static final String DEFAULT_NICKNAME_PREFIX = "kakao_user_"; + + private final MemberRepository memberRepository; + + @Override + @Transactional + public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException { + OAuth2User oAuthMember = super.loadUser(userRequest); + + SocialType providerId = getProviderId(userRequest); + String socialUid = getRequiredString(oAuthMember.getAttributes(), "id", "Kakao id is required."); + + OAuthDTO dto = switch (providerId) { + case KAKAO -> toKakaoDto(oAuthMember, socialUid); + default -> throw unsupportedProvider(); + }; + + Member member = memberRepository.findBySocialTypeAndSocialUid(dto.socialType(), dto.socialUid()) + .orElseGet(() -> memberRepository.save(Member.createSocial( + dto.socialType(), + dto.socialUid(), + dto.email(), + dto.nickname(), + dto.profileImageUrl() + ))); + + return new OAuthMember(member, oAuthMember.getAttributes()); + } + + private SocialType getProviderId(OAuth2UserRequest userRequest) { + try { + return SocialType.valueOf(userRequest.getClientRegistration() + .getRegistrationId() + .toUpperCase()); + } catch (IllegalArgumentException e) { + throw unsupportedProvider(); + } + } + + @SuppressWarnings("unchecked") + private KakaoDTO toKakaoDto(OAuth2User oAuthMember, String socialUid) { + Map attributes = oAuthMember.getAttributes(); + Map kakaoAccount = (Map) attributes.get(KAKAO_ACCOUNT); + if (kakaoAccount == null) { + throw invalidUserInfo("Kakao account is required."); + } + + Map profile = (Map) kakaoAccount.get(PROFILE); + String email = getRequiredString(kakaoAccount, "email", "Kakao email is required."); + String nickname = getString(profile, "nickname"); + String profileImageUrl = getString(profile, "profile_image_url"); + + if (nickname == null || nickname.isBlank()) { + nickname = DEFAULT_NICKNAME_PREFIX + socialUid; + } + + return new KakaoDTO(socialUid, email, nickname, profileImageUrl); + } + + private String getRequiredString(Map attributes, String key, String message) { + String value = getString(attributes, key); + if (value == null || value.isBlank()) { + throw invalidUserInfo(message); + } + return value; + } + + private String getString(Map attributes, String key) { + if (attributes == null) { + return null; + } + Object value = attributes.get(key); + return value != null ? String.valueOf(value) : null; + } + + private OAuth2AuthenticationException unsupportedProvider() { + OAuth2Error error = new OAuth2Error(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER.getCode()); + return new OAuth2AuthenticationException(error, MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER.getMessage()); + } + + private OAuth2AuthenticationException invalidUserInfo(String message) { + OAuth2Error error = new OAuth2Error("OAUTH_INVALID_USER_INFO"); + return new OAuth2AuthenticationException(error, message); + } +} diff --git a/src/main/java/com/example/umc10th/domain/auth/oauth/OAuthMember.java b/src/main/java/com/example/umc10th/domain/auth/oauth/OAuthMember.java new file mode 100644 index 00000000..0a5a28d4 --- /dev/null +++ b/src/main/java/com/example/umc10th/domain/auth/oauth/OAuthMember.java @@ -0,0 +1,43 @@ +package com.example.umc10th.domain.auth.oauth; + +import com.example.umc10th.domain.member.entity.Member; +import java.util.Collection; +import java.util.List; +import java.util.Map; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.core.authority.SimpleGrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; + +public class OAuthMember implements OAuth2User { + + private static final String DEFAULT_ROLE = "ROLE_USER"; + + private final Member member; + private final Map attributes; + private final Collection authorities; + + public OAuthMember(Member member, Map attributes) { + this.member = member; + this.attributes = attributes; + this.authorities = List.of(new SimpleGrantedAuthority(DEFAULT_ROLE)); + } + + public Member getMember() { + return member; + } + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public Collection getAuthorities() { + return authorities; + } + + @Override + public String getName() { + return member.getSocialUid(); + } +} diff --git a/src/main/java/com/example/umc10th/domain/auth/oauth/OAuthSuccessHandler.java b/src/main/java/com/example/umc10th/domain/auth/oauth/OAuthSuccessHandler.java new file mode 100644 index 00000000..ec6cf6cd --- /dev/null +++ b/src/main/java/com/example/umc10th/domain/auth/oauth/OAuthSuccessHandler.java @@ -0,0 +1,46 @@ +package com.example.umc10th.domain.auth.oauth; + +import com.example.umc10th.domain.auth.dto.LoginResponseDto; +import com.example.umc10th.domain.member.exception.code.MemberSuccessCode; +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.security.AuthMember; +import com.example.umc10th.global.security.JwtUtil; +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import java.io.IOException; +import java.nio.charset.StandardCharsets; +import lombok.RequiredArgsConstructor; +import org.springframework.http.MediaType; +import org.springframework.security.core.Authentication; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; + +@Component +@RequiredArgsConstructor +public class OAuthSuccessHandler implements AuthenticationSuccessHandler { + + private final JwtUtil jwtUtil; + private final ObjectMapper objectMapper; + + @Override + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) throws IOException, ServletException { + OAuthMember oAuthMember = (OAuthMember) authentication.getPrincipal(); + String accessToken = jwtUtil.createAccessToken(AuthMember.from(oAuthMember.getMember())); + + response.setStatus(MemberSuccessCode.MEMBER_LOGIN.getStatus().value()); + response.setContentType(MediaType.APPLICATION_JSON_VALUE); + response.setCharacterEncoding(StandardCharsets.UTF_8.name()); + + ApiResponse responseBody = ApiResponse.onSuccess( + MemberSuccessCode.MEMBER_LOGIN, + new LoginResponseDto(accessToken) + ); + objectMapper.writeValue(response.getWriter(), responseBody); + } +} diff --git a/src/main/java/com/example/umc10th/domain/auth/oauth/dto/KakaoDTO.java b/src/main/java/com/example/umc10th/domain/auth/oauth/dto/KakaoDTO.java new file mode 100644 index 00000000..1af43087 --- /dev/null +++ b/src/main/java/com/example/umc10th/domain/auth/oauth/dto/KakaoDTO.java @@ -0,0 +1,16 @@ +package com.example.umc10th.domain.auth.oauth.dto; + +import com.example.umc10th.domain.member.enums.SocialType; + +public record KakaoDTO( + String socialUid, + String email, + String nickname, + String profileImageUrl +) implements OAuthDTO { + + @Override + public SocialType socialType() { + return SocialType.KAKAO; + } +} diff --git a/src/main/java/com/example/umc10th/domain/auth/oauth/dto/OAuthDTO.java b/src/main/java/com/example/umc10th/domain/auth/oauth/dto/OAuthDTO.java new file mode 100644 index 00000000..313dcf63 --- /dev/null +++ b/src/main/java/com/example/umc10th/domain/auth/oauth/dto/OAuthDTO.java @@ -0,0 +1,16 @@ +package com.example.umc10th.domain.auth.oauth.dto; + +import com.example.umc10th.domain.member.enums.SocialType; + +public interface OAuthDTO { + + SocialType socialType(); + + String socialUid(); + + String email(); + + String nickname(); + + String profileImageUrl(); +} diff --git a/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index dafd89aa..8c85f51c 100644 --- a/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -1,5 +1,7 @@ package com.example.umc10th.global.config; +import com.example.umc10th.domain.auth.oauth.CustomOAuthService; +import com.example.umc10th.domain.auth.oauth.OAuthSuccessHandler; import com.example.umc10th.global.security.CustomAccessDeniedHandler; import com.example.umc10th.global.security.CustomAuthenticationEntryPoint; import com.example.umc10th.global.security.CustomUserDetailsService; @@ -25,6 +27,8 @@ public class SecurityConfig { private final CustomAccessDeniedHandler customAccessDeniedHandler; private final JwtUtil jwtUtil; private final CustomUserDetailsService customUserDetailsService; + private final CustomOAuthService customOAuthService; + private final OAuthSuccessHandler oAuthSuccessHandler; private static final String[] PUBLIC_URLS = { "/api/auth/**", @@ -43,6 +47,18 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .anyRequest().authenticated() ) .formLogin(AbstractHttpConfigurer::disable) + .oauth2Login(oauth2 -> oauth2 + .authorizationEndpoint(authorization -> authorization + .baseUri("/oauth2/authorization") + ) + .redirectionEndpoint(redirection -> redirection + .baseUri("/login/oauth2/code/*") + ) + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuthService) + ) + .successHandler(oAuthSuccessHandler) + ) .sessionManagement(AbstractHttpConfigurer::disable) .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) .exceptionHandling(exceptionHandling -> exceptionHandling