Skip to content

Commit

Permalink
Feat/#51 social (#60)
Browse files Browse the repository at this point in the history
* feat: oauth2 구현

* feat: oauth2 구현에 맞게 security 설정 변경

* feat: 테스트용 controller 생성

* comment: 당장 필요없는 메서드 주석 처리

* refactor: 필요없는 주석 삭제 및 코드 변경

* refactor: 랜덤 아이디 생성 로직 service 단으로 이동

* feat: 추후 가입된 아이디와 social 로그인 연동을 위한 로직 구현

* refactor: tab and swagger tag

(cherry picked from commit 7fd4b1a)

* 🔥 remove: Test Controller 삭제

* feat: 네이버 소셜로그인 추가

* fix: 빌드 에러 수정

---------

Co-authored-by: baebae02 <bae4614@gmail.com>
  • Loading branch information
dong2ast and baebae02 authored Nov 28, 2023
1 parent 8b63358 commit c0be1e2
Show file tree
Hide file tree
Showing 14 changed files with 388 additions and 44 deletions.
3 changes: 3 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,9 @@ dependencies {
//Redis
implementation 'org.springframework.boot:spring-boot-starter-data-redis'

//OAuth 2.0
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'

//swagger
implementation 'org.springdoc:springdoc-openapi-ui:1.7.0'

Expand Down
57 changes: 38 additions & 19 deletions src/main/java/com/uspray/uspray/config/SecurityConfig.java
Original file line number Diff line number Diff line change
@@ -1,12 +1,16 @@
package com.uspray.uspray.config;

import com.uspray.uspray.external.client.oauth2.CustomOAuth2UserService;
import com.uspray.uspray.external.client.oauth2.OAuth2LoginFailureHandler;
import com.uspray.uspray.external.client.oauth2.OAuth2LoginSuccessHandler;
import com.uspray.uspray.jwt.JwtAccessDeniedHandler;
import com.uspray.uspray.jwt.JwtAuthenticationEntryPoint;
import com.uspray.uspray.jwt.TokenProvider;
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.configuration.WebSecurityCustomizer;
import org.springframework.security.config.http.SessionCreationPolicy;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
Expand All @@ -15,11 +19,15 @@

@Configuration
@RequiredArgsConstructor
@EnableWebSecurity
public class SecurityConfig {

private final TokenProvider tokenProvider;
private final JwtAuthenticationEntryPoint jwtAuthenticationEntryPoint;
private final JwtAccessDeniedHandler jwtAccessDeniedHandler;
private final CustomOAuth2UserService customOAuth2UserService;
private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;
private final OAuth2LoginFailureHandler oAuth2LoginFailureHandler;

@Bean
public PasswordEncoder passwordEncoder() {
Expand All @@ -35,25 +43,36 @@ public WebSecurityCustomizer webSecurityCustomizer() {
@Bean
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception {
http.csrf().disable()
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)
.and()
.headers()
.frameOptions()
.sameOrigin()
.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)
.and()
.authorizeRequests()
.antMatchers("/auth/**").permitAll()
.antMatchers("/swagger-ui/**", "/v3/api-docs/**", "/api-docs/**", "/swagger-ui.html").permitAll()
.antMatchers("/sms/**").permitAll()
.antMatchers("/admin/**").permitAll()
.anyRequest().authenticated()
.and()
.apply(new JwtSecurityConfig(tokenProvider));
.exceptionHandling()
.authenticationEntryPoint(jwtAuthenticationEntryPoint)
.accessDeniedHandler(jwtAccessDeniedHandler)

.and()
.headers()
.frameOptions()
.sameOrigin()

.and()
.sessionManagement()
.sessionCreationPolicy(SessionCreationPolicy.STATELESS)

.and()
.authorizeRequests()
.antMatchers("/","/css/**","/images/**","/js/**","/favicon.ico","/h2-console/**").permitAll()
.antMatchers("/auth/**").permitAll()
.antMatchers("/swagger-ui/**", "/v3/api-docs/**", "/api-docs/**", "/swagger-ui.html").permitAll()
.antMatchers("/sms/**").permitAll()
.antMatchers("/admin/**").permitAll()
.anyRequest().authenticated()
.and()
.apply(new JwtSecurityConfig(tokenProvider));

http.oauth2Login()
.successHandler(oAuth2LoginSuccessHandler) // 동의하고 계속하기를 눌렀을 때 Handler 설정
.failureHandler(oAuth2LoginFailureHandler) // 소셜 로그인 실패 시 핸들러 설정
.userInfoEndpoint().userService(customOAuth2UserService) // customUserService 설정
.and()
.permitAll();

return http.build();
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@
import com.uspray.uspray.DTO.auth.request.MemberRequestDto;
import com.uspray.uspray.DTO.auth.response.MemberResponseDto;
import com.uspray.uspray.exception.SuccessStatus;
import com.uspray.uspray.jwt.TokenProvider;
import com.uspray.uspray.service.AuthService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.Parameter;
Expand All @@ -36,9 +35,7 @@
@RequiredArgsConstructor
@Tag(name = "Auth", description = "Auth 관련 API")
public class AuthController {

private final TokenProvider tokenProvider;
private final AuthService authService;
private final AuthService authService;

@PostMapping("/signup")
@ApiResponse(
Expand Down
41 changes: 21 additions & 20 deletions src/main/java/com/uspray/uspray/domain/Member.java
Original file line number Diff line number Diff line change
Expand Up @@ -23,37 +23,50 @@
@Where(clause = "deleted=false")
public class Member extends AuditingTimeEntity {

private final Boolean deleted = false;
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
@Column(name = "member_id")
private Long id;

private String userId;
private String password;

private String name;
private String phone;
private String birth;
private String gender;
private String firebaseToken;

private Boolean firstNotiAgree = true;
private Boolean secondNotiAgree = true;
private Boolean thirdNotiAgree = true;

private final Boolean deleted = false;

private String socialId;
@Enumerated(EnumType.STRING)
private Authority authority;

@OneToMany(mappedBy = "author")
private List<GroupPray> groupPrayList;

@ManyToMany
@JoinTable(name = "member_group",
joinColumns = @JoinColumn(name = "member_id"),
inverseJoinColumns = @JoinColumn(name = "group_id"))
private Set<Group> groups = new HashSet<>();

@OneToMany(mappedBy = "author")
private List<GroupPray> groupPrayList;

@Builder
public Member(String userId, String password, String name, String phone, String birth,
String gender, Authority authority, String socialId) {
this.userId = userId;
this.password = password;
this.name = name;
this.phone = phone;
this.birth = birth;
this.gender = gender;
this.socialId = socialId;
this.authority = authority;
}
public void changeSocialId(String socialId) {
this.socialId = socialId;
}

public void changeFirebaseToken(String firebaseToken) {
this.firebaseToken = firebaseToken;
Expand All @@ -75,18 +88,6 @@ public void leaveGroup(Group group) {
this.groups.remove(group);
}

@Builder
public Member(String userId, String password, String name, String phone, String birth,
String gender, Authority authority) {
this.userId = userId;
this.password = password;
this.name = name;
this.phone = phone;
this.birth = birth;
this.gender = gender;
this.authority = authority;
}

public void changeNotificationSetting(NotificationAgreeDto notificationAgreeDto) {
switch (notificationAgreeDto.getNotificationType()) {
case PRAY_TIME:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@
@Getter
@RequiredArgsConstructor(access = AccessLevel.PRIVATE)
public enum SuccessStatus {

/**
* 200 OK
*/
Expand Down Expand Up @@ -40,6 +39,7 @@ public enum SuccessStatus {
CHANGE_GROUP_LEADER_SUCCESS(HttpStatus.OK, "모임 리더 위임에 성공했습니다."),
KICK_GROUP_MEMBER_SUCCESS(HttpStatus.OK, "모임 멤버 내보내기에 성공했습니다."),
ADD_GROUP_MEMBER_SUCCESS(HttpStatus.OK, "모임 멤버 추가하기에 성공했습니다."),
TEST_SUCCESS(HttpStatus.OK, "Test :: OK"),

/*
* 201 created
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
package com.uspray.uspray.external.client.oauth2;

import com.uspray.uspray.Enums.Authority;
import java.util.Collection;
import java.util.Map;
import lombok.Getter;
import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;

@Getter
public class CustomOAuth2User extends DefaultOAuth2User {
private final Authority authority;

public CustomOAuth2User(
Collection<? extends GrantedAuthority> authorities,
Map<String, Object> attributes, String nameAttributeKey,
Authority authority) {
super(authorities, attributes, nameAttributeKey);
this.authority = authority;
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,100 @@
package com.uspray.uspray.external.client.oauth2;

import com.uspray.uspray.domain.Member;
import com.uspray.uspray.infrastructure.MemberRepository;
import java.util.Collections;
import lombok.RequiredArgsConstructor;
import org.apache.commons.lang3.RandomStringUtils;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.client.userinfo.OAuth2UserService;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService implements OAuth2UserService<OAuth2UserRequest, OAuth2User> {

private final MemberRepository memberRepository;

/**
* DefaultOAuth2UserService 객체를 생성하여, loadUser(userRequest)를 통해 DefaultOAuth2User 객체를 생성 후 반환
* DefaultOAuth2UserService의 loadUser()는 소셜 로그인 API의 사용자 정보 제공 URI로 요청을 보내서
* 사용자 정보를 얻은 후, 이를 통해 DefaultOAuth2User 객체를 생성 후 반환한다.
* 결과적으로, OAuth2User는 OAuth 서비스에서 가져온 유저 정보를 담고 있는 유저
*/
@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2UserService<OAuth2UserRequest, OAuth2User> delegate = new DefaultOAuth2UserService();
OAuth2User oAuth2User = delegate.loadUser(userRequest); // OAuth2 정보를 가져옵니다.

//현재 로그인 진행 중인 서비스를 구분하는 코드 (구글 or 네이버 or 카카오 ...)
String registrationId = userRequest.getClientRegistration().getRegistrationId();
//OAuth2 로그인 진행 시 키가되는 필드 값, Primary Key와 같은 의미
String userNameAttributeName = userRequest.getClientRegistration().getProviderDetails()
.getUserInfoEndpoint().getUserNameAttributeName();

//OAuth2UserService를 통해 가져온 OAuth2User의 attribute를 담을 클래스
// 소셜 로그인에서 API가 제공하는 userInfo의 Json 값(유저 정보들)
OAuthAttributes attributes = OAuthAttributes.of(registrationId, userNameAttributeName,
oAuth2User.getAttributes());

Member member = getMember(attributes);

return new CustomOAuth2User(
Collections.singleton(new SimpleGrantedAuthority(member.getAuthority().name())),
oAuth2User.getAttributes(),
attributes.getNameAttributeKey(),
member.getAuthority()
);
}

private Member getMember(OAuthAttributes attributes) {
Member findMember = memberRepository.findBySocialId(
attributes.getOAuth2UserInfo().getId()).orElse(null);
Authentication authentication = SecurityContextHolder.getContext().getAuthentication();

if (authentication != null) { //소셜 로그인 연동
return saveMember(attributes, authentication.getName());
}

if (findMember == null) {
return saveMember(attributes);
}

return findMember;
}

/**
* 이미 존재하는 회원이라면 이름과 프로필이미지를 업데이트해줍니다.
* 처음 가입하는 회원이라면 Member 테이블을 생성합니다. (소셜 회원가입)
**/
private Member saveMember(OAuthAttributes attributes, String userId) {
//기존 유저와 아이디가 같으면 같은 유저임
// update는 기존 유저의 소셜 ID 컬럼에 값을 추가하는 것 정도만 있으면 될듯
Member member = memberRepository.getMemberByUserId(userId);
member.changeSocialId(attributes.getOAuth2UserInfo().getId());
return memberRepository.save(member);
}

private Member saveMember(OAuthAttributes attributes) {
//기존 유저와 아이디가 같으면 같은 유저임
// update는 기존 유저의 소셜 ID 컬럼에 값을 추가하는 것 정도만 있으면 될듯
Member member = attributes.toEntity(attributes.getOAuth2UserInfo(), generateRandomId());
return memberRepository.save(member);
}

private String generateRandomId() {
while (true) {
String randomId = RandomStringUtils.random(15, true, true);
if (!memberRepository.existsByUserId(randomId)) {
return randomId;
}
}
}

}
Original file line number Diff line number Diff line change
@@ -0,0 +1,23 @@
package com.uspray.uspray.external.client.oauth2;

import java.io.IOException;
import javax.servlet.ServletException;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.core.AuthenticationException;
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
import org.springframework.stereotype.Component;

@Slf4j
@Component
public class OAuth2LoginFailureHandler implements AuthenticationFailureHandler {

@Override
public void onAuthenticationFailure(HttpServletRequest request, HttpServletResponse response,
AuthenticationException exception) throws IOException, ServletException {
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
response.getWriter().write("소셜 로그인이 실패하였습니다.");
log.error("소셜 로그인에 실패했습니다. 에러메세지 : {}", exception.getMessage());
}
}
Loading

0 comments on commit c0be1e2

Please sign in to comment.