Skip to content
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
Original file line number Diff line number Diff line change
Expand Up @@ -47,31 +47,11 @@
)
public class AuthController {

private final Map<String, Oauth2Service<?, ?>> oauth2Services;
private final LoginService loginService;
private final UserService userService;
private final JwtProvider jwtProvider;
private final OauthStateService oauthStateService;
private final RefreshTokenService refreshTokenService;

@Value("${google.client.id}")
private String googleClientId;

@Value("${google.redirect.uri}")
private String googleRedirectUri;

@Value("${kakao.client.id}")
private String kakaoClientId;

@Value("${kakao.redirect.uri}")
private String kakaoRedirectUri;

@Value("${github.client.id}")
private String githubClientId;

@Value("${github.redirect.uri}")
private String githubRedirectUri;


@Operation(
summary = "회원가입 API",
Expand Down Expand Up @@ -136,7 +116,7 @@ public ResponseEntity<SignupResponse> signup(@Valid @RequestBody SignupRequest r
"refreshToken": "eyJhbGciOiJIUzI1NiJ9...",
"userId": "1c54b9f3-8234-4e8f-b001-11cc4d9012ab",
"name": "홍길동",
"role": "USER",
"role": "TEAM_MEMBER",
"phoneNumber": "01012345678"
}
"""))
Expand Down Expand Up @@ -164,205 +144,6 @@ public ResponseEntity<LoginResponse> login(@Valid @RequestBody LoginRequest requ
.body(response);
}

// OAuth 로그인 시작 (state 생성 + 각 provider별 인증 URL 반환)
@Operation(
summary = "OAuth 로그인 시작 (INIT)",
description = "소셜 로그인 시작 시 각 Provider(GOOGLE, KAKAO, GITHUB)의 인증 URL을 반환합니다.",
responses = {
@ApiResponse(
responseCode = "200",
description = "OAuth 인증 URL 반환 성공",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = "\"https://accounts.google.com/o/oauth2/v2/auth?...\""))
),
@ApiResponse(responseCode = "400", description = "지원하지 않는 Provider 요청")
}
)
@GetMapping("/oauth/{provider}/init")
public ResponseEntity<String> startOauthLogin(
@Parameter(description = "소셜 로그인 제공자 (GOOGLE, KAKAO, GITHUB)", example = "GOOGLE")
@PathVariable String provider,
HttpSession session) {
String state = oauthStateService.generateAndSaveState(session);
String authUrl;

switch (provider.toUpperCase()) {
case "GOOGLE" -> authUrl = "https://accounts.google.com/o/oauth2/v2/auth" +
"?client_id=" + googleClientId +
"&redirect_uri=" + googleRedirectUri +
"&response_type=code" +
"&scope=email%20profile" +
"&state=" + state;
case "KAKAO" -> authUrl = "https://kauth.kakao.com/oauth/authorize" +
"?client_id=" + kakaoClientId +
"&redirect_uri=" + kakaoRedirectUri +
"&response_type=code" +
"&state=" + state;
case "GITHUB" -> authUrl = "https://github.com/login/oauth/authorize" +
"?client_id=" + githubClientId +
"&redirect_uri=" + githubRedirectUri +
"&scope=user:email" +
"&state=" + state;
default -> throw new IllegalArgumentException("Unknown provider " + provider);
}

log.debug("Generated OAuth URL for {}: {}", provider, authUrl);
return ResponseEntity.ok(authUrl);
}

//redirection api
@Operation(
summary = "OAuth 로그인 리다이렉트 (GET)",
description = "소셜 로그인 후 리다이렉션 시 호출되는 엔드포인트입니다. "
+ "code와 state 값을 받아 실제 로그인 과정을 처리하며 일반적으로 프론트엔드에서 이 요청을 자동으로 POST로 전달합니다."

)
@GetMapping("/login/{provider}")
public void handleOauthRedirect(
@Parameter(description = "소셜 로그인 제공자", example = "GOOGLE")
@PathVariable("provider") String provider,

@Parameter(description = "OAuth 인증 코드", example = "4/0AbCdEfG...")
@RequestParam("code") String code,

@Parameter(description = "CSRF 방지용 state 값", example = "a1b2c3d4")
@RequestParam("state") String state,

HttpSession session,
HttpServletResponse response
) throws IOException {

log.info("[{}] OAuth GET redirect received: code={}, state={}", provider, code, state);

// 기존 POST OauthLogin() 재활용 (로그인 처리 + 토큰 발급)
ResponseEntity<LoginResponse> result = OauthLogin(provider, code, state, session);
LoginResponse body = result.getBody();

if (body == null) {
log.error("OAuth 로그인 실패: 응답 본문이 null입니다.");
response.sendRedirect("http://localhost:5173/oauth/fail");
return;
}

// 프론트로 리다이렉트 (accessToken, userId, name 전달)
String redirectUrl = "http://localhost:5173/oauth/success"
+ "?accessToken=" + URLEncoder.encode(body.getAccessToken(), StandardCharsets.UTF_8)
+ "&userId=" + URLEncoder.encode(body.getUserId().toString(), StandardCharsets.UTF_8)
+ "&name=" + URLEncoder.encode(body.getName(), StandardCharsets.UTF_8);

log.info("[{}] OAuth 로그인 완료 → 프론트로 리다이렉트: {}", provider, redirectUrl);
response.sendRedirect(redirectUrl);
}


// OAuth 인증 완료 후 Code + State 처리
@Operation(
summary = "OAuth 로그인 완료 (POST)",
description = "OAuth 인증 후 전달된 code와 state를 이용해 토큰을 발급받고 사용자 로그인 처리합니다.",
responses = {
@ApiResponse(
responseCode = "200",
description = "OAuth 로그인 성공",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = """
{
"accessToken": "eyJhbGciOiJIUzI1NiJ9...",
"userId": "3a93f8c2-412b-4d9c-84a2-52bdfec91d11",
"name": "카카오홍길동",
"role": "USER",
"phoneNumber": "01099998888"
}
"""))
),
@ApiResponse(
responseCode = "401",
description = "잘못된 state 값 또는 만료된 인증 코드",
content = @Content(mediaType = "application/json",
examples = @ExampleObject(value = """
{
"message": "Invalid OAuth state or expired authorization code"
}
"""))
)
}
)
@PostMapping("/login/{provider}")
public ResponseEntity<LoginResponse> OauthLogin(
@Parameter(description = "소셜 로그인 제공자", example = "KAKAO") @PathVariable("provider") String provider,
@Parameter(description = "OAuth 인증 코드", example = "4/0AbCdEfG...") @RequestParam("code") String code,
@Parameter(description = "CSRF 방지용 state 값", example = "a1b2c3d4") @RequestParam("state") String state,
HttpSession session) {

// 서버에 저장된 state와 요청으로 받은 state 비교
String savedState = oauthStateService.getStateFromSession(session);

if(savedState == null || !savedState.equals(state)) {
log.warn("[{}] Invalid OAuth state detected. Expected={}, Received={}", provider, savedState, state);
return ResponseEntity.status(401).build();
}

oauthStateService.clearState(session);

Oauth2Service<?, ?> service = oauth2Services.get(provider.toUpperCase());
if (service == null) {
throw new IllegalArgumentException("Unknown provider " + provider);
}

User user = switch (provider.toUpperCase()) {
case "GOOGLE" -> {
var googleService = (Oauth2Service<GoogleTokenResponse, GoogleUserInfoResponse>) service;
var token = googleService.getAccessToken(code);
var info = googleService.getUserInfo(token.getAccessToken());
yield userService.findOrCreateUser(new GoogleUserInfoAdapter(info, token.getAccessToken()));
}
case "KAKAO" -> {
var kakaoService = (Oauth2Service<KakaoTokenResponse, KakaoUserInfoResponse>) service;
var token = kakaoService.getAccessToken(code);
var info = kakaoService.getUserInfo(token.getAccessToken());
yield userService.findOrCreateUser(new KakaoUserInfoAdapter(info, token.getAccessToken()));
}
case "GITHUB" -> {
var githubService = (Oauth2Service<GithubTokenResponse, GithubUserInfoResponse>) service;
var token = githubService.getAccessToken(code);
var info = githubService.getUserInfo(token.getAccessToken());
yield userService.findOrCreateUser(new GithubUserInfoAdapter(info, token.getAccessToken()));
}
default -> throw new IllegalArgumentException("Unknown provider " + provider);
};

// Access 토큰 발급
String accessToken = jwtProvider.createToken(user.getUserId(), user.getRole(), user.getEmail());

String refreshToken = jwtProvider.createRefreshToken(user.getUserId());

refreshTokenService.saveOrUpdateToken(user.getUserId(), refreshToken);

// HttpOnly 쿠키에 담기
ResponseCookie cookie = ResponseCookie.from("refresh", refreshToken)
.httpOnly(true)
.secure(true)
.sameSite("None")
.path("/")
.maxAge(60L * 60 * 24 * 14) // 2주
.build();

// LoginResponse 생성
LoginResponse response = LoginResponse.builder()
.accessToken(accessToken)
.userId(user.getUserId())
.name(user.getName())
.role(user.getRole())
.phoneNumber(user.getPhoneNumber())
.build();

log.info("{} 로그인 성공: userId={}, provider={}", provider.toUpperCase(), user.getUserId(), provider.toUpperCase());

return ResponseEntity.ok()
.header(HttpHeaders.SET_COOKIE, cookie.toString())
.header(HttpHeaders.AUTHORIZATION, "Bearer " + accessToken)
.body(response);
}

@Operation(
summary = "Access Token 재발급 API",
description = "만료된 Access Token을 Refresh Token으로 재발급받습니다.",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,15 +40,15 @@ public class SignupRequest {

@Schema(
description = "사용자 비밀번호 (8자 이상, 숫자/영문/특수문자 조합 권장)",
example = "abcd1234!",
example = "Abcd1234!",
requiredMode = Schema.RequiredMode.REQUIRED
)
@NotBlank(message = "비밀번호는 필수입니다.")
private String password;

@Schema(
description = "사용자 역할 (USER 또는 ADMIN 등)",
example = "USER",
example = "TEAM_MEMBER",
requiredMode = Schema.RequiredMode.REQUIRED
)
@NotNull(message = "역할은 필수입니다.")
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,120 @@
package org.sejongisc.backend.common.auth.config;

import jakarta.transaction.Transactional;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.sejongisc.backend.auth.dao.UserOauthAccountRepository;
import org.sejongisc.backend.auth.entity.AuthProvider;
import org.sejongisc.backend.auth.entity.UserOauthAccount;
import org.sejongisc.backend.user.dao.UserRepository;
import org.sejongisc.backend.user.entity.Role;
import org.sejongisc.backend.user.entity.User;
import org.sejongisc.backend.user.service.UserServiceImpl;
import org.springframework.security.core.authority.SimpleGrantedAuthority;
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.DefaultOAuth2User;
import org.springframework.security.oauth2.core.user.OAuth2User;
import org.springframework.stereotype.Service;

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

@Slf4j
@Service
@RequiredArgsConstructor
@Transactional
public class CustomOAuth2UserService implements OAuth2UserService <OAuth2UserRequest, OAuth2User> {
private final UserRepository userRepository;
private final UserOauthAccountRepository oauthAccountRepository;



@Override
public OAuth2User loadUser(OAuth2UserRequest req) throws OAuth2AuthenticationException {


DefaultOAuth2UserService delegate = new DefaultOAuth2UserService();
OAuth2User oauthUser = delegate.loadUser(req);

String provider = req.getClientRegistration().getRegistrationId();
Map<String, Object> attrs = oauthUser.getAttributes();

log.info("[OAuth2] Provider = {}", provider);
log.info("[OAuth2] Attributes = {}", attrs);

String providerUid;
String email = null;
String name = null;

if (provider.equals("google")) {
providerUid = (String) attrs.get("sub");
email = (String) attrs.get("email");
name = (String) attrs.get("name");

} else if (provider.equals("kakao")) {
providerUid = String.valueOf(attrs.get("id"));
Map<String, Object> account = (Map<String, Object>) attrs.get("kakao_account");
Map<String, Object> profile = (Map<String, Object>) account.get("profile");

email = (String) account.get("email");
if (email == null) email = "no-email@kakao.com";

name = (String) profile.get("nickname");

} else if (provider.equals("github")) {
providerUid = String.valueOf(attrs.get("id"));
email = (String) attrs.get("email");
if (email == null) email = "no-email@github.com";

name = (String) attrs.get("name");
if (name == null) name = email;

} else {
throw new OAuth2AuthenticationException("Unsupported provider: " + provider);
}

// 순환참조 해결: 여기서 userService 호출하지 않음

final String fProviderUid = providerUid;
final String fEmail = email;
final String fName = name;
final AuthProvider fAuthProvider = AuthProvider.valueOf(provider.toUpperCase());

User user = oauthAccountRepository
.findByProviderAndProviderUid(fAuthProvider, providerUid)
.map(UserOauthAccount::getUser)
.orElseGet(() -> {

User newUser = User.builder()
.email(fEmail)
.name(fName)
.role(Role.TEAM_MEMBER)
.build();

User saved = userRepository.save(newUser);

UserOauthAccount oauth = UserOauthAccount.builder()
.user(saved)
.provider(fAuthProvider)
.providerUid(fProviderUid)
.build();

oauthAccountRepository.save(oauth);

return saved;
});

return new DefaultOAuth2User(
List.of(new SimpleGrantedAuthority("ROLE_TEAM_MEMBER")),
Map.of(
"id", user.getUserId(),
"email", user.getEmail(),
"name", user.getName()
),
"email"
);
}
}
Loading