From 992208ed70c269ad4a915c6f960cb6b708dfec0b Mon Sep 17 00:00:00 2001 From: zeoueon Date: Sun, 24 May 2026 13:56:12 +0900 Subject: [PATCH 01/11] =?UTF-8?q?refactor:=20security=20=ED=8C=A8=ED=82=A4?= =?UTF-8?q?=EC=A7=80=20=EA=B5=AC=EC=A1=B0=20=EC=97=AD=ED=95=A0=EB=B3=84?= =?UTF-8?q?=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 --- src/main/java/umc/global/security/{ => entity}/AuthMember.java | 2 +- .../global/security/{ => exception}/CustomAccessDenied.java | 2 +- .../umc/global/security/{ => exception}/CustomEntryPoint.java | 2 +- .../security/{ => service}/CustomUserDetailsService.java | 3 ++- 4 files changed, 5 insertions(+), 4 deletions(-) rename src/main/java/umc/global/security/{ => entity}/AuthMember.java (95%) rename src/main/java/umc/global/security/{ => exception}/CustomAccessDenied.java (97%) rename src/main/java/umc/global/security/{ => exception}/CustomEntryPoint.java (97%) rename src/main/java/umc/global/security/{ => service}/CustomUserDetailsService.java (92%) diff --git a/src/main/java/umc/global/security/AuthMember.java b/src/main/java/umc/global/security/entity/AuthMember.java similarity index 95% rename from src/main/java/umc/global/security/AuthMember.java rename to src/main/java/umc/global/security/entity/AuthMember.java index 4a19574..3971ba2 100644 --- a/src/main/java/umc/global/security/AuthMember.java +++ b/src/main/java/umc/global/security/entity/AuthMember.java @@ -1,4 +1,4 @@ -package umc.global.security; +package umc.global.security.entity; import jakarta.annotation.Nullable; import lombok.Getter; diff --git a/src/main/java/umc/global/security/CustomAccessDenied.java b/src/main/java/umc/global/security/exception/CustomAccessDenied.java similarity index 97% rename from src/main/java/umc/global/security/CustomAccessDenied.java rename to src/main/java/umc/global/security/exception/CustomAccessDenied.java index 7fdd53e..93947e8 100644 --- a/src/main/java/umc/global/security/CustomAccessDenied.java +++ b/src/main/java/umc/global/security/exception/CustomAccessDenied.java @@ -1,4 +1,4 @@ -package umc.global.security; +package umc.global.security.exception; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.ServletException; diff --git a/src/main/java/umc/global/security/CustomEntryPoint.java b/src/main/java/umc/global/security/exception/CustomEntryPoint.java similarity index 97% rename from src/main/java/umc/global/security/CustomEntryPoint.java rename to src/main/java/umc/global/security/exception/CustomEntryPoint.java index d24bb2f..5e649dc 100644 --- a/src/main/java/umc/global/security/CustomEntryPoint.java +++ b/src/main/java/umc/global/security/exception/CustomEntryPoint.java @@ -1,4 +1,4 @@ -package umc.global.security; +package umc.global.security.exception; import com.fasterxml.jackson.databind.ObjectMapper; import jakarta.servlet.ServletException; diff --git a/src/main/java/umc/global/security/CustomUserDetailsService.java b/src/main/java/umc/global/security/service/CustomUserDetailsService.java similarity index 92% rename from src/main/java/umc/global/security/CustomUserDetailsService.java rename to src/main/java/umc/global/security/service/CustomUserDetailsService.java index 2a8494d..bf35614 100644 --- a/src/main/java/umc/global/security/CustomUserDetailsService.java +++ b/src/main/java/umc/global/security/service/CustomUserDetailsService.java @@ -1,4 +1,4 @@ -package umc.global.security; +package umc.global.security.service; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; @@ -9,6 +9,7 @@ import umc.domain.member.exception.MemberException; import umc.domain.member.exception.code.MemberErrorCode; import umc.domain.member.repository.MemberRepository; +import umc.global.security.entity.AuthMember; @Service @RequiredArgsConstructor From ff2d16e259960be45bbc35e2bdf634ff6e75814a Mon Sep 17 00:00:00 2001 From: zeoueon Date: Sun, 24 May 2026 13:56:47 +0900 Subject: [PATCH 02/11] =?UTF-8?q?chore:=20JWT=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EC=B6=94=EA=B0=80=20=EB=B0=8F=20yml=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 6 ++++++ src/main/resources/application.yml | 24 +++++++++++++++--------- 2 files changed, 21 insertions(+), 9 deletions(-) diff --git a/build.gradle b/build.gradle index 49fcfd1..bb3016b 100644 --- a/build.gradle +++ b/build.gradle @@ -36,6 +36,12 @@ dependencies { // Spring 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' } tasks.named('test') { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 79a562a..400b5f9 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -3,17 +3,23 @@ spring: name: "umc10th" # "umc10th" datasource: - driver-class-name: com.mysql.cj.jdbc.Driver # MySQL JDBC ???? ??? ?? - url: ${DB_URL} # jdbc:mysql://localhost:3306/{???????} - username: ${DB_USER} # MySQL ?? ?? - password: ${DB_PW} # MySQL ???? + driver-class-name: com.mysql.cj.jdbc.Driver + url: ${DB_URL} + username: ${DB_USER} + password: ${DB_PW} jpa: - database: mysql # ??? ?????? ?? ?? (MySQL) - database-platform: org.hibernate.dialect.MySQLDialect # Hibernate?? ??? MySQL ??(dialect) ?? - show-sql: true # ??? SQL ??? ??? ???? ?? ?? + database: mysql + database-platform: org.hibernate.dialect.MySQLDialect + show-sql: true hibernate: - ddl-auto: update # ?????? ?? ? ?????? ???? ??? ?? + ddl-auto: update properties: hibernate: - format_sql: true # ???? SQL ??? ?? ?? ??? + format_sql: true + + jwt: + token: + secretKey: ${JWT_SECRET_KEY} + expiration: + access: 1800000 # 30분 From f5260257d23f7fcb0bb2a56e7f9918320a86dd2d Mon Sep 17 00:00:00 2001 From: zeoueon Date: Sun, 24 May 2026 13:57:24 +0900 Subject: [PATCH 03/11] =?UTF-8?q?feat:=20JWT=20=EA=B8=B0=EB=B0=98=20?= =?UTF-8?q?=EB=A1=9C=EA=B7=B8=EC=9D=B8=20API=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 10 ++- .../java/umc/domain/auth/dto/AuthReqDTO.java | 8 ++ .../java/umc/domain/auth/dto/AuthResDTO.java | 5 ++ .../auth/exception/code/AuthErrorCode.java | 1 + .../umc/domain/auth/service/AuthService.java | 24 +++++ .../exception/code/MemberSuccessCode.java | 1 + .../umc/global/config/SecurityConfig.java | 26 ++++-- .../global/security/filter/JwtAuthFilter.java | 72 +++++++++++++++ .../umc/global/security/util/JwtUtil.java | 89 +++++++++++++++++++ 9 files changed, 228 insertions(+), 8 deletions(-) create mode 100644 src/main/java/umc/global/security/filter/JwtAuthFilter.java create mode 100644 src/main/java/umc/global/security/util/JwtUtil.java diff --git a/src/main/java/umc/domain/auth/controller/AuthController.java b/src/main/java/umc/domain/auth/controller/AuthController.java index 8fafbcc..b642f60 100644 --- a/src/main/java/umc/domain/auth/controller/AuthController.java +++ b/src/main/java/umc/domain/auth/controller/AuthController.java @@ -9,8 +9,6 @@ import umc.domain.auth.dto.AuthReqDTO; import umc.domain.auth.dto.AuthResDTO; import umc.domain.auth.service.AuthService; -import umc.domain.member.dto.MemberReqDTO; -import umc.domain.member.dto.MemberResDTO; import umc.domain.member.exception.code.MemberSuccessCode; import umc.global.apiPayload.ApiResponse; @@ -28,4 +26,12 @@ public ApiResponse signUp( AuthResDTO.SignUpDTO resDto = authService.signUp(reqDto); return ApiResponse.onSuccess(MemberSuccessCode.CREATED, resDto); } + + @PostMapping("/login") + public ApiResponse login( + @RequestBody @Valid AuthReqDTO.LoginDTO reqDto + ) { + AuthResDTO.LoginDTO resDto = authService.login(reqDto); + return ApiResponse.onSuccess(MemberSuccessCode.LOGIN_SUCCESS, resDto); + } } diff --git a/src/main/java/umc/domain/auth/dto/AuthReqDTO.java b/src/main/java/umc/domain/auth/dto/AuthReqDTO.java index 29d9a81..1e76a46 100644 --- a/src/main/java/umc/domain/auth/dto/AuthReqDTO.java +++ b/src/main/java/umc/domain/auth/dto/AuthReqDTO.java @@ -43,4 +43,12 @@ public record FoodPreferenceDTO( Long foodId ) {} } + + public record LoginDTO( + @NotBlank + @Email + String email, + @NotBlank + String password + ) {} } diff --git a/src/main/java/umc/domain/auth/dto/AuthResDTO.java b/src/main/java/umc/domain/auth/dto/AuthResDTO.java index b7dc401..e523461 100644 --- a/src/main/java/umc/domain/auth/dto/AuthResDTO.java +++ b/src/main/java/umc/domain/auth/dto/AuthResDTO.java @@ -14,4 +14,9 @@ public record SignUpDTO( LocalDate birth, String address ) {} + + @Builder + public record LoginDTO( + String accessToken + ) {} } diff --git a/src/main/java/umc/domain/auth/exception/code/AuthErrorCode.java b/src/main/java/umc/domain/auth/exception/code/AuthErrorCode.java index b29baa6..7404c88 100644 --- a/src/main/java/umc/domain/auth/exception/code/AuthErrorCode.java +++ b/src/main/java/umc/domain/auth/exception/code/AuthErrorCode.java @@ -14,6 +14,7 @@ public enum AuthErrorCode implements BaseErrorCode { INVALID_TERM(HttpStatus.BAD_REQUEST, "AUTH404_2", "유효하지 않은 약관입니다."), INVALID_FOOD(HttpStatus.BAD_REQUEST, "AUTH404_3", "유효하지 않은 음식입니다."), TERMS_MISMATCH(HttpStatus.BAD_REQUEST, "AUTH404_4", "모든 약관에 대한 동의 여부가 필요합니다."), + INVALID_LOGIN_FORM(HttpStatus.BAD_REQUEST, "AUTH404_5", "아이디나 비밀번호가 틀렸습니다.") ; private final HttpStatus status; diff --git a/src/main/java/umc/domain/auth/service/AuthService.java b/src/main/java/umc/domain/auth/service/AuthService.java index a6b7592..334e867 100644 --- a/src/main/java/umc/domain/auth/service/AuthService.java +++ b/src/main/java/umc/domain/auth/service/AuthService.java @@ -1,6 +1,10 @@ package umc.domain.auth.service; import lombok.RequiredArgsConstructor; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.authentication.UsernamePasswordAuthenticationToken; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.AuthenticationException; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -15,6 +19,8 @@ import umc.domain.member.repository.FoodRepository; import umc.domain.member.repository.MemberRepository; import umc.domain.member.repository.TermRepository; +import umc.global.security.entity.AuthMember; +import umc.global.security.util.JwtUtil; import java.util.List; import java.util.Map; @@ -30,6 +36,8 @@ public class AuthService { private final PasswordEncoder passwordEncoder; private final TermRepository termRepository; private final FoodRepository foodRepository; + private final AuthenticationManager authenticationManager; + private final JwtUtil jwtUtil; @Transactional public AuthResDTO.SignUpDTO signUp(AuthReqDTO.SignUpDTO reqDto) { @@ -47,6 +55,22 @@ public AuthResDTO.SignUpDTO signUp(AuthReqDTO.SignUpDTO reqDto) { return AuthConverter.toSignUpDTO(member); } + public AuthResDTO.LoginDTO login(AuthReqDTO.LoginDTO reqDto) { + Authentication authentication; + + try { + authentication = authenticationManager.authenticate( + new UsernamePasswordAuthenticationToken(reqDto.email(), reqDto.password()) + ); + } catch (AuthenticationException e) { + throw new AuthException(AuthErrorCode.INVALID_LOGIN_FORM); + } + + String accessToken = jwtUtil.createAccessToken((AuthMember) authentication.getPrincipal()); + + return new AuthResDTO.LoginDTO(accessToken); + } + private void addTermsToMember(Member member, List termDTOs) { List allTerms = termRepository.findAll(); diff --git a/src/main/java/umc/domain/member/exception/code/MemberSuccessCode.java b/src/main/java/umc/domain/member/exception/code/MemberSuccessCode.java index 1849b53..c935db0 100644 --- a/src/main/java/umc/domain/member/exception/code/MemberSuccessCode.java +++ b/src/main/java/umc/domain/member/exception/code/MemberSuccessCode.java @@ -12,6 +12,7 @@ public enum MemberSuccessCode implements BaseSuccessCode { CREATED(HttpStatus.CREATED, "MEMBER201_1", "회원 가입이 완료되었습니다."), HOME_VIEW(HttpStatus.OK, "MEMBER200_1", "홈 화면이 성공적으로 조회되었습니다."), MY_PAGE_VIEW(HttpStatus.OK, "MEMBER200_2", "마이페이지가 성공적으로 조회되었습니다."), + LOGIN_SUCCESS(HttpStatus.OK, "MEMBER200_3", "로그인이 성공적으로 완료되었습니다."), ; private final HttpStatus status; diff --git a/src/main/java/umc/global/config/SecurityConfig.java b/src/main/java/umc/global/config/SecurityConfig.java index ede0bec..22d744c 100644 --- a/src/main/java/umc/global/config/SecurityConfig.java +++ b/src/main/java/umc/global/config/SecurityConfig.java @@ -1,20 +1,28 @@ package umc.global.config; +import lombok.RequiredArgsConstructor; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.security.authentication.AuthenticationManager; +import org.springframework.security.config.annotation.authentication.configuration.AuthenticationConfiguration; 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 umc.global.security.CustomAccessDenied; -import umc.global.security.CustomEntryPoint; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import umc.global.security.exception.CustomAccessDenied; +import umc.global.security.exception.CustomEntryPoint; +import umc.global.security.filter.JwtAuthFilter; @EnableWebSecurity @Configuration +@RequiredArgsConstructor public class SecurityConfig { + private final JwtAuthFilter jwtAuthFilter; + private final String[] allowUris = { // Swagger 허용 "/swagger-ui/**", @@ -31,10 +39,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers(allowUris).permitAll() .anyRequest().authenticated() ) - .formLogin(form -> form - .defaultSuccessUrl("/swagger-ui/index.html", true) - .permitAll() - ) + .formLogin(AbstractHttpConfigurer::disable) + .sessionManagement(AbstractHttpConfigurer::disable) + .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") @@ -62,4 +69,11 @@ public CustomAccessDenied customAccessDenied() { public CustomEntryPoint customEntryPoint() { return new CustomEntryPoint(); } + + @Bean + public AuthenticationManager authenticationManager( + AuthenticationConfiguration configuration + ) throws Exception { + return configuration.getAuthenticationManager(); + } } diff --git a/src/main/java/umc/global/security/filter/JwtAuthFilter.java b/src/main/java/umc/global/security/filter/JwtAuthFilter.java new file mode 100644 index 0000000..4f7a489 --- /dev/null +++ b/src/main/java/umc/global/security/filter/JwtAuthFilter.java @@ -0,0 +1,72 @@ +package umc.global.security.filter; + +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 lombok.NonNull; +import lombok.RequiredArgsConstructor; +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.stereotype.Component; +import org.springframework.web.filter.OncePerRequestFilter; +import umc.global.apiPayload.ApiResponse; +import umc.global.apiPayload.code.BaseErrorCode; +import umc.global.apiPayload.code.GeneralErrorCode; +import umc.global.security.service.CustomUserDetailsService; +import umc.global.security.util.JwtUtil; + +import java.io.IOException; + +@RequiredArgsConstructor +@Component +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"); + + if (token == null || !token.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + + token = token.replace("Bearer ", ""); + if (jwtUtil.isValid(token)) { + String email = jwtUtil.getEmail(token); + + UserDetails user = customUserDetailsService.loadUserByUsername(email); + Authentication auth = new UsernamePasswordAuthenticationToken( + user, + null, + user.getAuthorities() + ); + + 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); + } + } +} diff --git a/src/main/java/umc/global/security/util/JwtUtil.java b/src/main/java/umc/global/security/util/JwtUtil.java new file mode 100644 index 0000000..e343310 --- /dev/null +++ b/src/main/java/umc/global/security/util/JwtUtil.java @@ -0,0 +1,89 @@ +package umc.global.security.util; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; +import umc.global.security.entity.AuthMember; + +import javax.crypto.SecretKey; +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.stream.Collectors; + +@Component +public class JwtUtil { + + private final SecretKey secretKey; + private final Duration accessExpiration; + + public JwtUtil( + @Value("${spring.jwt.token.secretKey}") String secret, + @Value("${spring.jwt.token.expiration.access}") Long accessExpiration + ) { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.accessExpiration = Duration.ofMillis(accessExpiration); + } + + 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()) + .claim("role", authorities) + .claim("email", member.getUsername()) + .issuedAt(Date.from(now)) + .expiration(Date.from(now.plus(expiration))) + .signWith(secretKey) + .compact(); + } + + private Jws getClaims(String token) throws JwtException { + return Jwts.parser() + .verifyWith(secretKey) + .clockSkewSeconds(60) + .build() + .parseSignedClaims(token); + } +} From c6ea5a07d6425b4ce92d355a9dafb4f9e46668f7 Mon Sep 17 00:00:00 2001 From: zeoueon Date: Sun, 24 May 2026 13:57:50 +0900 Subject: [PATCH 04/11] =?UTF-8?q?feat:=20=EB=A7=88=EC=9D=B4=ED=8E=98?= =?UTF-8?q?=EC=9D=B4=EC=A7=80=20API=EC=97=90=20@AuthenticationPrincipal=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 --- .../member/controller/MemberController.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/umc/domain/member/controller/MemberController.java b/src/main/java/umc/domain/member/controller/MemberController.java index 929d9f2..352f348 100644 --- a/src/main/java/umc/domain/member/controller/MemberController.java +++ b/src/main/java/umc/domain/member/controller/MemberController.java @@ -1,16 +1,19 @@ package umc.domain.member.controller; -import jakarta.validation.Valid; import jakarta.validation.constraints.Min; import jakarta.validation.constraints.NotBlank; import lombok.RequiredArgsConstructor; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.validation.annotation.Validated; -import org.springframework.web.bind.annotation.*; -import umc.domain.member.dto.MemberReqDTO; +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; import umc.domain.member.dto.MemberResDTO; import umc.domain.member.exception.code.MemberSuccessCode; import umc.domain.member.service.MemberService; import umc.global.apiPayload.ApiResponse; +import umc.global.security.entity.AuthMember; import java.time.LocalDate; @@ -40,8 +43,10 @@ public ApiResponse getHome( } @GetMapping("/me") - public ApiResponse getMyPage(){ - MemberResDTO.MyPageViewDTO resDto = memberService.getMyPage(1L); + public ApiResponse getMyPage( + @AuthenticationPrincipal AuthMember authMember + ){ + MemberResDTO.MyPageViewDTO resDto = memberService.getMyPage(authMember.getMember().getId()); return ApiResponse.onSuccess(MemberSuccessCode.MY_PAGE_VIEW, resDto); } } From 6186e3ed7514a404e24ff844139984048571821a Mon Sep 17 00:00:00 2001 From: zeoueon Date: Mon, 25 May 2026 01:58:16 +0900 Subject: [PATCH 05/11] =?UTF-8?q?refactor:=20ObjectMapper=20=EC=8B=B1?= =?UTF-8?q?=EA=B8=80=ED=86=A4=20=EB=B9=88=EC=9C=BC=EB=A1=9C=20=EA=B4=80?= =?UTF-8?q?=EB=A6=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/umc/global/config/AppConfig.java | 14 ++++++++++++++ .../security/exception/CustomAccessDenied.java | 12 +++++++----- .../security/exception/CustomEntryPoint.java | 12 +++++++----- .../umc/global/security/filter/JwtAuthFilter.java | 6 +++--- 4 files changed, 31 insertions(+), 13 deletions(-) create mode 100644 src/main/java/umc/global/config/AppConfig.java diff --git a/src/main/java/umc/global/config/AppConfig.java b/src/main/java/umc/global/config/AppConfig.java new file mode 100644 index 0000000..88c60fe --- /dev/null +++ b/src/main/java/umc/global/config/AppConfig.java @@ -0,0 +1,14 @@ +package umc.global.config; + +import com.fasterxml.jackson.databind.ObjectMapper; +import org.springframework.context.annotation.Bean; +import org.springframework.context.annotation.Configuration; + +@Configuration +public class AppConfig { + + @Bean + public ObjectMapper objectMapper() { + return new ObjectMapper(); + } +} diff --git a/src/main/java/umc/global/security/exception/CustomAccessDenied.java b/src/main/java/umc/global/security/exception/CustomAccessDenied.java index 93947e8..f179e36 100644 --- a/src/main/java/umc/global/security/exception/CustomAccessDenied.java +++ b/src/main/java/umc/global/security/exception/CustomAccessDenied.java @@ -4,33 +4,35 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; import org.springframework.security.access.AccessDeniedException; import org.springframework.security.web.access.AccessDeniedHandler; +import org.springframework.stereotype.Component; import umc.global.apiPayload.ApiResponse; import umc.global.apiPayload.code.BaseErrorCode; import umc.global.apiPayload.code.GeneralErrorCode; import java.io.IOException; +@Component +@RequiredArgsConstructor public class CustomAccessDenied implements AccessDeniedHandler { + private final ObjectMapper objectMapper; + @Override public void handle( HttpServletRequest request, HttpServletResponse response, AccessDeniedException accessDeniedException ) throws IOException, ServletException { - ObjectMapper objectMapper = new ObjectMapper(); BaseErrorCode code = GeneralErrorCode.FORBIDDEN; - // 응답 Content-Type, HTTP 상태코드 정의 response.setContentType("application/json;charset=UTF-8"); response.setStatus(code.getStatus().value()); - // Response Body에 응답통일한 객체를 넣기 - ApiResponse errorResponse = ApiResponse.onFailure(code,null); + ApiResponse errorResponse = ApiResponse.onFailure(code, null); - // 실제 Response로 덮어쓰기 objectMapper.writeValue(response.getOutputStream(), errorResponse); } } diff --git a/src/main/java/umc/global/security/exception/CustomEntryPoint.java b/src/main/java/umc/global/security/exception/CustomEntryPoint.java index 5e649dc..270e407 100644 --- a/src/main/java/umc/global/security/exception/CustomEntryPoint.java +++ b/src/main/java/umc/global/security/exception/CustomEntryPoint.java @@ -4,33 +4,35 @@ import jakarta.servlet.ServletException; import jakarta.servlet.http.HttpServletRequest; import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; import org.springframework.security.core.AuthenticationException; import org.springframework.security.web.AuthenticationEntryPoint; +import org.springframework.stereotype.Component; import umc.global.apiPayload.ApiResponse; import umc.global.apiPayload.code.BaseErrorCode; import umc.global.apiPayload.code.GeneralErrorCode; import java.io.IOException; +@Component +@RequiredArgsConstructor public class CustomEntryPoint implements AuthenticationEntryPoint { + private final ObjectMapper objectMapper; + @Override public void commence( HttpServletRequest request, HttpServletResponse response, AuthenticationException authException ) throws IOException, ServletException { - ObjectMapper objectMapper = new ObjectMapper(); BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED; - // 응답 Content-Type, HTTP 상태코드 정의 response.setContentType("application/json;charset=UTF-8"); response.setStatus(code.getStatus().value()); - // Response Body에 응답통일한 객체를 넣기 - ApiResponse errorResponse = ApiResponse.onFailure(code,null); + ApiResponse errorResponse = ApiResponse.onFailure(code, null); - // 실제 Response로 덮어쓰기 objectMapper.writeValue(response.getOutputStream(), errorResponse); } } diff --git a/src/main/java/umc/global/security/filter/JwtAuthFilter.java b/src/main/java/umc/global/security/filter/JwtAuthFilter.java index 4f7a489..bb3ccac 100644 --- a/src/main/java/umc/global/security/filter/JwtAuthFilter.java +++ b/src/main/java/umc/global/security/filter/JwtAuthFilter.java @@ -27,6 +27,7 @@ public class JwtAuthFilter extends OncePerRequestFilter { private final JwtUtil jwtUtil; private final CustomUserDetailsService customUserDetailsService; + private final ObjectMapper objectMapper; @Override protected void doFilterInternal( @@ -58,15 +59,14 @@ protected void doFilterInternal( } 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); + ApiResponse errorResponse = ApiResponse.onFailure(code, null); - mapper.writeValue(response.getOutputStream(), errorResponse); + objectMapper.writeValue(response.getOutputStream(), errorResponse); } } } From cd53eb362fbea3812e0c3b9925f47a28c10199cc Mon Sep 17 00:00:00 2001 From: zeoueon Date: Mon, 25 May 2026 16:18:06 +0900 Subject: [PATCH 06/11] =?UTF-8?q?refactor:=20=EC=9D=91=EB=8B=B5=20?= =?UTF-8?q?=EC=BD=94=EB=93=9C=20enum=20=EC=A0=95=EB=A6=AC=20=EB=B0=8F=20Au?= =?UTF-8?q?thSuccessCode=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../auth/controller/AuthController.java | 6 +++--- .../auth/exception/code/AuthErrorCode.java | 18 +++++++++++------ .../auth/exception/code/AuthSuccessCode.java | 20 ++++++++++++++++++- .../exception/code/MemberSuccessCode.java | 1 - .../apiPayload/code/GeneralSuccessCode.java | 4 ++-- 5 files changed, 36 insertions(+), 13 deletions(-) diff --git a/src/main/java/umc/domain/auth/controller/AuthController.java b/src/main/java/umc/domain/auth/controller/AuthController.java index b642f60..93ecfb2 100644 --- a/src/main/java/umc/domain/auth/controller/AuthController.java +++ b/src/main/java/umc/domain/auth/controller/AuthController.java @@ -8,8 +8,8 @@ import org.springframework.web.bind.annotation.RestController; import umc.domain.auth.dto.AuthReqDTO; import umc.domain.auth.dto.AuthResDTO; +import umc.domain.auth.exception.code.AuthSuccessCode; import umc.domain.auth.service.AuthService; -import umc.domain.member.exception.code.MemberSuccessCode; import umc.global.apiPayload.ApiResponse; @RestController @@ -24,7 +24,7 @@ public ApiResponse signUp( @RequestBody @Valid AuthReqDTO.SignUpDTO reqDto ){ AuthResDTO.SignUpDTO resDto = authService.signUp(reqDto); - return ApiResponse.onSuccess(MemberSuccessCode.CREATED, resDto); + return ApiResponse.onSuccess(AuthSuccessCode.SIGN_UP, resDto); } @PostMapping("/login") @@ -32,6 +32,6 @@ public ApiResponse login( @RequestBody @Valid AuthReqDTO.LoginDTO reqDto ) { AuthResDTO.LoginDTO resDto = authService.login(reqDto); - return ApiResponse.onSuccess(MemberSuccessCode.LOGIN_SUCCESS, resDto); + return ApiResponse.onSuccess(AuthSuccessCode.LOGIN_SUCCESS, resDto); } } diff --git a/src/main/java/umc/domain/auth/exception/code/AuthErrorCode.java b/src/main/java/umc/domain/auth/exception/code/AuthErrorCode.java index 7404c88..ee3342b 100644 --- a/src/main/java/umc/domain/auth/exception/code/AuthErrorCode.java +++ b/src/main/java/umc/domain/auth/exception/code/AuthErrorCode.java @@ -9,12 +9,18 @@ @AllArgsConstructor public enum AuthErrorCode implements BaseErrorCode { - DUPLICATED_EMAIL(HttpStatus.NOT_FOUND, "AUTH400_1", "이미 가입되어있는 이메일입니다."), - REQUIRED_TERM_NOT_AGREED(HttpStatus.BAD_REQUEST, "AUTH404_1", "필수 약관은 동의해야 합니다."), - INVALID_TERM(HttpStatus.BAD_REQUEST, "AUTH404_2", "유효하지 않은 약관입니다."), - INVALID_FOOD(HttpStatus.BAD_REQUEST, "AUTH404_3", "유효하지 않은 음식입니다."), - TERMS_MISMATCH(HttpStatus.BAD_REQUEST, "AUTH404_4", "모든 약관에 대한 동의 여부가 필요합니다."), - INVALID_LOGIN_FORM(HttpStatus.BAD_REQUEST, "AUTH404_5", "아이디나 비밀번호가 틀렸습니다.") + // 400 + REQUIRED_TERM_NOT_AGREED(HttpStatus.BAD_REQUEST, "AUTH400_1", "필수 약관은 동의해야 합니다."), + INVALID_TERM(HttpStatus.BAD_REQUEST, "AUTH400_2", "유효하지 않은 약관입니다."), + INVALID_FOOD(HttpStatus.BAD_REQUEST, "AUTH400_3", "유효하지 않은 음식입니다."), + TERMS_MISMATCH(HttpStatus.BAD_REQUEST, "AUTH400_4", "모든 약관에 대한 동의 여부가 필요합니다."), + NOT_SUPPORT_SOCIAL_PROVIDER(HttpStatus.BAD_REQUEST, "AUTH400_5", "지원하지 않는 소셜 로그인입니다."), + + // 401 + INVALID_LOGIN_FORM(HttpStatus.UNAUTHORIZED, "AUTH401_2", "아이디나 비밀번호가 틀렸습니다."), + + // 409 + DUPLICATED_EMAIL(HttpStatus.CONFLICT, "AUTH409_1", "이미 가입되어있는 이메일입니다."), ; private final HttpStatus status; diff --git a/src/main/java/umc/domain/auth/exception/code/AuthSuccessCode.java b/src/main/java/umc/domain/auth/exception/code/AuthSuccessCode.java index 75af0cb..a5c5e79 100644 --- a/src/main/java/umc/domain/auth/exception/code/AuthSuccessCode.java +++ b/src/main/java/umc/domain/auth/exception/code/AuthSuccessCode.java @@ -1,4 +1,22 @@ package umc.domain.auth.exception.code; -public enum AuthSuccessCode { +import lombok.AllArgsConstructor; +import lombok.Getter; +import org.springframework.http.HttpStatus; +import umc.global.apiPayload.code.BaseSuccessCode; + +@Getter +@AllArgsConstructor +public enum AuthSuccessCode implements BaseSuccessCode { + + // 200 + LOGIN_SUCCESS(HttpStatus.OK, "AUTH200_1", "로그인이 성공적으로 완료되었습니다."), + + // 201 + SIGN_UP(HttpStatus.CREATED, "AUTH201_1", "회원가입이 완료되었습니다."), + ; + + private final HttpStatus status; + private final String code; + private final String message; } diff --git a/src/main/java/umc/domain/member/exception/code/MemberSuccessCode.java b/src/main/java/umc/domain/member/exception/code/MemberSuccessCode.java index c935db0..1849b53 100644 --- a/src/main/java/umc/domain/member/exception/code/MemberSuccessCode.java +++ b/src/main/java/umc/domain/member/exception/code/MemberSuccessCode.java @@ -12,7 +12,6 @@ public enum MemberSuccessCode implements BaseSuccessCode { CREATED(HttpStatus.CREATED, "MEMBER201_1", "회원 가입이 완료되었습니다."), HOME_VIEW(HttpStatus.OK, "MEMBER200_1", "홈 화면이 성공적으로 조회되었습니다."), MY_PAGE_VIEW(HttpStatus.OK, "MEMBER200_2", "마이페이지가 성공적으로 조회되었습니다."), - LOGIN_SUCCESS(HttpStatus.OK, "MEMBER200_3", "로그인이 성공적으로 완료되었습니다."), ; private final HttpStatus status; diff --git a/src/main/java/umc/global/apiPayload/code/GeneralSuccessCode.java b/src/main/java/umc/global/apiPayload/code/GeneralSuccessCode.java index cad72ba..a3d65d6 100755 --- a/src/main/java/umc/global/apiPayload/code/GeneralSuccessCode.java +++ b/src/main/java/umc/global/apiPayload/code/GeneralSuccessCode.java @@ -11,9 +11,9 @@ public enum GeneralSuccessCode implements BaseSuccessCode { OK(HttpStatus.OK, "SUCCESS200_1", "요청이 성공적으로 처리되었습니다."), CREATED(HttpStatus.CREATED, "SUCCESS201_1", "리소스가 성공적으로 생성되었습니다."), ACCEPTED(HttpStatus.ACCEPTED, "SUCCESS202_1", "요청이 수락되었으며, 처리가 진행 중입니다."), - NO_CONTENT(HttpStatus.NO_CONTENT, "SUCCESS203_1", "요청은 성공했으나, 반환할 콘텐츠가 없습니다."); + NO_CONTENT(HttpStatus.NO_CONTENT, "SUCCESS204_1", "요청은 성공했으나, 반환할 콘텐츠가 없습니다."); private final HttpStatus status; private final String code; private final String message; -} \ No newline at end of file +} From ad73409dbf166ca6776694c37fd3bc4af175ae75 Mon Sep 17 00:00:00 2001 From: zeoueon Date: Mon, 25 May 2026 16:19:20 +0900 Subject: [PATCH 07/11] =?UTF-8?q?chore:=20Security=20=EC=84=A4=EC=A0=95=20?= =?UTF-8?q?OAuth2=20=EC=A0=81=EC=9A=A9=20=EB=B0=8F=20yml=20=EC=84=A4?= =?UTF-8?q?=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 3 ++ .../umc/global/config/SecurityConfig.java | 36 ++++++++++--------- src/main/resources/application.yml | 20 +++++++++++ 3 files changed, 43 insertions(+), 16 deletions(-) diff --git a/build.gradle b/build.gradle index bb3016b..52c4bbf 100644 --- a/build.gradle +++ b/build.gradle @@ -42,6 +42,9 @@ dependencies { 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') { diff --git a/src/main/java/umc/global/config/SecurityConfig.java b/src/main/java/umc/global/config/SecurityConfig.java index 22d744c..a08664f 100644 --- a/src/main/java/umc/global/config/SecurityConfig.java +++ b/src/main/java/umc/global/config/SecurityConfig.java @@ -8,6 +8,7 @@ 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.config.http.SessionCreationPolicy; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; @@ -15,6 +16,8 @@ import umc.global.security.exception.CustomAccessDenied; import umc.global.security.exception.CustomEntryPoint; import umc.global.security.filter.JwtAuthFilter; +import umc.global.security.filter.OAuthSuccessHandler; +import umc.global.security.service.CustomOAuth2UserService; @EnableWebSecurity @Configuration @@ -22,17 +25,21 @@ public class SecurityConfig { private final JwtAuthFilter jwtAuthFilter; + private final OAuthSuccessHandler oAuthSuccessHandler; + private final CustomEntryPoint customEntryPoint; + private final CustomAccessDenied customAccessDenied; private final String[] allowUris = { - // Swagger 허용 "/swagger-ui/**", "/swagger-resources/**", "/v3/api-docs/**", - "/auth/**" + "/auth/**", + "/oauth2/**", + "/login/oauth2/**" }; @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http, CustomOAuth2UserService customOAuth2UserService) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(requests -> requests @@ -40,7 +47,14 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .anyRequest().authenticated() ) .formLogin(AbstractHttpConfigurer::disable) - .sessionManagement(AbstractHttpConfigurer::disable) + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.STATELESS)) + .oauth2Login(oauth2 -> oauth2 + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuth2UserService) + ) + .successHandler(oAuthSuccessHandler) + ) .addFilterBefore(jwtAuthFilter, UsernamePasswordAuthenticationFilter.class) .logout(logout -> logout .logoutUrl("/logout") @@ -48,8 +62,8 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .permitAll() ) .exceptionHandling(exception -> exception - .accessDeniedHandler(customAccessDenied()) - .authenticationEntryPoint(customEntryPoint()) + .accessDeniedHandler(customAccessDenied) + .authenticationEntryPoint(customEntryPoint) ); return http.build(); @@ -60,16 +74,6 @@ public PasswordEncoder passwordEncoder() { return new BCryptPasswordEncoder(); } - @Bean - public CustomAccessDenied customAccessDenied() { - return new CustomAccessDenied(); - } - - @Bean - public CustomEntryPoint customEntryPoint() { - return new CustomEntryPoint(); - } - @Bean public AuthenticationManager authenticationManager( AuthenticationConfiguration configuration diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 400b5f9..b882c06 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -23,3 +23,23 @@ spring: secretKey: ${JWT_SECRET_KEY} expiration: access: 1800000 # 30분 + + security: + oauth2: + client: + registration: + kakao: + client-id: ${KAKAO_REST_API_KEY} + client-secret: ${KAKAO_REST_API_SECRET} + client-authentication-method: client_secret_post + authorization-grant-type: authorization_code + redirect-uri: "http://localhost:8080/login/oauth2/code/kakao" + 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" + userNameAttribute: id From adb2050ec08cdf6a570013ca2547fd889ff6574f Mon Sep 17 00:00:00 2001 From: zeoueon Date: Mon, 25 May 2026 16:20:06 +0900 Subject: [PATCH 08/11] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20?= =?UTF-8?q?=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EA=B5=AC?= =?UTF-8?q?=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/converter/MemberConverter.java | 12 ++++ .../java/umc/domain/member/entity/Member.java | 10 ++- .../member/repository/MemberRepository.java | 3 + .../umc/global/security/dto/KakaoDTO.java | 32 ++++++++++ .../umc/global/security/dto/OAuthDTO.java | 10 +++ .../global/security/entity/OAuthMember.java | 35 +++++++++++ .../security/filter/OAuthSuccessHandler.java | 51 +++++++++++++++ .../service/CustomOAuth2UserService.java | 62 +++++++++++++++++++ 8 files changed, 212 insertions(+), 3 deletions(-) create mode 100644 src/main/java/umc/global/security/dto/KakaoDTO.java create mode 100644 src/main/java/umc/global/security/dto/OAuthDTO.java create mode 100644 src/main/java/umc/global/security/entity/OAuthMember.java create mode 100644 src/main/java/umc/global/security/filter/OAuthSuccessHandler.java create mode 100644 src/main/java/umc/global/security/service/CustomOAuth2UserService.java diff --git a/src/main/java/umc/domain/member/converter/MemberConverter.java b/src/main/java/umc/domain/member/converter/MemberConverter.java index aaf2f9f..c1ba3c1 100644 --- a/src/main/java/umc/domain/member/converter/MemberConverter.java +++ b/src/main/java/umc/domain/member/converter/MemberConverter.java @@ -2,8 +2,10 @@ import umc.domain.member.dto.MemberResDTO; import umc.domain.member.entity.Member; +import umc.domain.member.enums.Gender; import umc.domain.mission.entity.Mission; import umc.domain.region.entity.Region; +import umc.global.security.dto.OAuthDTO; import java.time.LocalDate; import java.time.temporal.ChronoUnit; @@ -61,4 +63,14 @@ public static MemberResDTO.HomeViewDTO toHomeViewDTO( .hasNext(hasNextPage) .build(); } + + public static Member toMember(OAuthDTO oAuthDTO) { + return Member.builder() + .name(oAuthDTO.getName()) + .email(oAuthDTO.getEmail()) + .socialUid(oAuthDTO.getSocialUid()) + .socialType(oAuthDTO.getSocialType()) + .gender(Gender.NONE) + .build(); + } } diff --git a/src/main/java/umc/domain/member/entity/Member.java b/src/main/java/umc/domain/member/entity/Member.java index 20a5c3c..1edf58a 100644 --- a/src/main/java/umc/domain/member/entity/Member.java +++ b/src/main/java/umc/domain/member/entity/Member.java @@ -1,13 +1,14 @@ package umc.domain.member.entity; import jakarta.persistence.*; -import lombok.*; +import lombok.AllArgsConstructor; +import lombok.Builder; +import lombok.Getter; +import lombok.NoArgsConstructor; import umc.domain.member.entity.mapping.MemberFood; import umc.domain.member.entity.mapping.MemberTerm; import umc.domain.member.enums.Gender; import umc.domain.member.enums.SocialType; -import umc.domain.mission.entity.mapping.MemberMission; -import umc.domain.review.entity.Review; import umc.global.entity.BaseEntity; import java.time.LocalDate; @@ -61,6 +62,9 @@ public class Member extends BaseEntity { @Column(name = "social_type") private SocialType socialType; + @Column(name = "social_uid") + private String socialUid; + @OneToMany(mappedBy = "member", cascade = CascadeType.ALL, orphanRemoval = true) @Builder.Default private List memberTerms = new ArrayList<>(); diff --git a/src/main/java/umc/domain/member/repository/MemberRepository.java b/src/main/java/umc/domain/member/repository/MemberRepository.java index 831731e..f1a5dd4 100644 --- a/src/main/java/umc/domain/member/repository/MemberRepository.java +++ b/src/main/java/umc/domain/member/repository/MemberRepository.java @@ -3,6 +3,7 @@ import org.springframework.data.jpa.repository.JpaRepository; import org.springframework.stereotype.Repository; import umc.domain.member.entity.Member; +import umc.domain.member.enums.SocialType; import java.util.Optional; @@ -10,4 +11,6 @@ public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); + + Optional findBySocialTypeAndSocialUid(SocialType providerId, String socialUid); } diff --git a/src/main/java/umc/global/security/dto/KakaoDTO.java b/src/main/java/umc/global/security/dto/KakaoDTO.java new file mode 100644 index 0000000..ed5c4ef --- /dev/null +++ b/src/main/java/umc/global/security/dto/KakaoDTO.java @@ -0,0 +1,32 @@ +package umc.global.security.dto; + +import lombok.RequiredArgsConstructor; +import umc.domain.member.enums.SocialType; + +@RequiredArgsConstructor +public class KakaoDTO implements OAuthDTO { + + private final String id; + private final String email; + private final String name; + + @Override + public SocialType getSocialType() { + return SocialType.KAKAO; + } + + @Override + public String getSocialUid() { + return id; + } + + @Override + public String getEmail() { + return email; + } + + @Override + public String getName() { + return name; + } +} diff --git a/src/main/java/umc/global/security/dto/OAuthDTO.java b/src/main/java/umc/global/security/dto/OAuthDTO.java new file mode 100644 index 0000000..274cba7 --- /dev/null +++ b/src/main/java/umc/global/security/dto/OAuthDTO.java @@ -0,0 +1,10 @@ +package umc.global.security.dto; + +import umc.domain.member.enums.SocialType; + +public interface OAuthDTO { + SocialType getSocialType(); + String getSocialUid(); + String getEmail(); + String getName(); +} diff --git a/src/main/java/umc/global/security/entity/OAuthMember.java b/src/main/java/umc/global/security/entity/OAuthMember.java new file mode 100644 index 0000000..46a8a84 --- /dev/null +++ b/src/main/java/umc/global/security/entity/OAuthMember.java @@ -0,0 +1,35 @@ +package umc.global.security.entity; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; +import umc.domain.member.entity.Member; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +@RequiredArgsConstructor +public class OAuthMember implements OAuth2User { + + @Getter + private final Member member; + private final Map attributes; + + + @Override + public Map getAttributes() { + return attributes; + } + + @Override + public Collection getAuthorities() { + return List.of(); + } + + @Override + public String getName() { + return member.getSocialUid(); + } +} diff --git a/src/main/java/umc/global/security/filter/OAuthSuccessHandler.java b/src/main/java/umc/global/security/filter/OAuthSuccessHandler.java new file mode 100644 index 0000000..f751b0b --- /dev/null +++ b/src/main/java/umc/global/security/filter/OAuthSuccessHandler.java @@ -0,0 +1,51 @@ +package umc.global.security.filter; + +import com.fasterxml.jackson.databind.ObjectMapper; +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +import org.springframework.security.web.authentication.AuthenticationSuccessHandler; +import org.springframework.stereotype.Component; +import umc.domain.auth.dto.AuthResDTO; +import umc.domain.auth.exception.code.AuthSuccessCode; +import umc.global.apiPayload.ApiResponse; +import umc.global.apiPayload.code.BaseSuccessCode; +import umc.global.security.entity.AuthMember; +import umc.global.security.entity.OAuthMember; +import umc.global.security.util.JwtUtil; + +import java.io.IOException; + +@RequiredArgsConstructor +@Component +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 { + BaseSuccessCode code = AuthSuccessCode.LOGIN_SUCCESS; + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + OAuthMember member = (OAuthMember) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + String accessToken = jwtUtil.createAccessToken(new AuthMember(member.getMember())); + + ApiResponse responseBody = ApiResponse.onSuccess( + code, + new AuthResDTO.LoginDTO(accessToken) + ); + + objectMapper.writeValue(response.getOutputStream(), responseBody); + } +} diff --git a/src/main/java/umc/global/security/service/CustomOAuth2UserService.java b/src/main/java/umc/global/security/service/CustomOAuth2UserService.java new file mode 100644 index 0000000..fdbb792 --- /dev/null +++ b/src/main/java/umc/global/security/service/CustomOAuth2UserService.java @@ -0,0 +1,62 @@ +package umc.global.security.service; + +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.user.OAuth2User; +import org.springframework.stereotype.Service; +import umc.domain.auth.exception.AuthException; +import umc.domain.auth.exception.code.AuthErrorCode; +import umc.domain.member.converter.MemberConverter; +import umc.domain.member.entity.Member; +import umc.domain.member.enums.SocialType; +import umc.domain.member.repository.MemberRepository; +import umc.global.security.dto.KakaoDTO; +import umc.global.security.dto.OAuthDTO; +import umc.global.security.entity.OAuthMember; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class CustomOAuth2UserService extends DefaultOAuth2UserService { + + private final MemberRepository memberRepository; + + @Override + public OAuth2User loadUser( + OAuth2UserRequest userRequest + ) throws OAuth2AuthenticationException { + OAuth2User oAuth2User = super.loadUser(userRequest); + + SocialType providerId; + String socialUid; + Map attributes = oAuth2User.getAttribute("kakao_account"); + Map profile = (Map) attributes.get("profile"); + try { + providerId = SocialType.valueOf(userRequest.getClientRegistration().getRegistrationId().toUpperCase()); + socialUid = String.valueOf((Long) oAuth2User.getAttribute("id")); + } catch (IllegalArgumentException e) { + throw new AuthException(AuthErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER); + } + + OAuthDTO dto; + switch (providerId) { + case KAKAO -> { + String email = attributes.get("email").toString(); + String name = profile.get("nickname").toString(); + dto = new KakaoDTO(socialUid, email, name); + } + default -> throw new AuthException(AuthErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER); + } + + Member member = memberRepository.findBySocialTypeAndSocialUid(providerId, socialUid) + .orElseGet(() -> { + Member newMember = MemberConverter.toMember(dto); + memberRepository.save(newMember); + return newMember; + }); + return new OAuthMember(member, oAuth2User.getAttributes()); + } +} From 031a3be7151540122fe1bbbcf87ee6a4d2ca2048 Mon Sep 17 00:00:00 2001 From: zeoueon Date: Mon, 25 May 2026 23:12:14 +0900 Subject: [PATCH 09/11] =?UTF-8?q?fix:=20=ED=9A=8C=EC=9B=90=EA=B0=80?= =?UTF-8?q?=EC=9E=85=20=EC=8B=9C=20=EB=AA=A8=EB=93=A0=20=EC=95=BD=EA=B4=80?= =?UTF-8?q?=EC=97=90=20=EB=8C=80=ED=95=9C=20=EC=9E=85=EB=A0=A5=EC=9D=84=20?= =?UTF-8?q?=EA=B2=80=EC=A6=9D=ED=95=98=EC=A7=80=20=EB=AA=BB=ED=95=98?= =?UTF-8?q?=EB=8A=94=20=EC=98=A4=EB=A5=98=20=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/umc/domain/auth/service/AuthService.java | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/main/java/umc/domain/auth/service/AuthService.java b/src/main/java/umc/domain/auth/service/AuthService.java index 334e867..44f4113 100644 --- a/src/main/java/umc/domain/auth/service/AuthService.java +++ b/src/main/java/umc/domain/auth/service/AuthService.java @@ -82,7 +82,7 @@ private void addTermsToMember(Member member, List .map(AuthReqDTO.SignUpDTO.TermDTO::termId) .collect(Collectors.toSet()); - if (!allTermIds.containsAll(requestedTermIds)) { + if (termDTOs.size() != requestedTermIds.size() || !allTermIds.equals(requestedTermIds)) { throw new AuthException(AuthErrorCode.TERMS_MISMATCH); } From 4bd0416ba7c417bb14d9b0fb0c605c60532e2a09 Mon Sep 17 00:00:00 2001 From: zeoueon Date: Mon, 25 May 2026 23:14:41 +0900 Subject: [PATCH 10/11] =?UTF-8?q?refactor:=20=EC=84=A0=ED=98=B8=20?= =?UTF-8?q?=EC=9D=8C=EC=8B=9D=20=EC=A1=B0=ED=9A=8C=20=EC=BF=BC=EB=A6=AC=20?= =?UTF-8?q?=EC=B5=9C=EC=A0=81=ED=99=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../java/umc/domain/auth/service/AuthService.java | 15 ++++++++++----- 1 file changed, 10 insertions(+), 5 deletions(-) diff --git a/src/main/java/umc/domain/auth/service/AuthService.java b/src/main/java/umc/domain/auth/service/AuthService.java index 44f4113..16eb80b 100644 --- a/src/main/java/umc/domain/auth/service/AuthService.java +++ b/src/main/java/umc/domain/auth/service/AuthService.java @@ -99,10 +99,15 @@ private void addTermsToMember(Member member, List } private void addFoodsToMember(Member member, List foodDTOs) { - foodDTOs.forEach(foodDTO -> { - Food food = foodRepository.findById(foodDTO.foodId()) - .orElseThrow(() -> new AuthException(AuthErrorCode.INVALID_FOOD)); - member.addPreferenceFood(food); - }); + List foodIds = foodDTOs.stream() + .map(AuthReqDTO.SignUpDTO.FoodPreferenceDTO::foodId) + .toList(); + + List foods = foodRepository.findAllById(foodIds); + if (foodIds.size() != foods.size()) { + throw new AuthException(AuthErrorCode.INVALID_FOOD); + } + + foods.forEach(member::addPreferenceFood); } } From 04485977a3f902bee4c0175b370ce41d8c146ad8 Mon Sep 17 00:00:00 2001 From: zeoueon Date: Wed, 27 May 2026 13:13:33 +0900 Subject: [PATCH 11/11] =?UTF-8?q?fix:=20=EC=95=BD=EA=B4=80=20=EB=B9=88=20?= =?UTF-8?q?=EA=B0=92=20=EA=B2=80=EC=A6=9D=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/main/java/umc/domain/auth/dto/AuthReqDTO.java | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/main/java/umc/domain/auth/dto/AuthReqDTO.java b/src/main/java/umc/domain/auth/dto/AuthReqDTO.java index 1e76a46..37188a5 100644 --- a/src/main/java/umc/domain/auth/dto/AuthReqDTO.java +++ b/src/main/java/umc/domain/auth/dto/AuthReqDTO.java @@ -1,10 +1,7 @@ package umc.domain.auth.dto; import jakarta.validation.Valid; -import jakarta.validation.constraints.Email; -import jakarta.validation.constraints.NotBlank; -import jakarta.validation.constraints.NotNull; -import jakarta.validation.constraints.Pattern; +import jakarta.validation.constraints.*; import umc.domain.member.enums.Gender; import java.util.List; @@ -27,6 +24,7 @@ public record SignUpDTO( @NotBlank String address, @Valid + @NotEmpty List terms, @Valid List foodPreferences