diff --git a/src/main/java/Journey/Together/domain/member/controller/AuthController.java b/src/main/java/Journey/Together/domain/member/controller/AuthController.java index 7b7a5e8..faf508b 100644 --- a/src/main/java/Journey/Together/domain/member/controller/AuthController.java +++ b/src/main/java/Journey/Together/domain/member/controller/AuthController.java @@ -29,8 +29,9 @@ public void login(HttpServletResponse response) throws IOException { @Operation(summary = "로그인 API") @PostMapping("/sign-in") - public ApiResponse signIn(@RequestHeader("Authorization") String socialAccessToken, @RequestBody String type) { - return ApiResponse.success(Success.LOGIN_SUCCESS,authService.signIn(socialAccessToken,type)); + public ApiResponse signIn(@RequestHeader("Authorization") String token, + @RequestBody String type) { + return ApiResponse.success(Success.LOGIN_SUCCESS,authService.signIn(token,type)); } @Operation(summary = "로그아웃 API", description = "로그아웃된 JWT 블랙리스트 등록") diff --git a/src/main/java/Journey/Together/domain/member/entity/Member.java b/src/main/java/Journey/Together/domain/member/entity/Member.java index 2391d1a..3e76696 100644 --- a/src/main/java/Journey/Together/domain/member/entity/Member.java +++ b/src/main/java/Journey/Together/domain/member/entity/Member.java @@ -14,9 +14,10 @@ @NoArgsConstructor(access = AccessLevel.PROTECTED) @AllArgsConstructor public class Member extends BaseTimeEntity { + @Id @GeneratedValue(strategy = GenerationType.IDENTITY) - @Column(name = "member_id", nullable = false, columnDefinition = "bigint") + @Column(name = "member_id", columnDefinition = "bigint") private Long memberId; // 이메일은 최대 255자 + 1자(@) + 69자해서 최대 320글자이므로, varchar(320) 사용 diff --git a/src/main/java/Journey/Together/domain/member/service/AuthService.java b/src/main/java/Journey/Together/domain/member/service/AuthService.java index 5375906..f929f6a 100644 --- a/src/main/java/Journey/Together/domain/member/service/AuthService.java +++ b/src/main/java/Journey/Together/domain/member/service/AuthService.java @@ -11,9 +11,18 @@ import Journey.Together.global.security.kakao.dto.KakaoProfile; import Journey.Together.global.security.jwt.TokenProvider; import Journey.Together.global.security.jwt.dto.TokenDto; +import Journey.Together.global.security.kakao.dto.KakaoToken; +import Journey.Together.global.security.naver.dto.NaverProperties; +import Journey.Together.global.security.naver.dto.NaverUserResponse; import lombok.RequiredArgsConstructor; +import org.springframework.http.HttpEntity; +import org.springframework.http.HttpHeaders; +import org.springframework.http.HttpMethod; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; +import org.springframework.util.MultiValueMap; +import org.springframework.web.client.RestTemplate; @Service @Transactional(readOnly = true) @@ -23,32 +32,68 @@ public class AuthService { private final TokenProvider tokenProvider; private final MemberRepository memberRepository; + private final RestTemplate restTemplate = new RestTemplate(); + @Transactional - public LoginRes signIn(String kakaoAccessToken, String type) { - //Business Logic - // 카카오톡에 있는 사용자 정보 반환 - KakaoProfile kakaoProfile = kakaoClient.getMemberInfo(kakaoAccessToken); - // 반환된 정보의 이메일 기반으로 사용자 테이블에서 계정 정보 조회 진행 - Member member = memberRepository.findMemberByEmailAndDeletedAtIsNull(kakaoProfile.kakao_account().email()).orElse(null); - // 이메일 존재 시 로그인 , 존재하지 않을 경우 회원가입 진행 - if(member == null) { - Member newMember = Member.builder() - .email(kakaoProfile.kakao_account().email()) - .name(kakaoProfile.kakao_account().profile().nickname()) - .profileUrl(kakaoProfile.kakao_account().profile().profile_image_url()) - .memberType(MemberType.valueOf("GENERAL")) - .loginType(LoginType.valueOf("KAKAO")) - .build(); - member = memberRepository.save(newMember); - } - TokenDto tokenDto = tokenProvider.createToken(member); - member.setRefreshToken(tokenDto.refreshToken()); + public LoginRes signIn(String token, String type) { + Member member = null; + TokenDto tokenDto = null; - // Response - return LoginRes.of(member, tokenDto); + if(type.equals("KAKAO")) { + //Business Logic + // 카카오톡에 있는 사용자 정보 반환 + KakaoProfile kakaoProfile = kakaoClient.getMemberInfo(token); + // 반환된 정보의 이메일 기반으로 사용자 테이블에서 계정 정보 조회 진행 + member = memberRepository.findMemberByEmailAndDeletedAtIsNull(kakaoProfile.kakao_account().email()).orElse(null); + // 이메일 존재 시 로그인 , 존재하지 않을 경우 회원가입 진행 + if(member == null) { + Member newMember = Member.builder() + .email(kakaoProfile.kakao_account().email()) + .name(kakaoProfile.kakao_account().profile().nickname()) + .profileUrl(kakaoProfile.kakao_account().profile().profile_image_url()) + .memberType(MemberType.valueOf("GENERAL")) + .loginType(LoginType.valueOf("KAKAO")) + .build(); + member = memberRepository.save(newMember); + } + tokenDto = tokenProvider.createToken(member); + member.setRefreshToken(tokenDto.refreshToken()); + + // Response + return LoginRes.of(member, tokenDto); + + } else if (type.equals("NAVER")) { + NaverUserResponse.NaverUserDetail naverProfile = toRequestProfile(token.substring(7)); + member = memberRepository.findMemberByEmailAndDeletedAtIsNull(naverProfile.getEmail()).orElse(null); + + if (member == null) { + Member newMember = Member.builder() + .email(naverProfile.getEmail() != null ? naverProfile.getEmail() : "Unknown") + .profileUrl(naverProfile.getProfile_image() != null ? naverProfile.getProfile_image() : "Unknown") + .name(naverProfile.getName() != null ? naverProfile.getName() : "Unknown") + .memberType(MemberType.GENERAL) + .loginType(LoginType.NAVER) + .build(); + member = memberRepository.save(newMember); + } + + tokenDto = tokenProvider.createToken(member); + member.setRefreshToken(tokenDto.refreshToken()); + + } + return LoginRes.of(member, tokenDto); } + private NaverUserResponse.NaverUserDetail toRequestProfile(String accessToken) { + HttpHeaders headers = new HttpHeaders(); + headers.setBearerAuth(accessToken); + HttpEntity> request = new HttpEntity<>(headers); + + ResponseEntity response = + restTemplate.exchange("https://openapi.naver.com/v1/nid/me", HttpMethod.GET, request, NaverUserResponse.class); + return response.getBody().getNaverUserDetail(); + } public void signOut(String token, Member member) { // Validation @@ -56,6 +101,9 @@ public void signOut(String token, Member member) { tokenProvider.validateToken(accessToken); // Business Logic - Refresh Token 삭제 및 Access Token 블랙리스트 등록 + String key = member.getEmail(); +// redisClient.deleteValue(key); +// redisClient.setValue(accessToken, "logout", tokenProvider.getExpiration(accessToken)); member.setRefreshToken(null); // Response diff --git a/src/main/java/Journey/Together/domain/member/service/MemberService.java b/src/main/java/Journey/Together/domain/member/service/MemberService.java index acf4bce..e7d8e0a 100644 --- a/src/main/java/Journey/Together/domain/member/service/MemberService.java +++ b/src/main/java/Journey/Together/domain/member/service/MemberService.java @@ -12,4 +12,5 @@ public class MemberService { private final MemberRepository memberRepository; + } diff --git a/src/main/java/Journey/Together/global/config/SecurityConfig.java b/src/main/java/Journey/Together/global/config/SecurityConfig.java index 7c52491..9f23268 100644 --- a/src/main/java/Journey/Together/global/config/SecurityConfig.java +++ b/src/main/java/Journey/Together/global/config/SecurityConfig.java @@ -59,6 +59,7 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception { authorize.requestMatchers("/api-docs/**", "/swagger-ui/**", "/swagger-ui.html").permitAll() // 로그인 로직 접속 허용 .requestMatchers("/v1/auth/**", "/oauth2/**", "/login.html").permitAll() + .requestMatchers("/actuator/**").permitAll() .requestMatchers("/v1/member/**").authenticated() // 메인 페이지, 공고 페이지 등에 한해 인증 정보 없이 접근 가능 (추후 추가) // 이외의 모든 요청은 인증 정보 필요 diff --git a/src/main/java/Journey/Together/global/security/jwt/JwtFilter.java b/src/main/java/Journey/Together/global/security/jwt/JwtFilter.java index 5df657d..1977405 100644 --- a/src/main/java/Journey/Together/global/security/jwt/JwtFilter.java +++ b/src/main/java/Journey/Together/global/security/jwt/JwtFilter.java @@ -22,9 +22,8 @@ public class JwtFilter extends OncePerRequestFilter { @Override protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException { String requestURI = request.getRequestURI(); - - // 회원가입일때는 jwt 유효성 검사를 하지않음 - if ("/v1/auth/sign-in".equals(requestURI)) { + //jwt 유효성 검사를 하지않음 + if ("/v1/auth/sign-in".equals(requestURI) || "/actuator/health".equals(requestURI)) { filterChain.doFilter(request, response); return; } diff --git a/src/main/java/Journey/Together/global/security/kakao/KakaoClient.java b/src/main/java/Journey/Together/global/security/kakao/KakaoClient.java index 76334ed..8bc0620 100644 --- a/src/main/java/Journey/Together/global/security/kakao/KakaoClient.java +++ b/src/main/java/Journey/Together/global/security/kakao/KakaoClient.java @@ -34,13 +34,53 @@ public class KakaoClient { private String kakaoUserInfoUri; public KakaoProfile getMemberInfo(String access_token) { + /** + * 카카오 서버에 인가코드 기반으로 사용자의 토큰 정보를 조회하는 메소드 + * @param code - 카카오에서 발급해준 인가 코드 + * @return - 카카오에서 반환한 응답 토큰 객체 + */ + + public KakaoToken getKakaoAccessToken(String code) { + // 요청 보낼 객체 기본 생성 + WebClient webClient = WebClient.create(kakaoTokenUri); + + //요청 본문 + MultiValueMap params = new LinkedMultiValueMap<>(); + params.add("grant_type", kakaoGrantType); + params.add("client_id", kakaoClientId); + params.add("redirect_uri", kakaoRedirectUri); + params.add("code", code); + params.add("client_secret", kakaoClientSecret); + + // 요청 보내기 및 응답 수신 + String response = webClient.post() + .uri(kakaoTokenUri) + .header("Content-type", "application/x-www-form-urlencoded") + .body(BodyInserters.fromFormData(params)) + .retrieve() // 데이터 받는 방식, 스프링에서는 exchange는 메모리 누수 가능성 때문에 retrieve 권장 + .bodyToMono(String.class) // (Mono는 단일 데이터, Flux는 복수 데이터) + .block();// 비동기 방식의 데이터 수신 + + // 수신된 응답 Mapping + ObjectMapper objectMapper = new ObjectMapper(); + KakaoToken kakaoToken; + try { + kakaoToken = objectMapper.readValue(response, KakaoToken.class); + } catch (Exception e) { + throw new RuntimeException(e); + } + + return kakaoToken; + } + + public KakaoProfile getMemberInfo(String accesToken) { // 요청 기본 객체 생성 WebClient webClient = WebClient.create(kakaoUserInfoUri); // 요청 보내서 응답 받기 String response = webClient.post() .uri(kakaoUserInfoUri) .header("Content-Type", "application/x-www-form-urlencoded;charset=utf-8") - .header("Authorization", access_token) + .header("Authorization", accesToken) .retrieve() .bodyToMono(String.class) .block(); diff --git a/src/main/java/Journey/Together/global/security/naver/dto/NaverProperties.java b/src/main/java/Journey/Together/global/security/naver/dto/NaverProperties.java new file mode 100644 index 0000000..438c61b --- /dev/null +++ b/src/main/java/Journey/Together/global/security/naver/dto/NaverProperties.java @@ -0,0 +1,24 @@ +package Journey.Together.global.security.naver.dto; + +import lombok.Data; +import org.springframework.boot.context.properties.ConfigurationProperties; +import org.springframework.context.annotation.Configuration; +import org.springframework.web.util.UriComponentsBuilder; + +@Data +@Configuration +@ConfigurationProperties(prefix = "naver") +public class NaverProperties { + private String requestTokenUri; + private String clientId; + private String clientSecret; + + public String getRequestURL(String code) { + return UriComponentsBuilder.fromHttpUrl(requestTokenUri) + .queryParam("grant_type", "authorization_code") + .queryParam("client_id", clientId) + .queryParam("client_secret", clientSecret) + .queryParam("code", code) + .toUriString(); + } +} \ No newline at end of file diff --git a/src/main/java/Journey/Together/global/security/naver/dto/NaverTokenResponse.java b/src/main/java/Journey/Together/global/security/naver/dto/NaverTokenResponse.java new file mode 100644 index 0000000..d51504f --- /dev/null +++ b/src/main/java/Journey/Together/global/security/naver/dto/NaverTokenResponse.java @@ -0,0 +1,24 @@ +package Journey.Together.global.security.naver.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class NaverTokenResponse { + @JsonProperty("access_token") + private String accessToken; + @JsonProperty("refresh_token") + private String refreshToken; + @JsonProperty("token_type") + private String tokenType; + @JsonProperty("expires_in") + private String expiresIn; + @JsonProperty("error") + private String error; + @JsonProperty("error_description") + private String errorDescription; +} \ No newline at end of file diff --git a/src/main/java/Journey/Together/global/security/naver/dto/NaverUserResponse.java b/src/main/java/Journey/Together/global/security/naver/dto/NaverUserResponse.java new file mode 100644 index 0000000..a63dc8d --- /dev/null +++ b/src/main/java/Journey/Together/global/security/naver/dto/NaverUserResponse.java @@ -0,0 +1,29 @@ +package Journey.Together.global.security.naver.dto; + +import com.fasterxml.jackson.annotation.JsonProperty; +import lombok.AllArgsConstructor; +import lombok.Getter; +import lombok.NoArgsConstructor; + +@Getter +@NoArgsConstructor +@AllArgsConstructor +public class NaverUserResponse { + @JsonProperty("resultcode") + private String resultCode; + @JsonProperty("message") + private String message; + @JsonProperty("response") + private NaverUserDetail naverUserDetail; + + @Getter + @NoArgsConstructor + @AllArgsConstructor + public static class NaverUserDetail { + private String id; + private String name; + private String email; + private String nickname; + private String profile_image; + } +} \ No newline at end of file