Skip to content
Open
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
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -57,6 +57,9 @@ dependencies {

// modelmapper
implementation 'org.modelmapper:modelmapper:2.4.4'

// OAuth2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}


Expand Down
10 changes: 10 additions & 0 deletions src/main/java/com/chukapoka/server/common/authority/AppConfig.java
Original file line number Diff line number Diff line change
Expand Up @@ -6,10 +6,14 @@
import org.modelmapper.convention.MatchingStrategies;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;

@Configuration
@RequiredArgsConstructor
public class AppConfig {

/** ModelMapper 사용하기위한 Bean 등록 */
@Bean
public ModelMapper modelMapper() {
ModelMapper modelMapper = new ModelMapper();
Expand All @@ -19,4 +23,10 @@ public ModelMapper modelMapper() {
.setFieldAccessLevel(org.modelmapper.config.Configuration.AccessLevel.PRIVATE);
return modelMapper;
}

/** 비밀번호 암호화를 위해 BCryptPasswordEncoder 등록 */
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,9 @@

import com.chukapoka.server.common.authority.jwt.JwtAuthenticationFilter;
import com.chukapoka.server.common.authority.jwt.JwtTokenProvider;
import com.chukapoka.server.common.authority.oauth2.handler.CustomAuthenticationFailHandler;
import com.chukapoka.server.common.authority.oauth2.handler.CustomAuthenticationSuccessHandler;
import com.chukapoka.server.common.authority.oauth2.service.CustomOAuth2UserService;
import com.chukapoka.server.common.enums.Authority;
import com.chukapoka.server.common.repository.TokenRepository;
import lombok.RequiredArgsConstructor;
Expand All @@ -14,8 +17,6 @@

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;

Expand All @@ -28,6 +29,10 @@ public class SecurityConfig {
*/
private final JwtTokenProvider jwtTokenProvider;
private final TokenRepository tokenRepository;
private final CustomOAuth2UserService customOAuth2UserService;
private final CustomAuthenticationSuccessHandler oAuth2LoginSuccessHandler;
private final CustomAuthenticationFailHandler oAuthenticationFailHandler;


@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
Expand All @@ -49,12 +54,19 @@ public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
.requestMatchers("/api/user/logout", "api/user/reissue", "api/tree/**","api/treeItem/**").hasRole(Authority.USER.getAuthority()// hasAnyRole은 "ROLE_" 접두사를 자동으로 추가해줌 하지만 Authority는 "ROLE_USER"로 설정해야했음 이것떄문에 회원가입할떄 권한이 안넘어갔음
);
});

/** OAuth2 로그인 설정 */
http
.oauth2Login((oauth2) ->
oauth2
.userInfoEndpoint(userInfoEndpointConfig ->
userInfoEndpointConfig
.userService(customOAuth2UserService)) // OAuth2 로그인시 사용자 정보를 가져오는 엔드포인트와 사용자 서비스를 설정
.failureHandler(oAuthenticationFailHandler) // OAuth2 로그인 실패시 처리할 핸들러를 지정
.successHandler(oAuth2LoginSuccessHandler) // OAuth2 로그인 성공시 처리할 핸들러를 지정
);

return http.build();
}

// 비밀번호 암호화를 위해 BCryptPasswordEncoder 등록
@Bean
public PasswordEncoder passwordEncoder() {
return new BCryptPasswordEncoder();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,89 @@
package com.chukapoka.server.common.authority.oauth2.dto;

import com.chukapoka.server.common.enums.EmailType;
import lombok.*;

import java.util.HashMap;
import java.util.Map;

@ToString
@Builder(access = AccessLevel.PRIVATE) // Builder 메서드를 외부에서 사용하지 않으므로, Private 제어자로 지정
@Getter
public class OAuth2Attribute {
private Map<String, Object> attributes; // 사용자 속성 정보를 담는 Map
private String attributeId; // 사용자 속성의 키 값
private String emailType; //GOOGLE , NAVER
private String email; // 이메일 정보
private String name; //사용자 정보



public static OAuth2Attribute of(String emailType, String userNameAttributeName, Map<String, Object> attributes) {
switch (emailType) {
case "google":
return ofGoogle(userNameAttributeName, attributes);
case "kakao":
return ofKakao( userNameAttributeName, attributes);
case "naver":
return ofNaver(emailType, userNameAttributeName, attributes);
default:
throw new RuntimeException();
}
}

/**
* Google 로그인일 경우 사용하는 메서드, 사용자 정보가 따로 Wrapping 되지 않고 제공되어,
* 바로 get() 메서드로 접근이 가능하다.
* */
private static OAuth2Attribute ofGoogle(String userNameAttributeName,
Map<String, Object> attributes ) {
return OAuth2Attribute.builder()
.email(attributes.get("email").toString())
.emailType(EmailType.GOOGLE.name())
.attributes(attributes)
.attributeId(attributes.get(userNameAttributeName).toString())
.name( attributes.get("name").toString())
.build();
}
/**
* Kakao 로그인일 경우 사용하는 메서드, 필요한 사용자 정보가 kakaoAccount -> kakaoProfile 두번 감싸져 있어서,
* 두번 get() 메서드를 이용해 사용자 정보를 담고있는 Map을 꺼내야한다.
* */
private static OAuth2Attribute ofKakao( String userNameAttributeName,Map<String, Object> attributes) {

Map<String, Object> kakaoAccount = (Map<String, Object>) attributes.get("kakao_account");
Map<String, Object> profile = (Map<String, Object>) kakaoAccount.get("profile");
return OAuth2Attribute.builder()
.email(kakaoAccount.get("email").toString())
.emailType(EmailType.KAKAO.name())
.attributes(kakaoAccount)
.attributeId(attributes.get(userNameAttributeName).toString())
.name(profile.get("nickname").toString())
.build();
}
/*
* Naver 로그인일 경우 사용하는 메서드, 필요한 사용자 정보가 response Map에 감싸져 있어서,
* 한번 get() 메서드를 이용해 사용자 정보를 담고있는 Map을 꺼내야한다.
* */
private static OAuth2Attribute ofNaver(String provider, String userNameAttributeName, Map<String, Object> attributes) {
Map<String, Object> response = (Map<String, Object>) attributes.get(userNameAttributeName);

return OAuth2Attribute.builder()
.email( response.get("email").toString())
.emailType(provider.toUpperCase())
.attributes(response)
.attributeId( response.get("id").toString())
.name(response.get("name").toString())
.build();
}

/** OAuth2User 객체에 넣어주기 위해서 Map으로 값들을 반환 */
public Map<String, Object> convertToMap() {
Map<String, Object> map = new HashMap<>();
map.put("id", attributeId);
map.put("emailType", emailType);
map.put("email", email);
map.put("name", name);
return map;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package com.chukapoka.server.common.authority.oauth2.handler;

import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;

@Component
public class CustomAuthenticationFailHandler implements AuthenticationFailureHandler {
@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response, AuthenticationException exception) throws IOException, ServletException {
/** 인증 실패시 메인 url로 이동 */
response.sendRedirect("http://localhost:8080/");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,65 @@
package com.chukapoka.server.common.authority.oauth2.handler;

import com.chukapoka.server.common.authority.jwt.JwtTokenProvider;
import com.chukapoka.server.common.dto.BaseResponse;
import com.chukapoka.server.common.dto.CustomUserDetails;
import com.chukapoka.server.common.dto.TokenDto;
import com.chukapoka.server.common.dto.TokenResponseDto;
import com.chukapoka.server.common.entity.Token;
import com.chukapoka.server.common.enums.ResultType;
import com.chukapoka.server.common.repository.TokenRepository;
import com.chukapoka.server.user.dto.UserResponseDto;
import com.fasterxml.jackson.databind.ObjectMapper;
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import java.io.IOException;


/** OAuth2 인증이 성공했을 경우 */
@Slf4j
@Component
@RequiredArgsConstructor
public class CustomAuthenticationSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {
private final JwtTokenProvider jwtTokenProvider;
private final TokenRepository tokenRepository;
@Override
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException {

CustomUserDetails userDetails = (CustomUserDetails) authentication.getPrincipal();
// 1. authentication 유저 정보에 따른 토큰 생성
TokenResponseDto token = saveToken(authentication, userDetails.getUser().getId().toString());
// 2. baseResponse 객체 생성
UserResponseDto userResponseDto = new UserResponseDto(ResultType.SUCCESS, userDetails.getEmail(), userDetails.getUserId(), token);
BaseResponse<UserResponseDto> baseResponse = new BaseResponse<>(ResultType.SUCCESS, userResponseDto);
// 3. JSON 형식으로 변환
ObjectMapper objectMapper = new ObjectMapper();
String jsonResponse = objectMapper.writeValueAsString(baseResponse);
// 4. 응답 헤더 설정
response.setContentType("application/json");
response.setCharacterEncoding("UTF-8");
// 5. 클라이언트에게 응답 전송
response.getWriter().write(jsonResponse);
}

/** 토큰 생성 */
private TokenResponseDto saveToken(Authentication authentication, String id){
System.out.println("OAuth2 토큰 생성중");
// JWT 토큰 생성
TokenDto jwtToken = jwtTokenProvider.createToken(authentication);
Token token = Token.builder()
.key(id)
.atValue(jwtToken.getAccessToken())
.rtValue(jwtToken.getRefreshToken())
.atExpiration(jwtToken.getAtExpiration())
.rtExpiration(jwtToken.getRtExpiration())
.build();
return tokenRepository.save(token).toResponseDto();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
package com.chukapoka.server.common.authority.oauth2.service;

import com.chukapoka.server.common.authority.oauth2.dto.OAuth2Attribute;
import com.chukapoka.server.common.dto.CustomUserDetails;
import com.chukapoka.server.common.enums.Authority;
import com.chukapoka.server.user.entity.User;
import com.chukapoka.server.user.repository.UserRepository;
import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import java.util.Map;
import java.util.Optional;

@Slf4j
@Service
@Transactional
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
Comment on lines +26 to +27

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

CustomOAuth2UserService 클래스에서 loadUser 메서드가 구현되어 있습니다. 해당 메서드는 OAuth2 사용자 정보를 가져오고 회원 가입 여부를 확인하여 처리하는 중요한 역할을 합니다.

private final BCryptPasswordEncoder bCryptPasswordEncoder = new BCryptPasswordEncoder();
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
log.debug("Loading user from OAuth2: {}", userRequest);

// 1. 기본 OAuth2UserService 객체 생성
OAuth2UserService<OAuth2UserRequest, OAuth2User> oAuth2UserService = new DefaultOAuth2UserService();
System.out.println("oAuth2UserService = " + oAuth2UserService);
// 2. OAuth2UserService를 사용하여 OAuth2User 정보를 가져온다.
OAuth2User oAuth2User = oAuth2UserService.loadUser(userRequest);
System.out.println("oAuth2User = " + oAuth2User);
// 3. 클라이언트 등록 ID(google, naver, kakao)와 사용자 이름 속성을 가져온다.
String registrationId = userRequest.getClientRegistration().getRegistrationId();
String userNameAttributeName = userRequest.getClientRegistration()
.getProviderDetails().getUserInfoEndpoint().getUserNameAttributeName();
Comment on lines +40 to +42

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

클라이언트 등록 ID와 사용자 이름 속성을 가져오는 부분입니다. 이 정보들은 OAuth2 로그인 시 필요한 중요한 정보이므로 정확하게 가져오는 것이 중요합니다.


// 4. OAuth2UserService를 사용하여 가져온 OAuth2User 정보로 OAuth2Attribute 객체를 만든다.
OAuth2Attribute oAuth2Attribute =
OAuth2Attribute.of(registrationId, userNameAttributeName, oAuth2User.getAttributes());

// 5. OAuth2Attribute의 속성값들을 Map으로 반환 받는다.
Map<String, Object> memberAttribute = oAuth2Attribute.convertToMap();
Comment on lines +46 to +49

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

OAuth2Attribute 객체를 생성하고 속성값을 Map으로 변환하는 부분입니다. 이 과정에서 속성값들이 올바르게 매핑되고 있는지 확인해야 합니다.

System.out.println("memberAttribute = " + memberAttribute);
// 6. 사용자 email(또는 id) 정보를 가져온다.
String email = (String) memberAttribute.get("email");
log.debug("Email retrieved from OAuth2 attributes: {}", email);
Comment on lines +52 to +53

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

이메일 정보를 가져오는 부분입니다. 이메일은 회원 식별에 중요한 정보이므로 올바르게 가져오는지 확인해야 합니다.


// 7. 이메일로 가입된 회원인지 조회한다.
Optional<User> findMember = userRepository.findByEmail(email);


Comment on lines +56 to +58

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

이메일로 가입된 회원인지 조회하는 부분입니다. 존재하지 않는 경우에 대한 처리가 올바르게 이루어지고 있는지 확인이 필요합니다.

User user;
/** 회원이 존재하지 않을 경우 */
if (findMember.isEmpty()) {
// user의 패스워드가 null이기 때문에 OAuth 유저는 일반적인 로그인을 할 수 없음.
user = User.builder()
.email(email)
.emailType((String) memberAttribute.get("emailType"))
.authorities("ROLE_"+Authority.USER.getAuthority())
.password(bCryptPasswordEncoder.encode((String) memberAttribute.get("id")))
.build();
Comment on lines +61 to +68

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

회원이 존재하지 않을 경우의 처리 부분입니다. 새로운 회원을 추가하고 패스워드를 설정하는 부분이므로 안전한 방식으로 처리되고 있는지 확인이 필요합니다.

userRepository.save(user);
}

/** 회원이 존재할 경우 */
else {
user = findMember.get();
user.setEmail(email); // Email이 변경 될 경우 업데이트
}
Comment on lines +73 to +76

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

회원이 이미 존재하는 경우의 처리 부분입니다. 기존 회원의 정보를 업데이트하는 부분이므로 정확하게 동작하는지 확인이 필요합니다.

return new CustomUserDetails(user, memberAttribute);
}

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Image description CodeRabbit

CustomUserDetails 객체를 반환하는 부분입니다. 이 부분에서 회원 정보와 속성값이 올바르게 포함되어 있는지 확인이 필요합니다.

+ 위의 변경 사항을 검토한 결과, 코드에는 특별히 문제가 없어 보입니다.

}
Loading