From 968920a44407c404155eae3b7cb305f57a5815f8 Mon Sep 17 00:00:00 2001 From: LeeJaeJun1 Date: Wed, 27 May 2026 02:45:51 +0900 Subject: [PATCH 1/8] =?UTF-8?q?chore:=20JWT=20=EC=9D=98=EC=A1=B4=EC=84=B1?= =?UTF-8?q?=20=EB=B0=8F=20=ED=99=98=EA=B2=BD=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 --- build.gradle | 6 ++++++ src/main/resources/application.yml | 8 +++++++- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/build.gradle b/build.gradle index 7371c3f1..e28915ce 100644 --- a/build.gradle +++ b/build.gradle @@ -35,6 +35,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') { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index c3145f0c..9de5eb66 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -16,4 +16,10 @@ spring: ddl-auto: update properties: hibernate: - format_sql: true \ No newline at end of file + format_sql: true + +jwt: + token: + secretKey: ${JWT_SECRET_KEY} + expiration: + access: 1800000 # 30분 From a77708b9668fb98718cfacdcd1a146e34701dc8d Mon Sep 17 00:00:00 2001 From: LeeJaeJun1 Date: Wed, 27 May 2026 02:46:21 +0900 Subject: [PATCH 2/8] =?UTF-8?q?chore:=20OAuth2=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=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 --- build.gradle | 3 +++ 1 file changed, 3 insertions(+) diff --git a/build.gradle b/build.gradle index e28915ce..4e0e7a36 100644 --- a/build.gradle +++ b/build.gradle @@ -41,6 +41,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' + + // WebAuthn + implementation 'org.springframework.security:spring-security-webauthn' } tasks.named('test') { From b806ec4eb5b237eb69365d76144f94015b8fabeb Mon Sep 17 00:00:00 2001 From: LeeJaeJun1 Date: Wed, 27 May 2026 02:46:58 +0900 Subject: [PATCH 3/8] =?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=EC=9C=A0?= =?UTF-8?q?=ED=8B=B8=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../umc/global/security/util/JwtUtil.java | 96 +++++++++++++++++++ 1 file changed, 96 insertions(+) create mode 100644 src/main/java/umc/global/security/util/JwtUtil.java 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 00000000..7ae1dcba --- /dev/null +++ b/src/main/java/umc/global/security/util/JwtUtil.java @@ -0,0 +1,96 @@ +package umc.global.security.util; + +import java.nio.charset.StandardCharsets; +import java.time.Duration; +import java.time.Instant; +import java.util.Date; +import java.util.stream.Collectors; + +import javax.crypto.SecretKey; + +import org.springframework.beans.factory.annotation.Value; +import org.springframework.security.core.GrantedAuthority; +import org.springframework.stereotype.Component; + +import io.jsonwebtoken.Claims; +import io.jsonwebtoken.Jws; +import io.jsonwebtoken.JwtException; +import io.jsonwebtoken.Jwts; +import io.jsonwebtoken.security.Keys; + +import umc.global.security.entity.AuthMember; + +@Component +public class JwtUtil { + + private final SecretKey secretKey; + private final Duration accessExpiration; + + public JwtUtil( + @Value("${jwt.token.secretKey}") String secret, + @Value("${jwt.token.expiration.access}") Long accessExpiration + ) { + this.secretKey = Keys.hmacShaKeyFor(secret.getBytes(StandardCharsets.UTF_8)); + this.accessExpiration = Duration.ofMillis(accessExpiration); + } + + // AccessToken 생성 + public String createAccessToken(AuthMember member) { + return createToken(member, accessExpiration); + } + + /** 토큰에서 이메일 가져오기 + * + * @param token 유저 정보를 추출할 토큰 + * @return 유저 이메일을 토큰에서 추출합니다 + */ + public String getEmail(String token) { + try { + return getClaims(token).getPayload().getSubject(); // Parsing해서 Subject 가져오기 + } catch (JwtException e) { + return null; + } + } + + /** 토큰 유효성 확인 + * + * @param token 유효한지 확인할 토큰 + * @return True, False 반환합니다 + */ + public boolean isValid(String token) { + try { + getClaims(token); + return true; + } catch (JwtException e) { + return false; + } + } + + // 토큰 생성 + private String createToken(AuthMember member, Duration expiration) { + Instant now = Instant.now(); + + // 인가 정보 + String authorities = member.getAuthorities().stream() + .map(GrantedAuthority::getAuthority) + .collect(Collectors.joining(",")); + + return Jwts.builder() + .subject(member.getUsername()) // User 이메일을 Subject로 + .claim("role", authorities) + .claim("email", member.getUsername()) + .issuedAt(Date.from(now)) // 언제 발급한지 + .expiration(Date.from(now.plus(expiration))) // 언제까지 유효한지 + .signWith(secretKey) // sign할 Key + .compact(); + } + + // 토큰 정보 가져오기 + private Jws getClaims(String token) throws JwtException { + return Jwts.parser() + .verifyWith(secretKey) + .clockSkewSeconds(60) + .build() + .parseSignedClaims(token); + } +} \ No newline at end of file From 150694b61e2bbdc67d6cfd71721cce7df2ea6198 Mon Sep 17 00:00:00 2001 From: LeeJaeJun1 Date: Wed, 27 May 2026 02:48:10 +0900 Subject: [PATCH 4/8] =?UTF-8?q?feat:=20=EC=BB=A4=EC=8A=A4=ED=85=80=20JWT?= =?UTF-8?q?=20=ED=95=84=ED=84=B0=20=EA=B5=AC=ED=98=84=20=EB=B0=8F=20?= =?UTF-8?q?=EC=8B=9C=ED=81=90=EB=A6=AC=ED=8B=B0=20=EC=84=A4=EC=A0=95=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 --- .../umc/global/config/SecurityConfig.java | 36 ++++++++- .../global/security/filter/JwtAuthFilter.java | 75 +++++++++++++++++++ 2 files changed, 107 insertions(+), 4 deletions(-) create mode 100644 src/main/java/umc/global/security/filter/JwtAuthFilter.java diff --git a/src/main/java/umc/global/config/SecurityConfig.java b/src/main/java/umc/global/config/SecurityConfig.java index 371b581d..c77f50ef 100644 --- a/src/main/java/umc/global/config/SecurityConfig.java +++ b/src/main/java/umc/global/config/SecurityConfig.java @@ -2,20 +2,32 @@ import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; +import org.springframework.jdbc.core.JdbcOperations; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer; import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder; import org.springframework.security.crypto.password.PasswordEncoder; import org.springframework.security.web.SecurityFilterChain; +import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter; +import org.springframework.security.web.webauthn.management.JdbcPublicKeyCredentialUserEntityRepository; +import org.springframework.security.web.webauthn.management.JdbcUserCredentialRepository; +import lombok.RequiredArgsConstructor; +import umc.global.security.filter.JwtAuthFilter; +import umc.global.security.service.CustomUserDetailsService; import umc.global.security.util.CustomAccessDenied; import umc.global.security.util.CustomEntryPoint; +import umc.global.security.util.JwtUtil; @EnableWebSecurity // Spring Security 설정 활성화 @Configuration +@RequiredArgsConstructor public class SecurityConfig { + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + private final String[] allowUris = { // Swagger 허용 "/swagger-ui/**", @@ -23,6 +35,7 @@ public class SecurityConfig { "/v3/api-docs/**", "/auth/**", "/api/v1/signup", + "/api/v1/login", "/error/**", }; @@ -34,10 +47,9 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .requestMatchers(allowUris).permitAll() // allowUris에 있는 주소들은 누구나 접근 가능 .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") @@ -67,4 +79,20 @@ public CustomAccessDenied customAccessDenied() { public CustomEntryPoint customEntryPoint() { return new CustomEntryPoint(); } + + @Bean + public JwtAuthFilter jwtAuthFilter() { + return new JwtAuthFilter(jwtUtil, customUserDetailsService); + } + + @Bean + JdbcPublicKeyCredentialUserEntityRepository jdbcPublicKeyCredentialUserEntityRepository( + JdbcOperations jdbc) { + return new JdbcPublicKeyCredentialUserEntityRepository(jdbc); + } + + @Bean + JdbcUserCredentialRepository jdbcUserCredentialRepository(JdbcOperations jdbc) { + return new JdbcUserCredentialRepository(jdbc); + } } 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 00000000..29be1aa8 --- /dev/null +++ b/src/main/java/umc/global/security/filter/JwtAuthFilter.java @@ -0,0 +1,75 @@ +package umc.global.security.filter; + +import java.io.IOException; + +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 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 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; + +@RequiredArgsConstructor +public class JwtAuthFilter extends OncePerRequestFilter { + + private final JwtUtil jwtUtil; + private final CustomUserDetailsService customUserDetailsService; + + @Override + protected void doFilterInternal( + @NonNull HttpServletRequest request, + @NonNull HttpServletResponse response, + @NonNull FilterChain filterChain + ) throws ServletException, IOException { + + try { + // 토큰 가져오기 + String token = request.getHeader("Authorization"); + // token이 없거나 Bearer가 아니면 넘기기 + if (token == null || !token.startsWith("Bearer ")) { + filterChain.doFilter(request, response); + return; + } + // Bearer이면 추출 + token = token.replace("Bearer ", ""); + // AccessToken 검증하기: 올바른 토큰이면 + if (jwtUtil.isValid(token)) { + // 토큰에서 이메일 추출 + String email = jwtUtil.getEmail(token); + // 인증 객체 생성: 이메일로 찾아온 뒤, 인증 객체 생성 + UserDetails user = customUserDetailsService.loadUserByUsername(email); + Authentication auth = new UsernamePasswordAuthenticationToken( + user, + null, + user.getAuthorities() + ); + // 인증 완료 후 SecurityContextHolder에 넣기 + SecurityContextHolder.getContext().setAuthentication(auth); + } + filterChain.doFilter(request, response); + } catch (Exception e) { + ObjectMapper mapper = new ObjectMapper(); + BaseErrorCode code = GeneralErrorCode.UNAUTHORIZED; + + response.setContentType("application/json;charset=UTF-8"); + response.setStatus(code.getStatus().value()); + + ApiResponse errorResponse = ApiResponse.onFailure(code,null); + + mapper.writeValue(response.getOutputStream(), errorResponse); + } + } +} From 6294001c7bf71a140115ea93dccc6fdf569bb277 Mon Sep 17 00:00:00 2001 From: LeeJaeJun1 Date: Wed, 27 May 2026 02:51:12 +0900 Subject: [PATCH 5/8] =?UTF-8?q?feat:=20JWT=20=EB=A1=9C=EA=B7=B8=EC=9D=B8?= =?UTF-8?q?=20=EC=9A=94=EC=B2=AD/=EC=9D=91=EB=8B=B5=20DTO=20=EB=B0=8F=20?= =?UTF-8?q?=EC=98=88=EC=99=B8=20=EC=B2=98=EB=A6=AC=20=EC=BD=94=EB=93=9C=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 --- src/main/java/umc/domain/member/dto/MemberReqDTO.java | 8 ++++++++ src/main/java/umc/domain/member/dto/MemberResDTO.java | 8 ++++++++ .../umc/domain/member/exception/code/MemberErrorCode.java | 4 +++- 3 files changed, 19 insertions(+), 1 deletion(-) diff --git a/src/main/java/umc/domain/member/dto/MemberReqDTO.java b/src/main/java/umc/domain/member/dto/MemberReqDTO.java index 7f0ff409..747d8b34 100644 --- a/src/main/java/umc/domain/member/dto/MemberReqDTO.java +++ b/src/main/java/umc/domain/member/dto/MemberReqDTO.java @@ -39,4 +39,12 @@ public static class TermDTO { Long termId; Boolean isAgree; } + + @Getter + public static class LoginDTO { + @NotBlank(message = "이메일을 입력해주세요") + String email; + @NotBlank(message = "비밀번호를 입력해주세요") + String password; + } } diff --git a/src/main/java/umc/domain/member/dto/MemberResDTO.java b/src/main/java/umc/domain/member/dto/MemberResDTO.java index bb573a41..558477fd 100644 --- a/src/main/java/umc/domain/member/dto/MemberResDTO.java +++ b/src/main/java/umc/domain/member/dto/MemberResDTO.java @@ -78,4 +78,12 @@ public static class MissionDetailDTO { Integer point; LocalTime deadline; } + + @Builder + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class LoginResultDTO { + String accessToken; + } } diff --git a/src/main/java/umc/domain/member/exception/code/MemberErrorCode.java b/src/main/java/umc/domain/member/exception/code/MemberErrorCode.java index d0e8a7ea..b44b8d39 100644 --- a/src/main/java/umc/domain/member/exception/code/MemberErrorCode.java +++ b/src/main/java/umc/domain/member/exception/code/MemberErrorCode.java @@ -12,7 +12,9 @@ public enum MemberErrorCode implements BaseErrorCode { MEMBER_NOT_FOUND(HttpStatus.NOT_FOUND, "MEMBER404_1", "해당 사용자를 찾을 수 없습니다."), - EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER409_1", "이미 존재하는 이메일입니다."); + EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER409_1", "이미 존재하는 이메일입니다."), + + INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER400_1", "비밀번호가 일치하지 않습니다"); private final HttpStatus status; private final String code; From ad793a8af9b61047d6fb065bc798355140579270 Mon Sep 17 00:00:00 2001 From: LeeJaeJun1 Date: Wed, 27 May 2026 02:56:06 +0900 Subject: [PATCH 6/8] =?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=20?= =?UTF-8?q?=EB=B0=8F=20=ED=86=A0=ED=81=B0=20=EB=B0=A9=EC=8B=9D=20=EB=A7=88?= =?UTF-8?q?=EC=9D=B4=ED=8E=98=EC=9D=B4=EC=A7=80=20=EC=A1=B0=ED=9A=8C=20?= =?UTF-8?q?=EA=B0=9C=EC=84=A0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../member/controller/MemberController.java | 25 ++++++++++++----- .../member/converter/MemberConverter.java | 7 +++++ .../member/service/MemberCommandService.java | 27 +++++++++++++++++-- .../member/service/MemberQueryService.java | 5 ++-- 4 files changed, 54 insertions(+), 10 deletions(-) diff --git a/src/main/java/umc/domain/member/controller/MemberController.java b/src/main/java/umc/domain/member/controller/MemberController.java index 773b0684..a49c67d2 100644 --- a/src/main/java/umc/domain/member/controller/MemberController.java +++ b/src/main/java/umc/domain/member/controller/MemberController.java @@ -1,5 +1,6 @@ package umc.domain.member.controller; +import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.PostMapping; @@ -20,6 +21,7 @@ import umc.global.apiPayload.ApiResponse; import umc.global.apiPayload.code.BaseSuccessCode; import umc.global.apiPayload.code.GeneralSuccessCode; +import umc.global.security.entity.AuthMember; @RestController @RequiredArgsConstructor @@ -31,12 +33,12 @@ public class MemberController { private final MemberCommandService memberCommandService; // 마이페이지 - @GetMapping("/{memberId}/mypage") + @GetMapping("/users/me") @Operation(summary = "마이페이지 조회") public ApiResponse getInfo( - @PathVariable(name = "memberId") Long memberId + @AuthenticationPrincipal AuthMember member ) { - MemberResDTO.GetInfo getInfo = memberQueryService.getInfo(memberId); + MemberResDTO.GetInfo getInfo = memberQueryService.getInfo(member); return ApiResponse.onSuccess(MemberSuccessCode.OK, getInfo); } @@ -45,12 +47,12 @@ public ApiResponse getInfo( // 회원가입 @PostMapping("/signup") @Operation(summary = "회원가입") - public ApiResponse join(@RequestBody @Valid MemberReqDTO.JoinDTO request) { + public ApiResponse join(@RequestBody @Valid MemberReqDTO.JoinDTO request) { - Member joins = memberCommandService.joinMember(request); + MemberResDTO.JoinResultDTO joins = memberCommandService.joinMember(request); BaseSuccessCode code = MemberSuccessCode.MEMBER_JOINED; - return ApiResponse.onSuccess(code, joins.getEmail()); + return ApiResponse.onSuccess(code, joins); } @@ -66,4 +68,15 @@ public ApiResponse getHomeInfo( return ApiResponse.onSuccess(MemberSuccessCode.OK, result); } + + // 로그인 + @PostMapping("/login") + @Operation(summary = "로그인") + public ApiResponse login( + @RequestBody @Valid MemberReqDTO.LoginDTO request + ) { + MemberResDTO.LoginResultDTO result = memberCommandService.login(request); + + return ApiResponse.onSuccess(MemberSuccessCode.OK, result); + } } diff --git a/src/main/java/umc/domain/member/converter/MemberConverter.java b/src/main/java/umc/domain/member/converter/MemberConverter.java index 03f865e7..6f436d9d 100644 --- a/src/main/java/umc/domain/member/converter/MemberConverter.java +++ b/src/main/java/umc/domain/member/converter/MemberConverter.java @@ -1,5 +1,6 @@ package umc.domain.member.converter; +import java.time.LocalDateTime; import java.util.List; import java.util.stream.Collectors; @@ -76,4 +77,10 @@ public static Member toMember(MemberReqDTO.JoinDTO request, String encodedPasswo .address(request.getAddress()) .build(); } + public static MemberResDTO.JoinResultDTO toJoinDTO(Member member) { + return MemberResDTO.JoinResultDTO.builder() + .memberId(member.getId()) + .createdAt(LocalDateTime.now()) + .build(); + } } diff --git a/src/main/java/umc/domain/member/service/MemberCommandService.java b/src/main/java/umc/domain/member/service/MemberCommandService.java index 20fa6185..37f92521 100644 --- a/src/main/java/umc/domain/member/service/MemberCommandService.java +++ b/src/main/java/umc/domain/member/service/MemberCommandService.java @@ -10,6 +10,7 @@ import lombok.RequiredArgsConstructor; import umc.domain.member.converter.MemberConverter; import umc.domain.member.dto.MemberReqDTO; +import umc.domain.member.dto.MemberResDTO; import umc.domain.member.entity.Member; import umc.domain.member.entity.Term; import umc.domain.member.entity.mapping.MemberFood; @@ -27,6 +28,8 @@ import umc.domain.store.exception.StoreException; import umc.domain.store.exception.code.StoreErrorCode; import umc.domain.store.repository.FoodRepository; +import umc.global.security.entity.AuthMember; +import umc.global.security.util.JwtUtil; @Service @RequiredArgsConstructor @@ -38,9 +41,10 @@ public class MemberCommandService { private final MemberFoodRepository memberFoodRepository; private final TermRepository termRepository; private final MemberTermRepository memberTermRepository; + private final JwtUtil jwtUtil; @Transactional - public Member joinMember(MemberReqDTO.JoinDTO request) { + public MemberResDTO.JoinResultDTO joinMember(MemberReqDTO.JoinDTO request) { // 1. 이메일 중복 검증 if(memberRepository.existsByEmail(request.getEmail())) { @@ -98,6 +102,25 @@ public Member joinMember(MemberReqDTO.JoinDTO request) { memberTermRepository.saveAll(memberTermList); } - return savedMember; + return MemberConverter.toJoinDTO(savedMember); + } + + @Transactional + public MemberResDTO.LoginResultDTO login(MemberReqDTO.LoginDTO request) { + + Member member = memberRepository.findByEmail(request.getEmail()) + .orElseThrow(()-> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + + if (!passwordEncoder.matches(request.getPassword(), member.getPassword())) { + throw new MemberException(MemberErrorCode.INVALID_PASSWORD); + } + + AuthMember authMember = new AuthMember(member); + + String accessToken = jwtUtil.createAccessToken(authMember); + + return MemberResDTO.LoginResultDTO.builder() + .accessToken(accessToken) + .build(); } } diff --git a/src/main/java/umc/domain/member/service/MemberQueryService.java b/src/main/java/umc/domain/member/service/MemberQueryService.java index 8523e4d9..dbc3cff2 100644 --- a/src/main/java/umc/domain/member/service/MemberQueryService.java +++ b/src/main/java/umc/domain/member/service/MemberQueryService.java @@ -20,6 +20,7 @@ import umc.domain.store.exception.StoreException; import umc.domain.store.exception.code.StoreErrorCode; import umc.domain.store.repository.RegionRepository; +import umc.global.security.entity.AuthMember; @Service @RequiredArgsConstructor @@ -31,9 +32,9 @@ public class MemberQueryService { private final MissionRepository missionRepository; @Transactional(readOnly = true) - public MemberResDTO.GetInfo getInfo(Long memberId) { + public MemberResDTO.GetInfo getInfo(AuthMember authMember) { - Member member = memberRepository.findById(memberId) + Member member = memberRepository.findById(authMember.getMember().getId()) .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); return MemberConverter.toGetInfo(member); From 95c53c8c6067e3c978bba0436706bac934edc3a0 Mon Sep 17 00:00:00 2001 From: LeeJaeJun1 Date: Thu, 28 May 2026 03:02:49 +0900 Subject: [PATCH 7/8] =?UTF-8?q?chore:=20OAuth2=20=EC=9D=98=EC=A1=B4?= =?UTF-8?q?=EC=84=B1=20=EB=B0=8F=20=ED=99=98=EA=B2=BD=20=EC=84=A4=EC=A0=95?= =?UTF-8?q?=20=EC=B6=94=EA=B0=80?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- build.gradle | 4 ++++ src/main/resources/application.yml | 33 +++++++++++++++++++++++++----- 2 files changed, 32 insertions(+), 5 deletions(-) diff --git a/build.gradle b/build.gradle index 4e0e7a36..45718581 100644 --- a/build.gradle +++ b/build.gradle @@ -43,7 +43,11 @@ dependencies { implementation 'org.springframework.boot:spring-boot-configuration-processor' // WebAuthn + implementation 'org.springframework.boot:spring-boot-starter-jdbc' implementation 'org.springframework.security:spring-security-webauthn' + + // OAuth + implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' } tasks.named('test') { diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml index 9de5eb66..3ef7c8dd 100644 --- a/src/main/resources/application.yml +++ b/src/main/resources/application.yml @@ -1,6 +1,6 @@ spring: application: - name: "dmeo" + name: "demo" datasource: driver-class-name: com.mysql.cj.jdbc.Driver @@ -18,8 +18,31 @@ spring: hibernate: format_sql: true + + security: + oauth2: + client: + registration: + kakao: + provider: 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/oauth/callback/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 + + jwt: - token: - secretKey: ${JWT_SECRET_KEY} - expiration: - access: 1800000 # 30분 + token: + secretKey: ${JWT_SECRET_KEY} + expiration: + access: 1800000 # 30분 \ No newline at end of file From 4ae9e2cebb1a4dea615541a8938638c65f7be05a Mon Sep 17 00:00:00 2001 From: LeeJaeJun1 Date: Thu, 28 May 2026 03:05:37 +0900 Subject: [PATCH 8/8] =?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 | 20 +++++ .../java/umc/domain/member/entity/Member.java | 3 + .../java/umc/domain/member/enums/Gender.java | 3 +- .../exception/code/MemberErrorCode.java | 3 +- .../member/repository/MemberRepository.java | 3 + .../umc/global/config/SecurityConfig.java | 28 ++++++- .../umc/global/security/dto/KaKaoDTO.java | 31 +++++++ .../umc/global/security/dto/OAuthDTO.java | 10 +++ .../global/security/entity/AuthMember.java | 4 +- .../global/security/entity/OAuthMember.java | 35 ++++++++ .../global/security/filter/JwtAuthFilter.java | 21 +++-- .../security/handler/OAuthSuccessHandler.java | 61 ++++++++++++++ .../security/service/CustomOAuthService.java | 83 +++++++++++++++++++ .../service/CustomUserDetailsService.java | 10 +++ .../umc/global/security/util/JwtUtil.java | 20 ++++- 15 files changed, 321 insertions(+), 14 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/handler/OAuthSuccessHandler.java create mode 100644 src/main/java/umc/global/security/service/CustomOAuthService.java diff --git a/src/main/java/umc/domain/member/converter/MemberConverter.java b/src/main/java/umc/domain/member/converter/MemberConverter.java index 6f436d9d..6cc9f652 100644 --- a/src/main/java/umc/domain/member/converter/MemberConverter.java +++ b/src/main/java/umc/domain/member/converter/MemberConverter.java @@ -9,7 +9,10 @@ import umc.domain.member.dto.MemberReqDTO; import umc.domain.member.dto.MemberResDTO; import umc.domain.member.entity.Member; +import umc.domain.member.enums.Gender; +import umc.domain.member.enums.MemberStatus; import umc.domain.mission.entity.Mission; +import umc.global.security.dto.OAuthDTO; public class MemberConverter { @@ -83,4 +86,21 @@ public static MemberResDTO.JoinResultDTO toJoinDTO(Member member) { .createdAt(LocalDateTime.now()) .build(); } + + public static Member toMember(OAuthDTO oAuthDTO) { + return Member.builder() + .name(oAuthDTO.getName()) + .email(oAuthDTO.getSocialEmail()) + .socialUid(oAuthDTO.getSocialUid()) + .socialType(oAuthDTO.getSocialType()) + .gender(Gender.NONE) + .memberStatus(MemberStatus.ACTIVE) + .build(); + } + + public static MemberResDTO.LoginResultDTO toLogin(String accessToken) { + return MemberResDTO.LoginResultDTO.builder() + .accessToken(accessToken) + .build(); + } } diff --git a/src/main/java/umc/domain/member/entity/Member.java b/src/main/java/umc/domain/member/entity/Member.java index 64b27ab4..5d8dc99e 100644 --- a/src/main/java/umc/domain/member/entity/Member.java +++ b/src/main/java/umc/domain/member/entity/Member.java @@ -73,6 +73,9 @@ public class Member extends BaseEntity { @Enumerated(EnumType.STRING) private SocialType socialType; + @Column(name = "social_uid") + private String socialUid; + @Builder.Default @Column(nullable = false, length = 20) @Enumerated(EnumType.STRING) diff --git a/src/main/java/umc/domain/member/enums/Gender.java b/src/main/java/umc/domain/member/enums/Gender.java index d0953e11..ee7c8dab 100644 --- a/src/main/java/umc/domain/member/enums/Gender.java +++ b/src/main/java/umc/domain/member/enums/Gender.java @@ -8,7 +8,8 @@ public enum Gender { MALE("남자"), - FEMALE("여자"); + FEMALE("여자"), + NONE("선택안함"); private final String label; } diff --git a/src/main/java/umc/domain/member/exception/code/MemberErrorCode.java b/src/main/java/umc/domain/member/exception/code/MemberErrorCode.java index b44b8d39..fc2e067c 100644 --- a/src/main/java/umc/domain/member/exception/code/MemberErrorCode.java +++ b/src/main/java/umc/domain/member/exception/code/MemberErrorCode.java @@ -14,8 +14,9 @@ public enum MemberErrorCode implements BaseErrorCode { EMAIL_ALREADY_EXISTS(HttpStatus.CONFLICT, "MEMBER409_1", "이미 존재하는 이메일입니다."), - INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER400_1", "비밀번호가 일치하지 않습니다"); + INVALID_PASSWORD(HttpStatus.BAD_REQUEST, "MEMBER400_1", "비밀번호가 일치하지 않습니다"), + NOT_SUPPORT_SOCIAL_PROVIDER(HttpStatus.BAD_REQUEST, "MEMBER400_X", "지원하지 않는 소셜 로그인 제공자입니다."); private final HttpStatus status; private final String code; private final String message; diff --git a/src/main/java/umc/domain/member/repository/MemberRepository.java b/src/main/java/umc/domain/member/repository/MemberRepository.java index d305ceef..a882d10f 100644 --- a/src/main/java/umc/domain/member/repository/MemberRepository.java +++ b/src/main/java/umc/domain/member/repository/MemberRepository.java @@ -7,9 +7,12 @@ import org.springframework.data.repository.query.Param; import umc.domain.member.entity.Member; +import umc.domain.member.enums.SocialType; public interface MemberRepository extends JpaRepository { Optional findByEmail(String email); boolean existsByEmail(String email); + + Optional findBySocialTypeAndSocialUid(SocialType providerId, String socialUid); } diff --git a/src/main/java/umc/global/config/SecurityConfig.java b/src/main/java/umc/global/config/SecurityConfig.java index c77f50ef..21b141c8 100644 --- a/src/main/java/umc/global/config/SecurityConfig.java +++ b/src/main/java/umc/global/config/SecurityConfig.java @@ -15,6 +15,8 @@ import lombok.RequiredArgsConstructor; import umc.global.security.filter.JwtAuthFilter; +import umc.global.security.handler.OAuthSuccessHandler; +import umc.global.security.service.CustomOAuthService; import umc.global.security.service.CustomUserDetailsService; import umc.global.security.util.CustomAccessDenied; import umc.global.security.util.CustomEntryPoint; @@ -27,6 +29,7 @@ public class SecurityConfig { private final JwtUtil jwtUtil; private final CustomUserDetailsService customUserDetailsService; + private final CustomOAuthService customOAuthService; private final String[] allowUris = { // Swagger 허용 @@ -40,7 +43,7 @@ public class SecurityConfig { }; @Bean - public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { + public SecurityFilterChain securityFilterChain(HttpSecurity http, OAuthSuccessHandler oAuthSuccessHandler) throws Exception { http .csrf(AbstractHttpConfigurer::disable) .authorizeHttpRequests(requests -> requests @@ -50,6 +53,22 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti .formLogin(AbstractHttpConfigurer::disable) .sessionManagement(AbstractHttpConfigurer::disable) .addFilterBefore(jwtAuthFilter(), UsernamePasswordAuthenticationFilter.class) + .oauth2Login(oauth -> oauth + // 인증 엔트리 포인트 + .authorizationEndpoint(auth -> auth + .baseUri("/oauth/authorize") + ) + // 콜백 주소 + .redirectionEndpoint(redirect -> redirect + .baseUri("/oauth/callback/**") + ) + // 인증 완료 후 정보 활용 + .userInfoEndpoint(userInfo -> userInfo + .userService(customOAuthService) + ) + // 성공 시 JWT 토큰 발행할 핸들러 + .successHandler(oAuthSuccessHandler()) + ) .logout(logout -> logout .logoutUrl("/logout") .logoutSuccessUrl("/login?logout") @@ -95,4 +114,9 @@ JdbcPublicKeyCredentialUserEntityRepository jdbcPublicKeyCredentialUserEntityRep JdbcUserCredentialRepository jdbcUserCredentialRepository(JdbcOperations jdbc) { return new JdbcUserCredentialRepository(jdbc); } -} + + @Bean + public OAuthSuccessHandler oAuthSuccessHandler() { + return new OAuthSuccessHandler(jwtUtil); + } +} \ No newline at end of file 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 00000000..7c2ba003 --- /dev/null +++ b/src/main/java/umc/global/security/dto/KaKaoDTO.java @@ -0,0 +1,31 @@ +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 getSocialEmail() { + 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 00000000..6cdd118e --- /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 getSocialEmail(); + String getName(); +} diff --git a/src/main/java/umc/global/security/entity/AuthMember.java b/src/main/java/umc/global/security/entity/AuthMember.java index 600bf479..e11d705b 100644 --- a/src/main/java/umc/global/security/entity/AuthMember.java +++ b/src/main/java/umc/global/security/entity/AuthMember.java @@ -23,11 +23,11 @@ public Collection getAuthorities() { @Override public @Nullable String getPassword() { - return member.getPassword(); + return null; } @Override public String getUsername() { - return member.getEmail(); + return member.getSocialUid(); } } 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 00000000..91f918ae --- /dev/null +++ b/src/main/java/umc/global/security/entity/OAuthMember.java @@ -0,0 +1,35 @@ +package umc.global.security.entity; + +import java.util.Collection; +import java.util.List; +import java.util.Map; + +import org.springframework.security.core.GrantedAuthority; +import org.springframework.security.oauth2.core.user.OAuth2User; + +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import umc.domain.member.entity.Member; + +@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.getEmail(); + } + +} diff --git a/src/main/java/umc/global/security/filter/JwtAuthFilter.java b/src/main/java/umc/global/security/filter/JwtAuthFilter.java index 29be1aa8..ee46825a 100644 --- a/src/main/java/umc/global/security/filter/JwtAuthFilter.java +++ b/src/main/java/umc/global/security/filter/JwtAuthFilter.java @@ -16,6 +16,7 @@ import jakarta.servlet.http.HttpServletResponse; import lombok.NonNull; import lombok.RequiredArgsConstructor; +import umc.domain.member.enums.SocialType; import umc.global.apiPayload.ApiResponse; import umc.global.apiPayload.code.BaseErrorCode; import umc.global.apiPayload.code.GeneralErrorCode; @@ -45,18 +46,24 @@ protected void doFilterInternal( } // Bearer이면 추출 token = token.replace("Bearer ", ""); + // AccessToken 검증하기: 올바른 토큰이면 if (jwtUtil.isValid(token)) { - // 토큰에서 이메일 추출 - String email = jwtUtil.getEmail(token); - // 인증 객체 생성: 이메일로 찾아온 뒤, 인증 객체 생성 - UserDetails user = customUserDetailsService.loadUserByUsername(email); + + // 1. JWT 토큰에서 유저 정보 조회: UID와 소셜 로그인 타입 가져오기 + String uid = jwtUtil.getUid(token); + SocialType socialType = jwtUtil.getSocialType(token); + + // 2. 인증 객체 생성: 로그인 타입과 UID로 찾아온 뒤, 인증 객체 생성 + UserDetails member = customUserDetailsService.loadUserByUidAndSocialType(socialType, uid); + Authentication auth = new UsernamePasswordAuthenticationToken( - user, + member, null, - user.getAuthorities() + member.getAuthorities() ); - // 인증 완료 후 SecurityContextHolder에 넣기 + + // 3. 인증 완료 후 SecurityContextHolder에 넣기 SecurityContextHolder.getContext().setAuthentication(auth); } filterChain.doFilter(request, response); diff --git a/src/main/java/umc/global/security/handler/OAuthSuccessHandler.java b/src/main/java/umc/global/security/handler/OAuthSuccessHandler.java new file mode 100644 index 00000000..d619e268 --- /dev/null +++ b/src/main/java/umc/global/security/handler/OAuthSuccessHandler.java @@ -0,0 +1,61 @@ +package umc.global.security.handler; + +import java.io.IOException; + +import org.springframework.context.annotation.Bean; +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 com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.Builder; +import lombok.RequiredArgsConstructor; +import umc.domain.member.converter.MemberConverter; +import umc.domain.member.dto.MemberResDTO; +import umc.domain.member.exception.code.MemberSuccessCode; +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; + +@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 인증 객체 가져오기 + OAuthMember member = (OAuthMember) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); + + // 토큰 제작을 위해 OAuth 인증 객체에서 Member 추출 -> AuthMember 제작 + String accessToken = jwtUtil.createAccessToken(new AuthMember(member.getMember())); + + // 응답 통일 객체 래핑 + ApiResponse responseBody = ApiResponse.onSuccess( + code, + MemberConverter.toLogin(accessToken) + ); + + // 응답 출력 + objectMapper.writeValue(response.getOutputStream(), responseBody); + } +} diff --git a/src/main/java/umc/global/security/service/CustomOAuthService.java b/src/main/java/umc/global/security/service/CustomOAuthService.java new file mode 100644 index 00000000..439aa263 --- /dev/null +++ b/src/main/java/umc/global/security/service/CustomOAuthService.java @@ -0,0 +1,83 @@ +package umc.global.security.service; + +import java.io.IOException; +import java.util.Map; + +import org.springframework.security.core.Authentication; +import org.springframework.security.core.context.SecurityContextHolder; +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 com.fasterxml.jackson.databind.ObjectMapper; + +import jakarta.servlet.ServletException; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; +import lombok.RequiredArgsConstructor; +import umc.domain.member.converter.MemberConverter; +import umc.domain.member.dto.MemberResDTO; +import umc.domain.member.entity.Member; +import umc.domain.member.enums.SocialType; +import umc.domain.member.exception.MemberException; +import umc.domain.member.exception.code.MemberErrorCode; +import umc.domain.member.exception.code.MemberSuccessCode; +import umc.domain.member.repository.MemberRepository; +import umc.global.apiPayload.ApiResponse; +import umc.global.apiPayload.code.BaseSuccessCode; +import umc.global.security.dto.KaKaoDTO; +import umc.global.security.dto.OAuthDTO; +import umc.global.security.entity.AuthMember; +import umc.global.security.entity.OAuthMember; +import umc.global.security.util.JwtUtil; + +@Service +@RequiredArgsConstructor +public class CustomOAuthService extends DefaultOAuth2UserService { + + private final MemberRepository memberRepository; + private final JwtUtil jwtUtil; + + @Override + public OAuth2User loadUser( + OAuth2UserRequest userRequest + ) throws OAuth2AuthenticationException { + // (필수) 인증 서버의 일회성 토큰을 이용해 정보 조회 & 유저 객체 생성 + OAuth2User oAuthMember = super.loadUser(userRequest); + + // 유저 객체에서 정보 추출 + SocialType providerId; + String socialUid; + Map attributes = oAuthMember.getAttribute("kakao_account"); + Map profile = (Map) attributes.get("profile"); + try { + providerId = SocialType.valueOf(userRequest.getClientRegistration().getRegistrationId().toUpperCase()); + socialUid = String.valueOf((Long) oAuthMember.getAttribute("id")); + } catch (IllegalArgumentException e) { + throw new MemberException(MemberErrorCode.NOT_SUPPORT_SOCIAL_PROVIDER); + } + + // OAuth 공통 정보 DTO로 매핑 + 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 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()); + } +} + diff --git a/src/main/java/umc/global/security/service/CustomUserDetailsService.java b/src/main/java/umc/global/security/service/CustomUserDetailsService.java index 3742697a..b1c2522b 100644 --- a/src/main/java/umc/global/security/service/CustomUserDetailsService.java +++ b/src/main/java/umc/global/security/service/CustomUserDetailsService.java @@ -7,6 +7,7 @@ import lombok.RequiredArgsConstructor; import umc.domain.member.entity.Member; +import umc.domain.member.enums.SocialType; import umc.domain.member.exception.MemberException; import umc.domain.member.exception.code.MemberErrorCode; import umc.domain.member.repository.MemberRepository; @@ -26,4 +27,13 @@ public UserDetails loadUserByUsername( .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); return new AuthMember(member); } + + public UserDetails loadUserByUidAndSocialType( + SocialType socialType, + String username + ) throws UsernameNotFoundException { + Member member = memberRepository.findBySocialTypeAndSocialUid(socialType, username) + .orElseThrow(() -> new MemberException(MemberErrorCode.MEMBER_NOT_FOUND)); + return new AuthMember(member); + } } diff --git a/src/main/java/umc/global/security/util/JwtUtil.java b/src/main/java/umc/global/security/util/JwtUtil.java index 7ae1dcba..14396ef6 100644 --- a/src/main/java/umc/global/security/util/JwtUtil.java +++ b/src/main/java/umc/global/security/util/JwtUtil.java @@ -18,6 +18,7 @@ import io.jsonwebtoken.Jwts; import io.jsonwebtoken.security.Keys; +import umc.domain.member.enums.SocialType; import umc.global.security.entity.AuthMember; @Component @@ -66,6 +67,22 @@ public boolean isValid(String token) { } } + public SocialType getSocialType(String token) { + try { + return SocialType.valueOf(getClaims(token).getPayload().get("social_type").toString().toUpperCase()); + } catch (JwtException e) { + return null; + } + } + + public String getUid(String token) { + try { + return getClaims(token).getPayload().get("uid").toString(); + } catch (JwtException e) { + return null; + } + } + // 토큰 생성 private String createToken(AuthMember member, Duration expiration) { Instant now = Instant.now(); @@ -78,7 +95,8 @@ private String createToken(AuthMember member, Duration expiration) { return Jwts.builder() .subject(member.getUsername()) // User 이메일을 Subject로 .claim("role", authorities) - .claim("email", member.getUsername()) + .claim("uid", member.getMember().getSocialUid()) + .claim("social_type", member.getMember().getSocialType().name()) .issuedAt(Date.from(now)) // 언제 발급한지 .expiration(Date.from(now.plus(expiration))) // 언제까지 유효한지 .signWith(secretKey) // sign할 Key