From ac02ae1e56691ba615958a286b274ade0d71306f Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Fri, 22 May 2026 16:57:42 +0900 Subject: [PATCH 01/22] =?UTF-8?q?setting:=20JWT=20=EB=B0=8F=20Spring=20Sec?= =?UTF-8?q?urity=20=EC=9D=98=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jinyong/build.gradle | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/Jinyong/build.gradle b/Jinyong/build.gradle index d4404db0..ddb4c098 100644 --- a/Jinyong/build.gradle +++ b/Jinyong/build.gradle @@ -38,6 +38,12 @@ dependencies { // Security implementation 'org.springframework.boot:spring-boot-starter-security' testImplementation 'org.springframework.security:spring-security-test' + + // Jwt + implementation 'io.jsonwebtoken:jjwt-api:0.12.3' + implementation 'io.jsonwebtoken:jjwt-impl:0.12.3' + implementation 'io.jsonwebtoken:jjwt-jackson:0.12.3' + implementation 'org.springframework.boot:spring-boot-configuration-processor' } tasks.named('test') { From 6365805b8c2699ff80588f7fc12494c5e04c1bfc Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Mon, 25 May 2026 17:10:38 +0900 Subject: [PATCH 02/22] =?UTF-8?q?fix:=20AuthMember=20=EC=82=AC=EC=9A=A9?= =?UTF-8?q?=EC=9E=90=EB=AA=85=20=EB=B0=98=ED=99=98=20=EB=A1=9C=EC=A7=81=20?= =?UTF-8?q?=EC=88=98=EC=A0=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../com/example/umc10th/global/security/entity/AuthMember.java | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jinyong/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java b/Jinyong/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java index 61a32f48..7d94e9ef 100644 --- a/Jinyong/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java +++ b/Jinyong/src/main/java/com/example/umc10th/global/security/entity/AuthMember.java @@ -28,6 +28,9 @@ public Collection getAuthorities() { @Override public String getUsername() { + if (member.getSocialType() != null) { + return member.getSocialUid(); + } return member.getEmail(); } } From 46c2f7e34f3d6d6c6f4f404a50347cf6afb357d6 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Mon, 25 May 2026 17:12:57 +0900 Subject: [PATCH 03/22] =?UTF-8?q?setting:=20OAuth2=20Client=20=EC=9D=98?= =?UTF-8?q?=EC=A1=B4=EC=84=B1=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- Jinyong/build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/Jinyong/build.gradle b/Jinyong/build.gradle index ddb4c098..ca05b025 100644 --- a/Jinyong/build.gradle +++ b/Jinyong/build.gradle @@ -44,6 +44,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') { From 6442e0cd01c9ac305040fc88dc5d6d879339442a Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Mon, 25 May 2026 17:13:56 +0900 Subject: [PATCH 04/22] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20OAu?= =?UTF-8?q?th=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4?= =?UTF-8?q?=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../security/service/CustomOAuthService.java | 89 +++++++++++++++++++ 1 file changed, 89 insertions(+) create mode 100644 Jinyong/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java diff --git a/Jinyong/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java b/Jinyong/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java new file mode 100644 index 00000000..0554467e --- /dev/null +++ b/Jinyong/src/main/java/com/example/umc10th/global/security/service/CustomOAuthService.java @@ -0,0 +1,89 @@ +package com.example.umc10th.global.security.service; + +import com.example.umc10th.domain.member.converter.MemberConverter; +import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.domain.member.exception.MemberException; +import com.example.umc10th.domain.member.exception.code.MemberErrorCode; +import com.example.umc10th.domain.member.repository.MemberRepository; +import com.example.umc10th.global.security.dto.KakaoDTO; +import com.example.umc10th.global.security.dto.OAuthDTO; +import com.example.umc10th.global.security.entity.OAuthMember; +import com.example.umc10th.global.security.entity.SocialType; +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.OAuth2Error; +import org.springframework.security.oauth2.core.OAuth2AuthenticationException; +import org.springframework.security.oauth2.core.user.OAuth2User; +import org.springframework.stereotype.Service; + +import java.util.Map; + +@Service +@RequiredArgsConstructor +public class CustomOAuthService extends DefaultOAuth2UserService { + + private final MemberRepository memberRepository; + + @Override + public OAuth2User loadUser( + OAuth2UserRequest userRequest + ) throws OAuth2AuthenticationException { + // (필수) 인증 서버의 일회성 토큰을 이용해 정보 조회 & 유저 객체 생성 + OAuth2User oAuthMember = super.loadUser(userRequest); + + // 유저 객체에서 정보 추출 + SocialType providerId; + String socialUid; + try { + providerId = SocialType.valueOf(userRequest.getClientRegistration().getRegistrationId().toUpperCase()); + socialUid = getRequiredString(oAuthMember.getAttributes(), "id"); + } catch (IllegalArgumentException e) { + throw new MemberException(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER); + } + + // OAuth 공통 정보 DTO로 매핑 + OAuthDTO dto; + switch (providerId) { + case KAKAO -> { + Map attributes = getRequiredMap(oAuthMember.getAttributes(), "kakao_account"); + Map profile = getRequiredMap(attributes, "profile"); + String email = getRequiredString(attributes, "email"); + String name = getRequiredString(profile, "nickname"); + dto = new KakaoDTO(socialUid, email, name); + } + default -> throw new MemberException(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER); + } + + // DB 저장: 있다면 그 데이터 가져오고 없으면 새로 저장 + Member member = memberRepository.findBySocialTypeAndSocialUid(providerId, socialUid) + .orElseGet(() -> { + Member newMember = MemberConverter.toMember(dto); + memberRepository.save(newMember); + return newMember; + }); + return new OAuthMember(member, oAuthMember.getAttributes()); + } + + private Map getRequiredMap(Map attributes, String key) { + Object value = attributes.get(key); + if (!(value instanceof Map map)) { + throw new OAuth2AuthenticationException( + new OAuth2Error("invalid_kakao_response"), + "Kakao OAuth response missing map: " + key + ); + } + return (Map) map; + } + + private String getRequiredString(Map attributes, String key) { + Object value = attributes.get(key); + if (value == null) { + throw new OAuth2AuthenticationException( + new OAuth2Error("invalid_kakao_response"), + "Kakao OAuth response missing value: " + key + ); + } + return String.valueOf(value); + } +} From 7684c281f7f53a3a93ef84e0120618c73d1a3ae2 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Mon, 25 May 2026 17:14:53 +0900 Subject: [PATCH 05/22] =?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=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../service/CustomUserDetailsService.java | 19 ++++++++++++++++++- 1 file changed, 18 insertions(+), 1 deletion(-) diff --git a/Jinyong/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java b/Jinyong/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java index 7789bfc1..fae2fb99 100644 --- a/Jinyong/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java +++ b/Jinyong/src/main/java/com/example/umc10th/global/security/service/CustomUserDetailsService.java @@ -5,6 +5,7 @@ import com.example.umc10th.domain.member.exception.code.MemberErrorCode; import com.example.umc10th.domain.member.repository.MemberRepository; import com.example.umc10th.global.security.entity.AuthMember; +import com.example.umc10th.global.security.entity.SocialType; import lombok.RequiredArgsConstructor; import org.springframework.security.core.userdetails.UserDetails; import org.springframework.security.core.userdetails.UserDetailsService; @@ -23,6 +24,22 @@ public UserDetails loadUserByUsername( ) throws UsernameNotFoundException { Member member = memberRepository.findByEmail(username) .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + + return new AuthMember(member); + } + + public UserDetails loadUserByUidAndSocialType( + SocialType socialType, + String username + ) throws UsernameNotFoundException { + if (socialType == null) { + return loadUserByUsername(username); + } + + // DB에서 기존 회원 정보 조회 & 인증 객체 생성 + Member member = memberRepository.findBySocialTypeAndSocialUid(socialType, username) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + return new AuthMember(member); } -} \ No newline at end of file +} From 6079171418c25b55b3f8b9a8f93d9677a098fe77 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Mon, 25 May 2026 17:20:57 +0900 Subject: [PATCH 06/22] =?UTF-8?q?feat:=20JWT=20=EC=9D=B8=EC=A6=9D=20?= =?UTF-8?q?=ED=95=84=ED=84=B0=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/security/filter/JwtAuthFilter.java | 79 +++++++++++++++++++ 1 file changed, 79 insertions(+) create mode 100644 Jinyong/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java diff --git a/Jinyong/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java b/Jinyong/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java new file mode 100644 index 00000000..937a73c2 --- /dev/null +++ b/Jinyong/src/main/java/com/example/umc10th/global/security/filter/JwtAuthFilter.java @@ -0,0 +1,79 @@ +package com.example.umc10th.global.security.filter; + +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.example.umc10th.global.security.entity.SocialType; +import com.example.umc10th.global.security.service.CustomUserDetailsService; +import com.example.umc10th.global.security.util.JwtUtil; +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.web.filter.OncePerRequestFilter; + +import java.io.IOException; + +@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)) { + + // JWT 토큰에서 유저 정보 조회: UID와 소셜 로그인 타입 가져오기 + String uid = jwtUtil.getUid(token); + SocialType socialType = jwtUtil.getSocialType(token); + + // 인증 객체 생성: 로그인 타입과 UID로 찾아온 뒤, 인증 객체 생성 + UserDetails member = customUserDetailsService.loadUserByUidAndSocialType(socialType, uid); + Authentication auth = new UsernamePasswordAuthenticationToken( + member, + null, + member.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 4e5e7282e973c30c2c5c476f8a279bd6bdb6d41b Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Mon, 25 May 2026 17:21:45 +0900 Subject: [PATCH 07/22] =?UTF-8?q?feat:=20JWT=20=ED=86=A0=ED=81=B0=20?= =?UTF-8?q?=EC=83=9D=EC=84=B1=20=EB=B0=8F=20=EA=B2=80=EC=A6=9D=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/security/util/JwtUtil.java | 101 ++++++++++++++++++ 1 file changed, 101 insertions(+) create mode 100644 Jinyong/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java diff --git a/Jinyong/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java b/Jinyong/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java new file mode 100644 index 00000000..84d66542 --- /dev/null +++ b/Jinyong/src/main/java/com/example/umc10th/global/security/util/JwtUtil.java @@ -0,0 +1,101 @@ +package com.example.umc10th.global.security.util; + +import com.example.umc10th.global.security.entity.AuthMember; +import com.example.umc10th.global.security.entity.SocialType; +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 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("${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 SocialType getSocialType(String token) { + try { + Object socialType = getClaims(token).getPayload().get("social_type"); + if (socialType == null) { + return null; + } + return SocialType.valueOf(socialType.toString().toUpperCase()); + } 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; + } + } + + public String getUid(String token) { + return getClaims(token).getPayload().getSubject(); + } + + // 토큰 생성 + 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("social_type", member.getMember().getSocialType()) + .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); + } +} From 224a2e2a9520cfe223161d778100b16cc66a9e30 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Mon, 25 May 2026 17:22:29 +0900 Subject: [PATCH 08/22] =?UTF-8?q?feat:=20=EC=B9=B4=EC=B9=B4=EC=98=A4=20OAu?= =?UTF-8?q?th=20=EC=9D=91=EB=8B=B5=20DTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc10th/global/security/dto/KakaoDTO.java | 32 +++++++++++++++++++ 1 file changed, 32 insertions(+) create mode 100644 Jinyong/src/main/java/com/example/umc10th/global/security/dto/KakaoDTO.java diff --git a/Jinyong/src/main/java/com/example/umc10th/global/security/dto/KakaoDTO.java b/Jinyong/src/main/java/com/example/umc10th/global/security/dto/KakaoDTO.java new file mode 100644 index 00000000..7e932521 --- /dev/null +++ b/Jinyong/src/main/java/com/example/umc10th/global/security/dto/KakaoDTO.java @@ -0,0 +1,32 @@ +package com.example.umc10th.global.security.dto; + +import com.example.umc10th.global.security.entity.SocialType; +import lombok.RequiredArgsConstructor; + +@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 getSocialEmail() { + return email; + } + + @Override + public String getName() { + return name; + } +} From 225aa1f4e758a58d3fd932a0c6a86658997b0be4 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Mon, 25 May 2026 17:23:09 +0900 Subject: [PATCH 09/22] =?UTF-8?q?feat:=20Member=20=EC=97=94=ED=8B=B0?= =?UTF-8?q?=ED=8B=B0=EC=97=90=20=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=ED=95=84=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 --- .../com/example/umc10th/domain/member/entity/Member.java | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/entity/Member.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/entity/Member.java index 3c267c60..6319fcf9 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/member/entity/Member.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/entity/Member.java @@ -1,6 +1,7 @@ package com.example.umc10th.domain.member.entity; import com.example.umc10th.domain.BaseEntity; +import com.example.umc10th.global.security.entity.SocialType; import jakarta.persistence.*; import lombok.*; import java.time.LocalDate; @@ -38,4 +39,9 @@ public class Member extends BaseEntity { @Column(nullable = false) private LocalDate birth; -} \ No newline at end of file + + @Enumerated(EnumType.STRING) + private SocialType socialType; + + private String socialUid; +} From 4bfc0598da7fa1f4b1d6d17f8bb5a090c72d72c2 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Mon, 25 May 2026 17:23:54 +0900 Subject: [PATCH 10/22] =?UTF-8?q?feat:=20Member=20=EC=BB=A8=ED=8A=B8?= =?UTF-8?q?=EB=A1=A4=EB=9F=AC=EC=97=90=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EB=B0=8F=20=EB=82=B4=20=EC=A0=95=EB=B3=B4=20=EC=A1=B0=ED=9A=8C?= =?UTF-8?q?=20API=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 38 +++++++++++++++---- 1 file changed, 30 insertions(+), 8 deletions(-) diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java index a9a39488..39c8209b 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/controller/MemberController.java @@ -1,13 +1,21 @@ package com.example.umc10th.domain.member.controller; import com.example.umc10th.domain.member.code.MemberSuccessCode; -import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.dto.MemberReqDTO; +import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.service.MemberService; import com.example.umc10th.global.apiPayload.ApiResponse; import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import com.example.umc10th.global.security.entity.AuthMember; import lombok.RequiredArgsConstructor; -import org.springframework.web.bind.annotation.*; +import org.springframework.http.HttpStatus; +import org.springframework.http.ResponseEntity; +import org.springframework.security.core.annotation.AuthenticationPrincipal; +import org.springframework.web.bind.annotation.GetMapping; +import org.springframework.web.bind.annotation.PostMapping; +import org.springframework.web.bind.annotation.RequestBody; +import org.springframework.web.bind.annotation.RequestMapping; +import org.springframework.web.bind.annotation.RestController; @RestController @RequiredArgsConstructor @@ -16,21 +24,35 @@ public class MemberController { private final MemberService memberService; - // 마이페이지 @GetMapping("/v1/users/me") - public ApiResponse getInfo( - @RequestParam Long memberId + public ResponseEntity> getInfo( + @AuthenticationPrincipal AuthMember authMember ) { BaseSuccessCode code = MemberSuccessCode.OK; - return ApiResponse.onSuccess(code, memberService.getInfo(memberId)); + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.onSuccess(code, memberService.getInfo(authMember))); } // 회원가입 @PostMapping("/v1/auth/signup") - public ApiResponse signUp( + public ResponseEntity> signUp( @RequestBody MemberReqDTO.SignUp request ) { BaseSuccessCode code = MemberSuccessCode.CREATED; - return ApiResponse.onSuccess(code, memberService.signUp(request)); + return ResponseEntity + .status(HttpStatus.CREATED) + .body(ApiResponse.onSuccess(code, memberService.signUp(request))); + } + + // 로그인 + @PostMapping("/v1/auth/login") + public ResponseEntity> login( + @RequestBody MemberReqDTO.Login request + ) { + BaseSuccessCode code = MemberSuccessCode.OK; + return ResponseEntity + .status(HttpStatus.OK) + .body(ApiResponse.onSuccess(code, memberService.login(request))); } } From ec8ac3173ec24dde4f49a7c9badecd03ec10fb79 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Mon, 25 May 2026 17:24:44 +0900 Subject: [PATCH 11/22] =?UTF-8?q?feat:=20MemberConverter=EC=97=90=20?= =?UTF-8?q?=EC=86=8C=EC=85=9C=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20=EB=B3=80?= =?UTF-8?q?=ED=99=98=20=EB=A1=9C=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/converter/MemberConverter.java | 46 ++++++++++++++----- 1 file changed, 34 insertions(+), 12 deletions(-) diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java index 3745b129..52229a26 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/converter/MemberConverter.java @@ -4,6 +4,7 @@ import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.entity.Gender; import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.global.security.dto.OAuthDTO; import java.time.LocalDate; @@ -13,14 +14,14 @@ public class MemberConverter { public static MemberResDTO.GetInfo toGetInfo(Member member) { // 빌더 패턴을 사용하여 엔티티의 데이터를 DTO에 매핑합니다. - return MemberResDTO.GetInfo.builder() - .email(member.getEmail()) - .name(member.getName()) - .point(member.getPoint()) - .nickname(member.getNickname()) - .gender(member.getGender().name()) - .phoneNumber(null) - .build(); + return new MemberResDTO.GetInfo( + member.getName(), + member.getEmail(), + null, + member.getPoint(), + member.getNickname(), + member.getGender().name() + ); } public static Member toMember(MemberReqDTO.SignUp request, String encodedPassword) { @@ -35,10 +36,31 @@ public static Member toMember(MemberReqDTO.SignUp request, String encodedPasswor .build(); } - public static MemberResDTO.SignUp toSignUp(Member member) { - return MemberResDTO.SignUp.builder() - .memberId(member.getId()) - .email(member.getEmail()) + // 회원가입 + public static Member toMember(OAuthDTO dto) { + return Member.builder() + .email(dto.getSocialEmail()) + .password("") + .name(dto.getName()) + .nickname(dto.getName()) + .gender(Gender.NONE) + .point(0) + .birth(LocalDate.now()) + .socialType(dto.getSocialType()) + .socialUid(dto.getSocialUid()) .build(); } + + public static MemberResDTO.SignUp toSignUp(Member member) { + return new MemberResDTO.SignUp( + member.getId(), + member.getEmail() + ); + } + + + // 로그인 + public static MemberResDTO.Login toLogin(String accessToken) { + return new MemberResDTO.Login(accessToken); + } } From 414655b6eee36d633833512bc57c8097ab9375d4 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Mon, 25 May 2026 17:27:00 +0900 Subject: [PATCH 12/22] =?UTF-8?q?feat:=20=ED=9A=8C=EC=9B=90=20=EC=9D=B8?= =?UTF-8?q?=EC=A6=9D=20=EA=B4=80=EB=A0=A8=20=EC=97=90=EB=9F=AC=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 --- .../exception/code/MemberErrorCode.java | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java index c5a98d5f..cdc088f4 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/exception/code/MemberErrorCode.java @@ -1,12 +1,8 @@ package com.example.umc10th.domain.member.exception.code; import com.example.umc10th.global.apiPayload.code.BaseErrorCode; -import lombok.AllArgsConstructor; -import lombok.Getter; import org.springframework.http.HttpStatus; -@Getter -@AllArgsConstructor public enum MemberErrorCode implements BaseErrorCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, @@ -14,14 +10,36 @@ public enum MemberErrorCode implements BaseErrorCode { "해당 사용자를 찾을 수 없습니다."), MEMBER_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER409_1", - "이미 존재하는 이메일입니다."); + "이미 존재하는 이메일입니다."), + INVALID_PASSWORD(HttpStatus.UNAUTHORIZED, + "MEMBER401_1", + "비밀번호가 일치하지 않습니다."), + NOT_SUPPORT_SOCIAL_PROVIDER(HttpStatus.BAD_REQUEST, + "MEMBER400_1", + "지원하지 않는 소셜 로그인 제공자입니다."); private final HttpStatus httpStatus; private final String code; private final String message; + MemberErrorCode(HttpStatus httpStatus, String code, String message) { + this.httpStatus = httpStatus; + this.code = code; + this.message = message; + } + @Override public HttpStatus getStatus() { return httpStatus; } -} + + @Override + public String getCode() { + return code; + } + + @Override + public String getMessage() { + return message; + } +} \ No newline at end of file From 1699bd36b654666c77d0bffb77c733c482c13ce3 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Mon, 25 May 2026 17:27:33 +0900 Subject: [PATCH 13/22] =?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=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EB=A9=94=EC=84=9C=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 --- .../umc10th/domain/member/repository/MemberRepository.java | 2 ++ 1 file changed, 2 insertions(+) diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java index 0b676788..f83523b9 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/repository/MemberRepository.java @@ -1,10 +1,12 @@ package com.example.umc10th.domain.member.repository; import com.example.umc10th.domain.member.entity.Member; +import com.example.umc10th.global.security.entity.SocialType; import org.springframework.data.jpa.repository.JpaRepository; import java.util.Optional; public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); + Optional findBySocialTypeAndSocialUid(SocialType socialType, String socialUid); } From 6c7e3e4cc0638a32248e050efeabdaa10dc5ea88 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Mon, 25 May 2026 17:35:42 +0900 Subject: [PATCH 14/22] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=9A=94=EC=B2=AD=20DTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/umc10th/domain/member/dto/MemberReqDTO.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java index 1aaead74..aa7b18d4 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberReqDTO.java @@ -6,8 +6,15 @@ public record GetInfo( Long id ){} + // 회원가입 public record SignUp( String email, String password ){} + + // 로그인 + public record Login( + String email, + String password + ){} } From 55cd692cd0c9ebd074b7ccf249f1ecfb207eabc9 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Mon, 25 May 2026 17:36:59 +0900 Subject: [PATCH 15/22] =?UTF-8?q?feat:=20=EB=A1=9C=EA=B7=B8=EC=9D=B8=20?= =?UTF-8?q?=EC=9D=91=EB=8B=B5=20DTO=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/umc10th/domain/member/dto/MemberResDTO.java | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java index f183d7cc..3eef86f6 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/dto/MemberResDTO.java @@ -13,9 +13,16 @@ public record GetInfo( String gender ) {} + // 회원가입 @Builder public record SignUp( Long memberId, String email ) {} + + // 로그인 + @Builder + public record Login( + String accessToken + ) {} } From bfe4d2f07e46fb8087be8fc512f79f122b530096 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Mon, 25 May 2026 17:37:37 +0900 Subject: [PATCH 16/22] =?UTF-8?q?feat:=20=EC=9D=BC=EB=B0=98=20=EB=A1=9C?= =?UTF-8?q?=EA=B7=B8=EC=9D=B8=20=EC=84=9C=EB=B9=84=EC=8A=A4=20=EB=A1=9C?= =?UTF-8?q?=EC=A7=81=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../domain/member/service/MemberService.java | 30 +++++++++++++++---- 1 file changed, 24 insertions(+), 6 deletions(-) diff --git a/Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java b/Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java index e1645ddc..2e088f11 100644 --- a/Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java +++ b/Jinyong/src/main/java/com/example/umc10th/domain/member/service/MemberService.java @@ -7,6 +7,8 @@ import com.example.umc10th.domain.member.dto.MemberResDTO; import com.example.umc10th.domain.member.exception.MemberException; import com.example.umc10th.domain.member.repository.MemberRepository; +import com.example.umc10th.global.security.entity.AuthMember; +import com.example.umc10th.global.security.util.JwtUtil; import lombok.RequiredArgsConstructor; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.stereotype.Service; @@ -17,14 +19,17 @@ public class MemberService { private final MemberRepository memberRepository; private final PasswordEncoder passwordEncoder; - - public MemberResDTO.GetInfo getInfo(Long memberId) { - Member member = memberRepository.findById(memberId) - .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); - - return MemberConverter.toGetInfo(member); + private final JwtUtil jwtUtil; + + // 마이페이지 + public MemberResDTO.GetInfo getInfo( + AuthMember member + ) { + // 컨버터를 이용해서 응답 DTO 생성 & return + return MemberConverter.toGetInfo(member.getMember()); } + // 회원가입 public MemberResDTO.SignUp signUp(MemberReqDTO.SignUp request) { memberRepository.findByEmail(request.email()) .ifPresent(member -> { @@ -37,4 +42,17 @@ public MemberResDTO.SignUp signUp(MemberReqDTO.SignUp request) { return MemberConverter.toSignUp(savedMember); } + + // 로그인 + public MemberResDTO.Login login(MemberReqDTO.Login request) { + Member member = memberRepository.findByEmail(request.email()) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + + if (!passwordEncoder.matches(request.password(), member.getPassword())) { + throw new MemberException(MemberErrorCode.INVALID_PASSWORD); + } + + String accessToken = jwtUtil.createAccessToken(new AuthMember(member)); + return MemberConverter.toLogin(accessToken); + } } From bdb8f085bc6d91590292de7eec57d3264e8b5aee Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Tue, 26 May 2026 13:28:38 +0900 Subject: [PATCH 17/22] =?UTF-8?q?feat:=20OAuthDTO=20=EC=9D=B8=ED=84=B0?= =?UTF-8?q?=ED=8E=98=EC=9D=B4=EC=8A=A4=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/umc10th/global/security/dto/OAuthDTO.java | 10 ++++++++++ 1 file changed, 10 insertions(+) create mode 100644 Jinyong/src/main/java/com/example/umc10th/global/security/dto/OAuthDTO.java diff --git a/Jinyong/src/main/java/com/example/umc10th/global/security/dto/OAuthDTO.java b/Jinyong/src/main/java/com/example/umc10th/global/security/dto/OAuthDTO.java new file mode 100644 index 00000000..74c6b051 --- /dev/null +++ b/Jinyong/src/main/java/com/example/umc10th/global/security/dto/OAuthDTO.java @@ -0,0 +1,10 @@ +package com.example.umc10th.global.security.dto; + +import com.example.umc10th.global.security.entity.SocialType; + +public interface OAuthDTO { + SocialType getSocialType(); + String getSocialUid(); + String getSocialEmail(); + String getName(); +} From e0aeae1e08b71107d0af296eab33e8332e173acc Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Tue, 26 May 2026 13:29:20 +0900 Subject: [PATCH 18/22] =?UTF-8?q?feat:=20OAuthMember=20=EC=9D=B8=EC=A6=9D?= =?UTF-8?q?=20=EA=B0=9D=EC=B2=B4=20=EA=B5=AC=ED=98=84?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../global/security/entity/OAuthMember.java | 34 +++++++++++++++++++ 1 file changed, 34 insertions(+) create mode 100644 Jinyong/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java diff --git a/Jinyong/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java b/Jinyong/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java new file mode 100644 index 00000000..1af7b5a6 --- /dev/null +++ b/Jinyong/src/main/java/com/example/umc10th/global/security/entity/OAuthMember.java @@ -0,0 +1,34 @@ +package com.example.umc10th.global.security.entity; + +import com.example.umc10th.domain.member.entity.Member; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; + +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(); + } +} From 087564b1fddc6741f92050fc002db33eb9aec95b Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Tue, 26 May 2026 13:30:19 +0900 Subject: [PATCH 19/22] =?UTF-8?q?feat:=20OAuth=20=EB=A1=9C=EA=B7=B8?= =?UTF-8?q?=EC=9D=B8=20=EC=84=B1=EA=B3=B5=20=ED=95=B8=EB=93=A4=EB=9F=AC=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 --- .../security/handler/OAuthSuccessHandler.java | 59 +++++++++++++++++++ 1 file changed, 59 insertions(+) create mode 100644 Jinyong/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java diff --git a/Jinyong/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java b/Jinyong/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java new file mode 100644 index 00000000..3f095713 --- /dev/null +++ b/Jinyong/src/main/java/com/example/umc10th/global/security/handler/OAuthSuccessHandler.java @@ -0,0 +1,59 @@ +package com.example.umc10th.global.security.handler; + +import com.example.umc10th.domain.member.converter.MemberConverter; +import com.example.umc10th.domain.member.code.MemberSuccessCode; +import com.example.umc10th.domain.member.dto.MemberResDTO; +import com.example.umc10th.global.apiPayload.ApiResponse; +import com.example.umc10th.global.apiPayload.code.BaseSuccessCode; +import com.example.umc10th.global.security.entity.AuthMember; +import com.example.umc10th.global.security.entity.OAuthMember; +import com.example.umc10th.global.security.util.JwtUtil; +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.web.authentication.AuthenticationSuccessHandler; + +import java.io.IOException; + +@RequiredArgsConstructor +public class OAuthSuccessHandler implements AuthenticationSuccessHandler { + + private final JwtUtil jwtUtil; + + @Override + public void onAuthenticationSuccess( + HttpServletRequest request, + HttpServletResponse response, + Authentication authentication + ) throws IOException, ServletException { + // 사전 작업: Response 매핑할 ObjectMapper 선언 + ObjectMapper objectMapper = new ObjectMapper(); + BaseSuccessCode code = MemberSuccessCode.OK; + + // Content-Type, Status 설정 + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + // 인증 객체 컨테이너에서 OAuth 인증 객체 가져오기 + Object principal = authentication.getPrincipal(); + if (!(principal instanceof OAuthMember member)) { + response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "Invalid OAuth principal."); + return; + } + + // 토큰 제작을 위해 OAuth 인증 객체에서 Member 추출 -> AuthMember 제작 + String accessToken = jwtUtil.createAccessToken(new AuthMember(member.getMember())); + + // 응답 통일 객체 래핑 + ApiResponse responseBody = ApiResponse.onSuccess( + code, + MemberConverter.toLogin(accessToken) + ); + + // 응답 출력 + objectMapper.writeValue(response.getOutputStream(), responseBody); + } +} From 35f1247dd9a2948ebe7a552560a54474ff0d0fb4 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Tue, 26 May 2026 13:32:02 +0900 Subject: [PATCH 20/22] =?UTF-8?q?feat:=20SecurityConfig=EC=97=90=20JWT=20?= =?UTF-8?q?=EB=B0=8F=20OAuth=20=EC=9D=B8=EC=A6=9D=20=EC=84=A4=EC=A0=95=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 --- .../umc10th/global/config/SecurityConfig.java | 82 +++++++++++++++++-- 1 file changed, 77 insertions(+), 5 deletions(-) diff --git a/Jinyong/src/main/java/com/example/umc10th/global/config/SecurityConfig.java b/Jinyong/src/main/java/com/example/umc10th/global/config/SecurityConfig.java index 9d8e60dc..811b3693 100644 --- a/Jinyong/src/main/java/com/example/umc10th/global/config/SecurityConfig.java +++ b/Jinyong/src/main/java/com/example/umc10th/global/config/SecurityConfig.java @@ -1,20 +1,34 @@ package com.example.umc10th.global.config; +import com.example.umc10th.global.security.filter.JwtAuthFilter; import com.example.umc10th.global.security.handler.CustomAccessDenied; import com.example.umc10th.global.security.handler.CustomEntryPoint; +import com.example.umc10th.global.security.handler.OAuthSuccessHandler; +import com.example.umc10th.global.security.service.CustomOAuthService; +import com.example.umc10th.global.security.service.CustomUserDetailsService; +import com.example.umc10th.global.security.util.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.config.http.SessionCreationPolicy; 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; // 추가됨! @EnableWebSecurity @Configuration +@RequiredArgsConstructor // 의존성 주입을 위해 추가됨 public class SecurityConfig { + // 필터에 넣어줄 기술 도구들 주입받기 + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + private final CustomOAuthService customOAuthService; + private final String[] allowUris = { // Swagger "/swagger-ui/**", @@ -22,7 +36,17 @@ public class SecurityConfig { "/v3/api-docs/**", // 회원가입 - "/api/v1/auth/signup" + "/api/v1/auth/signup", + + // 로그인 + "/api/v1/auth/login", + + // OAuth + "/oauth/authorize/**", + "/oauth/callback/**", + "/login", + "/login/**", + "/error" }; @Bean @@ -33,10 +57,17 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers(allowUris).permitAll() .anyRequest().authenticated() ) - .formLogin(form -> form - .defaultSuccessUrl("/swagger-ui/index.html", true) - .permitAll() + // 1. 기존 폼 로그인 기능을 끈다 + .formLogin(AbstractHttpConfigurer::disable) + + // 2. 세션을 쓰지 않으므로 Stateless(무상태)로 설정하며 끈다. + .sessionManagement(session -> session + .sessionCreationPolicy(SessionCreationPolicy.IF_REQUIRED) ) + + // 3. 기존(UsernamePassword...) 바로 앞에 JWT 배치 + .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) + .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") @@ -45,8 +76,38 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .exceptionHandling(exception -> exception .accessDeniedHandler(customAccessDenied()) .authenticationEntryPoint(customEntryPoint()) - ); + ) + // OAuth + .oauth2Login(oauth -> oauth + // 인증 엔트리 포인트 + .authorizationEndpoint(auth -> auth + .baseUri("/oauth/authorize") + ) + // 콜백 주소 + .redirectionEndpoint(redirect -> redirect + .baseUri("/oauth/callback/kakao") + ) + // 인증 완료 후 정보 활용 + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuthService) + ) + // 성공 시 JWT 토큰 발행할 핸들러 + .successHandler(oAuthSuccessHandler()) + .failureHandler((request, response, exception) -> { + exception.printStackTrace(); + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(401); + response.getWriter().write(""" + { + "isSuccess": false, + "message": "OAuth 실패", + "error": "%s" + } + """.formatted(exception.getMessage())); + }) + ); return http.build(); } @@ -64,4 +125,15 @@ public CustomAccessDenied customAccessDenied() { public CustomEntryPoint customEntryPoint() { return new CustomEntryPoint(); } + + // 주입받은 것들은 Filter를 만들 때 넣어줌 + @Bean + public JwtAuthFilter jwtAuthFilter() { + return new JwtAuthFilter(jwtUtil, customUserDetailsService); + } + + @Bean + public OAuthSuccessHandler oAuthSuccessHandler() { + return new OAuthSuccessHandler(jwtUtil); + } } From ca25ccbf605744c2d35236a777fbf37107035a55 Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Tue, 26 May 2026 13:32:32 +0900 Subject: [PATCH 21/22] =?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=83=80=EC=9E=85=20enum=20=EC=B6=94?= =?UTF-8?q?=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../example/umc10th/global/security/entity/SocialType.java | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 Jinyong/src/main/java/com/example/umc10th/global/security/entity/SocialType.java diff --git a/Jinyong/src/main/java/com/example/umc10th/global/security/entity/SocialType.java b/Jinyong/src/main/java/com/example/umc10th/global/security/entity/SocialType.java new file mode 100644 index 00000000..aea6da33 --- /dev/null +++ b/Jinyong/src/main/java/com/example/umc10th/global/security/entity/SocialType.java @@ -0,0 +1,5 @@ +package com.example.umc10th.global.security.entity; + +public enum SocialType { + KAKAO +} From 76cc906260379c1079e0acd9ddbeb4ccc545092d Mon Sep 17 00:00:00 2001 From: JinYoung Shin Date: Tue, 26 May 2026 17:02:57 +0900 Subject: [PATCH 22/22] =?UTF-8?q?docs:=209=EC=A3=BC=EC=B0=A8=20=ED=95=B5?= =?UTF-8?q?=EC=8B=AC=20=ED=82=A4=EC=9B=8C=EB=93=9C=20=EC=A0=95=EB=A6=AC=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 --- Jinyong/keyword_summary/ch09.md | 95 +++++++++++++++++++++++++++++++++ 1 file changed, 95 insertions(+) create mode 100644 Jinyong/keyword_summary/ch09.md diff --git a/Jinyong/keyword_summary/ch09.md b/Jinyong/keyword_summary/ch09.md new file mode 100644 index 00000000..7fe555ce --- /dev/null +++ b/Jinyong/keyword_summary/ch09.md @@ -0,0 +1,95 @@ +- 세션과 토큰의 차이는? + + **세션 기반 인증**: 서버가 사용자의 로그인 상태를 기억하는 방식 + + → 사용자가 로그인 하면 서버는 세션 생성 + + 브라우저에 JSESSIONID 같은 세션 ID가 담긴 쿠키 전달 + + 이후 사용자가 요청을 보낼 때마다 쿠키가 함께 전송되고, 서버는 그 세션 ID를 보고 “이 사용자는 로그인한 사용자!”라고 판단함 + + **토큰 기반 인증**: 서버가 로그인 상태를 직접 저장하지 않음. 클라이언트가 토큰을 들고 다니는 방식임 + + → 로그인에 성공하면 서버가 JWT 같은 토큰 발급 + + 클라이언트는 이후 요청마다 `Authorization: Bearer 토큰값` 형태로 토큰을 보냄 + + → 서버는 토큰의 서명과 만료 시간 검증해 사용자 인증 + + | 구분 | 세션 방식 | 토큰 방식 | + | --- | --- | --- | + | 저장 위치 | 서버가 세션 저장 | 클라이언트가 토큰 저장 | + | 서버 상태 | Stateful | Stateless | + | 요청 방식 | 쿠키 기반 | Authorization Header 기반 | + | 장점 | 서버에서 세션 삭제 가능, 제어 쉬움 | 서버 확장에 유리 | + | 단점 | 서버가 세션을 관리해야 함 | 토큰 탈취 시 만료 전까지 위험 | + | 예시 | 전통적인 웹 로그인 | JWT 기반 REST API 로그인 | + + 최근에는 토큰 방식으로 로그인을 받는 추세이지만, 보안이 중요한 금융쪽이나 몇몇 분야는 세션방식을 고집한다. + +- 엑세스 토큰과 리프레시 토큰이란? + + **Access Token(액세스 토큰)**은 사용자가 보호된 API에 접근할 때 사용하는 실제 인증 토큰이다. + + Ex) 마이페이지 조회, 리뷰 작성, 미션 등록 같은 private API 호출할 때 클라이언트는 Access Token을 `Authorization` 헤더에 담아 보낸다. + + + + 서버는 토큰을 검증 후 정상이면 사용자를 인증 사용자로 처리한다. + + + + **Refresh Token(리프레시 토큰)은 Access Token이 만료되었을 때 새로운 Access Token을 발급받기 위한 토큰이**다. + + → API 요청에 직접 사용하는 토큰이 아니라 재발급용 토큰 + + Access Token은 보통 탈취 위험을 줄이기 위해 짧게 유지한다(30분~2시간) + + 하지만 Access Token이 너무 자주 만료되면 사용자는 계속 다시 로그인해야 + + 하므로 불편함! + + → 이 문제를 해결하기 위해 Refresh Token을 사용하는 것이다. + + | 구분 | Access Token | Refresh Token | + | --- | --- | --- | + | 목적 | API 접근 | Access Token 재발급 | + | 사용 위치 | 매 요청마다 사용 | 토큰 재발급 시 사용 | + | 만료 시간 | 짧게 설정 | 상대적으로 길게 설정 | + | 노출 위험 | 자주 전송되기에 탈취 위험 있음 | 더 오래 유효하기 때문에 안전하게 보관 필요 | + | 예시 | 마이페이지 요청, 리뷰 작성 | Access Token 재발급 요청 | + + Access Token은 “출입증”에 가깝고, Refresh Token은 “출입증을 다시 발급받을 수 있는 재발급 권한”에 가깝다. + +- OAuth 1.0과 OAuth 2.0의 차이는? + + OAuth는 사용자의 비밀번호를 직접 공유하지 않고, 제3자 애플리케이션이 사용자의 리소스에 제한적으로 접근할 수 있게 해주는 권한 위임 프로토콜이다. + + 쉽게 말하면, “카카오로 로그인”, “구글로 로그인” 같은 기능이 OAuth 기반으로 동작하는 것이다. + + 우리 서비스가 사용자의 카카오 비밀번호를 직접 받는 것이 아니라, 카카오가 인증을 처리하고 우리 서버는 카카오로부터 사용자 정보를 받아 회원가입 또는 로그인을 처리한다. + + OAuth 1.0은 초기 OAuth 프로토콜이며, 요청마다 복잡한 서명 과정을 거쳐야 하고 클라이언트가 요청을 만들 때 암호화 서명값을 포함해야 한다. + + OAuth 2.0은 위의 OAuth 1.0을 대체한 방식이다. OAuth 2.0 명세는 OAuth 1.0을 대체한다고 명시하고 있으며, 현재 대부분의 소셜 로그인과 API 권한 위임에서 OAuth 2.0이 사용된다. + + OAuth 2.0은 HTTPS를 전제로 하며, OAuth 1.0처럼 매 요청마다 복잡한 서명을 직접 만드는 방식보다 구현이 단순하다. 또한 웹, 모바일 앱, SPA, 서버 간 통신 등 다양한 환경에 맞는 인증 흐름을 제공한다. + + | 구분 | OAuth 1.0 | OAuth 2.0 | + | --- | --- | --- | + | 등장 시기 | 초기 방식 | OAuth 1.0 대체 | + | 보안 방식 | 요청마다 서명 필요 | HTTPS 기반 보호 | + | 구현 난이도 | 비교적 복잡 | 비교적 단순 | + | 토큰 방식 | Access Token 중심 | Access Token, Refresh Token 등 활용 | + | 사용 환경 | 초기 웹 중심 | 웹, 모바일, API 서버 등 다양함 | + | 현재 사용성 | 거의 안 쓰는 추세 | 현재 표준 | + + OAuth 1.0은 요청마다 직접 서명을 만들어야 해서 보안적으로 엄격하지만 구현이 복잡하다. + + OAuth 2.0은 HTTPS를 기반으로 보안을 확보하고, Access Token과 Refresh Token을 이용해 더 단순하고 유연한 구조를 제공한다. + + 따라서 현재 카카오, 구글, 네이버 로그인 같은 소셜 로그인은 대부분 OAuth 2.0 기반으로 이해하면 된다. \ No newline at end of file