Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Feat/#51 social #60

Merged
merged 14 commits into from
Nov 28, 2023
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
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;
dong2ast marked this conversation as resolved.
Show resolved Hide resolved

@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;
dong2ast marked this conversation as resolved.
Show resolved Hide resolved
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 객체를 생성 후 반환
dong2ast marked this conversation as resolved.
Show resolved Hide resolved
* 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