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

Feature: OAuth 기능 구현 #150

Open
wants to merge 8 commits into
base: develop
Choose a base branch
from
2 changes: 2 additions & 0 deletions backend/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,8 @@ dependencies {
implementation group: 'com.github.vladimir-bukhtoyarov', name: 'bucket4j-core', version: '8.0.1'
//aop
implementation 'org.springframework.boot:spring-boot-starter-aop'
//outh2
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
}

tasks.named('test') {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@
import middle_point_search.backend.common.security.filter.jwtFilter.JwtAuthenticationFilter;
import middle_point_search.backend.common.security.filter.jwtFilter.JwtTokenProvider;
import middle_point_search.backend.common.security.filter.loginFilter.LoginFilter;
import middle_point_search.backend.common.security.oAuth.CustomOAuth2UserService;
import middle_point_search.backend.common.security.oAuth.OAuth2LoginSuccessHandler;
import middle_point_search.backend.domains.member.repository.MemberRepository;

@Configuration
Expand All @@ -34,6 +36,8 @@ public class SecurityConfig {

private final UserDetailsService userDetailsService;
private final JwtTokenProvider jwtTokenProvider;
private final CustomOAuth2UserService customOAuth2UserService;
private final OAuth2LoginSuccessHandler oAuth2LoginSuccessHandler;

private final UrlBasedCorsConfigurationSource ConfigurationSource;
private final SecurityProperties securityProperties;
Expand Down Expand Up @@ -64,6 +68,12 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
.addFilterBefore(jwtAuthenticationFilter(), LoginFilter.class)
.addFilterBefore(exceptionHandlingFilter(), JwtAuthenticationFilter.class);

// oauth
http.oauth2Login((oauth2) -> oauth2
.userInfoEndpoint(userInfoEndpoint -> userInfoEndpoint
.userService(customOAuth2UserService))
.successHandler(oAuth2LoginSuccessHandler));

return http.build();
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,74 @@
package middle_point_search.backend.common.security.dto;

import java.util.Collection;
import java.util.List;
import java.util.Map;

import org.springframework.security.core.GrantedAuthority;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.oauth2.core.user.OAuth2User;

import lombok.Getter;
import middle_point_search.backend.domains.member.domain.Member;

@Getter
public class CustomUserDetails implements UserDetails, OAuth2User {

private Member member;
private Map<String, Object> attributes;

public CustomUserDetails(Member member, Map<String, Object> attributes) {
this.member = member;
this.attributes = attributes;
}

public CustomUserDetails(Member member) {
this.member = member;
}

@Override
public Map<String, Object> getAttributes() {
return this.attributes;
}

@Override
public Collection<? extends GrantedAuthority> getAuthorities() {
return List.<GrantedAuthority>of(new SimpleGrantedAuthority(member.getRole().getValue()));
}

@Override
public String getPassword() {
return member.getPw();
}

@Override
public String getUsername() {
return member.getEmail();
}

@Override
public boolean isAccountNonExpired() {
return true;
}

@Override
public boolean isAccountNonLocked() {
return true;
}

@Override
public boolean isCredentialsNonExpired() {
return true;
}

@Override
public boolean isEnabled() {
return true;
}

@Override
public String getName() {
return member.getName();
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
package middle_point_search.backend.common.security.dto;

import java.util.Map;

import lombok.Builder;
import lombok.Getter;
import lombok.ToString;
import middle_point_search.backend.domains.member.domain.Member;
import middle_point_search.backend.domains.member.domain.Role;

@Builder
@Getter
@ToString
public class OAuth2UserInfo {
private final static String GOOGLE = "google";
private final static String KAKAO = "kakao";
private final static String NAVER = "naver";

private String loginId;
private String email;
private String name;
private String provider;
private String providerId;

public static OAuth2UserInfo of(String provider, Map<String, Object> attributes) {
switch (provider) {
case GOOGLE:
return ofGoogle(attributes);
case KAKAO:
return ofKakao(attributes);
case NAVER:
return ofNaver(attributes);
default:
throw new RuntimeException();
}
}

private static OAuth2UserInfo ofGoogle(Map<String, Object> attributes) {
return OAuth2UserInfo.builder()
.provider(GOOGLE)
.loginId((String)attributes.get("email"))
.name((String)attributes.get("name"))
.email((String)attributes.get("email"))
.providerId((String)attributes.get("sub"))
.build();
}

private static OAuth2UserInfo ofKakao(Map<String, Object> attributes) {
return OAuth2UserInfo.builder()
.provider(KAKAO)
.loginId(attributes.get("id").toString())
.name((String)((Map)attributes.get("properties")).get("nickname"))
.providerId(attributes.get("id").toString())
.build();
}

private static OAuth2UserInfo ofNaver(Map<String, Object> attributes) {
return OAuth2UserInfo.builder()
.provider(NAVER)
.loginId((String)((Map)attributes.get("response")).get("id"))
.name((String)((Map)attributes.get("response")).get("name"))
.providerId((String)((Map)attributes.get("response")).get("id"))
.build();
}

public Member toEntity() {
return Member.createOAuthMember(loginId, name, Role.USER, provider, providerId);
}

}
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
package middle_point_search.backend.common.security.filter.loginFilter;

import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import middle_point_search.backend.common.security.dto.CustomUserDetails;
import middle_point_search.backend.domains.member.domain.Member;
import middle_point_search.backend.domains.member.repository.MemberRepository;

Expand All @@ -21,13 +21,6 @@ public UserDetails loadUserByUsername(String email) throws UsernameNotFoundExcep
Member member = memberRepository.findByEmail(email)
.orElseThrow(() -> new UsernameNotFoundException("해당하는 회원을 찾을 수 없습니다."));

String pw = member.getPw();
String role = member.getRole().getValue();

return User.builder()
.username(email)
.password(pw)
.roles(role)
.build();
return new CustomUserDetails(member);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -47,8 +47,7 @@ public Authentication attemptAuthentication(HttpServletRequest request, HttpServ
String email = obtainEmail(request);
String password = obtainPw(request);

UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email, password,
null);
UsernamePasswordAuthenticationToken authToken = new UsernamePasswordAuthenticationToken(email, password, null);

return getAuthenticationManager().authenticate(authToken);
}
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,43 @@
package middle_point_search.backend.common.security.oAuth;

import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
import org.springframework.security.oauth2.core.OAuth2AuthenticationException;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import middle_point_search.backend.common.security.dto.CustomUserDetails;
import middle_point_search.backend.common.security.dto.OAuth2UserInfo;
import middle_point_search.backend.domains.member.domain.Member;
import middle_point_search.backend.domains.member.repository.MemberRepository;

@Slf4j
@Service
@RequiredArgsConstructor
public class CustomOAuth2UserService extends DefaultOAuth2UserService {

private final MemberRepository memberRepository;

@Override
public OAuth2User loadUser(OAuth2UserRequest userRequest) throws OAuth2AuthenticationException {
OAuth2User oAuth2User = super.loadUser(userRequest);

String provider = userRequest.getClientRegistration().getRegistrationId();

// 3. 필요한 정보를 provider에 따라 다르게 mapping
OAuth2UserInfo oAuth2UserInfo = OAuth2UserInfo.of(provider, oAuth2User.getAttributes());

// 4. oAuth2UserInfo가 저장되어 있는지 유저 정보 확인
// 없으면 DB 저장 후 해당 유저를 저장
// 있으면 해당 유저를 저장
Member member = memberRepository.findByProviderAndProviderId(
oAuth2UserInfo.getProvider(),
oAuth2UserInfo.getProviderId())
.orElseGet(() -> memberRepository.save(oAuth2UserInfo.toEntity()));

// 5. UserDetails와 OAuth2User를 다중 상속한 CustomUserDetails
return new CustomUserDetails(member, oAuth2User.getAttributes());
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,35 @@
package middle_point_search.backend.common.security.oAuth;

import org.springframework.security.core.Authentication;
import org.springframework.security.web.authentication.SimpleUrlAuthenticationSuccessHandler;
import org.springframework.stereotype.Component;

import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import middle_point_search.backend.common.security.dto.CustomUserDetails;
import middle_point_search.backend.common.security.filter.jwtFilter.JwtTokenProvider;
import middle_point_search.backend.domains.member.domain.Member;

@Component
@RequiredArgsConstructor
public class OAuth2LoginSuccessHandler extends SimpleUrlAuthenticationSuccessHandler {

private final JwtTokenProvider jwtTokenProvider;

@Override
public void onAuthenticationSuccess(
HttpServletRequest request,
HttpServletResponse response,
Authentication authentication
) {
CustomUserDetails userDetails = (CustomUserDetails)authentication.getPrincipal();
Member member = userDetails.getMember();

String accessToken = jwtTokenProvider.createAccessToken(member.getId());
String refreshToken = jwtTokenProvider.createRefreshToken();

jwtTokenProvider.sendAccessAndRefreshToken(response, accessToken, refreshToken);
jwtTokenProvider.updateRefreshToken(member.getId(), refreshToken);
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,7 @@
package middle_point_search.backend.domains.member.domain;

import org.apache.commons.lang3.RandomStringUtils;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
import jakarta.persistence.EnumType;
Expand Down Expand Up @@ -35,15 +37,58 @@ public class Member extends BaseEntity {
@Column(nullable = false)
private Role role;

private Member(String email, String pw, String name, Role role) {
@Column(nullable = true)
private String provider;

@Column(nullable = true)
private String providerId;

private Member(
String email,
String pw,
String name,
Role role,
String provider,
String providerId
) {
this.email = email;
this.pw = pw;
this.name = name;
this.role = role;
this.provider = provider;
this.providerId = providerId;
}

public static Member from(String email, String pw, String name, Role role) {
public static Member createStandardMember(
String email,
String pw,
String name,
Role role
) {
return new Member(
email,
pw,
name,
role,
null,
null
);
}

return new Member(email, pw, name, role);
public static Member createOAuthMember(
String email,
String name,
Role role,
String provider,
String providerId
) {
return new Member(
email,
RandomStringUtils.randomAlphanumeric(20),
name,
role,
provider,
providerId
);
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -9,4 +9,6 @@
public interface MemberRepository extends JpaRepository<Member, Long> {

Optional<Member> findByEmail(String email);

Optional<Member> findByProviderAndProviderId(String provider, String providerId);
}
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ public class MemberService {
public void createMember(MemberCreateRequest request) {
String pw = passwordEncoderUtil.encodePassword(request.getPw());

Member member = Member.from(request.getEmail(), pw, request.getName(), Role.USER);
Member member = Member.createStandardMember(request.getEmail(), pw, request.getName(), Role.USER);
memberRepository.save(member);
}

Expand Down