Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ dependencies {
implementation 'io.jsonwebtoken:jjwt-api:0.11.5'
implementation 'io.jsonwebtoken:jjwt-impl:0.11.5'
implementation 'io.jsonwebtoken:jjwt-jackson:0.11.5'
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client' // Oauth
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@
import hs.kr.backend.devpals.global.exception.CustomException;
import hs.kr.backend.devpals.global.exception.ErrorException;
import hs.kr.backend.devpals.global.jwt.JwtTokenValidator;
import hs.kr.backend.devpals.infra.Aws.AwsS3Client;
import hs.kr.backend.devpals.infra.aws.AwsS3Client;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -11,11 +11,9 @@
import hs.kr.backend.devpals.global.common.ApiResponse;
import hs.kr.backend.devpals.global.exception.CustomException;
import hs.kr.backend.devpals.global.exception.ErrorException;
import hs.kr.backend.devpals.infra.Aws.AwsS3Client;
import hs.kr.backend.devpals.infra.aws.AwsS3Client;
import jakarta.annotation.PostConstruct;
import lombok.RequiredArgsConstructor;
import org.aspectj.weaver.Position;
import org.aspectj.weaver.patterns.AndPointcut;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,10 +3,8 @@
import hs.kr.backend.devpals.domain.Inquiry.dto.InquiryDto;
import hs.kr.backend.devpals.domain.Inquiry.entity.InquiryEntity;
import hs.kr.backend.devpals.domain.Inquiry.repository.InquiryRepository;
import hs.kr.backend.devpals.domain.project.dto.CommentDTO;
import hs.kr.backend.devpals.domain.project.entity.CommentEntity;
import hs.kr.backend.devpals.domain.project.repository.CommentRepoisitory;
import hs.kr.backend.devpals.domain.project.repository.RecommentRepository;
import hs.kr.backend.devpals.domain.project.service.ProjectService;
import hs.kr.backend.devpals.domain.user.dto.MyCommentResponse;
import hs.kr.backend.devpals.domain.user.dto.UserResponse;
Expand All @@ -18,15 +16,14 @@
import hs.kr.backend.devpals.global.exception.CustomException;
import hs.kr.backend.devpals.global.exception.ErrorException;
import hs.kr.backend.devpals.global.jwt.JwtTokenValidator;
import hs.kr.backend.devpals.infra.Aws.AwsS3Client;
import hs.kr.backend.devpals.infra.aws.AwsS3Client;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.multipart.MultipartFile;

import java.util.List;
import java.util.stream.Collectors;

@Service
@RequiredArgsConstructor
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,9 @@

import hs.kr.backend.devpals.domain.user.principal.CustomUserDetailsService;
import hs.kr.backend.devpals.global.jwt.jwtfilter.JwtAuthenticationFilter;
import hs.kr.backend.devpals.infra.oauth2.CustomOauth2UserService;
import hs.kr.backend.devpals.infra.oauth2.Oauth2LoginSuccessHandler;
import lombok.RequiredArgsConstructor;
import org.springframework.security.config.annotation.web.configurers.AbstractHttpConfigurer;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.context.annotation.Bean;
Expand All @@ -24,15 +27,14 @@
@Configuration
@EnableWebSecurity
@EnableMethodSecurity
@RequiredArgsConstructor
public class SecurityConfig {

private final JwtAuthenticationFilter jwtAuthenticationFilter;
private final CustomUserDetailsService userDetailsService;
private final CustomOauth2UserService customOauth2UserService;
private final Oauth2LoginSuccessHandler oauth2LoginSuccessHandler;

public SecurityConfig(JwtAuthenticationFilter jwtAuthenticationFilter, CustomUserDetailsService userDetailsService) {
this.jwtAuthenticationFilter = jwtAuthenticationFilter;
this.userDetailsService = userDetailsService;
}

@Bean
public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
Expand All @@ -44,6 +46,11 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.formLogin(AbstractHttpConfigurer::disable) // 폼 로그인 비활성화
.logout(AbstractHttpConfigurer::disable) // 로그아웃 비활성화
.authorizeHttpRequests(auth -> auth.anyRequest().permitAll()) // 모든 요청을 인증 없이 허용**
.oauth2Login(oauth2 -> oauth2
.userInfoEndpoint(userInfo -> userInfo
.userService(customOauth2UserService)) // 사용자 정보 처리
.successHandler(oauth2LoginSuccessHandler) // 로그인 성공 후 처리
)
.authenticationProvider(authenticationProvider()) // 커스텀 인증 제공자 추가
.addFilterBefore(jwtAuthenticationFilter, UsernamePasswordAuthenticationFilter.class); // JWT 필터 추가

Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package hs.kr.backend.devpals.infra.Aws;
package hs.kr.backend.devpals.infra.aws;

import hs.kr.backend.devpals.global.exception.CustomException;
import hs.kr.backend.devpals.global.exception.ErrorException;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,101 @@
package hs.kr.backend.devpals.infra.oauth2;

import hs.kr.backend.devpals.domain.user.entity.UserEntity;
import hs.kr.backend.devpals.domain.user.repository.UserRepository;
import jakarta.servlet.http.HttpServletRequest;
import lombok.RequiredArgsConstructor;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.Collections;
import java.util.Map;

@Service
@RequiredArgsConstructor
public class CustomOauth2UserService extends DefaultOAuth2UserService {

private final UserRepository userRepository;

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
OAuth2User oAuth2User = super.loadUser(userRequest);
String provider = userRequest.getClientRegistration().getRegistrationId();

// request에 provider 저장
ServletRequestAttributes attributes = (ServletRequestAttributes) RequestContextHolder.getRequestAttributes();
if (attributes != null) {
HttpServletRequest request = attributes.getRequest();
request.setAttribute("provider", provider);
}

String email = extractEmail(provider, oAuth2User);
String name = extractName(provider, oAuth2User);

if (email == null) {
throw new IllegalArgumentException("소셜 로그인 응답에서 email을 찾을 수 없습니다.");
}

UserEntity user = userRepository.findByEmail(email)
.orElseGet(() -> userRepository.save(new UserEntity(email, null, name, true)));

// attributes에 email이 없는 경우 추가해줌 (DefaultOAuth2User 생성시 필요)
Map<String, Object> attributesMap = new java.util.HashMap<>(oAuth2User.getAttributes());
attributesMap.put("email", email);

return new DefaultOAuth2User(
Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
attributesMap,
"email"
);
}

public static String extractEmail(String provider, OAuth2User oAuth2User) {
switch (provider) {
case "google":
return oAuth2User.getAttribute("email");

case "kakao":
Map<String, Object> kakaoAccount = oAuth2User.getAttribute("kakao_account");
return kakaoAccount != null ? (String) kakaoAccount.get("email") : null;

case "naver":
Map<String, Object> response = oAuth2User.getAttribute("response");
return response != null ? (String) response.get("email") : null;

case "github":
return oAuth2User.getAttribute("email");

default:
throw new IllegalArgumentException("Unsupported provider: " + provider);
}
}

public static String extractName(String provider, OAuth2User oAuth2User) {
switch (provider) {
case "google":
return oAuth2User.getAttribute("name");

case "kakao":
Map<String, Object> kakaoAccount = oAuth2User.getAttribute("kakao_account");
if (kakaoAccount == null) return null;
Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");
return profile != null ? (String) profile.get("nickname") : null;

case "naver":
Map<String, Object> response = oAuth2User.getAttribute("response");
return response != null ? (String) response.get("name") : null;

case "github":
return oAuth2User.getAttribute("name");

default:
throw new IllegalArgumentException("Unsupported provider: " + provider);
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,76 @@
package hs.kr.backend.devpals.infra.oauth2;

import com.fasterxml.jackson.databind.ObjectMapper;
import hs.kr.backend.devpals.domain.auth.dto.LoginResponse;
import hs.kr.backend.devpals.domain.auth.dto.TokenResponse;
import hs.kr.backend.devpals.domain.user.dto.LoginUserResponse;
import hs.kr.backend.devpals.domain.user.entity.UserEntity;
import hs.kr.backend.devpals.domain.user.repository.UserRepository;
import hs.kr.backend.devpals.global.exception.CustomException;
import hs.kr.backend.devpals.global.exception.ErrorException;
import hs.kr.backend.devpals.global.jwt.JwtTokenProvider;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.springframework.http.ResponseCookie;
import org.springframework.security.core.Authentication;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
@RequiredArgsConstructor
public class Oauth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

private final JwtTokenProvider jwtTokenProvider;
private final UserRepository userRepository;

@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
Authentication authentication) throws IOException, ServletException {

OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();

String provider = (String) request.getAttribute("provider");
if (provider == null) {
provider = oAuth2User.getAttributes().containsKey("kakao_account") ? "kakao" :
oAuth2User.getAttributes().containsKey("response") ? "naver" :
oAuth2User.getAttributes().containsKey("login") ? "github" : "google";
}

String email = CustomOauth2UserService.extractEmail(provider, oAuth2User);
if (email == null) {
throw new CustomException(ErrorException.USER_NOT_FOUND);
}

UserEntity user = userRepository.findByEmail(email)
.orElseThrow(() -> new CustomException(ErrorException.USER_NOT_FOUND));

String accessToken = jwtTokenProvider.generateToken(user.getId());
String refreshToken = jwtTokenProvider.generateRefreshToken(user.getId());

user.updateRefreshToken(refreshToken);
userRepository.save(user);

ResponseCookie refreshCookie = ResponseCookie.from("refreshToken", refreshToken)
.httpOnly(true)
.secure(true)
.path("/")
.maxAge(14 * 24 * 60 * 60)
.build();

LoginUserResponse userDto = LoginUserResponse.fromEntity(user);
TokenResponse tokenData = new TokenResponse(accessToken);
LoginResponse<TokenResponse> result = new LoginResponse<>(true, "소셜 로그인 성공", tokenData, userDto);

response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
response.setHeader("Set-Cookie", refreshCookie.toString());

ObjectMapper mapper = new ObjectMapper();
response.getWriter().write(mapper.writeValueAsString(result));
}
}