Skip to content

Commit 1f5e375

Browse files
authored
Merge pull request #192 from UDR-Sequence/feat/jsb/social-login
Feat/jsb/social login
2 parents 68ac608 + d1daa0a commit 1f5e375

14 files changed

Lines changed: 636 additions & 150 deletions

sequence_member/src/main/java/sequence/sequence_member/member/authority/OAuth2FailureHandler.java

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,27 +2,24 @@
22

33
import jakarta.servlet.http.HttpServletRequest;
44
import jakarta.servlet.http.HttpServletResponse;
5-
import org.slf4j.Logger;
6-
import org.slf4j.LoggerFactory;
5+
import lombok.extern.slf4j.Slf4j;
76
import org.springframework.security.core.AuthenticationException;
87
import org.springframework.security.web.authentication.AuthenticationFailureHandler;
98
import org.springframework.stereotype.Component;
109

1110
import java.io.IOException;
1211

12+
@Slf4j
1313
@Component
1414
public class OAuth2FailureHandler implements AuthenticationFailureHandler {
1515

16-
private static final Logger logger = LoggerFactory.getLogger(OAuth2FailureHandler.class);
17-
1816
@Override
1917
public void onAuthenticationFailure(HttpServletRequest request,
2018
HttpServletResponse response,
2119
AuthenticationException exception)
2220
throws IOException {
2321

24-
logger.error("소셜 로그인 실패: {}", exception.getMessage());
25-
exception.printStackTrace(); // 에러 로그 확인
22+
log.error("소셜 로그인 실패: {}", exception.getMessage());
2623
response.setStatus(HttpServletResponse.SC_UNAUTHORIZED);
2724
response.setContentType("application/json;charset=UTF-8");
2825
response.getWriter().write("{\"error\": \"소셜 로그인에 실패했습니다: " + exception.getMessage() + "\"}");

sequence_member/src/main/java/sequence/sequence_member/member/authority/OAuth2SuccessHandler.java

Lines changed: 14 additions & 27 deletions
Original file line numberDiff line numberDiff line change
@@ -5,33 +5,27 @@
55
import jakarta.servlet.http.HttpServletResponse;
66
import lombok.extern.slf4j.Slf4j;
77
import org.springframework.security.core.Authentication;
8-
import org.springframework.security.oauth2.core.oidc.user.DefaultOidcUser;
98
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
109
import org.springframework.stereotype.Component;
1110
import org.springframework.web.util.UriComponentsBuilder;
12-
import sequence.sequence_member.global.enums.enums.AuthProvider;
11+
import sequence.sequence_member.member.dto.MemberPrincipal;
1312
import sequence.sequence_member.member.entity.MemberEntity;
1413
import sequence.sequence_member.member.jwt.JWTUtil;
15-
import sequence.sequence_member.member.repository.MemberRepository;
1614
import sequence.sequence_member.member.service.TokenReissueService;
1715

1816
import java.io.IOException;
19-
import java.util.Optional;
2017

2118
@Slf4j
2219
@Component
2320
public class OAuth2SuccessHandler implements AuthenticationSuccessHandler {
24-
private final MemberRepository memberRepository;
2521
private final TokenReissueService tokenReissueService;
2622
private final JWTUtil jwtUtil;
27-
private final long ACCESS_TOKEN_EXPIRED_TIME = 600000L*60*1; // 1시간
28-
private final long REFRESH_TOKEN_EXPIRED_TIME = 600000L*60*24*7; // 7일
23+
private final long ACCESS_TOKEN_EXPIRED_TIME = 600000L * 60; // 1시간
24+
private final long REFRESH_TOKEN_EXPIRED_TIME = 600000L * 60 * 24 * 7; // 7일
2925

3026
public OAuth2SuccessHandler(
31-
MemberRepository memberRepository,
3227
TokenReissueService tokenReissueService,
3328
JWTUtil jwtUtil) {
34-
this.memberRepository = memberRepository;
3529
this.tokenReissueService = tokenReissueService;
3630
this.jwtUtil = jwtUtil;
3731
}
@@ -41,27 +35,20 @@ public void onAuthenticationSuccess(HttpServletRequest request,
4135
HttpServletResponse response,
4236
Authentication authentication) throws IOException {
4337

44-
DefaultOidcUser oidcUser = (DefaultOidcUser) authentication.getPrincipal();
38+
MemberPrincipal memberPrincipal = (MemberPrincipal) authentication.getPrincipal();
4539

46-
log.info("OAuth2 oidcUser={}", oidcUser);
40+
log.info("OAuth2 인증 성공, Principal: {}", memberPrincipal);
4741

48-
String email = oidcUser.getEmail();
49-
String name = oidcUser.getFullName();
50-
String providerId = oidcUser.getName(); // 고유 사용자 ID
42+
MemberEntity member = memberPrincipal.getMemberEntity();
5143

52-
Optional<MemberEntity> optionalUser = memberRepository.findByEmail(email);
44+
String email = member.getEmail();
45+
if (email == null || email.isEmpty()) {
46+
log.error("인증된 Principal에서 이메일(email)이 null 또는 비어있습니다. 로그인 처리 실패.");
47+
response.sendError(HttpServletResponse.SC_BAD_REQUEST, "Email not found in OAuth2 principal.");
48+
return;
49+
}
5350

54-
MemberEntity member = optionalUser.orElseGet(() -> {
55-
MemberEntity newUser = MemberEntity.createSocialMember(
56-
email,
57-
name,
58-
AuthProvider.GOOGLE,
59-
providerId
60-
);
61-
return memberRepository.save(newUser);
62-
});
63-
64-
log.info("OAuth2 로그인 성공: email={}, provider={}", member.getEmail(), member.getProvider());
51+
log.info("OAuth2 로그인 성공: email={}", email);
6552

6653
String username = member.getUsername();
6754

@@ -76,7 +63,7 @@ public void onAuthenticationSuccess(HttpServletRequest request,
7663
// 쿠키에 refreshToken 설정
7764
Cookie refreshTokenCookie = new Cookie("refresh", refresh);
7865
refreshTokenCookie.setHttpOnly(true);
79-
refreshTokenCookie.setSecure(false); // 배포 시 true (https)
66+
refreshTokenCookie.setSecure(false); // 로컬 HTTP 환경에서는 false, 배포 HTTPS 시 true로 변경해야 합니다.
8067
refreshTokenCookie.setPath("/");
8168
refreshTokenCookie.setMaxAge((int) (REFRESH_TOKEN_EXPIRED_TIME / 1000));
8269
response.addCookie(refreshTokenCookie);

sequence_member/src/main/java/sequence/sequence_member/member/config/SecurityConfig.java

Lines changed: 53 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -11,9 +11,16 @@
1111
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
1212
import org.springframework.security.config.http.SessionCreationPolicy;
1313
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
14+
import org.springframework.security.oauth2.client.web.AuthorizationRequestRepository;
15+
import org.springframework.security.oauth2.core.endpoint.OAuth2AuthorizationRequest;
16+
import org.springframework.security.web.AuthenticationEntryPoint;
1417
import org.springframework.security.web.SecurityFilterChain;
18+
import org.springframework.security.web.authentication.DelegatingAuthenticationEntryPoint;
19+
import org.springframework.security.web.authentication.Http403ForbiddenEntryPoint;
1520
import org.springframework.security.web.authentication.UsernamePasswordAuthenticationFilter;
1621
import org.springframework.security.web.authentication.logout.LogoutFilter;
22+
import org.springframework.security.web.util.matcher.AntPathRequestMatcher;
23+
import org.springframework.security.web.util.matcher.RequestMatcher;
1724
import org.springframework.web.cors.CorsConfiguration;
1825
import org.springframework.web.cors.CorsConfigurationSource;
1926
import sequence.sequence_member.member.authority.OAuth2FailureHandler;
@@ -22,13 +29,15 @@
2229
import sequence.sequence_member.member.jwt.JWTFilter;
2330
import sequence.sequence_member.member.jwt.JWTUtil;
2431
import sequence.sequence_member.member.jwt.LoginFilter;
32+
import sequence.sequence_member.member.repository.CustomAuthorizationRequestRepository;
2533
import sequence.sequence_member.member.repository.MemberRepository;
2634
import sequence.sequence_member.member.repository.RefreshRepository;
27-
import sequence.sequence_member.member.service.CustomOAuth2UserService;
35+
import sequence.sequence_member.member.service.CustomOidcUserService;
2836
import sequence.sequence_member.member.service.TokenReissueService;
2937

3038
import java.util.Arrays;
3139
import java.util.Collections;
40+
import java.util.LinkedHashMap;
3241

3342
@Configuration
3443
@EnableWebSecurity
@@ -43,7 +52,7 @@ public class SecurityConfig {
4352
private final MemberRepository memberRepository;
4453
private final OAuth2FailureHandler oAuth2FailureHandler;
4554
private final OAuth2SuccessHandler oAuth2SuccessHandler;
46-
private final CustomOAuth2UserService customOAuth2UserService;
55+
private final CustomOidcUserService customOidcUserService;
4756

4857
@Bean
4958
public AuthenticationManager authenticationManager(AuthenticationConfiguration configuration) throws Exception {
@@ -55,6 +64,11 @@ public BCryptPasswordEncoder bCryptPasswordEncoder(){
5564
return new BCryptPasswordEncoder();
5665
}
5766

67+
@Bean
68+
public AuthorizationRequestRepository<OAuth2AuthorizationRequest> authorizationRequestRepository() {
69+
return new CustomAuthorizationRequestRepository();
70+
}
71+
5872
@Bean
5973
public SecurityFilterChain filterChain(HttpSecurity http) throws Exception{
6074
// Custom LoginFilter 등록
@@ -100,24 +114,54 @@ public CorsConfiguration getCorsConfiguration(HttpServletRequest request) {
100114
//경로별 인가 작업
101115
http
102116
.authorizeHttpRequests((auth)->auth
103-
.requestMatchers("/api/login", "/api/users/join", "/api/token", "/api/users/check_username", "/api/users/check_email", "/api/users/check_nickname", "/api/skills/**", "/api/users/test", "/api/auth/**").permitAll()
117+
// 인증이 필요 없는 경로들을 먼저 정의합니다.
118+
.requestMatchers(
119+
"/api/login",
120+
"/api/users/join",
121+
"/api/token",
122+
"/api/users/check_username",
123+
"/api/users/check_email",
124+
"/api/users/check_nickname",
125+
"/api/skills/**",
126+
"/api/users/test",
127+
"/api/auth/**",
128+
"/error",
129+
"/actuator/**",
130+
"/oauth2/**",
131+
"/login/oauth2/code/**",
132+
"/favicon.ico",
133+
"/.well-known/**",
134+
"/"
135+
).permitAll()
104136
.requestMatchers(HttpMethod.GET,"/api/projects/**").permitAll()
105137
.requestMatchers(HttpMethod.GET, "/api/archive/projects/**").permitAll()
106138
.requestMatchers(HttpMethod.GET, "/api/archive/{archiveId}").permitAll()
107-
.requestMatchers("/api/archive/**").authenticated()
108-
.requestMatchers("/error").permitAll()
109-
.requestMatchers("/actuator/**").permitAll()
110-
.requestMatchers("/oauth2/**").permitAll()
111-
.anyRequest().authenticated());
139+
.requestMatchers("/api/archive/**").authenticated() // 인증 필요
140+
.anyRequest().authenticated()); // 나머지 모든 요청은 인증 필요
112141
http
113142
.oauth2Login(oauth2 -> oauth2
143+
.authorizationEndpoint(authEndpoint -> authEndpoint
144+
.authorizationRequestRepository(authorizationRequestRepository())
145+
)
114146
.userInfoEndpoint(userInfo -> userInfo
115-
.userService(customOAuth2UserService)
147+
.oidcUserService(customOidcUserService)
116148
)
117149
.successHandler(oAuth2SuccessHandler)
118150
.failureHandler(oAuth2FailureHandler)
119151
);
120152

153+
LinkedHashMap<RequestMatcher, AuthenticationEntryPoint> entryPoints = new LinkedHashMap<>();
154+
entryPoints.put(new AntPathRequestMatcher("/oauth2/authorization/**"), new Http403ForbiddenEntryPoint());
155+
entryPoints.put(new AntPathRequestMatcher("/favicon.ico"), new Http403ForbiddenEntryPoint());
156+
entryPoints.put(new AntPathRequestMatcher("/.well-known/**"), new Http403ForbiddenEntryPoint());
157+
158+
http
159+
.exceptionHandling(exceptionHandling -> {
160+
DelegatingAuthenticationEntryPoint delegatingEntryPoint = new DelegatingAuthenticationEntryPoint(entryPoints);
161+
delegatingEntryPoint.setDefaultEntryPoint(new Http403ForbiddenEntryPoint()); // 기본 엔트리포인트 명시적으로 설정
162+
exceptionHandling.authenticationEntryPoint(delegatingEntryPoint);
163+
});
164+
121165
http
122166
.addFilterBefore(new JWTFilter(jwtUtil), LoginFilter.class);
123167

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package sequence.sequence_member.member.controller;
2+
3+
import jakarta.servlet.http.HttpSession;
4+
import lombok.extern.slf4j.Slf4j;
5+
import org.springframework.http.ResponseEntity;
6+
import org.springframework.security.core.context.SecurityContextHolder;
7+
import org.springframework.web.bind.annotation.GetMapping;
8+
import org.springframework.web.bind.annotation.RequestMapping;
9+
import org.springframework.web.bind.annotation.RestController;
10+
import org.springframework.web.util.UriComponentsBuilder;
11+
import sequence.sequence_member.global.response.ApiResponseData;
12+
13+
import java.util.UUID;
14+
15+
@Slf4j
16+
@RestController
17+
@RequestMapping("/api/social")
18+
public class AuthenticationController {
19+
@GetMapping("/bind/google")
20+
public ResponseEntity<ApiResponseData<String>> bindGoogle(
21+
HttpSession session
22+
) {
23+
String username = SecurityContextHolder.getContext().getAuthentication().getName();
24+
log.info("계정 연동 요청: username = {}, provider = google", username);
25+
26+
String bindingDataSessionKey = "oauth2_binding_username";
27+
session.setAttribute(bindingDataSessionKey, username);
28+
log.info("세션에 바인딩할 사용자 이름 저장 완료: {}", username);
29+
30+
String bindingStateToken = "bind:" + UUID.randomUUID();
31+
String redirectUri = UriComponentsBuilder
32+
.fromUriString("http://localhost:8080" + "/oauth2/authorization/google")
33+
.queryParam("binding_state_token", bindingStateToken)
34+
.build()
35+
.toUriString();
36+
37+
return ResponseEntity.ok().body(ApiResponseData.success(redirectUri));
38+
}
39+
}

sequence_member/src/main/java/sequence/sequence_member/member/dto/MemberPrincipal.java

Lines changed: 30 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -3,21 +3,27 @@
33
import org.springframework.security.core.GrantedAuthority;
44
import org.springframework.security.core.authority.SimpleGrantedAuthority;
55
import org.springframework.security.core.userdetails.UserDetails;
6-
import org.springframework.security.oauth2.core.user.OAuth2User;
6+
import org.springframework.security.oauth2.core.oidc.OidcIdToken;
7+
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
8+
import org.springframework.security.oauth2.core.oidc.user.OidcUser;
79
import sequence.sequence_member.member.entity.MemberEntity;
810

911
import java.util.Collection;
1012
import java.util.Collections;
1113
import java.util.Map;
1214

13-
public class MemberPrincipal implements OAuth2User, UserDetails {
15+
public class MemberPrincipal implements OidcUser, UserDetails {
1416

1517
private final MemberEntity member;
1618
private final Map<String, Object> attributes;
19+
private final OidcIdToken idToken;
20+
private final OidcUserInfo userInfo;
1721

18-
public MemberPrincipal(MemberEntity member, Map<String, Object> attributes) {
22+
public MemberPrincipal(MemberEntity member, Map<String, Object> attributes, OidcIdToken idToken, OidcUserInfo userInfo) {
1923
this.member = member;
2024
this.attributes = attributes;
25+
this.idToken = idToken;
26+
this.userInfo = userInfo;
2127
}
2228

2329
public MemberEntity getMember() {
@@ -69,7 +75,26 @@ public String getName() {
6975
return member.getName();
7076
}
7177

72-
public static MemberPrincipal create(MemberEntity member, Map<String, Object> attributes) {
73-
return new MemberPrincipal(member, attributes);
78+
public static MemberPrincipal create(MemberEntity member, Map<String, Object> attributes, OidcIdToken idToken, OidcUserInfo userInfo) {
79+
return new MemberPrincipal(member, attributes, idToken, userInfo);
80+
}
81+
82+
@Override
83+
public Map<String, Object> getClaims() {
84+
return Map.of();
85+
}
86+
87+
@Override
88+
public OidcUserInfo getUserInfo() {
89+
return null;
90+
}
91+
92+
@Override
93+
public OidcIdToken getIdToken() {
94+
return null;
95+
}
96+
97+
public MemberEntity getMemberEntity() {
98+
return this.member;
7499
}
75100
}
Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,39 @@
1+
package sequence.sequence_member.member.entity;
2+
3+
import jakarta.persistence.*;
4+
import lombok.AccessLevel;
5+
import lombok.Getter;
6+
import lombok.NoArgsConstructor;
7+
import sequence.sequence_member.global.enums.enums.AuthProvider;
8+
9+
@Entity
10+
@Getter
11+
@NoArgsConstructor(access = AccessLevel.PROTECTED)
12+
@Table(name = "member_auth_provider")
13+
public class MemberAuthProvider {
14+
15+
@Id
16+
@GeneratedValue(strategy = GenerationType.IDENTITY)
17+
private Long id;
18+
19+
@ManyToOne(fetch = FetchType.LAZY)
20+
@JoinColumn(name = "member_id", nullable = false)
21+
private MemberEntity member;
22+
23+
@Enumerated(EnumType.STRING)
24+
@Column(name = "provider", nullable = false, length = 50)
25+
private AuthProvider provider;
26+
27+
@Column(name = "provider_id", nullable = true, unique = true, length = 255)
28+
private String providerId;
29+
30+
public MemberAuthProvider(MemberEntity member, AuthProvider provider, String providerId) {
31+
this.member = member;
32+
this.provider = provider;
33+
this.providerId = providerId;
34+
}
35+
36+
protected void setMember(MemberEntity member) {
37+
this.member = member;
38+
}
39+
}

0 commit comments

Comments
 (0)