Skip to content

Commit 602a801

Browse files
authored
Merge pull request #37 from May-I-AI-Integrated-Platform/feature/35/googleSocialLogin
[FEAT] #35 구글 소셜로그인 구현
2 parents ff635e4 + 3f41e5a commit 602a801

File tree

14 files changed

+233
-10
lines changed

14 files changed

+233
-10
lines changed

build.gradle

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,9 @@ dependencies {
4747

4848
//WebClient
4949
implementation 'org.springframework.boot:spring-boot-starter-webflux'
50+
51+
//oauth2
52+
implementation 'org.springframework.boot:spring-boot-starter-oauth2-client'
5053
}
5154
tasks.named('test') {
5255
useJUnitPlatform()

src/main/java/ai/Mayi/apiPayload/code/status/ErrorStatus.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,7 +28,8 @@ public enum ErrorStatus implements BaseErrorCode {
2828
_EXPIRED_ACCESS_TOKEN(HttpStatus.UNAUTHORIZED, "JWT402", "액세스 토큰이 만료되었습니다."),
2929
_UNSUPPORTED_JWT(HttpStatus.UNAUTHORIZED, "JWT403", "지원되지 않는 JWT 토큰입니다."),
3030
_EMPTY_JWT(HttpStatus.UNAUTHORIZED, "JWT404", "JWT 토큰이 비어 있습니다."),
31-
_EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "JWT405", "리프레쉬토큰이 만료되었습니다. 로그인을 진행해주세요."),
31+
_EXPIRED_REFRESH_TOKEN(HttpStatus.UNAUTHORIZED, "JWT405", "리프레쉬토큰이 만료되었거나, 없습니다. 로그인을 진행해주세요."),
32+
_REFRESHED_ACCESS_TOKEN(HttpStatus.valueOf(419), "JWT406", "AccessToken이 재발급되었습니다. 요청을 다시 보내주세요."),
3233

3334
//토큰 관련 응답
3435
_NOT_EXIST_TOKEN_TYPE(HttpStatus.NOT_FOUND, "TOKEN501", "존재하지 않는 토큰 타입입니다"),

src/main/java/ai/Mayi/config/SecurityConfig.java

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,8 @@
22

33
import ai.Mayi.jwt.JwtAuthenticationFilter;
44
import ai.Mayi.jwt.JwtUtil;
5+
import ai.Mayi.oauth.CustomOAuth2SuccessHandler;
6+
import ai.Mayi.oauth.CustomOAuth2UserService;
57
import ai.Mayi.repository.UserRepository;
68
import ai.Mayi.service.MyUserDetailsService;
79
import lombok.RequiredArgsConstructor;
@@ -22,6 +24,8 @@ public class SecurityConfig {
2224
private final JwtUtil jwtUtil;
2325
private final UserRepository userRepository;
2426
private final MyUserDetailsService myUserDetailsService;
27+
private final CustomOAuth2UserService customOAuth2UserService;
28+
private final CustomOAuth2SuccessHandler customOAuth2SuccessHandler;
2529

2630
@Bean
2731
public JwtAuthenticationFilter jwtAuthenticationFilter() {
@@ -45,8 +49,17 @@ public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Excepti
4549
// 요청에 대한 인증 및 권한 설정
4650
.authorizeHttpRequests(auth -> auth
4751
// .requestMatchers("/**").permitAll() //
48-
.requestMatchers("/user/test").hasRole("USER") // "USER" 권한테스트
49-
.anyRequest().permitAll() // 인증 x
52+
.requestMatchers("/user/test").hasRole("USER") // "USER" 권한테스트
53+
.anyRequest().permitAll() // 인증 x
54+
)
55+
// oauth
56+
.oauth2Login(oauth2 -> oauth2
57+
.userInfoEndpoint(userInfo -> userInfo
58+
// login
59+
.userService(customOAuth2UserService)
60+
)
61+
// After login
62+
.successHandler(customOAuth2SuccessHandler)
5063
)
5164
.addFilterBefore(jwtAuthenticationFilter(), UsernamePasswordAuthenticationFilter.class);
5265

src/main/java/ai/Mayi/domain/User.java

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,10 @@ public class User implements UserDetails{
3434
@Column(name = "refresh_token", length = 500)
3535
private String refreshToken; // Refresh Token 저장
3636

37+
private boolean social;
38+
39+
private String profileImageUrl;
40+
3741
@OneToMany(mappedBy = "user", cascade = CascadeType.ALL)
3842
private List<Chat> chats;
3943

src/main/java/ai/Mayi/jwt/CookieUtil.java

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,9 +3,10 @@
33
import jakarta.servlet.http.Cookie;
44
import jakarta.servlet.http.HttpServletRequest;
55
import jakarta.servlet.http.HttpServletResponse;
6+
import org.springframework.stereotype.Component;
67

78
import java.util.Arrays;
8-
9+
@Component
910
public class CookieUtil {
1011
public static void addCookie(HttpServletResponse response, String name, String value, int maxAge) {
1112
Cookie cookie = new Cookie(name, value);

src/main/java/ai/Mayi/jwt/JwtAuthenticationFilter.java

Lines changed: 7 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,12 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
3232
) throws ServletException, IOException {
3333

3434
String accessToken = CookieUtil.getCookieValue(request, "accessToken");
35+
String requestURI = request.getRequestURI();
3536

37+
// if (requestURI.startsWith("/oauth2/") || requestURI.startsWith("/login") || requestURI.equals("/")) {
38+
// chain.doFilter(request, response);
39+
// return;
40+
// }
3641
// accessToken 검사
3742
if (accessToken != null && jwtUtil.validateToken(accessToken)) {
3843
Authentication authentication = jwtUtil.getAuthentication(accessToken);
@@ -44,7 +49,6 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
4449

4550
if (refreshToken != null && jwtUtil.validateToken(refreshToken)) {
4651
String userEmail = jwtUtil.getUserEmail(refreshToken);
47-
log.info("RefreshToken 추출 userEmail {}", userEmail);
4852

4953
if (userRepository.findByUserEmail(userEmail).isPresent()) {
5054
log.info("리프레쉬 토큰안의 정보를 통한 이메일이 존재한다");
@@ -63,14 +67,12 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
6367
String newAccessToken = jwtUtil.generateAccessToken(authentication);
6468
sendTokenResponse(response, newAccessToken);
6569
SecurityContextHolder.getContext().setAuthentication(authentication);
70+
sendStatusResponse(response, ErrorStatus._REFRESHED_ACCESS_TOKEN);
6671
return;
6772
}
6873
}
6974
}
7075

71-
log.warn("저장되어있는 RefreshToken과 쿠키의 AccessToken이 매치되지 않습니다.");
72-
log.warn("401 에러가 나면 로그인페이지로 이동하게 만들기");
73-
// response.sendError(HttpServletResponse.SC_UNAUTHORIZED, "저장되어있는 RefreshToken과 쿠키의 AccessToken이 매치되지 않습니다.");
7476
sendStatusResponse(response, ErrorStatus._EXPIRED_REFRESH_TOKEN);
7577
return;
7678
}
@@ -82,7 +84,7 @@ protected void doFilterInternal(HttpServletRequest request, HttpServletResponse
8284
@Override
8385
protected boolean shouldNotFilter(HttpServletRequest request) throws ServletException {
8486
String path = request.getRequestURI();
85-
return !path.startsWith("/user/data"); // "/user/test"만 필터 적용, 나머지는 제외
87+
return !path.startsWith("/user/data");
8688
}
8789

8890
private void sendTokenResponse(HttpServletResponse response, String accessToken) {
Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,57 @@
1+
package ai.Mayi.oauth;
2+
3+
import ai.Mayi.domain.User;
4+
import ai.Mayi.jwt.CookieUtil;
5+
import ai.Mayi.jwt.JwtUtil;
6+
import ai.Mayi.repository.UserRepository;
7+
import ai.Mayi.web.dto.JwtTokenDTO;
8+
import jakarta.servlet.ServletException;
9+
import jakarta.servlet.http.HttpServletRequest;
10+
import jakarta.servlet.http.HttpServletResponse;
11+
import lombok.RequiredArgsConstructor;
12+
import org.springframework.security.authentication.UsernamePasswordAuthenticationToken;
13+
import org.springframework.security.core.Authentication;
14+
import org.springframework.security.oauth2.core.user.OAuth2User;
15+
import org.springframework.security.web.authentication.AuthenticationSuccessHandler;
16+
import org.springframework.stereotype.Component;
17+
18+
import java.io.IOException;
19+
import java.util.Optional;
20+
21+
@Component
22+
@RequiredArgsConstructor
23+
public class CustomOAuth2SuccessHandler implements AuthenticationSuccessHandler {
24+
25+
private final JwtUtil jwtUtil;
26+
private final CookieUtil cookieUtil;
27+
private final OAuth2Properties oAuth2Properties;
28+
private final UserRepository userRepository;
29+
30+
@Override
31+
public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response,
32+
Authentication authentication) throws IOException, ServletException {
33+
34+
// 이메일 추출
35+
OAuth2User oAuth2User = (OAuth2User) authentication.getPrincipal();
36+
String userEmail = oAuth2User.getAttribute("email");
37+
38+
//이메일로 유저찾기
39+
Optional<User> user = userRepository.findByUserEmail(userEmail);
40+
41+
42+
// JWT 발급
43+
UsernamePasswordAuthenticationToken token = new UsernamePasswordAuthenticationToken(userEmail, null, authentication.getAuthorities());
44+
JwtTokenDTO jwtToken = jwtUtil.generateToken(token);
45+
46+
// 쿠키에 저장
47+
cookieUtil.addCookie(response, "accessToken", jwtToken.getAccessToken(), 600);
48+
cookieUtil.addCookie(response, "refreshToken", jwtToken.getRefreshToken(), 3600);
49+
50+
user.get().updateRefreshToken(jwtToken.getRefreshToken());
51+
userRepository.save(user.get());
52+
53+
// 리다이렉트
54+
response.sendRedirect(oAuth2Properties.getSuccessRedirect());
55+
}
56+
}
57+
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
package ai.Mayi.oauth;
2+
3+
import org.springframework.security.core.GrantedAuthority;
4+
import org.springframework.security.oauth2.core.user.DefaultOAuth2User;
5+
6+
import java.util.Collection;
7+
import java.util.Map;
8+
9+
public class CustomOAuth2User extends DefaultOAuth2User {
10+
11+
public CustomOAuth2User(Collection<? extends GrantedAuthority> authorities,
12+
Map<String, Object> attributes,
13+
String nameAttributeKey) {
14+
super(authorities, attributes, nameAttributeKey);
15+
}
16+
17+
public String getEmail() {
18+
return (String) getAttributes().get("email");
19+
}
20+
}
Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,54 @@
1+
package ai.Mayi.oauth;
2+
3+
import ai.Mayi.domain.User;
4+
import ai.Mayi.repository.UserRepository;
5+
import lombok.RequiredArgsConstructor;
6+
import lombok.extern.slf4j.Slf4j;
7+
import org.springframework.security.core.authority.SimpleGrantedAuthority;
8+
import org.springframework.security.oauth2.client.userinfo.DefaultOAuth2UserService;
9+
import org.springframework.security.oauth2.client.userinfo.OAuth2UserRequest;
10+
import org.springframework.security.oauth2.core.user.OAuth2User;
11+
import org.springframework.stereotype.Service;
12+
13+
import java.util.*;
14+
15+
@Slf4j
16+
@Service
17+
@RequiredArgsConstructor
18+
public class CustomOAuth2UserService extends DefaultOAuth2UserService {
19+
20+
private final UserRepository userRepository;
21+
22+
@Override
23+
public OAuth2User loadUser(OAuth2UserRequest userRequest) {
24+
OAuth2User oAuth2User = super.loadUser(userRequest);
25+
Map<String, Object> attributes = oAuth2User.getAttributes();
26+
27+
//email, profile picture
28+
String userEmail = (String) attributes.get("email");
29+
String userPicture = (String) attributes.get("picture");
30+
String role = "USER";
31+
32+
// DB에 있는지 확인하고 없으면 새로 저장
33+
Optional<User> userOptional = userRepository.findByUserEmail(userEmail);
34+
User user = userOptional.orElseGet(() -> {
35+
log.info("현재 소셜로그인 후 DB에 유저 저장 진행 중..");
36+
User newUser = User.builder()
37+
.userEmail(userEmail)
38+
.userName((String) attributes.get("name"))
39+
// dummyPassword
40+
.userPassword(UUID.randomUUID().toString())
41+
.roles(List.of(role))
42+
.social(true)
43+
.profileImageUrl(userPicture)
44+
.build();
45+
return userRepository.save(newUser);
46+
});
47+
48+
return new CustomOAuth2User(
49+
Collections.singleton(new SimpleGrantedAuthority("ROLE_USER")),
50+
attributes,
51+
"email" // Principal name
52+
);
53+
}
54+
}
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package ai.Mayi.oauth;
2+
3+
import lombok.Getter;
4+
import org.springframework.boot.context.properties.ConfigurationProperties;
5+
import org.springframework.stereotype.Component;
6+
7+
@Component
8+
@ConfigurationProperties(prefix = "app.oauth2")
9+
@Getter
10+
public class OAuth2Properties {
11+
private String successRedirect;
12+
public void setSuccessRedirect(String successRedirect) {
13+
this.successRedirect = successRedirect;
14+
}
15+
}

0 commit comments

Comments
 (0)